diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5b7a033..1e58bb1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "spec-superflow", "description": "8-state workflow engine with 9 collaborative skills. Bridges planning and execution via execution-contract.md. Embedded schema validation, TDD, SDD, code review, systematic debugging, and delta spec sync.", - "version": "0.10.0", + "version": "0.11.0", "source": "./", "author": { "name": "MageByte", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d870552..69684f2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spec-superflow", - "version": "0.10.0", + "version": "0.11.0", "description": "Spec-first workflow: integrates OpenSpec planning engine + Superpowers execution discipline. 9 collaborative skills with embedded TDD, SDD, code review, debugging, and delta spec sync.", "source": "./", "author": { diff --git a/.claude/always/phase-guard.md b/.claude/always/phase-guard.md index d5295cf..e4e3729 100644 --- a/.claude/always/phase-guard.md +++ b/.claude/always/phase-guard.md @@ -1,3 +1,3 @@ -# spec-superflow v0.10.0 | 阶段: {{state}} | 工作流: {{workflow}} +# spec-superflow v0.11.0 | 阶段: {{state}} | 工作流: {{workflow}} 当前阶段允许的操作由 workflow-start 路由规则定义。 禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。 diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 8ad17aa..6b107d6 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spec-superflow", - "version": "0.10.0", + "version": "0.11.0", "description": "Spec-first workflow that bridges OpenSpec-style planning and Superpowers-style execution discipline.", "author": { "name": "MageByte", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 27fb9a5..0540bed 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -5,7 +5,7 @@ }, "metadata": { "description": "Spec-first workflow plugin marketplace for Cursor", - "version": "0.10.0" + "version": "0.11.0" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index b796d6d..0ac2292 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "spec-superflow", "displayName": "spec-superflow", "description": "Spec-first workflow: integrates OpenSpec planning engine + Superpowers execution discipline. 9 collaborative skills with embedded TDD, SDD, code review, debugging, and delta spec sync.", - "version": "0.10.0", + "version": "0.11.0", "author": { "name": "MageByte", "url": "https://github.com/MageByte-Zero" diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index c55f4fc..0b77711 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -6,13 +6,13 @@ }, "metadata": { "description": "Spec-first workflow plugins and skills for AI coding agents.", - "version": "0.10.0" + "version": "0.11.0" }, "plugins": [ { "name": "spec-superflow", "description": "Spec-first workflow with planning artifacts, execution contracts, TDD, review gates, systematic debugging, and delta spec sync.", - "version": "0.10.0", + "version": "0.11.0", "source": ".", "author": { "name": "MageByte", diff --git a/CHANGELOG.md b/CHANGELOG.md index f27ba3e..bc4331b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,21 @@ The format loosely follows Keep a Changelog. ## [Unreleased] +## [0.11.0] - 2026-07-21 + ### Added -- **Closes #47 — Recovery workflow commands**: publish `ssf resume`, `ssf switch`, and `ssf save` as a control-plane overlay. Resume/switch are read-only; save writes only the compatible checkpoint and never automatically commits, pushes, or syncs. WorkBuddy distributes the canonical `/ssf:resume`, `/ssf:switch`, and `/ssf:save` Markdown command adapters. +- **Closes #47 — Recovery workflow commands**: add `ssf resume`, `ssf switch`, and `ssf save` as a control-plane overlay. Resume/switch are read-only; save writes only the compatible checkpoint and never automatically commits, pushes, or syncs. WorkBuddy distributes the canonical `/ssf:resume`, `/ssf:switch`, and `/ssf:save` Markdown command adapters. +- **Closes #70 — Workflow path recommendation and selection**: `ssf workflow recommend/select/show` collects minimal intake facts, recommends full, hotfix, or tweak with reasons, and requires an explicit, auditable choice before workflow state routing continues. + +### Changed + +- **#65 — Planning artifact language inheritance**: proposal, specs, design, tasks, and execution contracts inherit the conversation language instead of defaulting to English. ### Fixed - **#64 — closing 终态生命周期对齐**:将 `release-archivist` 验证、`spec-merger` 同步和归档确认明确为 `executing` 内的 pre-closing 收尾步骤;`closing` 是 `CLOSED` 成功终态,且没有 next skill。 +- **#71 — Taskless hotfix closure**: hotfix changes without `tasks.md` can satisfy the closing guard after their minimal contract and required verification are complete. ## [0.10.0] - 2026-07-16 diff --git a/GEMINI.md b/GEMINI.md index 28784bc..3758d79 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -8,7 +8,7 @@ The workflow is self-contained and does not require OpenSpec or Superpowers at r -# spec-superflow v0.10.0 | 阶段: {{state}} | 工作流: {{workflow}} +# spec-superflow v0.11.0 | 阶段: {{state}} | 工作流: {{workflow}} 当前阶段允许的操作由 workflow-start 路由规则定义。 禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。 diff --git a/INSTALL.md b/INSTALL.md index ff1fa82..0e6ec44 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -7,7 +7,7 @@ - [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) — 规划引擎(Schema 验证、Delta Spec、工件解析) - [obra/superpowers](https://github.com/obra/superpowers) — 执行纪律(TDD 铁律、SDD、系统化调试、代码审查) -当前发布版本:**v0.10.0**。 +当前发布版本:**v0.11.0**。 --- diff --git a/README.md b/README.md index 194b861..f6bcd2f 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ npx spec-superflow list # 或通过 npx 使用 ### 版本 -- 当前版本:`v0.10.0` +- 当前版本:`v0.11.0` - v0.9.1 highlights:DP-4 执行模式推荐、跨 17 个平台的 portable runtime,以及无插件根路径的 raw-package smoke;详见 [CHANGELOG.md](CHANGELOG.md) - v0.9.0 highlights:支持 Node 20/22、model profiles 只读解析,以及 code-reviewer 的最小性审查 - 自包含插件,不需要运行时安装 OpenSpec 或 Superpowers diff --git a/commands/ssf/resume.md b/commands/ssf/resume.md index 3a13433..d79fe62 100644 --- a/commands/ssf/resume.md +++ b/commands/ssf/resume.md @@ -5,6 +5,6 @@ argument-hint: "[change-name-or-path]" allowed-tools: Bash(npx:*) --- -先检查 `$ARGUMENTS` 是否为非空目标:为空时运行 `npx --yes --package spec-superflow@0.10.0 ssf resume --json`,让 CLI 按唯一活跃 change 的既有确定性规则选择;非空时运行 `npx --yes --package spec-superflow@0.10.0 ssf resume --json "$ARGUMENTS"`,将整个目标作为单一字面参数。 +先检查 `$ARGUMENTS` 是否为非空目标:为空时运行 `npx --yes --package spec-superflow@0.11.0 ssf resume --json`,让 CLI 按唯一活跃 change 的既有确定性规则选择;非空时运行 `npx --yes --package spec-superflow@0.11.0 ssf resume --json "$ARGUMENTS"`,将整个目标作为单一字面参数。 只使用返回的 `change`、`blockers` 和 `next_action`:存在 blocker 时停止并展示修复命令;否则通过 `workflow-start` 进入 `next_action` 指定的下一 skill。不要直接修改状态文件。 diff --git a/commands/ssf/save.md b/commands/ssf/save.md index eb40360..9a05c17 100644 --- a/commands/ssf/save.md +++ b/commands/ssf/save.md @@ -7,4 +7,4 @@ allowed-tools: Bash(npx:*) 只把 `$ARGUMENTS` 作为对话输入,从中提取明确的 change、task 和 next,以及用户明确提供的可选 evidence 字段。信息不足时先询问一次;不要编造 verification 或 review 证据。 -确认参数后运行 `npx --yes --package spec-superflow@0.10.0 ssf save "" --task "" --next "" [--completed ""] [--verification ""] [--review ""] [--risk ""] [--commit-start ""] [--commit-end ""] --json`。每个值必须来自已提取或已确认的信息,并作为单独引用的字面参数传入。只报告 CLI 返回的 checkpoint 结果,并保留现有状态机和存储边界。 +确认参数后运行 `npx --yes --package spec-superflow@0.11.0 ssf save "" --task "" --next "" [--completed ""] [--verification ""] [--review ""] [--risk ""] [--commit-start ""] [--commit-end ""] --json`。每个值必须来自已提取或已确认的信息,并作为单独引用的字面参数传入。只报告 CLI 返回的 checkpoint 结果,并保留现有状态机和存储边界。 diff --git a/commands/ssf/switch.md b/commands/ssf/switch.md index 7f81e2a..48fcab0 100644 --- a/commands/ssf/switch.md +++ b/commands/ssf/switch.md @@ -7,4 +7,4 @@ allowed-tools: Bash(npx:*) `$ARGUMENTS` 必须提供一个非空的 change 名称或路径;若未提供,先询问用户选择哪个 change。 -确认目标非空后运行 `npx --yes --package spec-superflow@0.10.0 ssf switch --json "$ARGUMENTS"`,将整个目标作为单一字面参数。只使用返回的恢复上下文切换当前对话的关注对象。存在 blocker 时停止并展示修复命令;不得改变文件、工作目录或任何隐藏指针。 +确认目标非空后运行 `npx --yes --package spec-superflow@0.11.0 ssf switch --json "$ARGUMENTS"`,将整个目标作为单一字面参数。只使用返回的恢复上下文切换当前对话的关注对象。存在 blocker 时停止并展示修复命令;不得改变文件、工作目录或任何隐藏指针。 diff --git a/docs/README_en.md b/docs/README_en.md index aeddbca..01e4344 100644 --- a/docs/README_en.md +++ b/docs/README_en.md @@ -129,7 +129,7 @@ npm install -g spec-superflow ### Version -- Current: `v0.10.0` +- Current: `v0.11.0` - v0.9.1 highlights: DP-4 execution-mode recommendations, a portable runtime across 17 platforms, and a raw-package smoke with no plugin-root variable. - Self-contained — no OpenSpec or Superpowers runtime required - Upstream: [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec), [obra/superpowers](https://github.com/obra/superpowers) diff --git a/docs/decision-points.md b/docs/decision-points.md index a91cded..6bccf71 100644 --- a/docs/decision-points.md +++ b/docs/decision-points.md @@ -6,9 +6,11 @@ - **编号**:DP-0 - **名称**:设计前确认 -- **触发条件**:`workflow-start` 检测到规划工件不存在或不完整,准备路由到 `spec-writer` 之前 -- **所需输入**:变更名称与意图、已知约束(命名风格、兼容性、受影响平台)、是否包含相关优化、用户沟通偏好 -- **预期输出**:用户确认关键决策,或提出修改意见;`workflow-start` 将确认结果写入 `.spec-superflow.yaml` 的 `dp_0_*` 字段 +- **触发条件**:`workflow-start` 检测到 change 目录不存在、规划工件不存在或不完整、`dp_0_confirmed` 尚未确认,或 legacy change 的 workflow 为 `auto`/空时;该门禁覆盖 full 以及直接进入 hotfix/tweak 的 fast paths,不只发生在路由到 `spec-writer` 之前 +- **所需输入**:变更名称与意图、已知约束(命名风格、兼容性、受影响平台)、是否包含相关优化、用户沟通偏好;以及最少路径事实(任务数、文件数、是否仅配置/文档、是否涉及 schema/API、新模块和不确定性) +- **路径选择协议**:`workflow-start` 先读取 `ssf workflow show`;仅在 `missing_facts` 列出的字段缺失时提问,再运行 `ssf workflow recommend`。它必须向用户展示 Observed、Available、Recommended、Why 四项信息,推荐本身不改变状态也不写入 workflow。用户明确选择 `full`、`hotfix` 或 `tweak` 后,才可用 `ssf workflow select --confirm` 持久化;选择非推荐路径还必须显式传入 `--acknowledge-recommendation`。 +- **确认顺序**:可先解析 `artifact_language`,随后必须完成路径 receipt 读取、最少事实补全、建议展示和用户选择;路径摘要与其他 DP-0 决定合并确认后,才可设置 `dp_0_confirmed=true`。 +- **预期输出**:完整、防篡改的路径选择 receipt 固定保存在 change overlay 的 `.superpowers/sdd/workflow-selection.json`,用于恢复和审计;`.spec-superflow.yaml` 的 `dp_0_*` 只保存确认结果与幂等的 `workflow_path`/推荐对齐摘要,并保留既有 `scope` 和 `artifact_language`。空目录的 legacy artifact inference 可以返回 `full` 以兼容旧 API,但绝不能替代入口的用户选择。 - **关联 skill**:`spec-superflow:workflow-start` ## DP-1: 需求确认 @@ -43,8 +45,8 @@ - **编号**:DP-4 - **名称**:执行模式选择 - **触发条件**:build-executor 启动执行前,用户需要选择本次执行的开发模式 -- **所需输入**:已批准的 `execution-contract.md`、项目测试基础设施现状、用户对 TDD(测试驱动开发)和 SDD(规格驱动开发)两种模式的说明 -- **预期输出**:用户明确选择 TDD 模式或 SDD 模式,build-executor 据此调整执行策略 +- **所需输入**:已批准的 `execution-contract.md`、项目测试基础设施现状,以及 `ssf execution recommend` 提供的执行模式证据与建议 +- **预期输出**:用户明确选择 `Inline`、`Batch Inline` 或 `SDD` 执行模式,build-executor 据此创建受确认的执行计划。DP-4 不重新选择 DP-0 已确认的 `full`、`hotfix` 或 `tweak` 路径。 - **关联 skill**:`spec-superflow:build-executor` ## DP-5: 调试升级 diff --git a/docs/designs/2026-07-21-workflow-path-recommendation-design.md b/docs/designs/2026-07-21-workflow-path-recommendation-design.md new file mode 100644 index 0000000..3d4683e --- /dev/null +++ b/docs/designs/2026-07-21-workflow-path-recommendation-design.md @@ -0,0 +1,209 @@ +# 需求入口工作流路径推荐设计 + +## 背景与目标 + +Issue #70 要解决的是需求入口阶段的错误默认:当 `proposal.md` 和 +`tasks.md` 尚未生成时,现有 `ssf runtime infer ` 看不到任务和 +文件证据,只能安全地返回 `full`。这会在用户尚未选择前,把“信息缺失” +误当成“需求复杂”。 + +本设计在 DP-0 附近增加一个独立的需求路径推荐控制面。系统先收集分类所需 +的最少事实,再展示 `full`、`hotfix`、`tweak` 三种可选路径、推荐结果和 +理由,最后由用户明确选择。推荐不会自动成为最终 workflow。 + +该控制面与 DP-4 的 Inline、Batch Inline、SDD 执行模式推荐完全分离,也 +不改变三种 workflow 的现有阈值或状态转换门禁。 + +## 方案选择 + +采用独立的 `ssf workflow recommend/select/show` 命令组。 + +没有采用扩展 `runtime infer` 的方案,因为 `runtime infer` 的职责是根据已 +存在的规划工件做兼容性推断,而新功能处理的是工件生成前的用户决策。把 +推荐、确认和持久化塞进 infer 会混淆“观察”与“决定”。 + +没有采用纯 skill 提示词方案,因为它无法提供稳定的 receipt、恢复校验和 +CLI 行为测试,也不能可靠证明用户选择没有被自动覆盖。 + +## 用户流程 + +1. `workflow-start` 读取 state。若 workflow 已显式为 `full`、`hotfix` 或 + `tweak`,直接尊重该值,不再推荐。 +2. workflow 为 `auto`、空或未设置时,skill 只询问当前缺失的分类事实。 +3. skill 调用 `ssf workflow recommend` 保存观察事实和推荐 receipt。 +4. 命令返回全部可选路径、推荐路径、事实、理由和缺失事实。 +5. 信息不足时返回 `needs-input`,保持 workflow 为 `auto`,skill 继续询问 + 缺失项。 +6. 信息充分时,用户选择一个路径。skill 调用 `ssf workflow select --confirm`。 +7. 非推荐选择还必须传入 `--acknowledge-recommendation`,并保存用户理由。 +8. select 在完成全部验证后保存选择,把 state.workflow 设置为所选路径,并 + 把可审计摘要追加到 `dp_0_decisions`,不覆盖已有的范围和语言决定。 + +示例: + +```text +Observed: 2 tasks, 2 files, config/doc only: no, + schema/API/public interface: no, new module: no, + uncertainty: low +Available: full | hotfix | tweak +Recommended: hotfix +Why: bounded code change within hotfix thresholds +Choose a workflow: +``` + +## CLI 接口 + +### `ssf workflow recommend` + +```bash +ssf workflow recommend \ + --task-count \ + --file-count \ + --config-doc-only \ + --schema-api-change \ + --new-module \ + --uncertainty \ + [--json] +``` + +每次调用都以提供的完整事实快照替换上一份 observation,避免不同轮次的 +残缺输入被意外拼接。skill 可先用 `workflow show` 读取上一份 observation, +再补齐用户刚回答的字段后重新 recommend。 + +计数缺失或标志为 `unknown` 时,返回 `status: needs-input` 和 +`missing_facts`。此结果可以持久化,但不得产生可选择的 recommendation。 + +### `ssf workflow select` + +```bash +ssf workflow select \ + --mode \ + --confirm \ + --reason \ + [--acknowledge-recommendation] \ + [--json] +``` + +select 必须验证: + +- state.workflow 仍为 `auto`、空或未设置; +- 当前 receipt 信息充分且 hash 有效; +- mode 是三个可选路径之一; +- `--confirm` 已提供; +- 非推荐选择已显式确认; +- reason 非空,且不含换行或控制字符。 + +任一验证失败时,不得修改 receipt、state 或 DP-0。 + +### `ssf workflow show` + +输出当前 observation、推荐、选择、hash 有效性及 state 中的最终 workflow。 +如果用户此前已显式设置 workflow 且没有 receipt,输出 +`source: explicit-state`,而不是要求重新推荐。 + +## 事实模型与推荐算法 + +事实快照包含: + +- `task_count` +- `file_count` +- `config_doc_only` +- `schema_api_change`,同时代表 schema、API 或公共接口变化 +- `new_module` +- `uncertainty` + +推荐顺序为: + +1. 任一关键事实未知:`needs-input`,不推荐路径。 +2. `schema_api_change=yes`、`new_module=yes` 或 `uncertainty=high`:推荐 + `full`。 +3. `config_doc_only=yes`、任务数不超过 4、文件数不超过 4:推荐 `tweak`。 +4. `config_doc_only=no`、任务数不超过 2、文件数不超过 2:推荐 `hotfix`。 +5. 其余充分信息:推荐 `full`。 + +所有成功推荐都返回可核对的 reasons。算法复用现有阈值的常量语义,不修改 +guard 的 workflow 转换矩阵。 + +## 持久化与审计 + +详细记录保存到 change overlay: + +```text +/.superpowers/sdd/workflow-selection.json +``` + +记录包含: + +- schema version; +- 完整 facts; +- available modes; +- status、recommendation、reasons、missing facts; +- created_at; +- receipt hash; +- 用户选择、理由、是否遵循推荐、是否确认非推荐选择; +- selected_at。 + +写入采用临时文件加 rename,避免部分写入。hash 使用稳定 JSON 和 SHA-256, +读取时重新计算;损坏或被篡改的 receipt 不得用于 select。 + +成功选择后,在保留原内容的前提下向 `dp_0_decisions` 追加单行摘要: + +```text +workflow_path=hotfix; recommended=hotfix; followed_recommendation=true +``` + +因此现有 `ssf audit` 能通过 DP-0 展示选择摘要,而 `workflow show` 提供完整 +事实和 receipt。恢复时 `workflow-start` 先调用 show:未完成的推荐继续收集 +事实,已完成的选择直接复用。 + +## 代码边界 + +- `scripts/lib/workflow-recommendation.mjs`:纯推荐函数、receipt 读写、hash 和 + select 验证。 +- `scripts/lib/cmd-workflow.mjs`:CLI 参数解析、错误码、选择 receipt 与 + state/DP-0 的有序写入和失败恢复。 +- `scripts/spec-superflow.mjs`:注册 workflow 命令及 help。 +- `skills/workflow-start/SKILL.md`:改为先 show/recommend/select,再执行现有 + 路由;`runtime infer` 继续作为已有工件调用方的兼容接口,但不再承担需求 + 入口的用户选择。 +- `tests/lib/workflow-recommendation.test.mjs`:纯算法和 receipt 测试。 +- `tests/lib/cmd-workflow.test.mjs`:CLI、持久化、失败不变异和显式 workflow + 优先测试。 +- `tests/lib/infer-workflow.test.mjs`:锁定旧 infer 的向后兼容行为。 + +不新增 runtime dependency,不新增状态机阶段,不修改 DP-3/DP-4 的批准语义。 + +## 错误处理 + +- change state 文件缺失:用法错误并提示先 `state init`,不创建幽灵 state。 +- 输入枚举或计数非法:exit 2,不写文件。 +- 信息不足:正常业务结果,exit 0,返回 `needs-input`。 +- receipt 缺失、过期、hash 不符:select exit 1,不写 state。 +- workflow 已显式设置:recommend/show 返回显式来源;select 不覆盖。 +- 文件写入失败:使用原子单文件写入;验证失败不产生任何写入。如果 receipt + 已记录选择但 state 写入失败,show 将其报告为 `selection-pending`,允许同一 + 选择安全重试,不把未完成选择当作最终 workflow。 + +## 测试与验收 + +必须覆盖: + +1. 2 tasks / 2 files 的小型代码修复推荐 hotfix; +2. 4 tasks / 4 files 内的纯配置或文档调整推荐 tweak; +3. schema/API/公共接口、新模块、高不确定性或超过阈值推荐 full; +4. 事实未知返回 needs-input,workflow 仍为 auto; +5. 用户选择非推荐路径时,无确认则拒绝,有确认则可审计; +6. 已显式设置 workflow 时不重复推荐或覆盖; +7. receipt hash 损坏、非法参数和失败写入不会改变 state; +8. 现有 `runtime infer` 和三种 workflow guard 行为保持不变; +9. Node 20/22 构建、完整测试、doctor、raw-mode 和发布 smoke 全部通过。 + +## 发布安排 + +Issue #70 功能实现和聚焦评审完成后、最终 `executing -> closing` 转换前,执行 +`ssf version 0.11.0`,将 Unreleased 内容整理为 `0.11.0` 发布项,同步 README、 +英文 README、安装文档、全部 manifests 和九个 canonical skills。随后完成 +全分支评审、spec merge 和 closing;发布 PR 使用 `Closes #70`。 + +发布准备不包含创建 tag、npm publish、GitHub Release 或外部 marketplace +写操作;这些动作继续等待维护者单独授权。 diff --git a/docs/state-machine.md b/docs/state-machine.md index c17e4f2..7fa39b9 100644 --- a/docs/state-machine.md +++ b/docs/state-machine.md @@ -11,6 +11,29 @@ - no implementation is allowed - `need-explorer` is active +#### Workflow Path Intake + +At entry, `workflow-start` reads the persisted `workflow` selection first. An +explicit `full`, `hotfix`, or `tweak` selection wins. Otherwise it runs `ssf +workflow show`, asks only for `missing_facts`, runs `ssf workflow recommend`, +and presents Observed, Available, Recommended, and Why. Recommendation does +not change state or select a path: only an explicit `ssf workflow select +--confirm` writes `workflow`. A non-recommended selection requires +`--acknowledge-recommendation`. The legacy `runtime infer` compatibility API +may return `full` for an empty directory, but it never replaces the user's +intake selection. + +This intake completes before DP-0 is marked confirmed. Artifact language may +be resolved first, but `dp_0_confirmed=true` is written only after the selected +path summary and the remaining scope, constraints, and communication decisions +are confirmed together. The full selection receipt lives at +`.superpowers/sdd/workflow-selection.json`; DP-0 state stores only the +idempotent summary while preserving scope and `artifact_language`. + +This is the DP-0 planning-path decision. DP-4 remains the separate execution +mode decision among Inline, Batch Inline, and SDD. Neither recommendation nor +selection creates a ninth state or performs a phase transition. + ### `specifying` - planning artifacts are being written or revised diff --git a/gemini-extension.json b/gemini-extension.json index be9703e..341177e 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "spec-superflow", "description": "Spec-first workflow plugin: clarified requirements → execution contract → disciplined implementation", - "version": "0.10.0", + "version": "0.11.0", "contextFileName": "GEMINI.md" } diff --git a/hooks/session-start b/hooks/session-start index 6e2956d..a598dc2 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# v0.10.0: conditional injection — detects artifacts, injects workflow-start pointer +# v0.11.0: conditional injection — detects artifacts, injects workflow-start pointer msg="\nYou have spec-superflow installed. Use /spec-superflow:workflow-start ONLY when you detect an active spec-superflow change in the workspace (look for \`.spec-superflow.yaml\`, \`proposal.md\`, \`execution-contract.md\`, or \`specs/\` directories), OR when the user explicitly invokes it by name. For ordinary coding tasks without spec-superflow artifacts, do NOT invoke workflow-start — just handle the request directly.\n" # Three platforms share the same message, only output format differs. diff --git a/llms.txt b/llms.txt index a0ebca3..778f28f 100644 --- a/llms.txt +++ b/llms.txt @@ -3,7 +3,7 @@ ## Overview spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow. -Current version: v0.10.0. +Current version: v0.11.0. ## Key Documents - README.md: Chinese homepage with full usage guide and FAQ diff --git a/package-lock.json b/package-lock.json index a47bb91..449b9ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "spec-superflow", - "version": "0.9.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "spec-superflow", - "version": "0.9.0", + "version": "0.11.0", "license": "MIT", "bin": { "spec-superflow": "scripts/spec-superflow.mjs", diff --git a/package.json b/package.json index 8ca17b6..844ec2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spec-superflow", - "version": "0.10.0", + "version": "0.11.0", "description": "Spec-first workflow: integrates OpenSpec planning engine + Superpowers execution discipline via bridge contract. 9 collaborative skills for multi-agent coding tools.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/plugin.json b/plugin.json index 60eea3a..bf6e8ec 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "name": "spec-superflow", "description": "Spec-first workflow that bridges OpenSpec-style planning and Superpowers-style execution discipline.", - "version": "0.10.0", + "version": "0.11.0", "author": { "name": "MageByte", "url": "https://github.com/MageByte-Zero" diff --git a/scripts/check-version-consistency.mjs b/scripts/check-version-consistency.mjs index 33335f9..e898c57 100755 --- a/scripts/check-version-consistency.mjs +++ b/scripts/check-version-consistency.mjs @@ -25,6 +25,8 @@ const errors = []; // ── JSON manifests ── const JSON_CHECKS = [ + { file: 'package-lock.json', path: ['version'] }, + { file: 'package-lock.json', path: ['packages', '', 'version'] }, { file: 'plugin.json', path: ['version'] }, { file: '.claude-plugin/plugin.json', path: ['version'] }, { file: '.claude-plugin/marketplace.json', path: ['plugins', '0', 'version'] }, @@ -90,6 +92,9 @@ const RUNTIME_FILES = [ 'skills/build-executor/implementer-prompt.md', 'skills/build-executor/task-reviewer-prompt.md', 'skills/code-reviewer/code-reviewer-prompt.md', + 'commands/ssf/resume.md', + 'commands/ssf/switch.md', + 'commands/ssf/save.md', ]); for (const file of RUNTIME_FILES) { const fp = join(ROOT, file); diff --git a/scripts/lib/cmd-version.mjs b/scripts/lib/cmd-version.mjs index 2dcc98a..9b83daa 100644 --- a/scripts/lib/cmd-version.mjs +++ b/scripts/lib/cmd-version.mjs @@ -5,6 +5,8 @@ import { join } from 'node:path'; // ── JSON manifests with structured path ── const MANIFESTS = [ { file: 'package.json', path: ['version'] }, + { file: 'package-lock.json', path: ['version'] }, + { file: 'package-lock.json', path: ['packages', '', 'version'] }, { file: 'plugin.json', path: ['version'] }, { file: '.claude-plugin/plugin.json', path: ['version'] }, { file: '.claude-plugin/marketplace.json', path: ['plugins', '0', 'version'] }, @@ -38,6 +40,9 @@ const TEXT_FILES = [ { file: 'skills/bug-investigator/SKILL.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, { file: 'skills/release-archivist/SKILL.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, { file: 'skills/spec-merger/SKILL.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, + { file: 'commands/ssf/resume.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, + { file: 'commands/ssf/switch.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, + { file: 'commands/ssf/save.md', pattern: /(npx --yes --package spec-superflow@)0\.\d+\.\d+( ssf)/g, replacement: '$10.%MINOR%.%PATCH%$2' }, ]; function getNestedValue(obj, pathParts) { diff --git a/scripts/lib/cmd-workflow.mjs b/scripts/lib/cmd-workflow.mjs new file mode 100644 index 0000000..7cc8357 --- /dev/null +++ b/scripts/lib/cmd-workflow.mjs @@ -0,0 +1,244 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseArgs } from 'node:util'; +import { + WORKFLOW_MODES, + recommendWorkflowPath, + readWorkflowSelection, + recordWorkflowSelection, + saveWorkflowRecommendation, +} from './workflow-recommendation.mjs'; +import { readState, writeState } from './state-loader.mjs'; + +const OPTIONS = { + 'task-count': { type: 'string' }, + 'file-count': { type: 'string' }, + 'config-doc-only': { type: 'string' }, + 'schema-api-change': { type: 'string' }, + 'new-module': { type: 'string' }, + uncertainty: { type: 'string' }, + mode: { type: 'string' }, + confirm: { type: 'boolean', default: false }, + reason: { type: 'string' }, + 'acknowledge-recommendation': { type: 'boolean', default: false }, + json: { type: 'boolean', default: false }, + help: { type: 'boolean', default: false }, +}; + +const BOOLEAN_FACTS = { + 'config-doc-only': ['yes', 'no', 'unknown'], + 'schema-api-change': ['yes', 'no', 'unknown'], + 'new-module': ['yes', 'no', 'unknown'], +}; + +class UsageError extends Error {} + +export async function run(args) { + let parsed; + try { + parsed = parseArgs({ args, options: OPTIONS, allowPositionals: true }); + } catch (error) { + return fail(error.message, 2); + } + + const { positionals, values } = parsed; + const [subcommand, changeDir] = positionals; + if (values.help || subcommand === undefined) return printHelp(); + if (!['recommend', 'select', 'show'].includes(subcommand)) { + return fail('Usage: ssf workflow ', 2); + } + if (positionals.length !== 2 || !changeDir) { + return fail('Usage: ssf workflow ', 2); + } + + try { + requireStateFile(changeDir); + const state = readState(changeDir); + + if (subcommand === 'select' && isExplicitWorkflow(state.workflow)) { + return fail('workflow is already explicitly selected', 1); + } + if (subcommand === 'recommend' && isExplicitWorkflow(state.workflow)) { + return print({ source: 'explicit-state', workflow: state.workflow }, values.json); + } + if (subcommand === 'recommend') return recommend(changeDir, values); + if (subcommand === 'show') return show(changeDir, state, values.json); + return select(changeDir, state, values); + } catch (error) { + if (error instanceof UsageError) return fail(error.message, 2); + return fail(error.message, 1); + } +} + +function recommend(changeDir, values) { + const record = saveWorkflowRecommendation(changeDir, factsFrom(values)); + return print({ source: 'recommendation', ...record }, values.json); +} + +function select(changeDir, state, values) { + if (!WORKFLOW_MODES.includes(values.mode)) { + throw new UsageError(`--mode must be one of: ${WORKFLOW_MODES.join(', ')}`); + } + const record = recordWorkflowSelection(changeDir, { + mode: values.mode, + reason: values.reason, + confirmed: values.confirm, + acknowledged: values['acknowledge-recommendation'], + }); + const summary = `workflow_path=${record.selection.mode}; recommended=${record.recommendation.mode}; followed_recommendation=${record.selection.followed_recommendation}`; + state.workflow = record.selection.mode; + state.dp_0_decisions = appendDecision(state.dp_0_decisions, summary); + writeState(changeDir, state); + return print({ ok: true, source: 'user-confirmed', record }, values.json); +} + +function show(changeDir, state, json) { + const receipt = readWorkflowSelection(changeDir); + if (!receipt.exists) { + if (isExplicitWorkflow(state.workflow)) { + return print({ source: 'explicit-state', workflow: state.workflow }, json); + } + return print({ + source: 'missing-receipt', + ...recommendWorkflowPath({}), + workflow: state.workflow ?? 'auto', + receipt, + }, json); + } + if (!receipt.valid) { + if (isExplicitWorkflow(state.workflow)) { + return print({ + source: 'explicit-state', workflow: state.workflow, record: receipt.record, receipt, + }, json); + } + print({ + status: 'invalid', workflow: state.workflow ?? 'auto', record: receipt.record, receipt, + }, json); + process.exitCode = 1; + return; + } + + const record = receipt.record; + const selectedMode = record.selection?.mode; + if (selectedMode && state.workflow === selectedMode) { + return print({ status: 'selected', source: 'receipt', workflow: state.workflow, record }, json); + } + if (isExplicitWorkflow(state.workflow)) { + return print({ source: 'explicit-state', workflow: state.workflow, record }, json); + } + if (selectedMode) { + return print({ status: 'selection-pending', workflow: state.workflow ?? 'auto', record }, json); + } + return print({ status: record.status, workflow: state.workflow ?? 'auto', record }, json); +} + +function factsFrom(values) { + return { + task_count: parseCount(values['task-count'], 'task-count'), + file_count: parseCount(values['file-count'], 'file-count'), + config_doc_only: parseFact(values['config-doc-only'], 'config-doc-only'), + schema_api_change: parseFact(values['schema-api-change'], 'schema-api-change'), + new_module: parseFact(values['new-module'], 'new-module'), + uncertainty: parseFact(values.uncertainty, 'uncertainty'), + }; +} + +function parseCount(value, name) { + if (value === undefined) return null; + if (!/^\d+$/.test(value)) throw new UsageError(`${name} must be a non-negative integer`); + const count = Number.parseInt(value, 10); + if (!Number.isSafeInteger(count) || count < 0) { + throw new UsageError(`${name} must be a non-negative integer`); + } + return count; +} + +function parseFact(value, name) { + if (value === undefined) return 'unknown'; + const allowed = name === 'uncertainty' ? ['low', 'high', 'unknown'] : BOOLEAN_FACTS[name]; + if (!allowed.includes(value)) throw new UsageError(`${name} must be one of: ${allowed.join(', ')}`); + return value; +} + +function requireStateFile(changeDir) { + if (!existsSync(join(changeDir, '.spec-superflow.yaml'))) { + throw new Error('Workflow state is missing; run "ssf state init " first'); + } +} + +function appendDecision(existing, summary) { + const entries = typeof existing === 'string' + ? existing.split(/\s+\|\s+/).map(value => value.trim()).filter(Boolean) + : []; + return [...entries.filter(value => !value.startsWith('workflow_path=')), summary].join(' | '); +} + +function isExplicitWorkflow(workflow) { + return WORKFLOW_MODES.includes(workflow); +} + +function print(value, json) { + if (json) console.log(JSON.stringify(value)); + else console.log(format(value)); +} + +function format(value) { + const record = value.record ?? value; + if (value.source === 'explicit-state') { + return [ + `Workflow is explicitly set to ${value.workflow}.`, + ...formatRecordDetails(value, record), + ].join('\n'); + } + if (value.source === 'user-confirmed') return `Workflow selected: ${value.record.selection.mode}.`; + if (value.status) { + return [ + `Workflow status: ${value.status}.`, + ...formatRecordDetails(value, record), + ].join('\n'); + } + return JSON.stringify(value); +} + +function formatRecordDetails(value, record) { + const lines = []; + if (record?.facts) { + const observed = Object.entries(record.facts) + .map(([name, fact]) => `${name}=${fact}`) + .join(', '); + lines.push(`Observed: ${observed}`); + } + if (record?.available_modes) lines.push(`Available: ${record.available_modes.join(', ')}`); + if (record?.recommendation) { + lines.push(`Recommended: ${record.recommendation.mode}`); + lines.push(`Why: ${record.recommendation.reasons.join(' ')}`); + } + if (record?.missing_facts?.length) { + lines.push(`Missing facts: ${record.missing_facts.join(', ')}`); + } + if (record?.selection) { + lines.push(`Selection: mode=${record.selection.mode}, reason=${record.selection.reason}, followed_recommendation=${record.selection.followed_recommendation}`); + } + + if (value.receipt?.exists === false) lines.push('Hash valid: unavailable (receipt missing)'); + else if (value.status === 'invalid' || value.receipt?.valid === false) lines.push('Hash valid: false'); + else if (record?.hash) lines.push('Hash valid: true'); + else if (value.source === 'explicit-state') lines.push('Hash valid: not applicable (explicit state)'); + + if (value.receipt?.failures?.length) { + lines.push(`Receipt failures: ${value.receipt.failures.join('; ')}`); + } + return lines; +} + +function fail(message, exitCode) { + console.error(message); + process.exitCode = exitCode; +} + +function printHelp() { + console.log(`Usage: + ssf workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] [--json] + ssf workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] [--json] + ssf workflow show [--json]`); +} diff --git a/scripts/lib/sdd-overlay.mjs b/scripts/lib/sdd-overlay.mjs index 8ab6369..e63a709 100644 --- a/scripts/lib/sdd-overlay.mjs +++ b/scripts/lib/sdd-overlay.mjs @@ -21,6 +21,7 @@ export function getOverlayPaths(changeDir) { handoffs: join(root, 'handoffs'), executionPlan: join(root, 'execution-plan.json'), executionRecommendation: join(root, 'execution-recommendation.json'), + workflowSelection: join(root, 'workflow-selection.json'), reviews: join(root, 'reviews'), }; } diff --git a/scripts/lib/workflow-recommendation.mjs b/scripts/lib/workflow-recommendation.mjs new file mode 100644 index 0000000..fb40928 --- /dev/null +++ b/scripts/lib/workflow-recommendation.mjs @@ -0,0 +1,164 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { + existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, +} from 'node:fs'; +import { dirname } from 'node:path'; +import { getOverlayPaths } from './sdd-overlay.mjs'; + +export const WORKFLOW_MODES = Object.freeze(['full', 'hotfix', 'tweak']); + +const BOOLEAN_FACTS = ['config_doc_only', 'schema_api_change', 'new_module']; +const FACT_KEYS = ['task_count', 'file_count', ...BOOLEAN_FACTS, 'uncertainty']; + +export function normalizeWorkflowFacts(input = {}) { + return { + task_count: normalizeCount(input.task_count), + file_count: normalizeCount(input.file_count), + config_doc_only: normalizeEnum(input.config_doc_only, ['yes', 'no', 'unknown']), + schema_api_change: normalizeEnum(input.schema_api_change, ['yes', 'no', 'unknown']), + new_module: normalizeEnum(input.new_module, ['yes', 'no', 'unknown']), + uncertainty: normalizeEnum(input.uncertainty, ['low', 'high', 'unknown']), + }; +} + +export function recommendWorkflowPath(input = {}) { + const facts = normalizeWorkflowFacts(input); + const missing_facts = FACT_KEYS.filter((key) => facts[key] === null || facts[key] === 'unknown'); + const base = { available_modes: [...WORKFLOW_MODES], facts, missing_facts }; + + if (missing_facts.length) { + return { ...base, status: 'needs-input', recommendation: null }; + } + if (facts.schema_api_change === 'yes' || facts.new_module === 'yes' || facts.uncertainty === 'high') { + return ready(base, 'full', 'Risk or uncertainty requires the full workflow.'); + } + if (facts.config_doc_only === 'yes' && facts.task_count <= 4 && facts.file_count <= 4) { + return ready(base, 'tweak', 'Config/doc-only work is within the tweak thresholds.'); + } + if (facts.config_doc_only === 'no' && facts.task_count <= 2 && facts.file_count <= 2) { + return ready(base, 'hotfix', 'Bounded code work is within the hotfix thresholds.'); + } + return ready(base, 'full', 'The observed scope exceeds the fast-path thresholds.'); +} + +export function saveWorkflowRecommendation(changeDir, facts) { + const recommendation = recommendWorkflowPath(facts); + const record = withHash({ + schema_version: 1, + ...recommendation, + created_at: new Date().toISOString(), + selection: null, + }); + writeRecord(changeDir, record); + return record; +} + +export function readWorkflowSelection(changeDir) { + const path = getOverlayPaths(changeDir).workflowSelection; + if (!existsSync(path)) { + return { + exists: false, + valid: false, + record: null, + failures: ['workflow recommendation is missing'], + }; + } + try { + const record = JSON.parse(readFileSync(path, 'utf8')); + const valid = record.hash === hashRecord(record); + return { + exists: true, + valid, + record, + failures: valid ? [] : ['workflow recommendation hash mismatch'], + }; + } catch (error) { + return { + exists: true, + valid: false, + record: null, + failures: [error.message], + }; + } +} + +export function recordWorkflowSelection(changeDir, { mode, reason, confirmed, acknowledged }) { + const loaded = readWorkflowSelection(changeDir); + if (!loaded.valid) throw new Error(loaded.failures.join('; ')); + if (loaded.record.status !== 'ready' || !loaded.record.recommendation) { + throw new Error('workflow recommendation needs more input'); + } + if (!WORKFLOW_MODES.includes(mode)) throw new Error(`invalid workflow mode: ${mode}`); + if (confirmed !== true) throw new Error('workflow selection requires --confirm'); + if (!isSafeReason(reason)) { + throw new Error('workflow selection reason must be non-empty single-line text'); + } + const followed = mode === loaded.record.recommendation.mode; + if (!followed && acknowledged !== true) { + throw new Error('non-recommended workflow selection requires acknowledgement'); + } + const selected = withHash({ + ...withoutHash(loaded.record), + selection: { + mode, + reason, + followed_recommendation: followed, + acknowledged_non_recommendation: !followed && acknowledged === true, + selected_at: new Date().toISOString(), + }, + }); + writeRecord(changeDir, selected); + return selected; +} + +function ready(base, mode, reason) { + return { ...base, status: 'ready', recommendation: { mode, reasons: [reason] } }; +} + +function normalizeCount(value) { + if (value === null || value === undefined) return null; + if (!Number.isInteger(value) || value < 0) { + throw new Error('task_count and file_count must be non-negative integers'); + } + return value; +} + +function normalizeEnum(value, allowed) { + if (value === null || value === undefined) return 'unknown'; + if (!allowed.includes(value)) throw new Error(`invalid workflow fact value: ${value}`); + return value; +} + +function withoutHash(record) { + const { hash, ...content } = record; + return content; +} + +function stableJson(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; + return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`; +} + +function hashRecord(record) { + return `sha256:${createHash('sha256').update(stableJson(withoutHash(record))).digest('hex')}`; +} + +function withHash(content) { + const record = { ...content }; + return { ...record, hash: hashRecord(record) }; +} + +function writeRecord(changeDir, record) { + const target = getOverlayPaths(changeDir).workflowSelection; + mkdirSync(dirname(target), { recursive: true }); + const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`; + writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, 'utf8'); + renameSync(temporary, target); +} + +function isSafeReason(value) { + return typeof value === 'string' + && value.trim().length > 0 + && !/[\p{Cc}\p{Zl}\p{Zp}]/u.test(value); +} diff --git a/scripts/spec-superflow.mjs b/scripts/spec-superflow.mjs index 670f6b9..dec715d 100755 --- a/scripts/spec-superflow.mjs +++ b/scripts/spec-superflow.mjs @@ -22,6 +22,7 @@ const COMMANDS = { resume: () => import('./lib/cmd-resume.mjs'), switch: () => import('./lib/cmd-switch.mjs'), runtime: () => import('./lib/cmd-runtime.mjs'), + workflow: () => import('./lib/cmd-workflow.mjs'), 'install-cursor': () => import('./lib/cmd-install-cursor.mjs'), 'install-workbuddy': () => import('./lib/cmd-install-workbuddy.mjs'), 'install-cline': () => import('./lib/cmd-install-cline.mjs'), @@ -83,6 +84,12 @@ Commands: Recover an explicit change context without changing the shell runtime check-update Run a portable update check for canonical skills runtime infer Infer workflow mode without a plugin-root path + workflow recommend [--task-count ] [--file-count ] [--config-doc-only yes|no|unknown] [--schema-api-change yes|no|unknown] [--new-module yes|no|unknown] [--uncertainty low|high|unknown] + Persist observed intake facts and recommend full, hotfix, or tweak without selecting one + workflow select --mode full|hotfix|tweak --confirm --reason [--acknowledge-recommendation] + Persist a user-confirmed workflow choice after a ready recommendation + workflow show [--json] + Show the saved workflow recommendation or selection recovery state runtime guard ... Run a portable phase-transition guard runtime config ... Read effective configuration (writes are rejected) runtime asset read @@ -115,6 +122,8 @@ Examples: ssf state init changes/my-change/ ssf state check changes/my-change/ ssf state transition changes/my-change/ approved-for-build + ssf workflow recommend changes/fix-typo --task-count 1 --file-count 1 --config-doc-only no --schema-api-change no --new-module no --uncertainty low + ssf workflow select changes/fix-typo --mode hotfix --confirm --reason "bounded code fix" ssf state get changes/my-change/ batches_completed ssf checkpoint save changes/my-change/ --task 1.1 --next "Run focused tests" ssf checkpoint list changes/my-change/ diff --git a/skills/bug-investigator/SKILL.md b/skills/bug-investigator/SKILL.md index dc95d1a..efcfbb2 100644 --- a/skills/bug-investigator/SKILL.md +++ b/skills/bug-investigator/SKILL.md @@ -49,7 +49,7 @@ Scientific method: form a single hypothesis ("I think X is the root cause becaus ### DP-5: Debug Escalation (3+ Failures) -3+ failed fixes = architectural problem. Each fix revealing new problems elsewhere = wrong architecture. Record: `npx --yes --package spec-superflow@0.10.0 ssf state set dp_5_result `. Discuss with user before attempting more fixes. +3+ failed fixes = architectural problem. Each fix revealing new problems elsewhere = wrong architecture. Record: `npx --yes --package spec-superflow@0.11.0 ssf state set dp_5_result `. Discuss with user before attempting more fixes. ## Red Flags — Return to Phase 1 diff --git a/skills/build-executor/SKILL.md b/skills/build-executor/SKILL.md index f9bb7dd..b25933a 100644 --- a/skills/build-executor/SKILL.md +++ b/skills/build-executor/SKILL.md @@ -11,18 +11,18 @@ Controls the implementation phase. Uses `execution-contract.md` as the workflow Read: `execution-contract.md`, `tasks.md`, relevant `specs/`, relevant `design.md`. (Skip contract/spec requirements when workflow is `tweak`.) -Check workflow mode first: `npx --yes --package spec-superflow@0.10.0 ssf state get workflow`. If `tweak` → direct edit mode. If `hotfix` or `full` → standard contract-first discipline. +Check workflow mode first: `npx --yes --package spec-superflow@0.11.0 ssf state get workflow`. If `tweak` → direct edit mode. If `hotfix` or `full` → standard contract-first discipline. Branch/worktree preflight before ANY implementation edit (mandatory — do not skip): 1. Run the isolation check: ```bash - npx --yes --package spec-superflow@0.10.0 ssf isolate + npx --yes --package spec-superflow@0.11.0 ssf isolate ``` This script enforces git isolation: if you are on `main`/`master` it creates a git worktree (preferred) or a new branch, and exits non-zero if it cannot and you have not approved `--force`. -2. If `npx --yes --package spec-superflow@0.10.0 ssf isolate` exits non-zero: STOP. Do not edit `main`/`master` in place. - Ask the user for explicit approval (and re-run with `npx --yes --package spec-superflow@0.10.0 ssf isolate --force` +2. If `npx --yes --package spec-superflow@0.11.0 ssf isolate` exits non-zero: STOP. Do not edit `main`/`master` in place. + Ask the user for explicit approval (and re-run with `npx --yes --package spec-superflow@0.11.0 ssf isolate --force` only after they approve). 3. If it succeeds, report the chosen branch/worktree and make all implementation edits there. @@ -48,15 +48,15 @@ Return to `specifying` or `bridging` if: new behavior appears, interfaces change For `full`/`hotfix`, generate proposed waves from the approved contract, then use the recommendation as a decision aid rather than silently defaulting a mode: ```bash -npx --yes --package spec-superflow@0.10.0 ssf execution recommend \ +npx --yes --package spec-superflow@0.11.0 ssf execution recommend \ --wave ::[:] --json # Show every available mode, the observed facts, and the recommendation to the user. # The command writes a receipt tied to the artifacts, contract, and waves. After the user chooses, record that explicit confirmation: -npx --yes --package spec-superflow@0.10.0 ssf execution plan \ +npx --yes --package spec-superflow@0.11.0 ssf execution plan \ --mode --confirm --reason "user-selected execution mode" \ --wave ::[:] # Add --acknowledge-recommendation when the selection differs from the recommendation. -npx --yes --package spec-superflow@0.10.0 ssf execution show --json +npx --yes --package spec-superflow@0.11.0 ssf execution show --json ``` The optional fourth `--wave` segment names prerequisite wave IDs. `execution show --json` reports `current`, plus each wave's `depends_on`, `receipt`, `blockers`, `retryable`, and `eligible` status. A wave with `retryable: true` has a current `fail` receipt and is eligible only for its focused repair and re-review; its dependents remain blocked until its replacement `pass` receipt. Report the saved plan revision, selected mode, ordered waves, dependencies, and whether every `parallel` wave can actually be dispatched concurrently on the current platform. If concurrency is unavailable, state the capability and reason plainly; retain the planned `parallel` strategy and do not silently execute it as a serial or Batch Inline plan. @@ -69,7 +69,7 @@ The recommendation uses task count, configured `execution.inlineThreshold`, and | **Inline** | Recommended for a single sequential task; always available for a user-confirmed choice | | **Batch Inline** | Recommended for a bounded sequential batch; it remains serial and is never presented as parallel | -Do not transition to `executing` until `execution show` reports `current: true` and the phase guard passes. A revised plan must repeat `npx --yes --package spec-superflow@0.10.0 ssf execution recommend` and use `npx --yes --package spec-superflow@0.10.0 ssf execution revise --confirm`; it creates a new revision and invalidates receipts from the prior revision. +Do not transition to `executing` until `execution show` reports `current: true` and the phase guard passes. A revised plan must repeat `npx --yes --package spec-superflow@0.11.0 ssf execution recommend` and use `npx --yes --package spec-superflow@0.11.0 ssf execution revise --confirm`; it creates a new revision and invalidates receipts from the prior revision. ## Batch Inline Execution @@ -84,21 +84,21 @@ Boundaries: if any task touches >1 module, involves schema/API/config changes, o For full/hotfix by default. Dispatch according to the persisted plan, review each planned wave, and run a final broad review after all waves. ### Planned-Wave Loop -1. Read the current plan with `npx --yes --package spec-superflow@0.10.0 ssf execution show --json`; only waves shown with `current: true` and `eligible: true` may start. A `retryable: true` wave may only be repaired and re-reviewed; do not dispatch its dependents until its replacement receipt is `pass`. The CLI encodes dependencies in `--wave ::[:]` and rejects a review receipt for a wave whose prerequisites lack current `pass` receipts. +1. Read the current plan with `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`; only waves shown with `current: true` and `eligible: true` may start. A `retryable: true` wave may only be repaired and re-reviewed; do not dispatch its dependents until its replacement receipt is `pass`. The CLI encodes dependencies in `--wave ::[:]` and rejects a review receipt for a wave whose prerequisites lack current `pass` receipts. 2. A `parallel` wave may dispatch independent tasks simultaneously only when the platform supports concurrent dispatch. If it does not, disclose the unavailable capability and execute the same wave one task at a time without changing its stored strategy. 3. A `serial` wave dispatches one task at a time in listed order. 4. After every wave, write a non-empty persisted regular-file review report (separate from the implementer's report), then record exactly one receipt that names that review report: ```bash - npx --yes --package spec-superflow@0.10.0 ssf execution review \ + npx --yes --package spec-superflow@0.11.0 ssf execution review \ --wave --base --head --report --verdict ``` Do not begin a dependent wave until its predecessor receipt is `pass`. 5. Critical/Important findings require a `fail` receipt, a focused repair, re-review, then a replacement `pass` receipt. Never advance or close with a missing or failed receipt. ### Per-Task Loop -1. **Dispatch implementer**: Load the template with `npx --yes --package spec-superflow@0.10.0 ssf runtime asset read skills/build-executor/implementer-prompt.md`. Extract task brief with `scripts/task-brief PLAN_FILE N`. Include: where task fits, brief path, interfaces from prior tasks, report file path. +1. **Dispatch implementer**: Load the template with `npx --yes --package spec-superflow@0.11.0 ssf runtime asset read skills/build-executor/implementer-prompt.md`. Extract task brief with `scripts/task-brief PLAN_FILE N`. Include: where task fits, brief path, interfaces from prior tasks, report file path. 2. **Handle response**: DONE → generate review package + dispatch reviewer. DONE_WITH_CONCERNS → assess. NEEDS_CONTEXT → provide context. BLOCKED → re-dispatch with better model or escalate. -3. **Review**: Load `npx --yes --package spec-superflow@0.10.0 ssf runtime asset read skills/build-executor/task-reviewer-prompt.md`. Reviewer returns spec compliance + code quality verdicts with the wave ID, git range, report path, and `pass`/`fail` receipt command. +3. **Review**: Load `npx --yes --package spec-superflow@0.11.0 ssf runtime asset read skills/build-executor/task-reviewer-prompt.md`. Reviewer returns spec compliance + code quality verdicts with the wave ID, git range, report path, and `pass`/`fail` receipt command. 4. **Fix**: If Critical or Important issues, write the `fail` receipt, dispatch fix subagent, re-review, and write the replacement `pass` receipt. 5. **Mark complete**: Append to `.superpowers/sdd/progress.md`: `Task N: complete (commits .., review clean)` @@ -106,7 +106,7 @@ For full/hotfix by default. Dispatch according to the persisted plan, review eac Use the configured profile that matches the task role. Resolve it before dispatch: ```bash -npx --yes --package spec-superflow@0.10.0 ssf runtime config --resolve-model +npx --yes --package spec-superflow@0.11.0 ssf runtime config --resolve-model ``` | Profile | Role | @@ -119,11 +119,11 @@ npx --yes --package spec-superflow@0.10.0 ssf runtime config --resolve-model batches_completed `. +Track in `.superpowers/sdd/progress.md`. Check for existing ledger — completed tasks are done. After each batch: `npx --yes --package spec-superflow@0.11.0 ssf state set batches_completed `. ## Inline Execution Mode -Only after a user-confirmed `inline` selection is recorded by `npx --yes --package spec-superflow@0.10.0 ssf execution plan --confirm`; a non-recommended selection also records `--acknowledge-recommendation`. Executes in the current session and still writes one review receipt per planned wave. +Only after a user-confirmed `inline` selection is recorded by `npx --yes --package spec-superflow@0.11.0 ssf execution plan --confirm`; a non-recommended selection also records `--acknowledge-recommendation`. Executes in the current session and still writes one review receipt per planned wave. Per-task: extract brief → write failing test → confirm failure → implement → confirm green → checkpoint review (done-when criteria, SHALL/MUST verification) → commit → save a task-level recovery checkpoint when another task remains → append to progress ledger. @@ -131,7 +131,7 @@ After a task is committed and reviewed, when another task remains, save the recovery context with real evidence: ```bash -npx --yes --package spec-superflow@0.10.0 ssf checkpoint save \ +npx --yes --package spec-superflow@0.11.0 ssf checkpoint save \ --task --next "" --completed "" \ --verification "" --review "" \ --risk "" --commit-start --commit-end @@ -139,7 +139,7 @@ npx --yes --package spec-superflow@0.10.0 ssf checkpoint save \ This augments `.superpowers/sdd/progress.md`; it does not replace the progress ledger or add a new core workflow state. Do not claim a checkpoint is current -when `npx --yes --package spec-superflow@0.10.0 ssf checkpoint list` reports it as stale. +when `npx --yes --package spec-superflow@0.11.0 ssf checkpoint list` reports it as stale. If task hits BLOCKED (3+ fix failures or changes outside declared scope), escalate to SDD. @@ -149,8 +149,8 @@ Skip TDD. Apply changes directly. Verify file integrity (exists, non-empty, vali ## DP Records -DP-4 is written by `npx --yes --package spec-superflow@0.10.0 ssf execution plan`; do not write it with raw `state set`. -DP-5 (debug escalation): `npx --yes --package spec-superflow@0.10.0 ssf state set dp_5_result ""` + timestamp. +DP-4 is written by `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; do not write it with raw `state set`. +DP-5 (debug escalation): `npx --yes --package spec-superflow@0.11.0 ssf state set dp_5_result ""` + timestamp. ## Completion Standard diff --git a/skills/build-executor/implementer-prompt.md b/skills/build-executor/implementer-prompt.md index e54f8ff..6b3ca43 100644 --- a/skills/build-executor/implementer-prompt.md +++ b/skills/build-executor/implementer-prompt.md @@ -22,7 +22,7 @@ Subagent (general-purpose): ## Planned Wave You are assigned to planned wave [WAVE_ID] with strategy [WAVE_STRATEGY]. - Read `npx --yes --package spec-superflow@0.10.0 ssf execution show --json` before editing. Do not start + Read `npx --yes --package spec-superflow@0.11.0 ssf execution show --json` before editing. Do not start unless all declared dependencies have `pass` review receipts. A `parallel` label permits concurrent dispatch only when the controller confirms the platform supports it; never change the saved wave strategy yourself. diff --git a/skills/build-executor/task-reviewer-prompt.md b/skills/build-executor/task-reviewer-prompt.md index 25d5817..7885e3a 100644 --- a/skills/build-executor/task-reviewer-prompt.md +++ b/skills/build-executor/task-reviewer-prompt.md @@ -146,7 +146,7 @@ Subagent (general-purpose): command for the controller: ```bash - npx --yes --package spec-superflow@0.10.0 ssf execution review --wave [WAVE_ID] --base [BASE_SHA] --head [HEAD_SHA] --report [REVIEW_REPORT_FILE] --verdict + npx --yes --package spec-superflow@0.11.0 ssf execution review --wave [WAVE_ID] --base [BASE_SHA] --head [HEAD_SHA] --report [REVIEW_REPORT_FILE] --verdict ``` Use `fail` for any Critical/Important finding. A repair must be re-reviewed diff --git a/skills/code-reviewer/SKILL.md b/skills/code-reviewer/SKILL.md index 0024031..3089635 100644 --- a/skills/code-reviewer/SKILL.md +++ b/skills/code-reviewer/SKILL.md @@ -16,7 +16,7 @@ Two responsibilities: requesting review (dispatching a reviewer subagent) and re 1. Get SHAs: `BASE_SHA=$(git rev-parse HEAD~1)` and `HEAD_SHA=$(git rev-parse HEAD)` 2. Dispatch `general-purpose` subagent using template at `skills/code-reviewer/code-reviewer-prompt.md` 3. Fill placeholders: `[DESCRIPTION]` (what was built), `[PLAN_OR_REQUIREMENTS]` (contract/spec reference), `[BASE_SHA]`, `[HEAD_SHA]`, `[WAVE_ID]`, and a distinct `[REVIEW_REPORT_FILE]`. -4. Require the reviewer to write a non-empty persisted review report at `[REVIEW_REPORT_FILE]`, then record that exact path in the wave receipt: `npx --yes --package spec-superflow@0.10.0 ssf execution review --wave --base --head --report --verdict `. +4. Require the reviewer to write a non-empty persisted review report at `[REVIEW_REPORT_FILE]`, then record that exact path in the wave receipt: `npx --yes --package spec-superflow@0.11.0 ssf execution review --wave --base --head --report --verdict `. 5. Act on feedback: Critical/Important findings require a `fail` receipt, focused repair, re-review, and replacement `pass` receipt before a dependent wave or closing can proceed. Note Minor for later, push back with reasoning if reviewer is wrong. ### Minimality And Scope @@ -72,7 +72,7 @@ Suggestion breaks existing functionality, reviewer lacks context, violates YAGNI | Performative agreement | State requirement or just act | | Blind implementation | Verify against codebase first | | Batch without testing | One at a time, test each | -| Proceeding without a wave receipt | Record `pass`/`fail` via `npx --yes --package spec-superflow@0.10.0 ssf execution review` before the next dependent wave | +| Proceeding without a wave receipt | Record `pass`/`fail` via `npx --yes --package spec-superflow@0.11.0 ssf execution review` before the next dependent wave | | Assuming reviewer is right | Check if breaks things | | Avoiding pushback | Technical correctness > comfort | | Partial implementation | Clarify all items first | diff --git a/skills/code-reviewer/code-reviewer-prompt.md b/skills/code-reviewer/code-reviewer-prompt.md index 9e54904..5723642 100644 --- a/skills/code-reviewer/code-reviewer-prompt.md +++ b/skills/code-reviewer/code-reviewer-prompt.md @@ -93,7 +93,7 @@ Subagent (general-purpose): report path. End with the exact receipt command: ```bash - npx --yes --package spec-superflow@0.10.0 ssf execution review --wave [WAVE_ID] --base [BASE_SHA] --head [HEAD_SHA] --report [REVIEW_REPORT_FILE] --verdict + npx --yes --package spec-superflow@0.11.0 ssf execution review --wave [WAVE_ID] --base [BASE_SHA] --head [HEAD_SHA] --report [REVIEW_REPORT_FILE] --verdict ``` Use `fail` when any Critical or Important finding remains. A repair needs diff --git a/skills/contract-builder/SKILL.md b/skills/contract-builder/SKILL.md index 36dcfa4..50a3f27 100644 --- a/skills/contract-builder/SKILL.md +++ b/skills/contract-builder/SKILL.md @@ -5,11 +5,11 @@ description: Convert approved planning artifacts into an execution contract. Inv # Contract Builder -Converts planning artifacts into a single execution handshake: `execution-contract.md`. Load the baseline with `npx --yes --package spec-superflow@0.10.0 ssf runtime asset read templates/execution-contract.md`. +Converts planning artifacts into a single execution handshake: `execution-contract.md`. Load the baseline with `npx --yes --package spec-superflow@0.11.0 ssf runtime asset read templates/execution-contract.md`. Read before generating: `.spec-superflow.yaml` (especially `dp_0_decisions`), `proposal.md`, `specs/`, `design.md`, `tasks.md`, then load -`docs/artifact-contract.md` with `npx --yes --package spec-superflow@0.10.0 ssf runtime asset read docs/artifact-contract.md`. +`docs/artifact-contract.md` with `npx --yes --package spec-superflow@0.11.0 ssf runtime asset read docs/artifact-contract.md`. ## Artifact Language @@ -47,8 +47,8 @@ Must make obvious: approved behavior, out-of-scope, constraints, batches, test o After drafting: summarize handoff rules, identify ambiguity, flag unmapped requirements, ask user to approve explicitly. After approval: ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_3_result "approved: " -npx --yes --package spec-superflow@0.10.0 ssf state set dp_3_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_3_result "approved: " +npx --yes --package spec-superflow@0.11.0 ssf state set dp_3_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` DP-3 is a hard gate — no implementation without this record. @@ -69,9 +69,9 @@ Generate minimal contract: Intent Lock (one sentence), Task List (numbered), App ## Post-Generation -Run `npx --yes --package spec-superflow@0.10.0 ssf state init ` to create `.spec-superflow.yaml` with hashes. +Run `npx --yes --package spec-superflow@0.11.0 ssf state init ` to create `.spec-superflow.yaml` with hashes. -For hotfix, after writing the minimal contract, run `npx --yes --package spec-superflow@0.10.0 ssf state init ` or `npx --yes --package spec-superflow@0.10.0 ssf state rebuild ` so `contract_hash` is recorded. DP-3 remains mandatory before build. +For hotfix, after writing the minimal contract, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` or `npx --yes --package spec-superflow@0.11.0 ssf state rebuild ` so `contract_hash` is recorded. DP-3 remains mandatory before build. ## Exception Handling diff --git a/skills/need-explorer/SKILL.md b/skills/need-explorer/SKILL.md index 2074d2c..85b9288 100644 --- a/skills/need-explorer/SKILL.md +++ b/skills/need-explorer/SKILL.md @@ -37,8 +37,8 @@ Restate what you heard: "Here's what I'm hearing: [problem, scope, non-goals, su After user confirms the summary: ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_1_result "confirmed: " -npx --yes --package spec-superflow@0.10.0 ssf state set dp_1_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_1_result "confirmed: " +npx --yes --package spec-superflow@0.11.0 ssf state set dp_1_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` DP-1 confirms scope, non-goals, and success criteria before artifact creation. diff --git a/skills/release-archivist/SKILL.md b/skills/release-archivist/SKILL.md index dd2481b..ee1fa85 100644 --- a/skills/release-archivist/SKILL.md +++ b/skills/release-archivist/SKILL.md @@ -12,7 +12,7 @@ the final transition to `closing`. ## Execution-State Guard Before verification, `ssf audit`, any DP state write, or delta-spec merge, run -`npx --yes --package spec-superflow@0.10.0 ssf state get state`. +`npx --yes --package spec-superflow@0.11.0 ssf state get state`. Continue only when the persisted state is exactly `executing`. If it is `closing` → STOP: "Closing is terminal; release, audit, and archival work were completed before this transition." For any other state, or if the state cannot @@ -71,12 +71,12 @@ Check for files modified outside scope fence, new dependencies not in design. Un - Scope added without artifact updates? - Unresolved blockers or known risks? - Delta specs exist that need merging? -- Run `npx --yes --package spec-superflow@0.10.0 ssf audit ` — include `decision-point-audit.md` in archive +- Run `npx --yes --package spec-superflow@0.11.0 ssf audit ` — include `decision-point-audit.md` in archive ### DP-6 (Verification Outcome) ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_6_result ": " -npx --yes --package spec-superflow@0.10.0 ssf state set dp_6_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_6_result ": " +npx --yes --package spec-superflow@0.11.0 ssf state set dp_6_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` If FAIL, do NOT proceed to DP-7. Route back or ask about abandonment. @@ -84,13 +84,13 @@ After recording a PASS outcome, also record it as the verification gate so the `executing → closing` transition is allowed (the guard accepts either `test_result: pass` or a `dp_6_result` starting with `pass`): ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set test_result pass +npx --yes --package spec-superflow@0.11.0 ssf state set test_result pass ``` ### DP-7 (Archive Confirmation) ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_7_result "confirmed: " -npx --yes --package spec-superflow@0.10.0 ssf state set dp_7_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_7_result "confirmed: " +npx --yes --package spec-superflow@0.11.0 ssf state set dp_7_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` Verify DP-0 through DP-6 are recorded before DP-7. @@ -103,7 +103,7 @@ If implementation diverged from the contract, return to `bridging` before closur Complete every release, delta-spec synchronization, and audit action while the state remains `executing`. If delta specs exist, invoke `spec-merger` and resolve its outcome before the final `executing → closing` transition. Then -run `npx --yes --package spec-superflow@0.10.0 ssf state transition closing`. +run `npx --yes --package spec-superflow@0.11.0 ssf state transition closing`. `executing → closing` is the final action: once it succeeds, select no next skill and run no recovery scans. @@ -114,6 +114,6 @@ Verify files exist and are non-empty, run `node --check` on code files, skip 5-s ## Exception Handling - **Parse failures**: Report exact file and section -- **Missing files**: If audit can't generate, run `npx --yes --package spec-superflow@0.10.0 ssf audit` manually +- **Missing files**: If audit can't generate, run `npx --yes --package spec-superflow@0.11.0 ssf audit` manually - **User interruption**: Re-run verification from the beginning on resume - **DP gaps**: Flag missing DPs during DP-6; ask user whether to proceed or return diff --git a/skills/spec-merger/SKILL.md b/skills/spec-merger/SKILL.md index aa32e3f..5cb4e01 100644 --- a/skills/spec-merger/SKILL.md +++ b/skills/spec-merger/SKILL.md @@ -10,7 +10,7 @@ Before the final `executing → closing` transition, delta specs (ADDED/MODIFIED ## Execution-State Guard Before `ssf sync` or any other write, run -`npx --yes --package spec-superflow@0.10.0 ssf state get state`. +`npx --yes --package spec-superflow@0.11.0 ssf state get state`. Continue only when the persisted state is exactly `executing`. If it is `closing` → STOP: "Closing is terminal. Do not route this change to spec-merger; synchronization belongs before the final executing → closing transition." For @@ -20,7 +20,7 @@ any other state, or if the state cannot be read → STOP and route through ## Pre-Flight Checks ### Conflict Detection -Run `npx --yes --package spec-superflow@0.10.0 ssf sync `. If conflicts are detected (same requirement modified by multiple changes), present the conflict list to the user for resolution order. +Run `npx --yes --package spec-superflow@0.11.0 ssf sync `. If conflicts are detected (same requirement modified by multiple changes), present the conflict list to the user for resolution order. ## Sync Process @@ -62,7 +62,7 @@ Output sync report table: Capability, ADDED/MODIFIED/REMOVED/RENAMED counts, Sta 2. Change folder (including deltas) remains for traceability. 3. Record that merging is complete so the `executing → closing` guard allows closure: ```bash - npx --yes --package spec-superflow@0.10.0 ssf state set spec_merged true + npx --yes --package spec-superflow@0.11.0 ssf state set spec_merged true ``` (If the change had no delta sections, still set `spec_merged true` — there was nothing to merge.) diff --git a/skills/spec-writer/SKILL.md b/skills/spec-writer/SKILL.md index 0032623..22fd0ad 100644 --- a/skills/spec-writer/SKILL.md +++ b/skills/spec-writer/SKILL.md @@ -13,7 +13,7 @@ Read `.spec-superflow.yaml` (especially `dp_0_decisions`, `dp_0_confirmed`) and ## Config Check -Run: `npx --yes --package spec-superflow@0.10.0 ssf runtime config --get artifacts.order` — generate in configured order (default: proposal → specs → design → tasks). Run with `artifacts.skip` — skip any listed artifacts. +Run: `npx --yes --package spec-superflow@0.11.0 ssf runtime config --get artifacts.order` — generate in configured order (default: proposal → specs → design → tasks). Run with `artifacts.skip` — skip any listed artifacts. ## Artifact Roles @@ -73,8 +73,8 @@ Generate one at a time. Confirm each before next. This prevents scope drift — Present summary of all 4 artifacts (2-3 sentences each). Ask user for adjustments. After approval: ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_2_result "approved: " -npx --yes --package spec-superflow@0.10.0 ssf state set dp_2_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_2_result "approved: " +npx --yes --package spec-superflow@0.11.0 ssf state set dp_2_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` ## Handoff Rule diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index 60ec359..7f67e2a 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -15,7 +15,7 @@ Do NOT invoke for: general coding tasks outside spec-superflow changes, casual q ## States -`exploring` → `specifying` → `bridging` → `approved-for-build` → `executing` → `closing`, with `debugging` side-path from `executing`, and `abandoned` as terminal. If a transition is ambiguous, run `npx --yes --package spec-superflow@0.10.0 ssf runtime asset read docs/state-machine.md`. +`exploring` → `specifying` → `bridging` → `approved-for-build` → `executing` → `closing`, with `debugging` side-path from `executing`, and `abandoned` as terminal. If a transition is ambiguous, run `npx --yes --package spec-superflow@0.11.0 ssf runtime asset read docs/state-machine.md`. ## Terminal-State Short Circuit @@ -27,22 +27,24 @@ scan, or `release-archivist`; do not resume, hand off, or route any more work. ## Initialization -1. **Update check**: Run `npx --yes --package spec-superflow@0.10.0 ssf runtime check-update`. Exit 0 → continue. Exit 1 → non-blocking upgrade reminder. Exit 2 → skip. +1. **Update check**: Run `npx --yes --package spec-superflow@0.11.0 ssf runtime check-update`. Exit 0 → continue. Exit 1 → non-blocking upgrade reminder. Exit 2 → skip. 2. **Inspect change folder**: Check for `proposal.md`, `specs/`, `design.md`, `tasks.md`, `execution-contract.md`. Answer: Is the change fuzzy? Artifacts missing/unstable? Contract exist? User approved contract? Execution in progress or blocked? In verification/wrap-up? ## Overlay Recovery Scan -3. **Overlay recovery scan**: Run `npx --yes --package spec-superflow@0.10.0 ssf handoff list --json` and `npx --yes --package spec-superflow@0.10.0 ssf checkpoint list --json`. A `result-ready` handoff requires explicit review and `npx --yes --package spec-superflow@0.10.0 ssf handoff resolve` before resuming the affected work. An `active` handoff is non-blocking side work. Show a non-stale checkpoint as recovery context; show a stale checkpoint only as historical evidence. +3. **Overlay recovery scan**: Run `npx --yes --package spec-superflow@0.11.0 ssf handoff list --json` and `npx --yes --package spec-superflow@0.11.0 ssf checkpoint list --json`. A `result-ready` handoff requires explicit review and `npx --yes --package spec-superflow@0.11.0 ssf handoff resolve` before resuming the affected work. An `active` handoff is non-blocking side work. Show a non-stale checkpoint as recovery context; show a stale checkpoint only as historical evidence. ## Execution-Control Recovery Scan -4. **Execution-control recovery scan**: For `approved-for-build`, `executing`, or `debugging`, run `npx --yes --package spec-superflow@0.10.0 ssf execution show --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave; report plan revision, mode, next eligible wave, and every wave's receipt/blockers. A missing, invalid, or stale plan blocks implementation and routes to `build-executor`; do not infer progress from chat history. +4. **Execution-control recovery scan**: For `approved-for-build`, `executing`, or `debugging`, run `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave; report plan revision, mode, next eligible wave, and every wave's receipt/blockers. A missing, invalid, or stale plan blocks implementation and routes to `build-executor`; do not infer progress from chat history. ## DP-0: User Confirmation Gate -Run DP-0 when: change folder doesn't exist, planning artifacts missing/empty, or `dp_0_confirmed` ≠ `true`. Skip if `dp_0_confirmed` is `true`. - -Ask: change name + one-sentence intent, known constraints, related optimizations (include or stay focused?), communication preference (ask per decision or draft for review). +Run DP-0 when: change folder doesn't exist, planning artifacts are +missing/empty, `dp_0_confirmed` is not `true`, or a legacy change still has an +`auto`/empty workflow. Resolve the artifact language first, then complete the +workflow path intake. Do not set `dp_0_confirmed=true` while path facts or the +user's path choice are still missing. ### Artifact Language Resolution @@ -63,42 +65,76 @@ but this field is absent, resolve and append it before routing to `spec-writer`. All later planning skills reuse this field so one change does not switch languages without an explicit user request. -After confirmation: +### Workflow Path Intake (Mode Detection) + +Workflow path selection is a DP-0 intake decision. It selects the planning path +(`full`, `hotfix`, or `tweak`); it is separate from DP-4, which later selects +the execution mode (`Inline`, `Batch Inline`, or `SDD`). It does not add a +state or cause a phase transition. + +1. Obtain the change name and one-sentence intent before any state-dependent + command. Validate the change name as one non-empty relative path segment + (not `.` or `..`, with no `/` or `\\`), resolve the change dir as `/changes/`, and reject any normalized path that + escapes the project's `changes/` directory. +2. If the state file is absent or `dp_0_confirmed` is `false`/null, run `npx --yes --package spec-superflow@0.11.0 ssf state init ` before `show`; initialization must leave DP-0 unconfirmed. +3. Read `state.workflow`. An explicit workflow `full`/`hotfix`/`tweak` wins; + report it and skip the automatic recommendation flow. +4. For `auto`/`null`/unset, run `npx --yes --package spec-superflow@0.11.0 ssf workflow show --json` before collecting or changing any facts. A missing receipt is represented as `needs-input` with all six fixed facts in `missing_facts`. +5. If the response is `needs-input`, ask only for `missing_facts`; do not ask + for any fact not listed by the receipt. Do not invent facts from missing + artifacts and do not default the path to `full`. +6. Run `npx --yes --package spec-superflow@0.11.0 ssf workflow recommend ...` once with one complete fact snapshot. +7. Show the user `Observed`, `Available`, `Recommended`, and `Why`. A + recommendation is advice only: never persist it as the workflow selection. +8. Obtain the user's explicit path choice, then run + `npx --yes --package spec-superflow@0.11.0 ssf workflow select --mode --confirm --reason ""`. +9. Add `--acknowledge-recommendation` only after the user chooses a + non-recommended path. Report the persisted receipt and DP-0 audit summary. +10. If `show` reports `selection-pending`, explain that its signed receipt was + written before the state update and safely repeat the same explicit `select` + command. Do not overwrite an explicit mode unless the user asks. +11. Keep `npx --yes --package spec-superflow@0.11.0 ssf runtime infer ` only for legacy artifact inference and validation compatibility; it cannot replace user selection at intake. + +### Confirm DP-0 + +Only after an explicit workflow path is available, ask for the remaining DP-0 +decisions: change name and one-sentence intent, known constraints, related +optimizations (include or stay focused?), and communication preference (ask per +decision or draft for review). Confirm one combined summary containing those +decisions, the resolved `artifact_language`, and the persisted workflow path +plus recommendation-alignment summary. Preserve existing scope, constraints, +and language entries; never replace them with the path summary alone. + +After that combined confirmation: ```bash -npx --yes --package spec-superflow@0.10.0 ssf state set dp_0_decisions "" -npx --yes --package spec-superflow@0.10.0 ssf state set dp_0_result confirmed -npx --yes --package spec-superflow@0.10.0 ssf state set dp_0_confirmed true -npx --yes --package spec-superflow@0.10.0 ssf state set dp_0_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) +npx --yes --package spec-superflow@0.11.0 ssf state set dp_0_decisions "" +npx --yes --package spec-superflow@0.11.0 ssf state set dp_0_result confirmed +npx --yes --package spec-superflow@0.11.0 ssf state set dp_0_confirmed true +npx --yes --package spec-superflow@0.11.0 ssf state set dp_0_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ) ``` Config-aware routing: check `artifacts.order`, `artifacts.skip`, and `execution.defaultLanguage` from project config. -## Mode Detection - -If workflow is `auto`/`null`/unset: run `npx --yes --package spec-superflow@0.10.0 ssf runtime infer `. Inference: **hotfix** (≤2 tasks, ≤2 files, no schema/API/new modules), **tweak** (≤4 tasks, config/doc only), **full** (anything larger). Persist with `npx --yes --package spec-superflow@0.10.0 ssf state set workflow `. - -Validate mode against artifact content. If hotfix/tweak criteria not met → upgrade to `full` and output reason. Don't overwrite explicit mode unless user asks. - ## Routing Rules ### Route to need-explorer Change is fuzzy, scope unclear, comparing options, no stable change name. ### Route to spec-writer -Guard: `npx --yes --package spec-superflow@0.10.0 ssf runtime guard check exploring specifying --json` → fail = BLOCK. User knows what they want, artifacts missing/incomplete. +Guard: `npx --yes --package spec-superflow@0.11.0 ssf runtime guard check exploring specifying --json` → fail = BLOCK. User knows what they want, artifacts missing/incomplete. ### Route to contract-builder Guard: `... check specifying bridging --json` → fail = BLOCK. Artifacts exist, implementation requested, contract missing/stale. Include `DP-3: 契约批准`. ### Route to build-executor -Contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package spec-superflow@0.10.0 ssf execution recommend [--wave ...]`, show the user every available mode plus evidence and the recommendation, then obtain a clear selection. The command saves a current receipt; before the first implementation edit, `build-executor` must run `npx --yes --package spec-superflow@0.10.0 ssf execution plan --mode --confirm ...` (and `--acknowledge-recommendation` when the selected mode differs from the recommendation) using matching artifacts, contract, and waves, then `npx --yes --package spec-superflow@0.10.0 ssf execution show --json`; report the saved revision, selected mode, recommendation alignment, ordered waves, and actual concurrent-dispatch capability. A revision must repeat recommend and confirmation. Do not transition to `executing` until `show` reports `current: true`; then run `... check approved-for-build executing --json` → fail = BLOCK. +Contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package spec-superflow@0.11.0 ssf execution recommend [--wave ...]`, show the user every available mode plus evidence and the recommendation, then obtain a clear selection. The command saves a current receipt; before the first implementation edit, `build-executor` must run `npx --yes --package spec-superflow@0.11.0 ssf execution plan --mode --confirm ...` (and `--acknowledge-recommendation` when the selected mode differs from the recommendation) using matching artifacts, contract, and waves, then `npx --yes --package spec-superflow@0.11.0 ssf execution show --json`; report the saved revision, selected mode, recommendation alignment, ordered waves, and actual concurrent-dispatch capability. A revision must repeat recommend and confirmation. Do not transition to `executing` until `show` reports `current: true`; then run `... check approved-for-build executing --json` → fail = BLOCK. ### Route to bug-investigator Execution hit blockage: test failure, unexpected behavior, build error, task cannot proceed. After debugging, route back to build-executor. ### Route to code-reviewer -The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package spec-superflow@0.10.0 ssf execution review --wave --base --head --report --verdict ` receipt before any dependent wave or closing transition. +The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package spec-superflow@0.11.0 ssf execution review --wave --base --head --report --verdict ` receipt before any dependent wave or closing transition. ### Route to release-archivist Only while the current state is `executing`: implementation is complete and verification is ready. Run the guard `... check executing closing --json` → fail = BLOCK. `release-archivist` completes verification, audit, and any required delta merge before the final transition. Include `DP-7: 归档确认`. @@ -117,10 +153,10 @@ uncertainty. Do not create a prototype handoff or enter a prototype worktree until the user confirms. After confirmation: ```bash -npx --yes --package spec-superflow@0.10.0 ssf handoff create \ +npx --yes --package spec-superflow@0.11.0 ssf handoff create \ --type prototype --objective "" \ --expected-output "" --acceptance "" -npx --yes --package spec-superflow@0.10.0 ssf isolate prototype- +npx --yes --package spec-superflow@0.11.0 ssf isolate prototype- ``` Never suggest or enter this route automatically for backend, CLI, configuration, @@ -131,7 +167,7 @@ work. - **Hotfix**: Route to contract-builder (minimal), skip need-explorer + spec-writer, guard check `exploring bridging --workflow hotfix`, then `bridging -> approved-for-build`, after DP-3 → build-executor (recommend, show, and confirm an execution mode), after → release-archivist (lightweight). Hotfix may skip `proposal.md`, `design.md`, `tasks.md`, and `specs/`, but it still requires a fresh minimal `execution-contract.md`, DP-3 approval, and a current execution plan before build - **Tweak**: Route to build-executor (direct edit), skip need-explorer + spec-writer + contract-builder, guard check `exploring approved-for-build --workflow tweak`, after → release-archivist (lightweight) -Post-transition: 💡 `npx --yes --package spec-superflow@0.10.0 ssf inject ` to update phase-guard artifacts. +Post-transition: 💡 `npx --yes --package spec-superflow@0.11.0 ssf inject ` to update phase-guard artifacts. ## Staleness Detection @@ -146,7 +182,7 @@ Use content inspection, not timestamps. ## Guardrails - No implementation before planning artifacts or contract exist -- No implementation for full/hotfix without a current `npx --yes --package spec-superflow@0.10.0 ssf execution plan`; no state transition based on an unverified DP-4 string +- No implementation for full/hotfix without a current `npx --yes --package spec-superflow@0.11.0 ssf execution plan`; no state transition based on an unverified DP-4 string - No "continue" without state inspection - No implementation past stale contract - No implementation past bug without investigation diff --git a/specs/release-versioning/spec.md b/specs/release-versioning/spec.md new file mode 100644 index 0000000..225a9e3 --- /dev/null +++ b/specs/release-versioning/spec.md @@ -0,0 +1,26 @@ +# Release Versioning 规格 + +## MODIFIED Requirements + +### Requirement: 功能完成后统一准备 v0.11.0 + +发布准备 SHALL 在 Issue #70 功能实现、测试和聚焦评审完成后、最终 `executing -> closing` 转换前,把所有受版本同步工具管理的 manifests、文档、hooks、phase guards、prompts 与九个 canonical skills 统一更新为 `0.11.0`,并将自 v0.10.0 起的变更整理进 CHANGELOG。 + +#### Scenario: 执行统一版本同步 + +- **WHEN** Issue #70 功能已通过完整验证并执行 `ssf version 0.11.0` +- **THEN** version consistency 与 doctor 通过,版本 dry-run 不再报告待更新文件 + +#### Scenario: CHANGELOG 汇总发布内容 + +- **WHEN** 生成 v0.11.0 release candidate +- **THEN** CHANGELOG 包含 #47、#64、#65、#71 和 #70,并保留新的空 Unreleased 区段 + +### Requirement: 发布准备不自动发布 + +发布准备 MUST 不创建 git tag、不执行 npm publish、GitHub Release 或外部 marketplace 写操作,除非维护者另行明确授权。 + +#### Scenario: release candidate 验证完成 + +- **WHEN** 本地与 GitHub CI 发布检查全部通过 +- **THEN** 系统只提交 ready-for-review PR,并等待维护者授权实际发布动作 diff --git a/specs/workflow-entry-routing/spec.md b/specs/workflow-entry-routing/spec.md new file mode 100644 index 0000000..3cd00f6 --- /dev/null +++ b/specs/workflow-entry-routing/spec.md @@ -0,0 +1,40 @@ +# Workflow Entry Routing 规格 + +## MODIFIED Requirements + +### Requirement: 入口路由先完成需求路径选择 + +`workflow-start` SHALL 在 workflow 为 `auto`、空或未设置时,先读取已有 workflow selection receipt,只询问 `missing_facts` 对应的最少问题,展示 Observed、Available、Recommended 与 Why,并在用户明确选择后调用受保护的 select;不得把 recommendation 自动持久化为最终 workflow。 + +#### Scenario: 新需求尚无规划工件 + +- **WHEN** 用户提出新需求且 proposal/tasks 尚不存在,workflow 仍为 `auto` +- **THEN** 入口路由收集分类事实并请求用户选择,而不是调用无工件推断后默认进入 full + +#### Scenario: 恢复未完成的事实收集 + +- **WHEN** overlay 中存在 `needs-input` receipt +- **THEN** 入口路由复用已观察事实,只询问 receipt 列出的缺失项并重新生成完整事实快照 + +#### Scenario: 用户选择非推荐路径 + +- **WHEN** 用户明确选择与 recommendation 不同的 workflow +- **THEN** 入口路由说明偏离并在用户确认后传入 `--acknowledge-recommendation` + +### Requirement: 需求路径与执行模式保持分离 + +`workflow-start` MUST 将 full/hotfix/tweak 作为 DP-0 需求路径决定,将 Inline/Batch Inline/SDD 继续作为 DP-4 执行模式决定,不得互相替代批准或 receipt。 + +#### Scenario: 已选择 full 后进入执行规划 + +- **WHEN** 用户在入口选择 `full`,规划工件和执行契约随后获批 +- **THEN** DP-4 仍单独运行 execution recommend/plan 并要求用户选择执行模式 + +### Requirement: runtime infer 保持向后兼容 + +系统 SHALL 保持 `ssf runtime infer ` 的既有 artifact-based 推断结果,供现有调用方兼容使用,但入口用户选择不得由该命令替代。 + +#### Scenario: 旧调用方推断空目录 + +- **WHEN** 旧调用方直接对无规划工件的 change 运行 `runtime infer` +- **THEN** 命令继续返回既有 safe-default full 结果,而新版 workflow-start 不将该结果视为用户选择 diff --git a/specs/workflow-path-recommendation/spec.md b/specs/workflow-path-recommendation/spec.md new file mode 100644 index 0000000..2ca5ef6 --- /dev/null +++ b/specs/workflow-path-recommendation/spec.md @@ -0,0 +1,40 @@ +# Workflow Path Recommendation 规格 + +## ADDED Requirements + +### Requirement: 基于完整入口事实生成路径推荐 + +系统 SHALL 根据任务数、文件数、是否仅配置或文档、是否涉及 schema/API/公共接口、是否新增模块和不确定性等级,返回 `full`、`hotfix`、`tweak` 全部可选路径、一个推荐路径及可核对理由;推荐本身不得修改 `state.workflow`。 + +#### Scenario: 小型代码修复推荐 hotfix + +- **WHEN** 入口事实为不超过 2 个任务、不超过 2 个文件、非纯配置或文档、不涉及 schema/API/公共接口、不新增模块且不确定性低 +- **THEN** 系统推荐 `hotfix`,同时返回三种可选路径,并保持 `state.workflow` 为 `auto` + +#### Scenario: 小型配置或文档调整推荐 tweak + +- **WHEN** 入口事实为不超过 4 个任务、不超过 4 个文件、仅配置或文档、不涉及 schema/API/公共接口、不新增模块且不确定性低 +- **THEN** 系统推荐 `tweak`,并说明该事实位于 tweak 阈值内 + +#### Scenario: 风险或规模要求完整路径 + +- **WHEN** 入口事实包含 schema/API/公共接口变化、新模块、高不确定性,或超过 fast-path 阈值 +- **THEN** 系统推荐 `full`,并在理由中指出触发的风险或规模事实 + +### Requirement: 信息不足时继续探索 + +系统 MUST 将缺失计数或 `unknown` 分类事实返回为 `needs-input` 和稳定排序的 `missing_facts`,不得在信息不足时默认推荐或选择 `full`。 + +#### Scenario: 缺少文件数和接口风险事实 + +- **WHEN** 用户已提供任务数,但文件数缺失且 schema/API/公共接口事实为 `unknown` +- **THEN** 系统返回 `needs-input`、列出 `file_count` 与 `schema_api_change`,不产生 recommendation,也不修改 workflow + +### Requirement: 推荐阈值保持兼容 + +系统 SHALL 保持既有 hotfix 与 tweak 阈值语义,不修改 guard 的 workflow 转换矩阵。 + +#### Scenario: 现有边界值保持不变 + +- **WHEN** 输入分别位于 hotfix 的 2 tasks/2 files 边界或 tweak 的 4 tasks/4 files 边界 +- **THEN** 推荐结果继续接受对应 fast path,现有状态转换测试保持通过 diff --git a/specs/workflow-path-selection/spec.md b/specs/workflow-path-selection/spec.md new file mode 100644 index 0000000..60c8fed --- /dev/null +++ b/specs/workflow-path-selection/spec.md @@ -0,0 +1,59 @@ +# Workflow Path Selection 规格 + +## ADDED Requirements + +### Requirement: 用户确认后才选择 workflow + +系统 MUST 在存在有效且信息充分的 recommendation receipt、用户传入 `--confirm` 和非空单行理由后,才把所选路径写入 `state.workflow`;推荐命令不得代替该确认。 + +#### Scenario: 确认推荐路径 + +- **WHEN** 当前 receipt 推荐 `hotfix`,用户选择 `hotfix` 并显式确认 +- **THEN** 系统把 workflow 设置为 `hotfix`,记录用户理由和 `followed_recommendation=true` + +#### Scenario: 缺少确认 + +- **WHEN** 用户选择路径但未传入 `--confirm` +- **THEN** 系统拒绝选择,receipt、state 和 DP-0 保持不变 + +### Requirement: 非推荐选择需要额外确认 + +系统 MUST 允许用户选择任意可用的非推荐路径,但必须要求 `--acknowledge-recommendation`,并记录选择理由与风险确认。 + +#### Scenario: 未确认非推荐选择 + +- **WHEN** receipt 推荐 `hotfix`,用户选择 `full` 但未确认偏离推荐 +- **THEN** 系统拒绝选择且不修改任何持久化状态 + +#### Scenario: 已确认非推荐选择 + +- **WHEN** receipt 推荐 `hotfix`,用户选择 `full`、显式确认并确认偏离推荐 +- **THEN** 系统选择 `full`,记录 `followed_recommendation=false` 和 `acknowledged_non_recommendation=true` + +### Requirement: 选择证据可验证、可恢复、可审计 + +系统 SHALL 将 observation、recommendation、selection、时间戳和稳定 SHA-256 hash 保存到 change overlay,并将不覆盖既有范围与语言决定的 workflow 摘要追加到 DP-0。 + +#### Scenario: receipt 被篡改 + +- **WHEN** 已保存 receipt 的事实或选择在未重算 hash 的情况下被修改 +- **THEN** `workflow show` 报告 hash 无效,`workflow select` 拒绝使用该证据 + +#### Scenario: state 写入前中断 + +- **WHEN** receipt 已记录选择但 `state.workflow` 仍为 `auto` +- **THEN** `workflow show` 返回 `selection-pending`,允许相同选择安全重试,不把它当作已完成选择 + +#### Scenario: DP-0 已包含范围和语言决定 + +- **WHEN** workflow 选择成功且 `dp_0_decisions` 已包含 scope 与 `artifact_language=zh-CN` +- **THEN** 系统保留原决定并追加唯一的 `workflow_path`、recommended 和 followed_recommendation 摘要 + +### Requirement: 显式 workflow 优先 + +系统 MUST 尊重 state 中已显式设置的 `full`、`hotfix` 或 `tweak`,不得重复推荐或覆盖。 + +#### Scenario: 已显式选择 full + +- **WHEN** `state.workflow` 已为 `full` 且调用 recommend 或 show +- **THEN** 系统返回 `source=explicit-state` 与 `workflow=full`,不创建新的 recommendation receipt diff --git a/tests/lib/closing-terminal-semantics.test.mjs b/tests/lib/closing-terminal-semantics.test.mjs index 7563eea..235650c 100644 --- a/tests/lib/closing-terminal-semantics.test.mjs +++ b/tests/lib/closing-terminal-semantics.test.mjs @@ -5,6 +5,8 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const ROOT = process.cwd(); +const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version; +const RUNTIME_PREFIX = `npx --yes --package spec-superflow@${VERSION} ssf`; function read(relativePath) { return readFileSync(join(ROOT, relativePath), 'utf8'); @@ -90,11 +92,11 @@ describe('closing terminal lifecycle', () => { it('places the release guard before every closing side effect and makes transition last', () => { const archivist = read('skills/release-archivist/SKILL.md'); const guard = archivist.indexOf('## Execution-State Guard'); - const audit = archivist.indexOf('npx --yes --package spec-superflow@0.10.0 ssf audit '); + const audit = archivist.indexOf(`${RUNTIME_PREFIX} audit `); const dp6 = archivist.indexOf('### DP-6 (Verification Outcome)'); const dp7 = archivist.indexOf('### DP-7 (Archive Confirmation)'); const merger = archivist.indexOf('invoke `spec-merger`'); - const transition = archivist.indexOf('npx --yes --package spec-superflow@0.10.0 ssf state transition closing'); + const transition = archivist.indexOf(`${RUNTIME_PREFIX} state transition closing`); for (const [marker, index] of [ ['release guard', guard], @@ -113,7 +115,7 @@ describe('closing terminal lifecycle', () => { assert.ok(dp7 < merger, 'DP-7 must be recorded before invoking spec-merger'); assert.ok(merger < transition, 'spec-merger must run before the actual final transition'); assert.equal( - (archivist.match(/npx --yes --package spec-superflow@0\.10\.0 ssf state transition closing/g) || []).length, + archivist.split(`${RUNTIME_PREFIX} state transition closing`).length - 1, 1, 'the actual final transition command must occur exactly once' ); @@ -186,12 +188,14 @@ describe('closing terminal lifecycle', () => { } }); - it('records the #64 terminal closing repair in the unreleased changelog', () => { + it('records the #64 terminal closing repair in the dated current release', () => { const changelog = read('CHANGELOG.md'); const unreleased = section(changelog, '## [Unreleased]'); + const currentRelease = section(changelog, `## [${VERSION}] - 2026-07-21`); - assert.match(unreleased, /#64/); - assert.match(unreleased, /closing/i); - assert.match(unreleased, /终态/); + assert.equal(unreleased.trim(), '## [Unreleased]'); + assert.match(currentRelease, /#64/); + assert.match(currentRelease, /closing/i); + assert.match(currentRelease, /终态/); }); }); diff --git a/tests/lib/cmd-workflow.test.mjs b/tests/lib/cmd-workflow.test.mjs new file mode 100644 index 0000000..c8e2e34 --- /dev/null +++ b/tests/lib/cmd-workflow.test.mjs @@ -0,0 +1,397 @@ +import { afterEach, beforeEach, describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { getOverlayPaths } from '../../scripts/lib/sdd-overlay.mjs'; +import { readState } from '../../scripts/lib/state-loader.mjs'; +import { recordWorkflowSelection, saveWorkflowRecommendation } from '../../scripts/lib/workflow-recommendation.mjs'; + +const CLI = join(process.cwd(), 'scripts/spec-superflow.mjs'); +let changeDir; + +function runSsf(args) { + try { + const stdout = execFileSync(process.execPath, [CLI, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + return { exitCode: 0, stdout, stderr: '', json: tryJson(stdout) }; + } catch (error) { + const stdout = error.stdout?.toString() ?? ''; + return { + exitCode: error.status ?? 1, + stdout, + stderr: error.stderr?.toString() ?? '', + json: tryJson(stdout), + }; + } +} + +function tryJson(text) { + try { return JSON.parse(text); } catch { return null; } +} + +function writeState(contents = 'state: exploring\nworkflow: auto\n') { + writeFileSync(join(changeDir, '.spec-superflow.yaml'), contents); +} + +function snapshotWorkflowFiles() { + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + return { + state: readFileSync(join(changeDir, '.spec-superflow.yaml'), 'utf8'), + receipt: existsSync(receiptPath) ? readFileSync(receiptPath, 'utf8') : null, + }; +} + +function assertWorkflowFilesUnchanged(before) { + assert.equal(readFileSync(join(changeDir, '.spec-superflow.yaml'), 'utf8'), before.state); + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + assert.equal(existsSync(receiptPath) ? readFileSync(receiptPath, 'utf8') : null, before.receipt); +} + +function recommend(args = []) { + return runSsf(['workflow', 'recommend', changeDir, + '--task-count', '2', '--file-count', '2', '--config-doc-only', 'no', + '--schema-api-change', 'no', '--new-module', 'no', '--uncertainty', 'low', + '--json', ...args]); +} + +beforeEach(() => { + changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-cmd-')); + writeState(); +}); + +afterEach(() => { + rmSync(changeDir, { recursive: true, force: true }); +}); + +describe('ssf workflow', () => { + it('does not set workflow until the user confirms a selection', () => { + const recommended = recommend(); + assert.equal(recommended.exitCode, 0, recommended.stderr); + assert.equal(recommended.json.recommendation.mode, 'hotfix'); + assert.equal(readState(changeDir).workflow, 'auto'); + + const beforeUnconfirmed = snapshotWorkflowFiles(); + const unconfirmed = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--reason', 'bounded code fix', '--json']); + assert.equal(unconfirmed.exitCode, 1); + assert.match(unconfirmed.stderr, /confirm/i); + assertWorkflowFilesUnchanged(beforeUnconfirmed); + assert.equal(readState(changeDir).dp_0_decisions, null); + + const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--confirm', '--reason', 'bounded code fix', '--json']); + assert.equal(selected.exitCode, 0, selected.stderr); + assert.equal(readState(changeDir).workflow, 'hotfix'); + assert.match(readState(changeDir).dp_0_decisions, /workflow_path=hotfix/); + }); + + it('shows complete ready recommendations in human-readable recommend and show output', () => { + const recommended = runSsf(['workflow', 'recommend', changeDir, + '--task-count', '2', '--file-count', '2', '--config-doc-only', 'no', + '--schema-api-change', 'no', '--new-module', 'no', '--uncertainty', 'low']); + assert.equal(recommended.exitCode, 0, recommended.stderr); + assert.match(recommended.stdout, /Observed:/); + assert.match(recommended.stdout, /Available:.*full.*hotfix.*tweak/); + assert.match(recommended.stdout, /Recommended: hotfix/); + assert.match(recommended.stdout, /Why:.*bounded code work/i); + + const shown = runSsf(['workflow', 'show', changeDir]); + assert.equal(shown.exitCode, 0, shown.stderr); + assert.match(shown.stdout, /Observed:/); + assert.match(shown.stdout, /Available:.*full.*hotfix.*tweak/); + assert.match(shown.stdout, /Recommended: hotfix/); + assert.match(shown.stdout, /Why:.*bounded code work/i); + assert.match(shown.stdout, /Hash valid: true/i); + }); + + it('returns needs-input without changing auto workflow', () => { + const result = runSsf(['workflow', 'recommend', changeDir, + '--task-count', '2', '--config-doc-only', 'no', '--schema-api-change', 'unknown', + '--new-module', 'no', '--uncertainty', 'low', '--json']); + assert.equal(result.exitCode, 0, result.stderr); + assert.equal(result.json.status, 'needs-input'); + assert.deepEqual(result.json.missing_facts, ['file_count', 'schema_api_change']); + assert.equal(readState(changeDir).workflow, 'auto'); + }); + + it('restores needs-input context in human and JSON show output', () => { + const recommended = runSsf(['workflow', 'recommend', changeDir, + '--task-count', '2', '--config-doc-only', 'no', '--schema-api-change', 'unknown', + '--new-module', 'no', '--uncertainty', 'low', '--json']); + assert.equal(recommended.exitCode, 0, recommended.stderr); + + const human = runSsf(['workflow', 'show', changeDir]); + assert.equal(human.exitCode, 0, human.stderr); + assert.match(human.stdout, /Workflow status: needs-input/i); + assert.match(human.stdout, /Observed:.*task_count=2.*file_count=null/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); + assert.match(human.stdout, /Missing facts: file_count, schema_api_change/i); + assert.match(human.stdout, /Hash valid: true/i); + + const json = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(json.exitCode, 0, json.stderr); + assert.equal(json.json.status, 'needs-input'); + assert.equal(json.json.workflow, 'auto'); + assert.deepEqual(json.json.record.missing_facts, ['file_count', 'schema_api_change']); + assert.equal(json.json.record.facts.task_count, 2); + assert.equal(json.json.record.recommendation, null); + }); + + it('respects an explicitly configured workflow without creating a receipt', () => { + writeState('state: exploring\nworkflow: full\n'); + const result = runSsf(['workflow', 'recommend', changeDir, '--json']); + assert.equal(result.exitCode, 0, result.stderr); + assert.equal(result.json.source, 'explicit-state'); + assert.equal(result.json.workflow, 'full'); + assert.equal(existsSync(getOverlayPaths(changeDir).workflowSelection), false); + + const select = runSsf(['workflow', 'select', changeDir, '--mode', 'full', + '--confirm', '--reason', 'do not override', '--json']); + assert.equal(select.exitCode, 1); + assert.match(select.stderr, /already explicitly selected/i); + }); + + it('prioritizes explicit state over an invalid stale receipt', () => { + assert.equal(recommend().exitCode, 0); + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + const tampered = JSON.parse(readFileSync(receiptPath, 'utf8')); + tampered.facts.file_count = 99; + writeFileSync(receiptPath, JSON.stringify(tampered)); + writeState('state: exploring\nworkflow: full\n'); + + const result = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(result.exitCode, 0, result.stderr); + assert.equal(result.json.source, 'explicit-state'); + assert.equal(result.json.workflow, 'full'); + + const human = runSsf(['workflow', 'show', changeDir]); + assert.equal(human.exitCode, 0, human.stderr); + assert.match(human.stdout, /explicitly set to full/i); + assert.match(human.stdout, /Hash valid: false/i); + assert.match(human.stdout, /hash mismatch/i); + }); + + it('rejects selection from a tampered receipt without mutating receipt or state', () => { + assert.equal(recommend().exitCode, 0); + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + const tampered = JSON.parse(readFileSync(receiptPath, 'utf8')); + tampered.facts.file_count = 99; + writeFileSync(receiptPath, JSON.stringify(tampered)); + const before = snapshotWorkflowFiles(); + + const result = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--confirm', '--reason', 'bounded code fix', '--json']); + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /hash mismatch/i); + assertWorkflowFilesUnchanged(before); + assert.equal(readState(changeDir).dp_0_decisions, null); + }); + + it('requires acknowledgement before accepting a non-recommended selection', () => { + assert.equal(recommend().exitCode, 0); + const rejected = runSsf(['workflow', 'select', changeDir, '--mode', 'full', + '--confirm', '--reason', 'operator preference', '--json']); + assert.equal(rejected.exitCode, 1); + assert.match(rejected.stderr, /acknowledge/i); + assert.equal(readState(changeDir).workflow, 'auto'); + + const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'full', + '--confirm', '--reason', 'operator preference', '--acknowledge-recommendation', '--json']); + assert.equal(selected.exitCode, 0, selected.stderr); + assert.equal(selected.json.record.selection.followed_recommendation, false); + assert.equal(readState(changeDir).workflow, 'full'); + }); + + it('treats a missing receipt as needs-input with all fixed facts missing', () => { + let result = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(result.exitCode, 0, result.stderr); + assert.equal(result.json.status, 'needs-input'); + assert.equal(result.json.source, 'missing-receipt'); + assert.deepEqual(result.json.missing_facts, [ + 'task_count', 'file_count', 'config_doc_only', 'schema_api_change', + 'new_module', 'uncertainty', + ]); + assert.deepEqual(result.json.available_modes, ['full', 'hotfix', 'tweak']); + assert.equal(result.json.recommendation, null); + assert.equal(result.json.receipt.exists, false); + + result = runSsf(['workflow', 'show', changeDir]); + assert.equal(result.exitCode, 0, result.stderr); + assert.match(result.stdout, /Workflow status: needs-input/i); + assert.match(result.stdout, /Missing facts: task_count, file_count, config_doc_only, schema_api_change, new_module, uncertainty/i); + assert.match(result.stdout, /Hash valid: unavailable/i); + }); + + it('restores invalid receipt evidence in human and JSON show output', () => { + saveWorkflowRecommendation(changeDir, { + task_count: 2, file_count: 2, config_doc_only: 'no', schema_api_change: 'no', + new_module: 'no', uncertainty: 'low', + }); + const receiptPath = getOverlayPaths(changeDir).workflowSelection; + const tampered = JSON.parse(readFileSync(receiptPath, 'utf8')); + tampered.facts.file_count = 99; + writeFileSync(receiptPath, JSON.stringify(tampered)); + + const human = runSsf(['workflow', 'show', changeDir]); + assert.equal(human.exitCode, 1); + assert.match(human.stdout, /Workflow status: invalid/i); + assert.match(human.stdout, /Observed:.*file_count=99/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); + assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Why:/i); + assert.match(human.stdout, /Hash valid: false/i); + assert.match(human.stdout, /hash mismatch/i); + + const json = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(json.exitCode, 1); + assert.equal(json.json.status, 'invalid'); + assert.equal(json.json.receipt.valid, false); + assert.equal(json.json.receipt.record.facts.file_count, 99); + assert.match(json.json.receipt.failures.join(' '), /hash mismatch/i); + }); + + it('restores selection-pending evidence in human and JSON show output', () => { + saveWorkflowRecommendation(changeDir, { + task_count: 2, file_count: 2, config_doc_only: 'no', schema_api_change: 'no', + new_module: 'no', uncertainty: 'low', + }); + recordWorkflowSelection(changeDir, { + mode: 'hotfix', reason: 'recoverable selection', confirmed: true, acknowledged: false, + }); + + const human = runSsf(['workflow', 'show', changeDir]); + assert.equal(human.exitCode, 0, human.stderr); + assert.match(human.stdout, /Workflow status: selection-pending/i); + assert.match(human.stdout, /Observed:/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); + assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Why:/i); + assert.match(human.stdout, /Selection:.*mode=hotfix.*reason=recoverable selection/i); + assert.match(human.stdout, /Hash valid: true/i); + + const json = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(json.exitCode, 0, json.stderr); + assert.equal(json.json.status, 'selection-pending'); + assert.equal(json.json.workflow, 'auto'); + assert.equal(json.json.record.selection.mode, 'hotfix'); + assert.equal(json.json.record.selection.reason, 'recoverable selection'); + }); + + it('restores selected evidence in human and JSON show output', () => { + assert.equal(recommend().exitCode, 0); + + let result = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--confirm', '--reason', 'recoverable selection', '--json']); + assert.equal(result.exitCode, 0, result.stderr); + + const human = runSsf(['workflow', 'show', changeDir]); + assert.equal(human.exitCode, 0, human.stderr); + assert.match(human.stdout, /Workflow status: selected/i); + assert.match(human.stdout, /Observed:/i); + assert.match(human.stdout, /Available:.*full.*hotfix.*tweak/i); + assert.match(human.stdout, /Recommended: hotfix/i); + assert.match(human.stdout, /Why:/i); + assert.match(human.stdout, /Selection:.*mode=hotfix.*reason=recoverable selection/i); + assert.match(human.stdout, /Hash valid: true/i); + + result = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(result.exitCode, 0, result.stderr); + assert.equal(result.json.status, 'selected'); + assert.equal(result.json.workflow, 'hotfix'); + assert.equal(result.json.record.selection.mode, 'hotfix'); + assert.equal(result.json.record.selection.followed_recommendation, true); + }); + + it('initializes a brand-new change before showing all missing workflow facts', () => { + rmSync(changeDir, { recursive: true, force: true }); + assert.equal(existsSync(changeDir), false); + + const initialized = runSsf(['state', 'init', changeDir, '--json']); + assert.equal(initialized.exitCode, 0, initialized.stderr); + const state = readState(changeDir); + assert.equal(state.workflow, 'auto'); + assert.equal(state.dp_0_confirmed, null); + + const shown = runSsf(['workflow', 'show', changeDir, '--json']); + assert.equal(shown.exitCode, 0, shown.stderr); + assert.equal(shown.json.status, 'needs-input'); + assert.deepEqual(shown.json.missing_facts, [ + 'task_count', 'file_count', 'config_doc_only', 'schema_api_change', + 'new_module', 'uncertainty', + ]); + assert.equal(readState(changeDir).dp_0_confirmed, null); + }); + + it('rejects a missing state file without creating a ghost state', () => { + rmSync(join(changeDir, '.spec-superflow.yaml')); + const result = recommend(); + assert.equal(result.exitCode, 1); + assert.match(result.stderr, /state init/i); + assert.equal(existsSync(join(changeDir, '.spec-superflow.yaml')), false); + assert.equal(existsSync(getOverlayPaths(changeDir).workflowSelection), false); + }); + + it('rejects invalid count and enum input with usage exit code before writing a receipt', () => { + for (const args of [ + ['--task-count', '-1'], + ['--file-count', 'one'], + ['--config-doc-only', 'maybe'], + ['--schema-api-change', 'sometimes'], + ['--new-module', 'possibly'], + ['--uncertainty', 'medium'], + ]) { + const result = recommend(args); + assert.equal(result.exitCode, 2, `${args.join(' ')}: ${result.stderr}`); + } + assert.equal(existsSync(getOverlayPaths(changeDir).workflowSelection), false); + }); + + it('rejects unsafe and overflowing counts without mutating receipt or state', () => { + assert.equal(recommend().exitCode, 0); + for (const count of ['9007199254740992', '9'.repeat(400)]) { + const before = snapshotWorkflowFiles(); + const result = recommend(['--task-count', count]); + assert.equal(result.exitCode, 2, `${count}: ${result.stderr}`); + assert.match(result.stderr, /non-negative integer/i); + assertWorkflowFilesUnchanged(before); + } + }); + + it('rejects extra positionals without mutating receipt or state', () => { + let before = snapshotWorkflowFiles(); + const malformedRecommend = runSsf(['workflow', 'recommend', changeDir, 'extra', + '--task-count', '2', '--file-count', '2', '--config-doc-only', 'no', + '--schema-api-change', 'no', '--new-module', 'no', '--uncertainty', 'low']); + assert.equal(malformedRecommend.exitCode, 2, malformedRecommend.stderr); + assertWorkflowFilesUnchanged(before); + + assert.equal(recommend().exitCode, 0); + before = snapshotWorkflowFiles(); + const malformedSelect = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--confirm', '--reason', 'bounded', 'code', 'fix', '--json']); + assert.equal(malformedSelect.exitCode, 2, malformedSelect.stderr); + assertWorkflowFilesUnchanged(before); + }); + + it('preserves scope and artifact language while replacing the workflow summary idempotently', () => { + writeState([ + 'state: exploring', + 'workflow: auto', + 'dp_0_decisions: scope=issue 70 | artifact_language=zh-CN | workflow_path=full; recommended=full; followed_recommendation=true', + '', + ].join('\n')); + assert.equal(recommend().exitCode, 0); + const selected = runSsf(['workflow', 'select', changeDir, '--mode', 'hotfix', + '--confirm', '--reason', 'bounded code fix', '--json']); + assert.equal(selected.exitCode, 0, selected.stderr); + const decisions = readState(changeDir).dp_0_decisions; + assert.match(decisions, /scope=issue 70/); + assert.match(decisions, /artifact_language=zh-CN/); + assert.equal((decisions.match(/workflow_path=/g) ?? []).length, 1); + assert.match(decisions, /workflow_path=hotfix; recommended=hotfix; followed_recommendation=true/); + }); +}); diff --git a/tests/lib/infer-workflow.test.mjs b/tests/lib/infer-workflow.test.mjs index 6fca93e..260e675 100644 --- a/tests/lib/infer-workflow.test.mjs +++ b/tests/lib/infer-workflow.test.mjs @@ -22,7 +22,7 @@ describe('infer-workflow: inferMode()', () => { if (tempDir) rmSync(tempDir, { recursive: true, force: true }); }); - it('returns full mode for empty directory (no artifacts)', () => { + it('keeps the legacy artifact inference safe default for compatibility', () => { const result = inferMode(tempDir); assert.equal(result.mode, 'full'); assert.equal(result.explicit, false); diff --git a/tests/lib/platform-runtime-distribution.test.mjs b/tests/lib/platform-runtime-distribution.test.mjs index 01ed2a9..3faeb68 100644 --- a/tests/lib/platform-runtime-distribution.test.mjs +++ b/tests/lib/platform-runtime-distribution.test.mjs @@ -3,8 +3,8 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, realpathSync } from 'node:fs'; -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, rmSync } from 'node:fs'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { PLATFORM_RUNTIME_INVENTORY, ZCODE_COMPATIBILITY_PATH } from '../../scripts/lib/platform-runtime-inventory.mjs'; @@ -143,6 +143,84 @@ describe('runtime version synchronization', () => { assert.match(output, /skills\/build-executor\/task-reviewer-prompt\.md: version string updated/); assert.match(output, /skills\/code-reviewer\/code-reviewer-prompt\.md: version string updated/); }); + + it('updates npm lock metadata and recovery command runtime pins', () => { + const target = mkdtempSync(join(tmpdir(), 'ssf-version-release-assets-')); + try { + mkdirSync(join(target, 'commands', 'ssf'), { recursive: true }); + writeFileSync(join(target, 'package.json'), '{"name":"fixture","version":"0.10.0"}\n'); + writeFileSync(join(target, 'package-lock.json'), `${JSON.stringify({ + name: 'fixture', + version: '0.10.0', + lockfileVersion: 3, + packages: { '': { name: 'fixture', version: '0.10.0' } }, + }, null, 2)}\n`); + for (const name of ['resume', 'switch', 'save']) { + writeFileSync( + join(target, 'commands', 'ssf', `${name}.md`), + `Run \`npx --yes --package spec-superflow@0.10.0 ssf ${name}\`.\n`, + ); + } + + execFileSync(process.execPath, [CLI, 'version', '0.11.0'], { + cwd: target, + encoding: 'utf8', + }); + + const lock = JSON.parse(readFileSync(join(target, 'package-lock.json'), 'utf8')); + assert.equal(lock.version, '0.11.0'); + assert.equal(lock.packages[''].version, '0.11.0'); + for (const name of ['resume', 'switch', 'save']) { + assert.match( + readFileSync(join(target, 'commands', 'ssf', `${name}.md`), 'utf8'), + new RegExp(`spec-superflow@0\\.11\\.0 ssf ${name}`), + ); + } + } finally { + rmSync(target, { recursive: true, force: true }); + } + }); + + it('reports drift in npm lock metadata and recovery command runtime pins', () => { + const target = mkdtempSync(join(tmpdir(), 'ssf-version-consistency-')); + const fixture = join(target, 'repo'); + try { + cpSync(ROOT, fixture, { + recursive: true, + filter(source) { + const relative = source.slice(ROOT.length).replace(/^\//, ''); + return relative !== '.git' && !relative.startsWith('.git/') + && !relative.startsWith('node_modules') + && !relative.startsWith('changes') + && !relative.startsWith('.superpowers'); + }, + }); + const lockPath = join(fixture, 'package-lock.json'); + const lock = JSON.parse(readFileSync(lockPath, 'utf8')); + lock.version = '9.9.9'; + lock.packages[''].version = '9.9.9'; + writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + for (const name of ['resume', 'switch', 'save']) { + const path = join(fixture, 'commands', 'ssf', `${name}.md`); + writeFileSync(path, readFileSync(path, 'utf8').replace(/spec-superflow@\d+\.\d+\.\d+/g, 'spec-superflow@9.9.9')); + } + + const result = spawnSync(process.execPath, [join(fixture, 'scripts', 'check-version-consistency.mjs')], { + cwd: fixture, + encoding: 'utf8', + }); + const output = `${result.stdout}${result.stderr}`; + + assert.equal(result.status, 1); + assert.match(output, /package-lock\.json \[version\]/); + assert.match(output, /package-lock\.json \[packages\.\.version\]/); + for (const name of ['resume', 'switch', 'save']) { + assert.match(output, new RegExp(`commands/ssf/${name}\\.md`)); + } + } finally { + rmSync(target, { recursive: true, force: true }); + } + }); }); function idsWithMode(id, mode) { diff --git a/tests/lib/recovery-command-assets.test.mjs b/tests/lib/recovery-command-assets.test.mjs index 0a85a83..e02b1c2 100644 --- a/tests/lib/recovery-command-assets.test.mjs +++ b/tests/lib/recovery-command-assets.test.mjs @@ -16,6 +16,8 @@ function read(path) { return readFileSync(join(process.cwd(), path), 'utf8'); } +const VERSION = JSON.parse(read('package.json')).version; + function executableSsfCommands(content) { return [...content.matchAll(/`([^`\n]*\bssf\s+(?:resume|switch|save)\b[^`]*)`/g)] .map(match => match[1]); @@ -56,7 +58,7 @@ describe('SSF recovery command assets', () => { assert.match(content, /^---\n[\s\S]+description:/); assert.match(content, /argument-hint:/); - assert.match(content, new RegExp(`spec-superflow@0\\.10\\.0 ssf ${name}`)); + assert.match(content, new RegExp(`spec-superflow@${VERSION.replaceAll('.', '\\.')} ssf ${name}`)); assert.match(content, /\$ARGUMENTS/); assert.doesNotMatch(content, /state set|state transition|active-change|\bcd\s/); assertNoCheckoutAbsolutePaths(content); @@ -93,15 +95,15 @@ describe('SSF recovery command assets', () => { }); it('rejects unquoted raw arguments after executable command flags', () => { - const unsafeResume = 'Run `npx --yes --package spec-superflow@0.10.0 ssf resume --json $ARGUMENTS`.'; - const unsafeSwitch = 'Run `npx --yes --package spec-superflow@0.10.0 ssf switch --flag $ARGUMENTS`.'; + const unsafeResume = `Run \`npx --yes --package spec-superflow@${VERSION} ssf resume --json $ARGUMENTS\`.`; + const unsafeSwitch = `Run \`npx --yes --package spec-superflow@${VERSION} ssf switch --flag $ARGUMENTS\`.`; assert.throws(() => assertNoUnquotedArguments(unsafeResume), /\$ARGUMENTS/); assert.throws(() => assertNoUnquotedArguments(unsafeSwitch), /\$ARGUMENTS/); }); it('accepts quoted argument input and prose-only argument mentions', () => { - const safeResume = 'Run `npx --yes --package spec-superflow@0.10.0 ssf resume --json "$ARGUMENTS"`. $ARGUMENTS is conversational input.'; + const safeResume = `Run \`npx --yes --package spec-superflow@${VERSION} ssf resume --json "$ARGUMENTS"\`. $ARGUMENTS is conversational input.`; assert.doesNotThrow(() => assertNoUnquotedArguments(safeResume)); assert.doesNotThrow(() => assertNoUnquotedArguments(read('commands/ssf/save.md'))); @@ -125,7 +127,7 @@ describe('SSF recovery command assets', () => { }); it('does not mistake the pinned portable npx entrypoint for a checkout path', () => { - const portable = 'Run `npx --yes --package spec-superflow@0.10.0 ssf resume --json "$ARGUMENTS"`.'; + const portable = `Run \`npx --yes --package spec-superflow@${VERSION} ssf resume --json "$ARGUMENTS"\`.`; assert.doesNotThrow(() => assertNoCheckoutAbsolutePaths(portable)); }); diff --git a/tests/lib/workflow-recommendation.test.mjs b/tests/lib/workflow-recommendation.test.mjs new file mode 100644 index 0000000..621f322 --- /dev/null +++ b/tests/lib/workflow-recommendation.test.mjs @@ -0,0 +1,98 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { + recommendWorkflowPath, + recordWorkflowSelection, + readWorkflowSelection, + saveWorkflowRecommendation, + WORKFLOW_MODES, +} from '../../scripts/lib/workflow-recommendation.mjs'; +import { getOverlayPaths } from '../../scripts/lib/sdd-overlay.mjs'; + +const base = { + task_count: 2, + file_count: 2, + config_doc_only: 'no', + schema_api_change: 'no', + new_module: 'no', + uncertainty: 'low', +}; + +describe('workflow path recommendation', () => { + it('recommends hotfix for a bounded code change', () => { + const result = recommendWorkflowPath(base); + assert.equal(result.status, 'ready'); + assert.deepEqual(result.available_modes, WORKFLOW_MODES); + assert.equal(result.recommendation.mode, 'hotfix'); + }); + + it('recommends tweak for a small config/doc-only change', () => { + const result = recommendWorkflowPath({ ...base, task_count: 4, file_count: 4, config_doc_only: 'yes' }); + assert.equal(result.recommendation.mode, 'tweak'); + }); + + it('recommends full for risk, uncertainty, or threshold overflow', () => { + for (const facts of [ + { ...base, schema_api_change: 'yes' }, + { ...base, new_module: 'yes' }, + { ...base, uncertainty: 'high' }, + { ...base, task_count: 3 }, + ]) assert.equal(recommendWorkflowPath(facts).recommendation.mode, 'full'); + }); + + it('returns needs-input instead of full when facts are unknown', () => { + const result = recommendWorkflowPath({ ...base, file_count: null, new_module: 'unknown' }); + assert.equal(result.status, 'needs-input'); + assert.equal(result.recommendation, null); + assert.deepEqual(result.missing_facts, ['file_count', 'new_module']); + }); + + it('persists a hashed recommendation and detects tampering', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-recommend-')); + try { + const saved = saveWorkflowRecommendation(changeDir, base); + assert.match(saved.hash, /^sha256:/); + assert.equal(readWorkflowSelection(changeDir).valid, true); + const file = getOverlayPaths(changeDir).workflowSelection; + const tampered = JSON.parse(readFileSync(file, 'utf8')); + tampered.facts.file_count = 99; + writeFileSync(file, JSON.stringify(tampered)); + assert.equal(readWorkflowSelection(changeDir).valid, false); + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); + + it('requires acknowledgement for a non-recommended selection', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-select-')); + try { + saveWorkflowRecommendation(changeDir, base); + assert.throws(() => recordWorkflowSelection(changeDir, { + mode: 'full', reason: 'operator preference', confirmed: true, acknowledged: false, + }), /acknowledge/i); + const selected = recordWorkflowSelection(changeDir, { + mode: 'full', reason: 'operator preference', confirmed: true, acknowledged: true, + }); + assert.equal(selected.selection.followed_recommendation, false); + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); + + it('rejects Unicode control characters and line separators in selection reasons', () => { + const changeDir = mkdtempSync(join(tmpdir(), 'ssf-workflow-reason-')); + try { + saveWorkflowRecommendation(changeDir, base); + for (const reason of ['contains\u0085c1 control', 'contains\u2028line separator']) { + assert.throws(() => recordWorkflowSelection(changeDir, { + mode: 'hotfix', reason, confirmed: true, acknowledged: false, + }), /single-line/i); + } + } finally { + rmSync(changeDir, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/lib/workflow-start-recommendation.test.mjs b/tests/lib/workflow-start-recommendation.test.mjs new file mode 100644 index 0000000..a81a261 --- /dev/null +++ b/tests/lib/workflow-start-recommendation.test.mjs @@ -0,0 +1,117 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +function read(path) { + return readFileSync(new URL(`../../${path}`, import.meta.url), 'utf8'); +} + +const AUTO_INTAKE_STEPS = [ + ['explicit workflow', /explicit[^\n]*full[^\n]*hotfix[^\n]*tweak/i], + ['show receipt', /ssf workflow show/], + ['only missing facts', /only[^\n]*missing_facts|missing_facts[^\n]*only/i], + ['recommend', /ssf workflow recommend/], + ['Observed', /Observed/], + ['Available', /Available/], + ['Recommended', /Recommended/], + ['Why', /Why/], + ['user choice', /user(?:'s)? explicit path choice/i], + ['persist selection', /ssf workflow select/], + ['DP-0 confirmation', /Confirm DP-0/], + ['confirmed state', /dp_0_confirmed true/], +]; + +function protocolErrors(source) { + const errors = []; + let previousIndex = -1; + + for (const [label, pattern] of AUTO_INTAKE_STEPS) { + const match = pattern.exec(source); + if (!match) { + errors.push(`missing ${label}`); + continue; + } + if (match.index <= previousIndex) errors.push(`${label} is out of order`); + previousIndex = Math.max(previousIndex, match.index); + } + + return errors; +} + +describe('workflow-start path recommendation protocol', () => { + it('validates and initializes a brand-new change before workflow show', () => { + const skill = read('skills/workflow-start/SKILL.md'); + const intake = skill.match(/### Workflow Path Intake[\s\S]*?(?=### Confirm DP-0)/)?.[0] ?? ''; + + assert.match(intake, /obtain[^\n]*change name/i); + assert.match(intake, /validate[^\n]*change name/i); + assert.match(intake, /change dir[^\n]*changes\//i); + assert.match(intake, /dp_0_confirmed[^\n]*false[^\n]*ssf state init/i); + assert.ok(intake.indexOf('ssf state init') < intake.indexOf('ssf workflow show')); + }); + + it('requires recommendation and user selection before persisting an automatic workflow', () => { + const skill = read('skills/workflow-start/SKILL.md'); + + assert.deepEqual(protocolErrors(skill), []); + assert.match(skill, /needs-input/); + assert.match(skill, /acknowledge-recommendation/); + assert.doesNotMatch(skill, /No artifacts.*safe default to full/i); + }); + + it('rejects a protocol that persists selection before recommendation', () => { + const wrongOrder = [ + 'explicit workflow full hotfix tweak', + 'ssf workflow show', + 'only missing_facts', + 'ssf workflow select', + 'ssf workflow recommend', + 'Observed', + 'Available', + 'Recommended', + 'Why', + "user's explicit path choice", + 'Confirm DP-0', + 'dp_0_confirmed true', + ].join('\n'); + + assert.ok(protocolErrors(wrongOrder).some((error) => error.includes('out of order'))); + }); + + it('rejects a protocol that omits a required recommendation display field', () => { + const missingWhy = [ + 'explicit workflow full hotfix tweak', + 'ssf workflow show', + 'only missing_facts', + 'ssf workflow recommend', + 'Observed', + 'Available', + 'Recommended', + "user's explicit path choice", + 'ssf workflow select', + 'Confirm DP-0', + 'dp_0_confirmed true', + ].join('\n'); + + assert.ok(protocolErrors(missingWhy).includes('missing Why')); + }); + + it('documents intake selection separately from DP-4 execution mode', () => { + const decisions = read('docs/decision-points.md'); + + assert.match(decisions, /full.*hotfix.*tweak/s); + assert.match(decisions, /Inline.*Batch Inline.*SDD/s); + assert.match(decisions, /\.superpowers\/sdd\/workflow-selection\.json/); + assert.match(decisions, /\.spec-superflow\.yaml[^\n]*dp_0_[^\n]*(?:scope|artifact_language)/); + }); + + it('documents every DP-0 trigger including legacy recovery and fast paths', () => { + const decisions = read('docs/decision-points.md'); + const trigger = decisions.match(/- \*\*触发条件\*\*:([^\n]+)/)?.[1] ?? ''; + + assert.match(trigger, /auto/i); + assert.match(trigger, /空|empty/i); + assert.match(trigger, /legacy/i); + assert.match(trigger, /fast path/i); + }); +});