diff --git a/CHANGELOG.md b/CHANGELOG.md index f1805a31d1..0638f534b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ +## [Unreleased] + +### Added + +- feat(commands): add `/speckit.revise` to edit the current feature spec in place (add/remove/reword ACs, FRs, stories) and cascade into plan/tasks + +### Changed + +- teach `/speckit.implement` and `/speckit.taskstoissues` to skip cancelled revision tasks +- teach lean implement the same skip rules; refuse to regenerate `tasks.md` after a revision +- teach `/speckit.converge` to inventory only live spec IDs and stop when revision tasks are still open +- teach plan (core and lean) and analyze to ignore SUPERSEDED/RETIRED IDs; lean specify must not overwrite a living spec +- stop implement from sending a Revision-phase task list back to `/speckit.tasks` +- first-generation tasks inventory only live IDs; persist `plan_status` on `revisions.md` +- clarify/checklist do not overwrite living-spec history; init lists `/speckit.revise` +- hook git auto-commit on `before_revise` / `after_revise` +- document living-spec revise in evolving-specs, spec-persistence, and agentic-sdd + ## [0.16.4] - 2026-08-14 ### Changed diff --git a/README.md b/README.md index 3452eb4a3f..e891ace45e 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ Additional commands for enhanced quality and validation: | Command | Agent Skill | Description | | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `/speckit.clarify` | `speckit-clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) | +| `/speckit.revise` | `speckit-revise` | After plan/tasks/implementation, revise FRs, SCs, or ACs and cascade into related artifacts | | `/speckit.analyze` | `speckit-analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) | | `/speckit.checklist` | `speckit-checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") | diff --git a/README.zh-CN.md b/README.zh-CN.md index b90809eee7..59af2b6d73 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -184,6 +184,7 @@ Spec Kit 可与 30 多个 AI 编码助手协作 —— 既包括 CLI 工具, | 命令 | 助手技能 | 说明 | | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- | | `/speckit.clarify` | `speckit-clarify` | 澄清描述不充分的部分(建议在 `/speckit.plan` 之前使用;旧称 `/quizme`) | +| `/speckit.revise` | `speckit-revise` | 在已有 plan/tasks/实现之后,修订 FR、SC 或 AC 并级联相关产物 | | `/speckit.analyze` | `speckit-analyze` | 跨制品的一致性与覆盖度分析(在 `/speckit.tasks` 之后、`/speckit.implement` 之前运行) | | `/speckit.checklist` | `speckit-checklist` | 生成自定义质量清单,校验需求的完整性、清晰度与一致性(好比"为自然语言写单元测试") | diff --git a/docs/concepts/spec-persistence.md b/docs/concepts/spec-persistence.md index dcef441116..ef6e56a1bc 100644 --- a/docs/concepts/spec-persistence.md +++ b/docs/concepts/spec-persistence.md @@ -71,7 +71,7 @@ derived from it. In this model, teams update `spec.md` first and then regenerate or revise `plan.md` and `tasks.md` from that source. The plan and task list are still valuable, but they are treated as disposable derivations rather than permanent -sources of truth. +sources of truth. Use `/speckit.revise` to edit the current spec in place. Living spec works well when: diff --git a/docs/guides/evolving-specs.md b/docs/guides/evolving-specs.md index e2941f08b3..cefe00c5fb 100644 --- a/docs/guides/evolving-specs.md +++ b/docs/guides/evolving-specs.md @@ -39,7 +39,8 @@ derived from it. When intended behavior changes, revise the existing `spec.md` first. Then regenerate or manually revise downstream artifacts so they match the updated -spec: +spec. Use `/speckit.revise` when plan, tasks, or implementation already exist +and you need to change FRs, SCs, or ACs and the related artifacts. 1. Start from a clean working tree or a dedicated branch so every generated change is reviewable. @@ -53,6 +54,8 @@ spec: 6. Run `/speckit.implement`, then review the code and artifact diffs together. 7. Run `/speckit.converge` to assess completion and append any remaining work to `tasks.md`. If tasks are appended, repeat `/speckit.implement` and `/speckit.converge` until the feature is fully complete. +If you used `/speckit.revise`, skip steps 2–4 unless `plan.md`/`tasks.md` are missing or the latest `revisions.md` entry has `plan_status: needs-rebuild`. Then continue from analyze. + Preserve important implementation rationale before replacing derived artifacts. If a plan or task list contains decisions that still matter, carry them forward explicitly. diff --git a/docs/installation.md b/docs/installation.md index 67b69505e6..ec23dbaee7 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -123,6 +123,7 @@ After initialization, you should see the following commands available in your co - `/speckit.implement` - Execute implementation tasks - `/speckit.analyze` - Validate cross-artifact consistency - `/speckit.clarify` - Identify and resolve ambiguities +- `/speckit.revise` - After plan, tasks, or implementation, revise FRs, SCs, or ACs and related artifacts - `/speckit.checklist` - Generate quality checklists - `/speckit.constitution` - Create or update project principles - `/speckit.converge` - Assess codebase against artifacts and append remaining tasks diff --git a/docs/quickstart.md b/docs/quickstart.md index 4d4eaf89e0..c0b3fdca2e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -120,6 +120,14 @@ Checks the codebase against the spec, plan, and tasks. If it finds gaps, it appe /speckit.converge ``` +### After plan, tasks, or implementation: `/speckit.revise` — change FRs, SCs, or ACs + +`/speckit.specify` can still create or update a spec before plan, tasks, or implementation exist. Once any of those exist, use `/speckit.revise` to change FRs, SCs, or ACs and cascade into the related artifacts. If revise reports `plan_status: needs-rebuild`, rerun `/speckit.plan`. Then analyze, implement, and converge. + +```text +/speckit.revise Remove password login. Add AC: Given an expired session, When the user submits, Then redirect to SSO +``` + > [!TIP] > For a full reference on each command — arguments, output, phased implementation, and how they interact — see [Agentic SDD](reference/agentic-sdd.md). diff --git a/docs/reference/agentic-sdd.md b/docs/reference/agentic-sdd.md index dc38e76a5a..3b7a5cfb05 100644 --- a/docs/reference/agentic-sdd.md +++ b/docs/reference/agentic-sdd.md @@ -9,6 +9,8 @@ The commands are designed to run in order, but only `/speckit.specify` is strict ```text /speckit.constitution -> /speckit.specify -> /speckit.clarify -> /speckit.plan -> /speckit.checklist -> /speckit.tasks -> /speckit.analyze -> /speckit.implement -> /speckit.converge +# after ship, when the spec itself changes: +/speckit.revise -> [/speckit.plan if missing or plan_status: needs-rebuild] -> [/speckit.tasks if tasks.md missing] -> /speckit.analyze -> /speckit.implement -> /speckit.converge ``` ## `/speckit.constitution` @@ -21,7 +23,7 @@ Creates or updates the project **constitution** — the guiding principles that ## `/speckit.specify` -Creates or updates the feature **specification** from a natural-language description. Focus on the **what** and **why** — the user-facing behavior and goals — not the tech stack, which belongs in `/speckit.plan`. +Creates or updates the feature **specification** from a natural-language description. Focus on the **what** and **why** — the user-facing behavior and goals — not the tech stack, which belongs in `/speckit.plan`. After plan, tasks, or implementation exist, use `/speckit.revise` to change FRs, SCs, or ACs. This workflow may also maintain `checklists/requirements.md`, the built-in spec-quality checklist that `/speckit.specify` creates and `/speckit.clarify` re-evaluates. That lifecycle is separate from custom checklists generated by `/speckit.checklist`. @@ -37,7 +39,7 @@ Asks up to five targeted questions about underspecified areas of the current spe /speckit.clarify Focus on the task card behavior: status changes, comment limits, and who can be assigned. ``` -Clarifying before planning keeps you from designing on top of ambiguity. If `/speckit.analyze` later surfaces requirement gaps, come back and run `/speckit.clarify` (or `/speckit.specify`) again. +Clarifying before planning keeps you from designing on top of ambiguity. If `/speckit.analyze` later surfaces requirement gaps, come back and run `/speckit.clarify` (or `/speckit.specify`) again. For a known add, remove, or reword on the current spec, use `/speckit.revise`. When `checklists/requirements.md` exists, `/speckit.clarify` may update its evaluated state as part of tightening the spec. This exception applies only to the built-in requirements checklist, not to custom review checklists. @@ -65,7 +67,7 @@ Run it with no arguments for a broad pass, or pass a focus area to target one as /speckit.checklist Focus on the Kanban board interactions and comment permissions. ``` -Review the generated checklist. If it surfaces gaps, loop back to `/speckit.clarify` or `/speckit.specify` to tighten the spec before breaking the work down, then mark each custom checklist item `[x]` only after the requirements-quality criterion has been reviewed and satisfied. +Review the generated checklist. If it surfaces gaps, loop back to `/speckit.clarify` or `/speckit.specify` to tighten the spec before breaking the work down, then mark each custom checklist item `[x]` only after the requirements-quality criterion has been reviewed and satisfied. For a known current-spec delta, use `/speckit.revise`. ## `/speckit.tasks` @@ -83,7 +85,7 @@ Performs a **read-only** cross-artifact consistency and quality analysis across /speckit.analyze ``` -Run it before implementing, while the artifacts can still be adjusted cheaply. If it surfaces issues, **return to the earlier step that owns them** and fix them at the source — `/speckit.specify` or `/speckit.clarify` for requirement problems, `/speckit.plan` for design problems, `/speckit.tasks` to regenerate the task list — then re-run `/speckit.analyze` until it comes back clean. You can also run `/speckit.analyze` again after implementation as an extra review. +Run it before implementing, while the artifacts can still be adjusted cheaply. If it surfaces issues, **return to the earlier step that owns them** and fix them at the source — `/speckit.specify` or `/speckit.clarify` for requirement problems, `/speckit.plan` for design problems, `/speckit.tasks` to regenerate the task list — then re-run `/speckit.analyze` until it comes back clean. You can also run `/speckit.analyze` again after implementation as an extra review. For a known current-spec delta, use `/speckit.revise`. ## `/speckit.implement` @@ -109,6 +111,16 @@ For a large feature, work in stages to avoid overwhelming the agent's context Verify each stage works before moving to the next. +## `/speckit.revise` + +Applies a **known requirement delta** to the **current** feature spec — add, remove, or reword acceptance criteria, functional requirements, stories, or success criteria — then cascades into `plan.md` / `tasks.md` when those files exist and records the change in `revisions.md`. It does not create a new feature directory and does not write application code. + +```text +/speckit.revise Remove password login. Add AC: Given an expired session, When the user submits, Then redirect to SSO +``` + +Run this when FRs, SCs, or ACs change **after** plan, tasks, or implementation exist. Then `/speckit.analyze` (if plan and tasks exist), `/speckit.implement` (skips cancelled tasks), then `/speckit.converge`. Do not run `/speckit.converge` first — converge assumes the spec is stable. + ## `/speckit.converge` Assesses the codebase against the feature's spec, plan, and tasks to confirm nothing was missed. It is **append-only**: it never edits or deletes code, and its only possible write is adding tasks to `tasks.md`. Run it only after `/speckit.implement` has run on the current `tasks.md`. diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index a7bece0b89..3451226b93 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -635,6 +635,8 @@ Standard events (defined by core): - `after_constitution` - After constitution update - `before_taskstoissues` - Before tasks-to-issues conversion - `after_taskstoissues` - After tasks-to-issues conversion +- `before_revise` - Before `/speckit.revise` living-spec revision +- `after_revise` - After `/speckit.revise` living-spec revision ### Hook Configuration diff --git a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md index ac78029f2a..aa47b18508 100644 --- a/extensions/EXTENSION-DEVELOPMENT-GUIDE.md +++ b/extensions/EXTENSION-DEVELOPMENT-GUIDE.md @@ -225,6 +225,7 @@ Available hook points: - `before_analyze` / `after_analyze`: Before/after cross-artifact analysis - `before_checklist` / `after_checklist`: Before/after checklist generation - `before_clarify` / `after_clarify`: Before/after spec clarification +- `before_revise` / `after_revise`: Before/after living-spec revision - `before_constitution` / `after_constitution`: Before/after constitution update - `before_taskstoissues` / `after_taskstoissues`: Before/after tasks-to-issues conversion diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index c3391dbc75..4f4d0ffdeb 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -406,7 +406,8 @@ settings: # before_tasks, after_tasks, before_implement, after_implement, # before_analyze, after_analyze, before_checklist, after_checklist, # before_clarify, after_clarify, before_constitution, after_constitution, -# before_taskstoissues, after_taskstoissues +# before_taskstoissues, after_taskstoissues, +# before_revise, after_revise hooks: after_tasks: - extension: jira diff --git a/extensions/git/README.md b/extensions/git/README.md index c0cb7b5d00..fa99d0caec 100644 --- a/extensions/git/README.md +++ b/extensions/git/README.md @@ -35,6 +35,7 @@ This extension provides Git operations as an optional, self-contained module. It | `before_checklist` | `speckit.git.commit` | Yes | Commit outstanding changes before checklist | | `before_analyze` | `speckit.git.commit` | Yes | Commit outstanding changes before analysis | | `before_taskstoissues` | `speckit.git.commit` | Yes | Commit outstanding changes before issue sync | +| `before_revise` | `speckit.git.commit` | Yes | Commit outstanding changes before living-spec revision | | `after_constitution` | `speckit.git.commit` | Yes | Auto-commit after constitution update | | `after_specify` | `speckit.git.commit` | Yes | Auto-commit after specification | | `after_clarify` | `speckit.git.commit` | Yes | Auto-commit after clarification | @@ -44,6 +45,7 @@ This extension provides Git operations as an optional, self-contained module. It | `after_checklist` | `speckit.git.commit` | Yes | Auto-commit after checklist | | `after_analyze` | `speckit.git.commit` | Yes | Auto-commit after analysis | | `after_taskstoissues` | `speckit.git.commit` | Yes | Auto-commit after issue sync | +| `after_revise` | `speckit.git.commit` | Yes | Auto-commit after living-spec revision | ## Configuration diff --git a/extensions/git/config-template.yml b/extensions/git/config-template.yml index 2ea3471742..0613d1118f 100644 --- a/extensions/git/config-template.yml +++ b/extensions/git/config-template.yml @@ -50,6 +50,9 @@ auto_commit: before_taskstoissues: enabled: false message: "[Spec Kit] Save progress before issue sync" + before_revise: + enabled: false + message: "[Spec Kit] Save progress before spec revision" after_constitution: enabled: false message: "[Spec Kit] Add project constitution" @@ -77,3 +80,6 @@ auto_commit: after_taskstoissues: enabled: false message: "[Spec Kit] Sync tasks to issues" + after_revise: + enabled: false + message: "[Spec Kit] Revise specification" diff --git a/extensions/git/extension.yml b/extensions/git/extension.yml index c92322d8b1..10a6d8b0b6 100644 --- a/extensions/git/extension.yml +++ b/extensions/git/extension.yml @@ -83,6 +83,11 @@ hooks: optional: true prompt: "Commit outstanding changes before issue sync?" description: "Auto-commit before tasks-to-issues conversion" + before_revise: + command: speckit.git.commit + optional: true + prompt: "Commit outstanding changes before spec revision?" + description: "Auto-commit before living-spec revision" after_constitution: command: speckit.git.commit optional: true @@ -128,6 +133,11 @@ hooks: optional: true prompt: "Commit after syncing issues?" description: "Auto-commit after tasks-to-issues conversion" + after_revise: + command: speckit.git.commit + optional: true + prompt: "Commit specification revision?" + description: "Auto-commit after living-spec revision" tags: - "git" diff --git a/extensions/git/git-config.yml b/extensions/git/git-config.yml index 2ea3471742..0613d1118f 100644 --- a/extensions/git/git-config.yml +++ b/extensions/git/git-config.yml @@ -50,6 +50,9 @@ auto_commit: before_taskstoissues: enabled: false message: "[Spec Kit] Save progress before issue sync" + before_revise: + enabled: false + message: "[Spec Kit] Save progress before spec revision" after_constitution: enabled: false message: "[Spec Kit] Add project constitution" @@ -77,3 +80,6 @@ auto_commit: after_taskstoissues: enabled: false message: "[Spec Kit] Sync tasks to issues" + after_revise: + enabled: false + message: "[Spec Kit] Revise specification" diff --git a/presets/lean/README.md b/presets/lean/README.md index ab17257f96..523ac37d27 100644 --- a/presets/lean/README.md +++ b/presets/lean/README.md @@ -13,13 +13,15 @@ Use Lean when you want the structured specify → plan → tasks → implement p | `speckit.specify` | `spec.md` | Create a specification from a feature description | | `speckit.plan` | `plan.md` | Create an implementation plan from the spec | | `speckit.tasks` | `tasks.md` | Create dependency-ordered tasks from spec and plan | -| `speckit.implement` | *(code)* | Execute all tasks in order, marking progress | +| `speckit.implement` | *(code)* | Execute live tasks in order, skipping cancelled or superseded ones | | `speckit.constitution` | `constitution.md` | Create or update the project constitution | ## What It Replaces Lean overrides the five core workflow commands with self-contained prompts that produce each artifact directly — no separate template files involved. The result is a shorter, more direct workflow. +Living-spec edits still use the unmodified core `/speckit.revise` (lean does not replace it). + ## Installation ```bash diff --git a/presets/lean/commands/speckit.implement.md b/presets/lean/commands/speckit.implement.md index fc68a1f8b1..1ff00cf834 100644 --- a/presets/lean/commands/speckit.implement.md +++ b/presets/lean/commands/speckit.implement.md @@ -15,8 +15,10 @@ $ARGUMENTS 2. **Load context**: `.specify/memory/constitution.md` and `/spec.md` and `/plan.md` and `/tasks.md`. 3. **Execute tasks** in order: - - Complete each task before moving to the next + - Skip any task line that contains `CANCELLED`, `SUPERSEDED`, or a struck-through task ID (`~~T012~~`), even when the checkbox is still `- [ ]`. Do not implement them, do not mark them `[x]`, and do not count them as remaining work. + - Prefer the latest `Revision R#` phase plus any cleanup tasks it added + - Complete each remaining live task before moving to the next - Mark completed tasks by changing `- [ ]` to `- [x]` in `/tasks.md` - Halt on failure and report the issue -4. **Validate**: Verify all tasks are completed and the implementation matches the spec. +4. **Validate**: Verify all **live** (non-cancelled, non-superseded) tasks are completed and the implementation matches **live** items in the spec (struck `SUPERSEDED` / `RETIRED` lines are not required). diff --git a/presets/lean/commands/speckit.plan.md b/presets/lean/commands/speckit.plan.md index 9fbbe4c371..9806ffbb8c 100644 --- a/presets/lean/commands/speckit.plan.md +++ b/presets/lean/commands/speckit.plan.md @@ -15,5 +15,7 @@ $ARGUMENTS 2. **Load context**: `.specify/memory/constitution.md` and `/spec.md`. 3. Create an implementation plan and store it in `/plan.md`. + - Only plan **live** (unmarked) FRs, ACs, and SCs. Lines marked `SUPERSEDED` or `RETIRED` are historical — do not restore them. + - If `plan.md` already exists and `revisions.md` is present, do **not** overwrite it unless the latest `revisions.md` entry has `plan_status: needs-rebuild` (or the user asked). Even then, do not resurrect retired IDs. - Technical context: tech stack, dependencies, project structure - Design decisions, architecture, file structure diff --git a/presets/lean/commands/speckit.specify.md b/presets/lean/commands/speckit.specify.md index c15353557a..b0ede4be19 100644 --- a/presets/lean/commands/speckit.specify.md +++ b/presets/lean/commands/speckit.specify.md @@ -10,6 +10,8 @@ $ARGUMENTS ## Outline +If `plan.md`, `tasks.md`, or implementation already exist and the user stated a **concrete delta** (add, remove, or reword a named AC, FR, SC, or story), recommend `/speckit.revise` and stop. If only `spec.md` exists, this command may **update** it. + 1. **Ask the user** for the feature directory path (e.g., `specs/my-feature`). Do not proceed until provided. 2. Create the directory and write `.specify/feature.json`: @@ -17,7 +19,8 @@ $ARGUMENTS { "feature_directory": "" } ``` -3. Create a specification from the user input and store it in `/spec.md`. +3. Create or update the specification from the user input and store it in `/spec.md`. + - If that path already has a `spec.md` and `plan.md` or `tasks.md` exist, stop — recommend `/speckit.revise`. Otherwise you may update the existing spec. - Overview, functional requirements, user scenarios, success criteria - Every requirement must be testable - Make informed defaults for unspecified details diff --git a/presets/lean/commands/speckit.tasks.md b/presets/lean/commands/speckit.tasks.md index 724a7b8400..6f9573bb5d 100644 --- a/presets/lean/commands/speckit.tasks.md +++ b/presets/lean/commands/speckit.tasks.md @@ -15,5 +15,7 @@ $ARGUMENTS 2. **Load context**: `.specify/memory/constitution.md` and `/spec.md` and `/plan.md`. 3. Create dependency-ordered implementation tasks and store them in `/tasks.md`. + - If `tasks.md` already exists and has a `Revision R#` phase (or `revisions.md` is present), do **not** overwrite it. Stop and tell the user `/speckit.revise` already maintains the list. + - First generation: inventory only **live** (unmarked) FRs, ACs, and SCs. Never emit tasks for `SUPERSEDED` or `RETIRED` lines. - Every task uses checklist format: `- [ ] [TaskID] Description with file path` - Organized by phase: setup, foundational, user stories in priority order, polish diff --git a/spec-driven.md b/spec-driven.md index 28259ae28f..9ed12fe98e 100644 --- a/spec-driven.md +++ b/spec-driven.md @@ -83,6 +83,18 @@ This command transforms a simple feature description (the user-prompt) into a co 3. **Template-Based Generation**: Copies and customizes the feature specification template with your requirements 4. **Directory Structure**: Creates the proper `specs/[branch-name]/` structure for all related documents +This command **starts a new feature**. To change acceptance criteria or requirements on an existing spec, use `/speckit.revise` instead. + +### The `/speckit.revise` Command + +When the contract of the **current** feature changes (add, remove, or reword an acceptance criterion, functional requirement, or story), this command updates `spec.md` in place: + +1. **Same feature directory**: Does not create `specs/00N-…` or a new branch +2. **Stable IDs**: Existing FR/SC/AC numbers stay; retired IDs are never reused +3. **Revision log**: Appends `revisions.md` so the delta is reviewable +4. **Cascade**: Patches `plan.md` and appends or cancels tasks in `tasks.md` when those files exist +5. **No application code**: Implementation stays `/speckit.implement` + ### The `/speckit.plan` Command Once a feature specification exists, this command creates a comprehensive implementation plan: diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index f8afcf4f55..bb2f3916c9 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -374,6 +374,7 @@ def _print_cli_warning( "converge": "Assess the codebase against spec.md, plan.md, and tasks.md and append remaining work as new tasks.", "analyze": "Perform cross-artifact consistency analysis across spec.md, plan.md, and tasks.md.", "clarify": "Structured clarification workflow for underspecified requirements.", + "revise": "Revise the current feature spec in place and cascade the change into plan and tasks.", "constitution": "Create or update project governing principles and development guidelines.", "checklist": "Generate custom quality checklists for validating requirements completeness and clarity.", "taskstoissues": "Convert tasks from tasks.md into GitHub issues.", diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 2bb8452025..2020342445 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -1103,6 +1103,7 @@ def _display_cmd(name: str) -> str: f"○ [cyan]{_display_cmd('clarify')}[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]{_display_cmd('plan')}[/] if used)", f"○ [cyan]{_display_cmd('analyze')}[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]{_display_cmd('tasks')}[/], before [cyan]{_display_cmd('implement')}[/])", f"○ [cyan]{_display_cmd('checklist')}[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]{_display_cmd('plan')}[/])", + f"○ [cyan]{_display_cmd('revise')}[/] [bright_black](optional)[/bright_black] - Revise FRs, SCs, or ACs after plan, tasks, or implementation exist", ] enhancements_title = ( "Enhancement Skills" if native_skill_mode else "Enhancement Commands" diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index fb4a30519d..cfd01ca947 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -54,6 +54,7 @@ "converge", "implement", "plan", + "revise", "specify", "tasks", "taskstoissues", diff --git a/src/specify_cli/integrations/alquimia/__init__.py b/src/specify_cli/integrations/alquimia/__init__.py index 132615206d..39de519dac 100644 --- a/src/specify_cli/integrations/alquimia/__init__.py +++ b/src/specify_cli/integrations/alquimia/__init__.py @@ -16,6 +16,7 @@ "implement": "Optional implementation guidance or task filter", "analyze": "Optional focus areas for analysis", "clarify": "Optional areas to clarify in the spec", + "revise": "Requirement delta: add, remove, or reword ACs, FRs, or stories", "constitution": "Principles or values for the project constitution", "checklist": "Domain or focus area for the checklist", "taskstoissues": "Optional filter or label for GitHub issues", diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 03c7a90e74..b86dd73dee 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -49,6 +49,7 @@ "converge", "plan", "checklist", + "revise", "specify", "tasks", "taskstoissues", diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 2ce7fb6dcc..81fb4a34dc 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -16,6 +16,7 @@ "implement": "Optional implementation guidance or task filter", "analyze": "Optional focus areas for analysis", "clarify": "Optional areas to clarify in the spec", + "revise": "Requirement delta: add, remove, or reword ACs, FRs, or stories", "constitution": "Principles or values for the project constitution", "checklist": "Domain or focus area for the checklist", "taskstoissues": "Optional filter or label for GitHub issues", diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index 0a9b4e1591..472ed3ce2e 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -38,6 +38,7 @@ "converge", "implement", "plan", + "revise", "specify", "tasks", "taskstoissues", diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 2cd83bd7c0..96a1273123 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -68,6 +68,7 @@ Run `{SCRIPT}` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_ - SPEC = FEATURE_DIR/spec.md - PLAN = FEATURE_DIR/plan.md - TASKS = FEATURE_DIR/tasks.md +- REVISIONS = FEATURE_DIR/revisions.md (optional; living-spec history) Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). @@ -79,10 +80,11 @@ Load only the minimal necessary context from each artifact: **From spec.md:** - Overview/Context -- Functional Requirements -- Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) -- User Stories -- Edge Cases (if present) +- Only **live** (unmarked) Functional Requirements +- Only **live** Success Criteria (measurable outcomes — e.g., performance, security, availability, user success, business impact) +- Only **live** User Stories +- Edge Cases (if present and not SUPERSEDED / RETIRED) +- Lines marked `SUPERSEDED` or `RETIRED` (usually struck through) are historical. Do not inventory them. **From plan.md:** @@ -90,6 +92,7 @@ Load only the minimal necessary context from each artifact: - Data Model references - Phases - Technical constraints +- Skip plan bullets marked `SUPERSEDED` or `RETIRED`. Only flag a retired ID as in-scope when a **live** (unmarked) plan bullet or live task still requires it. **From tasks.md:** @@ -98,6 +101,17 @@ Load only the minimal necessary context from each artifact: - Phase grouping - Parallel markers [P] - Referenced file paths +- Cancelled or superseded tasks (`CANCELLED` / `SUPERSEDED` / struck-through IDs) — exclude them from coverage gaps + +**From spec.md (current contract):** + +- Only **live** FRs, ACs, and SCs count. Lines marked `SUPERSEDED` or `RETIRED` (usually struck through) are historical. Do not require tasks or plan coverage for them. + +**From revisions.md (if present):** + +- This file is a **dated log only**, not a spec. Ignore summaries except for ID lists. +- Collect `superseded:` and `retired:` IDs as historical. +- Current requirements come only from **live** lines in `spec.md`. **From constitution:** @@ -107,7 +121,7 @@ Load only the minimal necessary context from each artifact: Create internal representations (do not include raw artifacts in output): -- **Requirements inventory**: For each Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). +- **Requirements inventory**: For each **live** (unmarked) Functional Requirement (FR-###) and Success Criterion (SC-###), record a stable key. Do not create keys for `SUPERSEDED` or `RETIRED` IDs. Use the explicit FR-/SC- identifier as the primary key when present, and optionally also derive an imperative-phrase slug for readability (e.g., "User can upload file" → `user-can-upload-file`). Include only Success Criteria items that require buildable work (e.g., load-testing infrastructure, security audit tooling), and exclude post-launch outcome metrics and business KPIs (e.g., "Reduce support tickets by 50%"). - **User story/action inventory**: Discrete user actions with acceptance criteria - **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) - **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements @@ -149,6 +163,9 @@ Focus on high-signal findings. Limit to 50 findings total; aggregate remainder i - Data entities referenced in plan but absent in spec (or vice versa) - Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) - Conflicting requirements (e.g., one requires Next.js while other specifies Vue) +- Plan or tasks still treating a **retired** ID from `revisions.md` as in-scope (the current `spec.md` no longer requires it) + +If requirement problems need a follow-up edit: use `__SPECKIT_COMMAND_SPECIFY__` to create or update a spec that is not yet planned, tasked, or implemented; use `__SPECKIT_COMMAND_CLARIFY__` to fill pre-plan gaps; if `plan.md`, `tasks.md`, or implementation already exist and the user knows the FR / SC / AC delta, recommend `__SPECKIT_COMMAND_REVISE__`. ### 5. Severity Assignment @@ -195,7 +212,7 @@ At end of report, output a concise Next Actions block: - If CRITICAL issues exist: Recommend resolving before `__SPECKIT_COMMAND_IMPLEMENT__` - If only LOW/MEDIUM: User may proceed, but provide improvement suggestions -- Provide explicit command suggestions: e.g., "Run __SPECKIT_COMMAND_SPECIFY__ with refinement", "Run __SPECKIT_COMMAND_PLAN__ to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" +- Provide explicit command suggestions: e.g., "Run __SPECKIT_COMMAND_SPECIFY__ for a new feature", "Run __SPECKIT_COMMAND_REVISE__ for a known add/remove/reword on the current spec", "Run __SPECKIT_COMMAND_PLAN__ to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" ### 8. Offer Remediation diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index 5ee239d309..cd8ae1fd31 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -34,7 +34,7 @@ scripts: - `[x]` does NOT mean implementation work is complete. - This command generates or appends checklist items; it MUST NOT mark generated items `[x]`. - An agent may assist with evaluating items only when explicitly asked by the reviewer. -- `checklists/requirements.md` is a separate built-in spec-quality checklist maintained by `__SPECKIT_COMMAND_SPECIFY__` and `__SPECKIT_COMMAND_CLARIFY__`; do not treat that exception as applying to custom checklists generated here. +- `checklists/requirements.md` is a separate built-in spec-quality checklist maintained by `__SPECKIT_COMMAND_SPECIFY__`, `__SPECKIT_COMMAND_CLARIFY__`, and `__SPECKIT_COMMAND_REVISE__` (revise re-evaluates checkboxes against live spec lines; it does not rewrite the file); do not treat that exception as applying to custom checklists generated here. ## User Input @@ -126,9 +126,11 @@ You **MUST** consider the user input before proceeding (if not empty). - Infer any missing context from spec/plan/tasks (do NOT hallucinate) 5. **Load feature context**: Read from FEATURE_DIR: - - spec.md: Feature requirements and scope - - plan.md (if exists): Technical details, dependencies - - tasks.md (if exists): Implementation tasks + - spec.md: Feature requirements and scope — only **live** (unmarked) FRs, ACs, and SCs. Skip `SUPERSEDED` / `RETIRED` lines. + - plan.md (if exists): Technical details, dependencies (skip struck plan bullets) + - tasks.md (if exists): Implementation tasks (skip `CANCELLED` / `SUPERSEDED` task lines) + + If `plan.md`, `tasks.md`, or implementation already exist and the gap is an add/remove/reword, recommend `__SPECKIT_COMMAND_REVISE__` rather than `__SPECKIT_COMMAND_SPECIFY__`. **Context Loading Strategy**: - Load only necessary portions relevant to active focus areas (avoid full-file dumping) diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index ea2f20d519..ce1d85c2a5 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -57,6 +57,10 @@ You **MUST** consider the user input before proceeding (if not empty). Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. +If the user already stated a **concrete delta** (add, remove, or reword a named AC, FR, story, or success criterion), do **not** start a clarification question loop. Recommend `__SPECKIT_COMMAND_REVISE__` and stop. + +If `revisions.md` exists, or `plan.md` / `tasks.md` already exist, do **not** replace or delete live FR/AC/SC lines and do **not** drop `SUPERSEDED` / `RETIRED` history. Recommend `__SPECKIT_COMMAND_REVISE__` for any add/remove/reword or anything that would invalidate an earlier requirement. You may still add a Clarifications session and tighten wording that does not change meaning. + Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `__SPECKIT_COMMAND_PLAN__`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases. Execution steps: @@ -192,8 +196,8 @@ Execution steps: - Non-functional constraint → Add/modify measurable criteria in Success Criteria > Measurable Outcomes (convert vague adjective to metric or explicit target). - Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it). - Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once. - - If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text. - - Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite). + - If the clarification invalidates an earlier ambiguous statement and there is no `revisions.md` / `plan.md` / `tasks.md` yet, replace that statement instead of duplicating. Once those exist, do **not** replace — recommend `__SPECKIT_COMMAND_REVISE__`. + - Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite). Do not wipe `SUPERSEDED` / `RETIRED` history. - Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact. - Keep each inserted clarification minimal and testable (avoid narrative drift). @@ -201,7 +205,7 @@ Execution steps: - Clarifications session contains exactly one bullet per accepted answer (no duplicates). - Total asked (accepted) questions ≤ 5. - Updated sections contain no lingering vague placeholders the new answer was meant to resolve. - - No contradictory earlier statement remains (scan for now-invalid alternative choices removed). + - No contradictory earlier statement remains (scan for now-invalid alternative choices removed). If `revisions.md` / `plan.md` / `tasks.md` exist, do not remove the old live line to satisfy this check — recommend `__SPECKIT_COMMAND_REVISE__` instead. - Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`. - Terminology consistency: same canonical term used across all updated sections. diff --git a/templates/commands/converge.md b/templates/commands/converge.md index eadb96ee58..1ef94b88c9 100644 --- a/templates/commands/converge.md +++ b/templates/commands/converge.md @@ -68,6 +68,15 @@ of remaining work as a new, traceable task** at the bottom of `tasks.md` so that This is **not** a diff tool and does **not** track changes. It assesses the present state of the code relative to the feature's artifacts — no git, no branch comparison, no history. +If the user just stated a requirement change (add/remove/reword an AC, FR, story, or +success criterion), **STOP** and recommend `__SPECKIT_COMMAND_REVISE__` +instead of converging. + +If `tasks.md` has a `Revision R#` phase whose live (non-cancelled, non-superseded) +tasks are still `- [ ]`, **STOP** and recommend `__SPECKIT_COMMAND_IMPLEMENT__` first. +Converge assumes the **spec is stable** and the latest revision has been implemented. +Re-appending tasks for behavior the spec just retired would undo a living-spec edit. + ## Operating Constraints **APPEND-ONLY, NEVER REWRITE**: The command's **only** write is appending a new @@ -108,11 +117,13 @@ Load only the minimal necessary context from each artifact: **From spec.md:** -- Functional Requirements (FR-###) -- Success Criteria (SC-###) — include only items requiring buildable work; exclude +- Only **live** (unmarked) Functional Requirements (FR-###) +- Only **live** Success Criteria (SC-###) — include only items requiring buildable work; exclude post-launch outcome metrics and business KPIs -- User Stories and their Acceptance Scenarios -- Edge Cases (if present) +- Only **live** User Stories and their Acceptance Scenarios +- Edge Cases (if present and not SUPERSEDED / RETIRED) +- Lines marked `SUPERSEDED` or `RETIRED` (usually struck through) are historical. Do not + inventory them and do not append tasks to restore them. **From plan.md:** @@ -120,11 +131,13 @@ Load only the minimal necessary context from each artifact: - Data Model references - Phases and named touch-points (files/components the plan says will be created or edited) - Technical constraints +- Skip plan bullets marked `SUPERSEDED` or `RETIRED` **From tasks.md:** - Task IDs (to compute the next ID and next phase number) - Descriptions, phase grouping, and referenced file paths +- Ignore `CANCELLED` / `SUPERSEDED` / struck-through task lines when deciding remaining work **From constitution (if not an unfilled template):** @@ -134,9 +147,9 @@ Load only the minimal necessary context from each artifact: Create an internal model (do not echo raw artifacts): -- **Requirements inventory**: one stable key per FR-### / SC-### / user-story acceptance +- **Requirements inventory**: one stable key per **live** FR-### / SC-### / user-story acceptance scenario (e.g. `US1/AC2`), plus the plan decisions and constitution principles that - impose buildable obligations. + impose buildable obligations. Do not create keys for `SUPERSEDED` or `RETIRED` IDs. - **Code-scope map**: from the file paths named in `plan.md` and `tasks.md`, plus a keyword search for the concepts each requirement describes, derive the set of source files and components in scope for assessment. Bound the assessment to these — do **not** infer diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 742c45e185..cf46607c36 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -141,12 +141,15 @@ You **MUST** consider the user input before proceeding (if not empty). - **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt` 5. Parse tasks.md structure and extract: - - **Task phases**: Setup, Tests, Core, Integration, Polish + - **Task phases**: Setup, Tests, Core, Integration, Polish, Convergence, and any `Revision R#` phases - **Task dependencies**: Sequential vs parallel execution rules - **Task details**: ID, description, file paths, parallel markers [P] - **Execution flow**: Order and dependency requirements + - **Cancelled or superseded tasks**: A task line is not executable if it contains `CANCELLED`, `SUPERSEDED`, or a struck-through task ID (`~~T012~~`), even when the checkbox is still `- [ ]`. Do not implement them, do not mark them `[X]`, and do not count them as remaining work. 6. Execute implementation following the task plan: + - **Skip cancelled/superseded tasks**: never execute those lines; do the replacement task (`T020` in `SUPERSEDED (R2 → T020)`) instead + - **Revision phases**: after earlier open (live) work, prefer the latest `Phase N: Revision R#` plus any cleanup tasks it added - **Phase-by-phase execution**: Complete each phase before moving to the next - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks @@ -169,12 +172,13 @@ You **MUST** consider the user input before proceeding (if not empty). - **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file. 9. Completion validation: - - Verify all required tasks are completed - - Check that implemented features match the original specification + - Verify all required **live** tasks are completed + - Ignore `CANCELLED` / `SUPERSEDED` tasks when deciding whether work remains + - Check that implemented features match **live** items in `spec.md` (struck `SUPERSEDED` / `RETIRED` lines are not required) - Validate that tests pass and coverage meets requirements - Confirm the implementation follows the technical plan -Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `__SPECKIT_COMMAND_TASKS__` first to regenerate the task list. +Note: This command assumes a task list exists in `tasks.md`. Suggest `__SPECKIT_COMMAND_TASKS__` only when `tasks.md` is **absent**. If the file exists and has cancelled/superseded lines or a `Revision R#` phase, that is the current list — implement the live remainder (or tell the user to run `__SPECKIT_COMMAND_REVISE__` to add work). Do not regenerate. ## Mandatory Post-Execution Hooks @@ -216,7 +220,7 @@ Report final status with summary of completed work. ## Done When -- [ ] All tasks in tasks.md completed and marked `[X]` +- [ ] All live (non-cancelled, non-superseded) tasks in tasks.md completed and marked `[X]` - [ ] Implementation validated against specification, plan, and test coverage - [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above - [ ] Completion reported to user with summary of completed work diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 664f428114..92d4eafb8f 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -59,9 +59,13 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline +Plan only **live** (unmarked) FRs, ACs, and SCs from the spec. Lines marked `SUPERSEDED` or `RETIRED` are historical — do not restore them in `plan.md`, `data-model.md`, or contracts. + +If `revisions.md` exists and `plan.md` was already patched, do **not** rebuild unless the latest `revisions.md` entry has `plan_status: needs-rebuild` or the user asked. Even on a rebuild, never re-plan a retired ID. + 1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). -2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). +2. **Load context**: Read FEATURE_SPEC and `/memory/constitution.md`. Load IMPL_PLAN template (already copied). Use only **live** spec lines; skip `SUPERSEDED` / `RETIRED`. 3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to: - Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION") diff --git a/templates/commands/revise.md b/templates/commands/revise.md new file mode 100644 index 0000000000..28f9722111 --- /dev/null +++ b/templates/commands/revise.md @@ -0,0 +1,161 @@ +--- +description: "Revise the current feature spec in place (add/remove ACs, FRs, stories) and cascade the change into plan and tasks." +handoffs: + - label: Create Plan + agent: speckit.plan + prompt: Only if plan.md is missing, or the latest revisions.md entry has plan_status: needs-rebuild. Inventory only live spec IDs. Do not restore SUPERSEDED or RETIRED lines. + - label: Create Tasks + agent: speckit.tasks + prompt: Only if tasks.md does not exist. Never regenerate an existing task list — revise already appended a Revision phase. Inventory only live spec IDs. + - label: Implement Revision Tasks + agent: speckit.implement + prompt: Implement only the new and cleanup tasks from the latest revision phase. Skip if plan.md is missing or plan_status is needs-rebuild. +scripts: + sh: scripts/bash/check-prerequisites.sh --json --paths-only + ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly + py: scripts/python/check_prerequisites.py --json --paths-only +--- + +The user is changing requirements on the **current** feature. That is why this command exists. + +Do **not** rewrite `spec.md`. Do **not** rewrite `plan.md`. Do **not** rewrite `tasks.md`. Mid-flight requirement changes used to do that — wipe the file and regenerate — and it destroyed history. You only mark old lines, append new ones, and add a short log. + +`__SPECKIT_COMMAND_SPECIFY__` still **creates or updates** a spec when there is no plan, tasks, or implementation yet. Use **this** command after `plan.md`, `tasks.md`, or implementation exist, to revise FRs / SCs / ACs and cascade into those artifacts. Not `__SPECKIT_COMMAND_PLAN__` or `__SPECKIT_COMMAND_TASKS__` (those rebuild artifacts). Not `__SPECKIT_COMMAND_CONVERGE__` (spec unchanged, code lagged). You do not write application code; `__SPECKIT_COMMAND_IMPLEMENT__` does. + +## Input + +```text +$ARGUMENTS +``` + +Use it if it isn't empty. Empty → ask what changed, or stop. Don't invent a delta. + +New product (different user, different outcome, no shared stories) → `__SPECKIT_COMMAND_SPECIFY__`. + +## Before you start (and again after you write) + +If `.specify/extensions.yml` exists, run `hooks.before_revise` now and `hooks.after_revise` after writes, before you report. + +Skip if the file is missing or invalid. Skip `enabled: false`. Skip hooks with a `condition`. Missing `enabled` means on. + +Mandatory (`optional: false`) — print this and **run** it. Skills-mode names may differ. The block alone is not enough. + +```text +## Extension Hooks +**Automatic Pre-Hook**: {extension} +Executing: `/{command}` +EXECUTE_COMMAND: {command} +``` + +Optional (`optional: true`): + +```text +## Extension Hooks +**Optional Pre-Hook**: {extension} +Command: `/{command}` +Description: {description} +Prompt: {prompt} +To execute: `/{command}` +``` + +After writes, drop "Pre-" from the labels. + +## The point + +Stay in this feature folder. Never create a new `specs/` directory. + +`spec.md` is the contract. Live lines are unmarked. Keep old lines visible as `SUPERSEDED by {new-id} (R{N})` or `RETIRED (R{N})`. Never two live items that contradict. + +Don't edit a live FR/AC/SC in place to mean something else. That hides the change. **Supersede or add.** Reword only for typos / tighter wording of the *same* behavior, and only if nothing has been implemented for it yet. + +`revisions.md` is a dated log, not a spec. One sentence + IDs. + +Don't renumber. Don't reuse IDs. Next ID = highest ever used + 1 (struck lines and the log count). + +Spec stays functional (what/why, not stack). Drop anything that violates a constitution `MUST`. Ignore an empty constitution template. + +## Already implemented? + +An ID is implemented only if a task that **references that ID** is `[x]` / `[X]`, or they said that behavior already shipped. A checked setup or foundational task does **not** mean later stories shipped. Any `[x]` on the feature is only a hint that *some* code exists — not a blanket remove-code trigger. + +| Change | Spec | If that ID is not implemented yet | If that ID is already implemented | +|---|---|---|---| +| New requirement | Add a new ID | Plan + tasks to **build** it | Same: plan + tasks to **add** the new code | +| Replaces a live item | SUPERSEDE old, add new ID | Plan + tasks for the new behavior; cancel open tasks for the old ID | Plan + tasks to **add** new code **and remove** the old code | +| No longer valid | RETIRE the old line; no new ID | Cancel open tasks for that ID | Plan + tasks to **remove** the old code | + +## Classify + +- Named `FR-004` / `US1/AC2` → that ID. +- Description matches one live item → that item. +- Ambiguous → ask. Don't guess. +- Same idea, clearer words, **not** implemented → reword, keep the ID. +- Swaps or contradicts a live item → supersede (old stays, new ID). +- Drop, nothing replaces it → retire. +- New, no conflict → add. New AC goes on the story they named, or the only story that fits. + +Already true on a **live** line (or same as last `R#`) is a duplicate. Skip it. All duplicates → stop, write nothing, don't bump `R{N}`. Mixed → do the new parts only. + +## Do the work + +1. Run `{SCRIPT}` once. Need `spec.md`. Also read `plan.md`, `tasks.md`, `revisions.md`, `constitution.md` if present. No spec → `__SPECKIT_COMMAND_SPECIFY__`. Quotes: `'I'\''m Groot'`. + +2. List live stories, ACs, FRs, SCs. Remember every ID ever issued. If `tasks.md` exists, note next T-id, next phase, and which IDs already have a referencing task marked `[x]`. + +3. Turn the input into add / reword / supersede / remove. Drop duplicates and constitution clashes. Nothing left → stop. + +4. Preview only if they asked, or a retire would wipe the last AC on a P1 story. `N` = 1 or last `R#`+1. + +5. **spec.md — append and mark, never replace the file.** + - add: next ID (AC holes are fine). + - reword: only the live sentence, same ID. + - supersede: `- **FR-004** ~~password login~~ — SUPERSEDED by **FR-008** (R2)` plus a new FR-008 line. + - remove: strike + `RETIRED (R2)`. + - Set `**Last Revised**: {today} (R{N})`. Leave `**Created**`. + +6. **plan.md — only if it exists. Never regenerate.** + - New / supersede: a short bullet for **adding** the new behavior. + - Supersede or retire **and that old ID is implemented**: a short bullet for **removing** the old behavior (what to delete, not a new architecture). + - Supersede or retire **and that old ID is not implemented**: no remove-code bullet; just stop planning the old thing. + - Strike the old plan bullet (`SUPERSEDED` / `RETIRED`). Don't delete it. + - Prefer a patch. If you truly can't patch, set `plan_status: needs-rebuild` — still don't wipe the file; next command is `__SPECKIT_COMMAND_PLAN__`. + - No plan yet → `plan_status: missing`; next command is `__SPECKIT_COMMAND_PLAN__`. + +7. **tasks.md — only if it exists. Never regenerate. Only append.** + - Add `## Phase {n}: Revision R{N}`. + - New/supersede: tasks to **write the new code**. + - That old ID is implemented + supersede/retire: tasks to **delete or stop using the old code**. + - Open task for an old ID: `- [ ] ~~T012~~ SUPERSEDED (R{N} → T020)` or `CANCELLED` if nothing replaces it. Don't delete the line. + - Finished task for an old ID: leave `[x]`; add the cleanup task in the new phase. + - No tasks file yet → `__SPECKIT_COMMAND_TASKS__` (or plan first). Do not invent a full task list here. Never hand off to tasks when `tasks.md` already exists. + +8. Append to `revisions.md` (create with a one-line header: spec.md is the source of truth): + + ```markdown + ## R{N} — {YYYY-MM-DD} + {one sentence} + - added: {id} + - superseded: {old} → {new} + - retired: {id} + - reworded: {id} + - plan_status: patched | needs-rebuild | missing | unchanged + ``` + + Skip empty bullets. Don't edit old entries. Don't append on a no-op. Always persist `plan_status` on a real revision so the next session can see it. + +9. Refresh `checklists/requirements.md` if it exists, **before** after-hooks. Re-evaluate checkboxes against **live** spec lines only. Do not rewrite the file from scratch. + +10. Run `hooks.after_revise`. + +Tell them once what was marked, what was added, and whether implement should **add** code, **remove** code, or both. Always include `plan_status:` as one of `patched` (plan bullets added or struck), `needs-rebuild` (could not patch safely), `missing` (no `plan.md`), or `unchanged` (no-op or plan needed no edit). Persist that same value on the new `revisions.md` entry. + +Next command: `needs-rebuild` or `missing` → `__SPECKIT_COMMAND_PLAN__`. Then if `tasks.md` is missing → `__SPECKIT_COMMAND_TASKS__`. Otherwise `__SPECKIT_COMMAND_IMPLEMENT__`. + +## Done When + +- [ ] Same folder; spec was not rewritten from scratch +- [ ] Old requirements SUPERSEDED or RETIRED; new ones are new IDs +- [ ] `tasks.md` / `plan.md` were not regenerated — only patched or skipped +- [ ] After implement: tasks exist to add new code and/or remove old code +- [ ] Dated log only if something changed; no application code +- [ ] Hooks handled; short report diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 54151e8b42..da8a6d45e4 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -18,6 +18,8 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +If the current feature already has `plan.md`, `tasks.md`, or implemented work, and the user stated a **concrete delta** (add, remove, or reword a named AC, FR, SC, or story), do **not** create another directory and do **not** run `before_specify` hooks. Recommend `__SPECKIT_COMMAND_REVISE__` and stop. If only `spec.md` exists (not yet planned, tasked, or implemented), this command can still **update** that spec. A genuinely new product continues below as a new feature. + ## Pre-Execution Checks **Check for extension hooks (before specification)**: @@ -109,6 +111,7 @@ Given that feature description, do this: - You must only create one feature per `__SPECKIT_COMMAND_SPECIFY__` invocation - The spec directory name and the git branch name are independent — they may be the same but that is the user's choice - The spec directory and file are always created by this command, never by the hook + - This command can create a new feature or **update** a spec that has no plan, tasks, or implementation yet. If `plan.md`, `tasks.md`, or implementation already exist and the user wants to add, remove, or reword FRs / SCs / ACs, do not create another directory — recommend `__SPECKIT_COMMAND_REVISE__` instead 4. Load the resolved active `spec-template` file to understand required sections. diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 64146a35aa..7e83953447 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -60,6 +60,8 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline +If `tasks.md` already exists and either `revisions.md` is present or `tasks.md` already has a `Revision R#` phase, do **not** regenerate the file. Stop and tell the user `__SPECKIT_COMMAND_REVISE__` already maintains the task list — it appends a Revision phase and cancels retired work. Only generate a full `tasks.md` when the file is missing. + 1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE_CONTENT, TASKS_TEMPLATE, and AVAILABLE_DOCS list. `FEATURE_DIR` and `TASKS_TEMPLATE` must be absolute paths when provided. `AVAILABLE_DOCS` is a list of document names/relative paths available under `FEATURE_DIR` (for example `research.md` or `contracts/`). For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot"). 2. **Load design documents**: Read from FEATURE_DIR: @@ -67,10 +69,11 @@ You **MUST** consider the user input before proceeding (if not empty). - **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios) - **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints - Note: Not all projects have all documents. Generate tasks based on what's available. + - Inventory only **live** (unmarked) FRs, ACs, and SCs. Never emit tasks for `SUPERSEDED` or `RETIRED` lines, even on first generation. 3. **Execute task generation workflow**: - - Load plan.md and extract tech stack, libraries, project structure - - Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.) + - Load plan.md and extract tech stack, libraries, project structure (skip `SUPERSEDED` / `RETIRED` bullets) + - Load spec.md and extract **live** user stories with their priorities (P1, P2, P3, etc.) - If data-model.md exists: Extract entities and map to user stories - If contracts/ exists: Map interface contracts to user stories - If research.md exists: Extract decisions for setup tasks diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md index 36c12316e5..b667578708 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -67,6 +67,7 @@ git config --get remote.origin.url 1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `__SPECKIT_COMMAND_CONVERGE__` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked. 1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: `, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`). - **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`). + - **Skip** any task line that contains `CANCELLED`, `SUPERSEDED`, or a struck-through task ID (`~~T012~~`). Retired or replaced work must not become a GitHub issue. Still create issues for the **new** live tasks under a `Revision R#` phase. - Only create issues for tasks that do not yet have a matching issue. > [!CAUTION] diff --git a/tests/extensions/git/test_git_extension.py b/tests/extensions/git/test_git_extension.py index f6be51caf6..72a8e8e3a1 100644 --- a/tests/extensions/git/test_git_extension.py +++ b/tests/extensions/git/test_git_extension.py @@ -168,8 +168,12 @@ def test_manifest_hooks(self): assert "before_specify" in m.hooks assert "after_specify" in m.hooks assert "after_implement" in m.hooks + assert "before_revise" in m.hooks + assert "after_revise" in m.hooks assert m.hooks["before_constitution"]["command"] == "speckit.git.initialize" assert m.hooks["before_specify"]["command"] == "speckit.git.feature" + assert m.hooks["before_revise"]["command"] == "speckit.git.commit" + assert m.hooks["after_revise"]["command"] == "speckit.git.commit" def test_manifest_command_files_exist(self): """All command files referenced in the manifest exist.""" diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index e94f58c6ff..baca7faf2a 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -220,7 +220,7 @@ def test_integration_flag_creates_files(self, tmp_path): COMMAND_STEMS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 015152be01..b088c607ce 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -95,7 +95,7 @@ def test_skill_directory_structure(self, tmp_path): expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", } # Derive command names from the skill directory names @@ -466,7 +466,7 @@ def test_options_include_skills_flag(self): _SKILL_COMMANDS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", ] def _expected_files(self, script_variant: str) -> list[str]: diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index 1b1bb18807..4664680897 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -468,6 +468,7 @@ def test_integration_flag_creates_files(self, tmp_path): "implement", "plan", "checklist", + "revise", "specify", "tasks", "taskstoissues", diff --git a/tests/integrations/test_integration_base_yaml.py b/tests/integrations/test_integration_base_yaml.py index 01914c5988..8325779931 100644 --- a/tests/integrations/test_integration_base_yaml.py +++ b/tests/integrations/test_integration_base_yaml.py @@ -382,6 +382,7 @@ def test_integration_flag_creates_files(self, tmp_path): "implement", "plan", "checklist", + "revise", "specify", "tasks", "taskstoissues", diff --git a/tests/integrations/test_integration_bob.py b/tests/integrations/test_integration_bob.py index 52a25ae2c6..84af679b5f 100644 --- a/tests/integrations/test_integration_bob.py +++ b/tests/integrations/test_integration_bob.py @@ -246,7 +246,7 @@ def test_skill_directory_structure(self, tmp_path): expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", } actual_commands = {f.parent.name.removeprefix("speckit-") for f in created} assert actual_commands == expected_commands @@ -409,7 +409,7 @@ def test_init_default_complete_file_inventory_sh(self, tmp_path): commands = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", ] for cmd in commands: assert (project / ".bob" / "skills" / f"speckit-{cmd}" / "SKILL.md").exists(), ( diff --git a/tests/integrations/test_integration_copilot.py b/tests/integrations/test_integration_copilot.py index 35d30faf0e..28adad9417 100644 --- a/tests/integrations/test_integration_copilot.py +++ b/tests/integrations/test_integration_copilot.py @@ -145,10 +145,10 @@ def test_directory_structure(self, tmp_path): agents_dir = tmp_path / ".github" / "agents" assert agents_dir.is_dir() agent_files = sorted(agents_dir.glob("speckit.*.agent.md")) - assert len(agent_files) == 10 + assert len(agent_files) == 11 expected_commands = { "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} assert actual_commands == expected_commands @@ -256,6 +256,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/agents/speckit.converge.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", + ".github/agents/speckit.revise.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", @@ -266,6 +267,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".github/prompts/speckit.converge.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", + ".github/prompts/speckit.revise.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", @@ -321,6 +323,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/agents/speckit.converge.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", + ".github/agents/speckit.revise.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", @@ -331,6 +334,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".github/prompts/speckit.converge.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", + ".github/prompts/speckit.revise.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", @@ -366,7 +370,7 @@ class TestCopilotSkillsMode: _SKILL_COMMANDS = [ "analyze", "clarify", "constitution", "converge", "implement", - "plan", "checklist", "specify", "tasks", "taskstoissues", + "plan", "checklist", "revise", "specify", "tasks", "taskstoissues", ] def _make_copilot(self): diff --git a/tests/integrations/test_integration_generic.py b/tests/integrations/test_integration_generic.py index 02176be1b0..c548549830 100644 --- a/tests/integrations/test_integration_generic.py +++ b/tests/integrations/test_integration_generic.py @@ -266,6 +266,7 @@ def test_implement_loads_constitution_context(self, tmp_path): "implement", "plan", "checklist", + "revise", "specify", "tasks", "taskstoissues", @@ -335,6 +336,7 @@ def test_complete_file_inventory_sh(self, tmp_path): ".myagent/commands/speckit.converge.md", ".myagent/commands/speckit.implement.md", ".myagent/commands/speckit.plan.md", + ".myagent/commands/speckit.revise.md", ".myagent/commands/speckit.specify.md", ".myagent/commands/speckit.tasks.md", ".myagent/commands/speckit.taskstoissues.md", @@ -394,6 +396,7 @@ def test_complete_file_inventory_ps(self, tmp_path): ".myagent/commands/speckit.converge.md", ".myagent/commands/speckit.implement.md", ".myagent/commands/speckit.plan.md", + ".myagent/commands/speckit.revise.md", ".myagent/commands/speckit.specify.md", ".myagent/commands/speckit.tasks.md", ".myagent/commands/speckit.taskstoissues.md", diff --git a/tests/test_revise_command.py b/tests/test_revise_command.py new file mode 100644 index 0000000000..5278e9015e --- /dev/null +++ b/tests/test_revise_command.py @@ -0,0 +1,266 @@ +"""Contracts for the core ``/speckit.revise`` command. + +The command file is the behavior. These checks lock the rules that make +revise a living-spec edit instead of a second specify, and lock the +cascade so implement / taskstoissues / analyze stay safe. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from specify_cli.integrations.base import IntegrationBase + + +REPO_ROOT = Path(__file__).resolve().parent.parent +COMMANDS = REPO_ROOT / "templates" / "commands" +REVISE = COMMANDS / "revise.md" + + +def test_revise_template_exists(): + assert REVISE.is_file() + + +def test_list_command_templates_includes_revise(): + from specify_cli.integrations.base import MarkdownIntegration + + class _Probe(MarkdownIntegration): + key = "probe" + config = { + "name": "Probe", + "folder": ".probe/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".probe/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + + stems = {p.stem for p in _Probe().list_command_templates()} + assert "revise" in stems + assert "specify" in stems + + +def test_revise_command_ref_resolves_to_speckit_revise(): + resolved = IntegrationBase.resolve_command_refs( + "next: __SPECKIT_COMMAND_REVISE__", separator=".", prefix="/" + ) + assert resolved == "next: /speckit.revise" + + +def test_skill_descriptions_include_revise(): + from specify_cli import SKILL_DESCRIPTIONS + + assert "revise" in SKILL_DESCRIPTIONS + assert "Create or update feature specifications" in SKILL_DESCRIPTIONS["specify"] + + +def test_revise_has_scripts_frontmatter(): + text = REVISE.read_text(encoding="utf-8") + assert "sh: scripts/bash/check-prerequisites.sh --json --paths-only" in text + assert "ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly" in text + assert "py: scripts/python/check_prerequisites.py --json --paths-only" in text + + +@pytest.mark.parametrize("script_type", ["sh", "ps", "py"]) +def test_revise_template_renders(script_type: str, monkeypatch): + monkeypatch.setattr( + "specify_cli.integrations.base.shutil.which", + lambda name: "/usr/bin/python3" if name == "python3" else None, + ) + monkeypatch.setattr( + "specify_cli.integrations.base.IntegrationBase._interpreter_runs", + staticmethod(lambda path: True), + ) + content = REVISE.read_text(encoding="utf-8") + result = IntegrationBase.process_template(content, "agent", script_type) + assert "{SCRIPT}" not in result + assert "$ARGUMENTS" in result or "{{args}}" in result + + +class TestReviseInvariants: + def setup_method(self): + self.text = REVISE.read_text(encoding="utf-8") + + def test_does_not_create_a_new_feature_directory(self): + assert "after `plan.md`, `tasks.md`, or implementation exist" in self.text + + def test_edits_spec_in_place(self): + assert "in place" in self.text + assert "spec.md" in self.text + + def test_records_revisions_changelog(self): + assert "revisions.md" in self.text + assert "dated log" in self.text + assert "not a spec" in self.text + + def test_duplicates_are_a_noop(self): + assert "duplicate" in self.text + assert "don't bump `R{N}`" in self.text or "don't append" in self.text + + def test_stable_ids_are_never_reused(self): + assert "Don't reuse" in self.text or "Don't renumber" in self.text + + def test_handles_add_and_remove_acceptance_criteria(self): + assert "Brand new" in self.text or "add" in self.text + assert "RETIRED" in self.text + + def test_cancels_open_tasks_instead_of_deleting_them(self): + assert "CANCELLED" in self.text + assert "SUPERSEDED" in self.text + + def test_supersede_marks_old_and_adds_new(self): + assert "supersede" in self.text + assert "SUPERSEDED by" in self.text + assert "contradict" in self.text + + def test_does_not_write_application_code(self): + assert "application code" in self.text + + def test_never_rewrites_artifacts(self): + assert "Never regenerate" in self.text or "never regenerate" in self.text.lower() or "Do **not** rewrite" in self.text + assert "the old code" in self.text + + def test_implemented_is_per_id_not_any_checked_task(self): + assert "references that ID" in self.text + assert "not a blanket remove-code trigger" in self.text + + def test_reports_named_plan_status(self): + assert "plan_status:" in self.text + assert "needs-rebuild" in self.text + assert "patched" in self.text + assert "missing" in self.text + assert "- plan_status:" in self.text + assert "Persist that same value" in self.text + + def test_tasks_handoff_refuses_to_regenerate(self): + assert "agent: speckit.tasks" in self.text + assert "Never regenerate an existing task list" in self.text + + def test_hands_off_to_implement_plan_or_tasks(self): + assert "__SPECKIT_COMMAND_IMPLEMENT__" in self.text + assert "__SPECKIT_COMMAND_PLAN__" in self.text + assert "agent: speckit.tasks" in self.text + assert "agent: speckit.plan" in self.text + assert "plan_status: needs-rebuild" in self.text + + def test_uses_script_placeholder(self): + assert "{SCRIPT}" in self.text + + def test_has_mandatory_post_execution_hooks(self): + assert "hooks.after_revise" in self.text + assert "EXECUTE_COMMAND:" in self.text + assert "## Done When" in self.text + + +class TestCascadeContracts: + def test_implement_skips_cancelled_tasks(self): + text = (COMMANDS / "implement.md").read_text(encoding="utf-8") + assert "CANCELLED" in text + assert "SUPERSEDED" in text + assert "not executable" in text + assert "only when `tasks.md` is **absent**" in text + assert "Do not regenerate" in text + + def test_taskstoissues_skips_cancelled_tasks(self): + text = (COMMANDS / "taskstoissues.md").read_text(encoding="utf-8") + assert "CANCELLED" in text + assert "must not become a GitHub issue" in text + + def test_analyze_treats_revisions_as_history(self): + text = (COMMANDS / "analyze.md").read_text(encoding="utf-8") + assert "revisions.md" in text + assert "retired" in text.lower() + assert "__SPECKIT_COMMAND_REVISE__" in text + assert "Only **live**" in text + assert "Do not create keys for `SUPERSEDED` or `RETIRED` IDs" in text + assert "Skip plan bullets marked `SUPERSEDED` or `RETIRED`" in text + + def test_specify_defers_to_revise_after_plan_tasks_or_impl(self): + text = (COMMANDS / "specify.md").read_text(encoding="utf-8") + assert "Create or update the feature specification" in text + assert "__SPECKIT_COMMAND_REVISE__" in text + assert "`plan.md`, `tasks.md`, or implemented work" in text + assert "this command can still **update** that spec" in text + + def test_converge_defers_to_revise_when_spec_changed(self): + text = (COMMANDS / "converge.md").read_text(encoding="utf-8") + assert "__SPECKIT_COMMAND_REVISE__" in text + assert "SUPERSEDED" in text + assert "RETIRED" in text + assert "Only **live**" in text + assert "Do not create keys for `SUPERSEDED` or `RETIRED` IDs" in text + assert "Revision R#" in text + assert "__SPECKIT_COMMAND_IMPLEMENT__" in text + + def test_tasks_refuses_to_regenerate_after_revise(self): + text = (COMMANDS / "tasks.md").read_text(encoding="utf-8") + assert "do **not** regenerate the file" in text + assert "Revision R#" in text + assert "__SPECKIT_COMMAND_REVISE__" in text + assert "Inventory only **live**" in text + assert "even on first generation" in text + + def test_lean_implement_skips_cancelled_tasks(self): + text = ( + REPO_ROOT + / "presets" + / "lean" + / "commands" + / "speckit.implement.md" + ).read_text(encoding="utf-8") + assert "CANCELLED" in text + assert "SUPERSEDED" in text + assert "not count them as remaining work" in text + + def test_plan_inventories_only_live_ids(self): + text = (COMMANDS / "plan.md").read_text(encoding="utf-8") + assert "SUPERSEDED" in text + assert "RETIRED" in text + assert "do not restore them" in text + assert "plan_status: needs-rebuild" in text + + def test_lean_specify_defers_to_revise_after_plan_or_tasks(self): + text = ( + REPO_ROOT / "presets" / "lean" / "commands" / "speckit.specify.md" + ).read_text(encoding="utf-8") + assert "/speckit.revise" in text + assert "`plan.md`, `tasks.md`, or implementation already exist" in text + assert "this command may **update** it" in text + + def test_lean_plan_skips_retired_ids(self): + text = ( + REPO_ROOT / "presets" / "lean" / "commands" / "speckit.plan.md" + ).read_text(encoding="utf-8") + assert "SUPERSEDED" in text + assert "RETIRED" in text + assert "do not restore them" in text + assert "plan_status: needs-rebuild" in text + + def test_lean_tasks_refuses_to_overwrite_revision_list(self): + text = ( + REPO_ROOT / "presets" / "lean" / "commands" / "speckit.tasks.md" + ).read_text(encoding="utf-8") + assert "do **not** overwrite it" in text + assert "Revision R#" in text + assert "Never emit tasks for `SUPERSEDED` or `RETIRED` lines" in text + + def test_clarify_defers_known_deltas_to_revise(self): + text = (COMMANDS / "clarify.md").read_text(encoding="utf-8") + assert "__SPECKIT_COMMAND_REVISE__" in text + assert "concrete delta" in text + assert "do **not** replace or delete live FR/AC/SC lines" in text + assert "do **not** drop `SUPERSEDED` / `RETIRED` history" in text + assert "do not remove the old live line" in text + + def test_checklist_inventories_only_live_ids(self): + text = (COMMANDS / "checklist.md").read_text(encoding="utf-8") + assert "only **live**" in text + assert "SUPERSEDED" in text + assert "__SPECKIT_COMMAND_REVISE__" in text