diff --git a/pyproject.toml b/pyproject.toml index 5589876474..9d8f4fe8f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ packages = ["src/specify_cli"] # Page templates (exclude commands/ — bundled separately below to avoid duplication) "templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md" "templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md" +"templates/fix-template.md" = "specify_cli/core_pack/templates/fix-template.md" "templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-template.md" "templates/plan-template.md" = "specify_cli/core_pack/templates/plan-template.md" "templates/spec-template.md" = "specify_cli/core_pack/templates/spec-template.md" diff --git a/templates/commands/ask.md b/templates/commands/ask.md new file mode 100644 index 0000000000..1599e3ff36 --- /dev/null +++ b/templates/commands/ask.md @@ -0,0 +1,158 @@ +--- +description: Answer any question about the current feature, project, or Spec Kit workflow — grounded in the constitution, existing specs, and best practices — and route to the right next command. +handoffs: + - label: Fix an Error + agent: speckit.fix + prompt: "Fix this error: " + - label: Write a Spec + agent: speckit.specify + prompt: "Specify the following feature: " +scripts: + sh: scripts/bash/check-prerequisites.sh --json --paths-only + ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). This may be any question: conceptual, technical, workflow-related, or about a specific feature. + +--- + +## Goal + +Answer questions about the current Spec Kit project with grounded, actionable responses — and route to the right command when further action is needed. You are a knowledgeable guide, not an executor. You read before you answer. You route before you act. + +--- + +## Phase 0 — Classify the question + +Before reading any file, classify the input into one of these categories (zero file I/O): + +| Category | Examples | Files to read | Format | +|---|---|---|---| +| **simple** | "Reformulate this", "Give me a prompt for X", "What command do I run next?", "Explain Y in simple terms", "What is the order of commands?" | none — answer from knowledge | plain reply, no structured block, skip Phase 3 | +| **spec** | "Does my spec cover X?", "Is this user story complete?" | `spec.md` (relevant section only) | structured block | +| **plan** | "Is this architecture decision correct?", "Should I use X or Y?" | `plan.md` (relevant section only) | structured block | +| **constitution** | "Does this violate a project principle?", "Is X allowed?" | `constitution.md` | structured block | +| **error** | "Why is X failing?", "What is wrong with my code?" | redirect → `/speckit.fix` immediately | redirect only | +| **feature-gap** | "How do I add X?", "We need a new behavior" | redirect → `/speckit.specify` immediately | redirect only | +| **consistency** | "Are spec and plan aligned?", "Is tasks.md up to date?" | `spec.md` + `plan.md` + `tasks.md` | structured block | +| **open** | General question not fitting above | `constitution.md` only | structured block | + +**If category = `simple`:** answer immediately with no structured header block (no QUESTION/CATEGORY/GROUNDED IN/CONFIDENCE labels), no Phase 1 file loading, and no Phase 3 routing. The reply is the answer itself — nothing more. + +**Fast redirects (do not proceed past Phase 0):** +- If the question describes a broken behavior or an error → output redirect block and stop: + ``` + → This is a correction request, not a question. + Run: /speckit.fix "[paste your error here]" + ``` +- If the question requests a new feature or behavior → output redirect block and stop: + ``` + → This is a feature request, not a question. + Run: /speckit.specify "[describe what you need]" + ``` + +--- + +## Phase 1 — Load context + +Run `{SCRIPT}` from repo root only if the question category requires reading a project file (see table above). Parse `FEATURE_DIR` and `AVAILABLE_DOCS`. + +Load only the files identified in Phase 0 — and only the sections relevant to the question. Do not load artifacts speculatively. + +**Always read `constitution.md` when:** +- The question touches a project principle, constraint, or architectural decision +- The answer would suggest a change to an existing artifact +- The question category is `constitution` or `consistency` + +**Never read `constitution.md` proactively** for pure workflow questions. + +**For category `open`:** load `constitution.md` only. Load additional artifacts only if `constitution.md` content explicitly points to them. Do not guess which artifact is "closest". + +--- + +## Phase 2 — Answer + +**If category = `simple` (set in Phase 0):** skip this entire phase. Do not produce the structured block below. Write the answer directly as plain text and stop — do not proceed to Phase 3 or Phase 4. + +For all other categories, produce a structured response: + +``` +QUESTION : [restate the question in one line] +CATEGORY : [spec | plan | constitution | consistency | open] +GROUNDED IN : [knowledge | constitution.md | spec.md | plan.md | tasks.md | multiple] +CONFIDENCE : [high — answer is unambiguous | medium — interpretation required | low — insufficient context] + +ANSWER +────── +[Direct, precise answer. Reference file:section when quoting a spec or plan. + If CONFIDENCE = low, state clearly what additional context is needed and why. + Do not hedge unnecessarily — if you know, say it directly.] +``` + +### Rules for the answer + +1. **Base every answer on evidence** — quote the relevant section of the artifact when possible. +2. **Separate fact from recommendation** — clearly distinguish "the spec says X" from "best practice suggests Y". +3. **Respect the constitution** — if the answer would conflict with a principle, say so explicitly. Do not suggest actions that violate it. +4. **Acknowledge gaps honestly** — if the information needed to answer is absent from all artifacts, say so. Do not invent an answer. +5. **One question at a time** — if the input contains multiple questions, answer them in order, each with its own block. Do not merge unrelated answers. + +--- + +## Phase 3 — Route (conditional) + +Only produce a routing suggestion if the answer **explicitly reveals an actionable gap, inconsistency, or next step**. If the question was self-contained (a reformulation, a direct factual answer, a generated prompt, an explanation), **skip Phase 3 entirely — do not output a "SUGGESTED NEXT" block**. + +Ask yourself: "Did my answer uncover something that requires a follow-up command?" If no, stop after Phase 2. + +If routing is warranted, output: + +``` +SUGGESTED NEXT +────────────── +[command] [reason — what this command would do given what was just answered] +``` + +Use this routing table only when the answer reveals one of these conditions: + +| What the answer revealed | Suggested command | +|---|---| +| The spec has a gap or ambiguity | `/speckit.clarify "[the unresolved point]"` | +| A new behavior needs to be defined | `/speckit.specify "[what the system must do]"` | +| A technical decision needs to be made or revisited | `/speckit.plan` | +| Artifacts are inconsistent with each other | `/speckit.analyze` | +| A task is missing or mis-ordered | `/speckit.tasks` | +| An error or broken behavior was surfaced | `/speckit.fix "[the error]"` | +| Tasks are ready to execute | `/speckit.implement` | +| Edge cases should be tracked as issues | `/speckit.taskstoissues` | +| Cross-feature impact is possible | `/speckit.analyze` (after the fix or change) | + +**Never suggest a command for a question that was fully answered.** A complete, self-contained answer requires no routing. + +**Never suggest a command without a reason.** Each suggestion must say *why* that command is warranted given the answer. + +--- + +## Phase 4 — Confidence check + +If `CONFIDENCE = low` was set in Phase 2: +- **Suppress Phase 3 entirely** — do not output any `SUGGESTED NEXT` block. Missing context must be resolved before a command can be meaningfully suggested. +- Append: + +``` +BEFORE PROCEEDING +───────────────── +To answer this confidently, I need: + 1. [specific missing piece — e.g., "the full stack trace", "the spec.md of feature X", "which architecture was chosen in plan.md"] + 2. [optional second missing piece] + +Provide this directly in your next message. +``` + +Do not ask more than 2 clarifying questions. Do not ask for information that can be inferred from the artifacts already loaded. diff --git a/templates/commands/fix.md b/templates/commands/fix.md new file mode 100644 index 0000000000..fdded9f4df --- /dev/null +++ b/templates/commands/fix.md @@ -0,0 +1,407 @@ +--- +description: Receive an error (screenshot, log, message), diagnose it, and apply surgical corrections to spec files and source code within the current Spec Kit feature scope. +scripts: + sh: scripts/bash/check-prerequisites.sh --json --include-tasks + ps: scripts/powershell/check-prerequisites.ps1 -Json -IncludeTasks +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). This may contain an error message, a log block, a file path, or a description of what is broken. + +--- + +## Interactions with other Spec Kit commands + +`/speckit.fix` does not work in isolation. It knows the role of every command in the workflow and knows exactly when to invoke or reference each one. Full map: + +``` +/speckit.constitution → project-wide principles and constraints +/speckit.specify → defines the WHAT and WHY (user stories) +/speckit.clarify → clarifies ambiguous areas before planning +/speckit.plan → technical architecture and implementation decisions +/speckit.analyze → cross-artifact consistency check +/speckit.tasks → breaks plan into ordered, actionable tasks +/speckit.implement → executes tasks +/speckit.taskstoissues → converts tasks into GitHub issues +/speckit.fix → (you) post-implementation error correction +/speckit.ask → ask any question, get a grounded answer and routing suggestion +``` + +--- + +## Data Path Quick Reference + +Every project has a data flow chain, but naming varies by framework and architecture. +**Step 1: identify the project's own chain from the stack trace paths and directory names.** +**Step 2: map the error to a functional role. Fix the role where the error *originates*, not where it *surfaces*.** + +### Step 1 — Infer the project's chain + +Look at the file paths in the stack trace (or the `FEATURE_DIR` structure). Match against the most common patterns: + +| Pattern | Typical chain | Common in | +|---|---|---| +| `views/`, `serializers/`, `models/` | `urls → middleware → view → serializer → model → db` | Django, DRF | +| `controllers/`, `services/`, `models/` | `router → middleware → controller → service → model → db` | Express, NestJS, Laravel, Rails | +| `handlers/`, `usecases/`, `repositories/` | `handler → use case → repository → data source` | Clean Architecture, Hexagonal | +| `resolvers/`, `schema/` | `query → resolver → data loader → db` | GraphQL (Apollo, Strawberry) | +| `commands/`, `lib/`, `cli/` | `entrypoint → argument parser → command → lib` | CLI tools | +| `consumers/`, `producers/`, `aggregates/` | `event bus → consumer → aggregate → event store` | Event-driven, CQRS | +| `components/`, `hooks/`, `store/` | `component → hook/store → api client → backend` | React, Vue, Angular (frontend only) | +| `functions/`, `triggers/` | `trigger → function → external service` | Serverless (Lambda, Azure Functions) | + +If the project does not match any pattern, derive the chain from the actual directory names present in the stack trace. Name each role yourself — do not force a known pattern. + +### Step 2 — Map the error signature to a functional role + +Use **functional role names** that map to the project's own naming: + +| Error signature | Functional role | Typical files (adapt to project) | +|---|---|---| +| `IntegrityError`, `OperationalError`, `ColumnNotFound`, `ForeignKeyViolation` | **data-access** | `models/`, `repositories/`, `entities/`, `dao/` | +| `ValidationError`, `SchemaError`, `SerializerError`, `422 Unprocessable` | **validation** | `serializers/`, `schemas/`, `validators/`, `forms/` | +| `AttributeError`, `TypeError`, `KeyError` inside a logic file | **business-logic** | `services/`, `usecases/`, `domain/`, `lib/` | +| `500 Internal Server Error`, unhandled exception in entry point | **entry-point** | `views/`, `controllers/`, `handlers/`, `resolvers/` | +| `401 Unauthorized`, `403 Forbidden`, `InvalidTokenError` | **guard** | `middleware/`, `auth/`, `guards/`, `interceptors/` | +| `404 Not Found`, route/path not matched | **routing** | `urls.py`, `routes/`, `router.*`, `app.*` | +| `ModuleNotFoundError`, `ImportError` | **config** | the importing file only | +| `FAILED tests/test_.*::test_` | **test** | `tests/test_.*` + the file under test | +| JS/TS `TypeError`, `Cannot read properties of undefined` | **ui** | `components/`, `pages/`, `views/` (frontend) | +| `fetch failed`, `axios error`, `CORS`, network error on client | **api-bridge** | `services/api.*`, `client.*`, `middleware/cors.*` | + +**Rule: fix the functional role where the error *originates*, not where it *surfaces*.** +A `NullPointerException` at the entry point often originates in the data-access layer returning `None`. +An HTTP 500 in a view often originates in the business-logic layer throwing an uncaught exception. + +--- + +## Phase 0 — Pre-flight + +Before any extraction or triage, run these four checks in order. Each one can short-circuit the full workflow. + +### 0.1 — Confidence threshold + +If the input is too incomplete to triage (truncated log, blurry screenshot, ambiguous description), **do not guess**. Ask exactly one targeted question: + +> "To diagnose this precisely, I need: [the one missing piece — full stack trace / the file path / the action that triggered the error]. Can you provide it?" + +Do not proceed until you have enough information to fill the TRIAGE block. + +### 0.2 — Multi-error input + +If the input contains more than one distinct error (multiple FAILED tests, multiple exceptions): +1. List all errors found. +2. Identify the **most blocking** one (the one that causes others downstream, or the first in the execution chain). +3. Fix that one first. State explicitly: _"Fixing [error A] first. [Error B] and [Error C] are noted and will be addressed next if still present."_ + +### 0.3 — Recurrent error check + +If `specs/[feature]/fix.md` exists, scan it (titles only — do not read full entries) before diagnosing: +- If a previous `FIX-NNN` entry addresses the same error → read that entry's `ROOT CAUSE` and `Decisions` sections before building the TRIAGE. +- If a previous fix was applied and the error recurred → the root cause was misidentified. Flag this explicitly in Phase 2: `RECURRENT: YES — previous fix FIX-NNN did not resolve the root cause`. + +### 0.4 — Trivial fast path + +If the error is trivially identifiable (one of the below), skip Phases 1–2 entirely and go directly to Phase 3: + +| Trivial error | Direct action | +|---|---| +| `SyntaxError` with file:line | Open the file, fix the syntax, done | +| `ModuleNotFoundError: No module named 'x'` | Add the import or install the dependency | +| `NameError: name 'x' is not defined` | Check for typo or missing import | +| Typo in a config key (e.g. `DATABSE_URL`) | Fix the key name, done | +| `IndentationError` | Fix indentation at the given line | + +For trivial fixes: write the `fix.md` entry with `SCOPE: 1 file`, skip Phase 4 invariants (write `not applicable — trivial fix`). + +--- + +## Phase 1 — Extraction & Context reading + +### 1.1 Extract the error + +If `FEATURE_DIR` is not identifiable from the stack trace paths, run `{SCRIPT}` from repo root to derive it. + +If an image is provided, extract: +- The **exact error message** (verbatim text) +- The **stack trace** if present (file, line, column) +- The **error type** (runtime, compile, test, lint, network, logic) +- The **visible context** (which screen, which action, which endpoint) + +If code or logs are pasted, identify: +- The first abnormal line (the true entry point of the error) +- The call chain that led to this state + +### 1.2 Triage — classify the error + +**Before opening any file**, produce this block from the error message and stack trace alone (zero file I/O): + +``` +TRIAGE + Error type : [ValueError | HTTP 500 | FAILED | TypeError | etc.] + Stack entry : [file:line — the exact line that threw] + Role : [functional role in this project's chain — e.g. data-access | validation | business-logic | entry-point | guard | routing | ui | api-bridge | config | test] + Read set : [2–5 files to open — derived from the Data Path Quick Reference — and nothing else] +``` + +Use **Step 1** of the Data Path Quick Reference to identify the project's own chain, then **Step 2** to map the error signature to a functional role. Open only the files listed in `Read set`. + +If multiple features exist, identify the one related to the error (module name, endpoint, component) before building the read set. + +**Third-party guard**: if `Stack entry` points to a file inside `node_modules/`, `site-packages/`, `vendor/`, or any external dependency directory, the bug is in your call site, not in the library. Shift `Stack entry` to the last in-project frame in the stack trace, and derive `Read set` from that frame instead. + +### 1.3 Selective spec read + +Read spec/plan/tasks **only if** one of these conditions holds after the triage — otherwise skip directly to Phase 2: + +| Condition | What to read | +|---|---| +| The fix would change a public API or data contract | `plan.md` — the relevant section only | +| The expected behavior is unclear from the code alone | `spec.md` — the section relevant to the broken feature only | +| A task is confirmed missing or wrongly sequenced | `tasks.md` only | +| The fix may violate a project-wide constraint | `constitution.md` — if violated, **STOP** | +| None of the above | **Read nothing.** Fix directly from the Read Set. | + +`constitution.md` is **never** read proactively — only as a guard when a fix might violate it. + +After reading (if applicable): +- **Does the fix violate a principle in `constitution.md`?** → if yes, STOP +- **Does the error come from a gap between the plan and the implementation?** +- **Does the spec describe a different behavior from what is coded?** +- **Is there a task in `tasks.md` that was completed incorrectly or is missing entirely?** +- **Does the fix touch multiple features?** → recommend `/speckit.analyze` afterwards + +--- + +## Phase 2 — Structured diagnosis + +Produce a layered diagnosis before writing anything: + +``` +LAYER : [functional role in this project — e.g. data-access | validation | business-logic | entry-point | guard | routing | ui | api-bridge | config | test] +ROOT CAUSE : [precise technical cause, 1 sentence, referencing file:line] +CHAIN IMPACT : [does this error propagate to upstream roles? YES / NO — which ones?] +SPEC IMPACT : [none | spec.md | plan.md | tasks.md | multiple — only if triage triggered a read] +NEW FEATURE : [YES / NO — does a full resolution require behavior absent from all specs?] +SCOPE : [2–5 files maximum — code files only unless spec read was triggered] +``` + +**If `SCOPE` lists more than 5 files → this is not a fix, it is a refactoring. Stop. Recommend `/speckit.plan` to revisit the architecture before proceeding.** + +**If `NEW FEATURE = YES` → stop immediately and go to Phase 2b. Do not modify any file.** + +--- + +## Phase 2b — Escalation: new feature detected + +This phase is triggered **only if `NEW FEATURE = YES`** in the diagnosis. + +### When to escalate? + +The error requires a new feature (not a correction) if: +- The expected behavior exists **nowhere** in `spec.md`, `plan.md`, or `tasks.md` +- Implementing the fix would require adding a new module, endpoint, flow, or role not in scope +- The fix would impose an architectural decision that exceeds the scope of a correction +- The spec explicitly covers a different behavior — changing it would be an **evolution**, not a fix + +### What you do + +1. **Apply no correction.** Zero file changes. +2. **Explain the gap** in 2-3 sentences: what feature is missing, why the fix cannot exist without it. +3. **Generate a ready-to-use `/speckit.specify` prompt**, precisely describing what is missing. + +### Escalation output format + +``` +⚠️ ESCALATION — New feature required + +This error cannot be fixed within the current spec scope. + +**Gap identified**: [2-3 sentence description of the missing behavior and why +it does not exist in the spec] + +**Closest existing feature**: [feature or user story in spec.md that comes +closest, or "none" if entirely new] + +--- + +Run this command to specify the missing feature: + +/speckit.specify "[full description of the need — what the system must do, +in what context, for which user, with what expected outcome. Do not mention +the tech stack. Be precise about the WHY: why this behavior is necessary. +Include nominal cases and expected failure cases.]" + +--- + +Full workflow to follow next: + /speckit.specify → define the need + /speckit.clarify → (recommended) resolve ambiguities + /speckit.plan → technical architecture + /speckit.analyze → check consistency with existing features + /speckit.tasks → break into tasks + /speckit.implement → implement + /speckit.fix → correct any errors that appear during implementation +``` + +### Rules for building the `/speckit.specify` prompt + +The generated prompt must: +- Describe the **what** and **why**, not the **how** +- Mention the relevant user (role, usage context) +- Cover the **nominal case** (what must work) +- Cover at least **one failure case** (what must be handled) +- Be usable as-is without modification — it is a working prompt, not a draft + +Well-formed prompt example: +``` +/speckit.specify "When a user attempts an action that exceeds their permissions, +the system must display an explicit error message indicating what they can do instead, +rather than silently failing or redirecting to the home page. +Nominal case: the user sees the message and can navigate to an authorized action. +Failure case: if no alternative action exists, the message states this clearly." +``` + +--- + +### When `/speckit.fix` interacts with each command + +| Command | `/speckit.fix` interacts when... | Action taken by `/speckit.fix` | +|---|---|---| +| `constitution` | The fix violates or exceeds a governing principle | Flag the conflict, **do not fix** — this file is read-only | +| `specify` | The error reveals unspecified behavior → new feature needed | Produce a ready-to-use `/speckit.specify` prompt (Phase 2b) | +| `clarify` | The spec is ambiguous and multiple interpretations are possible | Recommend `/speckit.clarify` before proceeding | +| `plan` | The fix requires revisiting an architectural decision | Update `plan.md` AND flag that `/speckit.plan` must be re-validated | +| `analyze` | The fix touches multiple features or creates cross-artifact inconsistency | Recommend `/speckit.analyze` after applying the fix | +| `tasks` | A task in `tasks.md` is missing, mis-ordered, or poorly defined | Update `tasks.md` directly; add any missing tasks | +| `implement` | The fix corrects an incomplete implementation of an existing task | Fix the code AND mark the relevant task in `tasks.md` | +| `taskstoissues` | After the fix, uncovered edge cases should be tracked as issues | Suggest `/speckit.taskstoissues` to open them | + +--- + +## Phase 3 — Applying corrections + +### Absolute rules + +1. **Read before writing**: read the full file before any modification. +2. **Minimal change**: only modify what is broken. No opportunistic refactoring. +3. **Spec ↔ code consistency**: if you fix code in a way that diverges from the spec, update the spec at the same time. +4. **Respect the constitution**: every correction must stay within the constraints defined in `constitution.md`. +5. **No regression**: before applying, verify the fix does not break another user story. + +### Corrections in spec `.md` files + +Based on the `SPEC IMPACT` identified in Phase 2: + +**`spec.md` affected** (ambiguous requirement, uncovered case, undefined behavior): +- Add or correct the relevant user story or acceptance criteria +- If the ambiguity runs deep → recommend `/speckit.clarify` before proceeding + +**`plan.md` affected** (incorrect or incomplete technical decision): +- Adjust the faulty technical decision +- Explicitly note: _"This plan change should be re-validated via `/speckit.plan`"_ + +**`tasks.md` affected** (missing, mis-ordered, or poorly defined task): +- Mark the affected task as revised +- Add any missing sub-tasks or tasks with their dependencies +- If follow-up tasks should be tracked → suggest `/speckit.taskstoissues` + +**Multiple artifacts affected**: +- Apply in order: `spec.md` → `plan.md` → `tasks.md` → code +- After applying → recommend `/speckit.analyze` to verify global consistency + +Traceability marker in all modified `.md` files: +```markdown + +``` +Add this comment on the line above every modified section. + +### Corrections in code + +Apply changes directly. For each modified file, state: +- The file and relevant line +- The exact problem +- The change applied (clear mental diff) + +--- + +## Phase 4 — Break the Logic + +After correction, **break the logic into verifiable invariants**. For each applied fix, explicitly state: + +``` +INVARIANT 1 : [condition that must ALWAYS be true after this fix] +INVARIANT 2 : [condition that must ALWAYS be true after this fix] +EDGE CASE : [boundary condition this fix does NOT yet cover — to watch] +``` + +Examples: +- `INVARIANT: a user without 'admin' role can never reach /admin/*` +- `INVARIANT: an account balance can never be negative after a transfer` +- `EDGE CASE: two concurrent transfers on the same account are not handled here` + +For each edge case listed → evaluate whether a follow-up issue is warranted and suggest `/speckit.taskstoissues` if so. + +### Validation test (mandatory) + +Before moving to Phase 5, state how to verify the fix is effective: + +``` +VALIDATION : [exact command, scenario, or navigation path that confirms the error is gone + — e.g. "run pytest tests/test_payment.py::test_transfer", + "POST /api/orders with missing field → expect 422 not 500", + "navigate to /checkout as anonymous user → expect redirect to /login"] +``` + +If no automated test covers this scenario → flag it: +`COVERAGE GAP: this fix has no automated test. Consider adding one via /speckit.tasks.` + +--- + +## Phase 5 — Write to fix.md + final report + +### 5.1 Write the entry in `specs/[###-feature-name]/fix.md` + +**This step is mandatory, not optional.** + +- If `fix.md` does not yet exist → create it from `.specify/templates/fix-template.md`, then write the first entry. +- If `fix.md` exists → read the last entry number, increment, **prepend** the new entry (most recent at top). + +Fill every field in the template — leave nothing blank. If a section does not apply (e.g. no `.md` files modified), write `not modified` explicitly — do not delete the line. + +### 5.2 Report displayed in chat + +After writing to `fix.md`, display this summary in the conversation: + +``` +## Correction Report + +**Error addressed** : [original error message] +**Root cause** : [cause in 1 sentence] +**Entry logged** : specs/[###-feature-name]/fix.md → FIX-[NNN] + +**Files modified**: +- [ ] `specs//spec.md` — [description or "not modified"] +- [ ] `specs//plan.md` — [description or "not modified"] +- [ ] `specs//tasks.md` — [description or "not modified"] +- [ ] `src/...` — [description of the change] + +**Recommended Spec Kit follow-up commands**: +- [ ] /speckit.clarify — [if residual ambiguity remains in the spec] +- [ ] /speckit.plan — [if plan.md was modified] +- [ ] /speckit.analyze — [if multiple features were touched] +- [ ] /speckit.taskstoissues — [if edge cases should be tracked as issues] + +**Invariants established**: +- [list] + +**Edge cases not covered**: +- [list — full honesty] +``` + + diff --git a/templates/fix-template.md b/templates/fix-template.md new file mode 100644 index 0000000000..409f22e0d2 --- /dev/null +++ b/templates/fix-template.md @@ -0,0 +1,82 @@ + + +# Fix Log — [FEATURE NAME] + +Branch: [###-feature-name] | Spec: [link to spec.md] | Plan: [link to plan.md] + +> Chronological record of all corrections applied to this feature after implementation. +> Order: newest first. Each entry is written by `/speckit.fix` at the time of the correction. + +--- + + + +## FIX-001 · [YYYY-MM-DD] · [Short title — what was broken and what was done] + +> **Error origin** +> ``` +> [Verbatim error message, stack trace, or description of the screenshot] +> ``` + +| Field | Value | +|---|---| +| **Error type** | `runtime` \| `compile` \| `test` \| `lint` \| `network` \| `logic` | +| **Detected in** | `[file path or UI screen where the error appeared]` | +| **Root cause** | [One precise sentence — the actual cause, not a paraphrase of the error] | +| **Spec impact** | `none` \| `spec.md` \| `plan.md` \| `tasks.md` \| `multiple` | + +--- + +### Decisions + +| # | Decision | Rationale | +|---|---|---| +| 1 | [Technical or spec choice made] | [Why this and not an alternative] | +| 2 | [Technical or spec choice made] | [Why this and not an alternative] | + +--- + +### Files modified + +| File | Type | Change description | +|---|---|---| +| `specs/[###-feature-name]/spec.md` | `spec` | [What changed and why — or "not modified"] | +| `specs/[###-feature-name]/plan.md` | `plan` | [What changed and why — or "not modified"] | +| `specs/[###-feature-name]/tasks.md` | `tasks` | [What changed and why — or "not modified"] | +| `src/[path/to/file]` | `code` | [What changed and why] | + +--- + +### Invariants established + +- `INVARIANT:` [Condition that must always be true after this fix] +- `INVARIANT:` [Condition that must always be true after this fix] + +### Edge cases not covered + +- `EDGE CASE:` [Boundary condition identified but not addressed in this fix] + +### Spec Kit follow-up commands + +- [ ] `/speckit.clarify` — [reason, if applicable] +- [ ] `/speckit.plan` — [reason if plan.md was modified] +- [ ] `/speckit.analyze` — [reason if multiple features were touched] +- [ ] `/speckit.taskstoissues` — [reason if edge cases should be tracked as issues] + +--- + + diff --git a/tests/test_fix_feature.py b/tests/test_fix_feature.py new file mode 100644 index 0000000000..253d4bdc3e --- /dev/null +++ b/tests/test_fix_feature.py @@ -0,0 +1,638 @@ +""" +Tests for the /speckit.fix feature (templates/commands/fix.md and templates/fix-template.md). + +Invariants verified +─────────────────── + fix.md — command template + • Valid YAML frontmatter (parseable by yaml.safe_load) + • Has non-empty 'description' field + • Has 'scripts.sh' referencing check-prerequisites.sh + • Has 'scripts.ps' referencing check-prerequisites.ps1 + • Contains the $ARGUMENTS placeholder + • Body references all 9 Spec Kit workflow commands + • Contains the mandatory 4-point diagnosis block (ROOT CAUSE, SPEC IMPACT, + NEW FEATURE, SCOPE) + • Contains the escalation guard ("NEW FEATURE = YES") + • Contains all 5 execution phases + • References fix.md as the output log file + • No stray double-brace placeholders leaked from TOML format + + fix-template.md — log scaffold + • File exists in templates/ + • Contains the FIX-NNN · date · title header pattern + • Contains all 4 metadata table rows (Error type, Detected in, Root cause, + Spec impact) + • Contains the Decisions, Files modified, Invariants established, and + Edge cases not covered sections + • Uses the INVARIANT: and EDGE CASE: prefixes + • Contains the Spec Kit follow-up commands checklist + • References all four follow-up commands (/speckit.clarify, /speckit.plan, + /speckit.analyze, /speckit.taskstoissues) + • Newest-first ordering comment is present + + bundle inclusion + • 'fix' stem is returned by _get_source_template_stems() + • pyproject.toml declares fix-template.md in the data-files section +""" + +import re +from pathlib import Path + +import pytest +import yaml + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_FIX_CMD = _REPO_ROOT / "templates" / "commands" / "fix.md" +_FIX_TMPL = _REPO_ROOT / "templates" / "fix-template.md" +_PYPROJECT = _REPO_ROOT / "pyproject.toml" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _parse_frontmatter(text: str) -> tuple[dict, str]: + """Return (frontmatter_dict, body) for a Markdown file with YAML frontmatter. + + Returns ({}, text) if no frontmatter delimiters are found. + """ + if not text.startswith("---"): + return {}, text + end = text.index("---", 3) + fm_text = text[3:end].strip() + body = text[end + 3:].strip() + parsed = yaml.safe_load(fm_text) or {} + return parsed, body + + +# --------------------------------------------------------------------------- +# 1. fix.md — YAML frontmatter +# --------------------------------------------------------------------------- + +class TestFixCommandFrontmatter: + """Validate the YAML frontmatter block in templates/commands/fix.md.""" + + @pytest.fixture(scope="class") + def content(self) -> str: + return _FIX_CMD.read_text(encoding="utf-8") + + @pytest.fixture(scope="class") + def frontmatter(self, content) -> dict: + fm, _ = _parse_frontmatter(content) + return fm + + def test_file_exists(self): + assert _FIX_CMD.is_file(), "templates/commands/fix.md is missing" + + def test_frontmatter_is_parseable(self, content): + """File must open with --- and contain valid YAML.""" + assert content.startswith("---"), "fix.md must start with a YAML frontmatter block (---)" + fm, _ = _parse_frontmatter(content) + assert isinstance(fm, dict), "Frontmatter could not be parsed as a YAML mapping" + + def test_has_nonempty_description(self, frontmatter): + assert "description" in frontmatter, "Frontmatter missing 'description' key" + assert frontmatter["description"], "'description' must not be empty" + + def test_has_scripts_sh(self, frontmatter): + scripts = frontmatter.get("scripts", {}) or {} + assert "sh" in scripts, "Frontmatter missing 'scripts.sh'" + assert "check-prerequisites.sh" in scripts["sh"], ( + "'scripts.sh' must reference check-prerequisites.sh" + ) + + def test_has_scripts_ps(self, frontmatter): + scripts = frontmatter.get("scripts", {}) or {} + assert "ps" in scripts, "Frontmatter missing 'scripts.ps'" + assert "check-prerequisites.ps1" in scripts["ps"], ( + "'scripts.ps' must reference check-prerequisites.ps1" + ) + + def test_scripts_sh_includes_json_flag(self, frontmatter): + """check-prerequisites must be invoked with --json so output is machine-readable.""" + sh_cmd = (frontmatter.get("scripts") or {}).get("sh", "") + assert "--json" in sh_cmd, "'scripts.sh' must include --json flag" + + def test_scripts_ps_includes_json_flag(self, frontmatter): + ps_cmd = (frontmatter.get("scripts") or {}).get("ps", "") + assert "-Json" in ps_cmd, "'scripts.ps' must include -Json flag" + + +# --------------------------------------------------------------------------- +# 2. fix.md — body content +# --------------------------------------------------------------------------- + +class TestFixCommandBody: + """Validate the substantive content in the body of fix.md.""" + + @pytest.fixture(scope="class") + def body(self) -> str: + content = _FIX_CMD.read_text(encoding="utf-8") + _, b = _parse_frontmatter(content) + return b + + def test_contains_arguments_placeholder(self, body): + assert "$ARGUMENTS" in body, "fix.md must contain $ARGUMENTS placeholder" + + def test_references_all_workflow_commands(self, body): + """The command map must list every Spec Kit command.""" + expected_commands = [ + "speckit.constitution", + "speckit.specify", + "speckit.clarify", + "speckit.plan", + "speckit.analyze", + "speckit.tasks", + "speckit.implement", + "speckit.taskstoissues", + "speckit.fix", + ] + for cmd in expected_commands: + assert cmd in body, f"fix.md must reference /{cmd} in the command map" + + def test_contains_four_point_diagnosis(self, body): + """Phase 2 must instruct the agent to produce the 4-point diagnosis.""" + required_labels = ["ROOT CAUSE", "SPEC IMPACT", "NEW FEATURE", "SCOPE"] + for label in required_labels: + assert label in body, f"fix.md diagnosis block must contain '{label}'" + + def test_contains_escalation_guard(self, body): + """If NEW FEATURE = YES the agent must stop and escalate.""" + assert "NEW FEATURE = YES" in body, ( + "fix.md must contain escalation guard 'NEW FEATURE = YES'" + ) + + def test_five_phases_present(self, body): + """fix.md must describe all 5 execution phases.""" + for phase_num in range(1, 6): + assert f"Phase {phase_num}" in body, ( + f"fix.md is missing 'Phase {phase_num}'" + ) + + def test_references_fix_log_file(self, body): + """The command must direct output to the fix.md log file.""" + assert "fix.md" in body, "fix.md command body must reference the fix.md log file" + + def test_script_placeholder_present(self, body): + """{SCRIPT} placeholder must appear so scaffold rewrites it to the real path.""" + assert "{SCRIPT}" in body, "fix.md must contain the {SCRIPT} placeholder" + + def test_no_toml_double_brace_leak(self, body): + """Markdown files must not contain TOML-style {{args}} placeholders.""" + assert "{{args}}" not in body, ( + "fix.md must not contain TOML-style {{args}} — use $ARGUMENTS instead" + ) + + def test_constitution_is_read_only(self, body): + """The constitution.md must be declared read-only (agents must not modify it).""" + assert "read-only" in body.lower(), ( + "fix.md must mark constitution.md as read-only" + ) + + +# --------------------------------------------------------------------------- +# 2b. fix.md — triage and layered diagnosis (smart error localization) +# --------------------------------------------------------------------------- + +class TestFixCommandTriage: + """Validate the triage step and layered diagnosis added for token-efficient diagnosis.""" + + @pytest.fixture(scope="class") + def body(self) -> str: + content = _FIX_CMD.read_text(encoding="utf-8") + _, b = _parse_frontmatter(content) + return b + + def test_triage_block_present(self, body): + """Phase 1.2 must instruct the agent to produce a TRIAGE block before opening files.""" + assert "TRIAGE" in body, ( + "fix.md must contain a TRIAGE block in Phase 1.2 for zero-file-I/O error classification" + ) + + def test_triage_has_layer_field(self, body): + """TRIAGE block must include a Role field to identify the functional layer.""" + assert "Role" in body, ( + "fix.md TRIAGE block must include a 'Role' field (functional role in the project chain)" + ) + + def test_triage_has_read_set_field(self, body): + """TRIAGE block must include a Read set field listing the minimal files to open.""" + assert "Read set" in body, ( + "fix.md TRIAGE block must include a 'Read set' field (minimal files to open)" + ) + + def test_data_path_chain_present(self, body): + """Functional role names covering all major project types must be documented.""" + for role in ("data-access", "business-logic", "entry-point", "guard", "routing", "validation"): + assert role in body, ( + f"fix.md must document functional role '{role}' in the Data Path Quick Reference" + ) + + def test_architecture_pattern_detection(self, body): + """The command must instruct the agent to infer the project's own chain before mapping errors.""" + # At least 3 distinct architecture patterns must be documented + patterns = ["Django", "Express", "GraphQL", "CLI", "Serverless", "Event-driven", + "Clean Architecture", "NestJS", "Laravel", "React", "Vue"] + found = [p for p in patterns if p in body] + assert len(found) >= 3, ( + f"fix.md must document at least 3 architecture patterns for chain inference — found: {found}" + ) + + def test_constitution_never_read_proactively(self, body): + """constitution.md must be documented as never read proactively.""" + assert "never" in body.lower() and "proactively" in body.lower(), ( + "fix.md must state that constitution.md is never read proactively" + ) + + def test_chain_impact_field_in_diagnosis(self, body): + """The layered diagnosis must include a CHAIN IMPACT field.""" + assert "CHAIN IMPACT" in body, ( + "fix.md diagnosis block must contain 'CHAIN IMPACT' to flag error propagation" + ) + + def test_layer_field_in_diagnosis(self, body): + """The layered diagnosis must include a LAYER field.""" + assert "LAYER" in body, ( + "fix.md diagnosis block must contain 'LAYER' to identify the data-path layer" + ) + + def test_third_party_guard_present(self, body): + """Phase 1.2 must instruct the agent to skip third-party frames in the stack trace.""" + for dep_dir in ("node_modules", "site-packages", "vendor"): + assert dep_dir in body, ( + f"fix.md must contain a third-party stack trace guard for '{dep_dir}' in Phase 1.2" + ) + + def test_scope_creep_guard_present(self, body): + """Phase 2 must stop execution when SCOPE exceeds 5 files (refactoring, not a fix).""" + assert "5 files" in body, ( + "fix.md must contain a scope creep guard that stops at > 5 files in Phase 2" + ) + + def test_validation_test_in_phase_4(self, body): + """Phase 4 must require the agent to write a concrete VALIDATION block.""" + assert "VALIDATION" in body, ( + "fix.md Phase 4 must contain a mandatory 'VALIDATION' block before Phase 5" + ) + + def test_coverage_gap_flag_present(self, body): + """When no automated test covers the scenario the agent must flag COVERAGE GAP.""" + assert "COVERAGE GAP" in body, ( + "fix.md must contain a 'COVERAGE GAP' flag for fixes without automated tests" + ) + + +# --------------------------------------------------------------------------- +# 2c. fix.md — Phase 0 pre-flight checks +# --------------------------------------------------------------------------- + +class TestFixCommandPhaseZero: + """Validate the Phase 0 pre-flight checks for token-efficient short-circuit workflows.""" + + @pytest.fixture(scope="class") + def body(self) -> str: + content = _FIX_CMD.read_text(encoding="utf-8") + _, b = _parse_frontmatter(content) + return b + + def test_phase_zero_present(self, body): + assert "Phase 0" in body, "fix.md must contain a 'Phase 0' pre-flight section" + + def test_confidence_threshold_present(self, body): + """0.1 — ambiguous input must trigger exactly one targeted question, not a guess.""" + assert "Confidence threshold" in body or "0.1" in body, ( + "fix.md Phase 0 must contain a confidence threshold check (0.1)" + ) + + def test_multi_error_handling_present(self, body): + """0.2 — multiple errors in one input must be ranked and fixed in priority order.""" + assert "Multi-error" in body or "0.2" in body, ( + "fix.md Phase 0 must contain multi-error input handling (0.2)" + ) + + def test_recurrent_error_check_present(self, body): + """0.3 — existing fix.md must be scanned for previous entries before diagnosing.""" + assert "Recurrent error" in body or "0.3" in body, ( + "fix.md Phase 0 must contain a recurrent error check (0.3)" + ) + + def test_trivial_fast_path_present(self, body): + """0.4 — trivially diagnosable errors must bypass Phases 1–2 entirely.""" + assert "fast path" in body.lower() or "0.4" in body, ( + "fix.md Phase 0 must contain a trivial fast path (0.4)" + ) + + def test_trivial_fast_path_covers_syntax_error(self, body): + assert "SyntaxError" in body, ( + "fix.md trivial fast path must enumerate SyntaxError as a direct-fix case" + ) + + def test_trivial_fast_path_covers_module_not_found(self, body): + assert "ModuleNotFoundError" in body, ( + "fix.md trivial fast path must enumerate ModuleNotFoundError as a direct-fix case" + ) + + def test_recurrent_flag_in_diagnosis(self, body): + """The RECURRENT flag must be documented for agents to use in Phase 2.""" + assert "RECURRENT" in body, ( + "fix.md must document a RECURRENT flag for errors that recur after a previous fix" + ) + + +# --------------------------------------------------------------------------- +# 3. fix-template.md — log scaffold +# --------------------------------------------------------------------------- + +class TestFixTemplate: + """Validate the structure and required sections of templates/fix-template.md.""" + + @pytest.fixture(scope="class") + def content(self) -> str: + return _FIX_TMPL.read_text(encoding="utf-8") + + def test_file_exists(self): + assert _FIX_TMPL.is_file(), "templates/fix-template.md is missing" + + def test_fix_entry_header_pattern(self, content): + """Must contain at least one FIX-NNN header with date and title.""" + assert re.search(r"##\s+FIX-\d{3}\s*·", content), ( + "fix-template.md must contain a FIX-NNN · date · title header" + ) + + def test_metadata_table_has_error_type(self, content): + assert "Error type" in content, "fix-template.md metadata table must have 'Error type' row" + + def test_metadata_table_has_detected_in(self, content): + assert "Detected in" in content, "fix-template.md must have 'Detected in' row" + + def test_metadata_table_has_root_cause(self, content): + assert "Root cause" in content, "fix-template.md must have 'Root cause' row" + + def test_metadata_table_has_spec_impact(self, content): + assert "Spec impact" in content, "fix-template.md must have 'Spec impact' row" + + def test_decisions_section_present(self, content): + assert "### Decisions" in content, "fix-template.md must contain '### Decisions' section" + + def test_files_modified_section_present(self, content): + assert "### Files modified" in content, ( + "fix-template.md must contain '### Files modified' section" + ) + + def test_invariants_section_present(self, content): + assert "### Invariants established" in content, ( + "fix-template.md must contain '### Invariants established' section" + ) + + def test_invariant_prefix_present(self, content): + assert "INVARIANT:" in content, ( + "fix-template.md must use 'INVARIANT:' prefix for invariants" + ) + + def test_edge_cases_section_present(self, content): + assert "### Edge cases not covered" in content, ( + "fix-template.md must contain '### Edge cases not covered' section" + ) + + def test_edge_case_prefix_present(self, content): + assert "EDGE CASE:" in content, ( + "fix-template.md must use 'EDGE CASE:' prefix" + ) + + def test_followup_commands_section_present(self, content): + assert "### Spec Kit follow-up commands" in content, ( + "fix-template.md must contain '### Spec Kit follow-up commands' section" + ) + + def test_followup_references_clarify(self, content): + assert "/speckit.clarify" in content, ( + "fix-template.md must reference /speckit.clarify in follow-up" + ) + + def test_followup_references_plan(self, content): + assert "/speckit.plan" in content, ( + "fix-template.md must reference /speckit.plan in follow-up" + ) + + def test_followup_references_analyze(self, content): + assert "/speckit.analyze" in content, ( + "fix-template.md must reference /speckit.analyze in follow-up" + ) + + def test_followup_references_taskstoissues(self, content): + assert "/speckit.taskstoissues" in content, ( + "fix-template.md must reference /speckit.taskstoissues in follow-up" + ) + + def test_newest_first_comment_present(self, content): + """Template must remind agents to prepend newest entries.""" + assert "newest first" in content.lower(), ( + "fix-template.md must document the newest-first ordering convention" + ) + + def test_error_origin_block_present(self, content): + """Each entry must have a verbatim error capture block.""" + assert "Error origin" in content, ( + "fix-template.md must contain an 'Error origin' block for verbatim error capture" + ) + + def test_location_comment_present(self, content): + """Template must document where the file lives (specs//fix.md).""" + assert "fix.md" in content and "specs/" in content, ( + "fix-template.md must document its target location (specs//fix.md)" + ) + + +# --------------------------------------------------------------------------- +# 4. Bundle inclusion +# --------------------------------------------------------------------------- + +class TestFixCommandBundleInclusion: + """Verify fix.md is wired into packaging and scaffold discovery.""" + + def test_fix_stem_in_source_template_stems(self): + """_get_source_template_stems() must include 'fix' so scaffold copies it.""" + from specify_cli import _locate_core_pack + + core = _locate_core_pack() + if core and (core / "commands").is_dir(): + commands_dir = core / "commands" + else: + commands_dir = _REPO_ROOT / "templates" / "commands" + + stems = sorted(p.stem for p in commands_dir.glob("*.md")) + assert "fix" in stems, ( + f"'fix' not found in command template stems: {stems}" + ) + + def test_fix_template_in_pyproject_data_files(self): + """pyproject.toml must declare fix-template.md as a data file for wheel packaging.""" + pyproject_text = _PYPROJECT.read_text(encoding="utf-8") + assert "fix-template.md" in pyproject_text, ( + "pyproject.toml must include fix-template.md in data-files so it is bundled in the wheel" + ) + + def test_fix_command_in_commands_directory(self): + """templates/commands/ must contain fix.md so the scaffold loop picks it up.""" + commands_dir = _REPO_ROOT / "templates" / "commands" + fix_cmd = commands_dir / "fix.md" + assert fix_cmd.is_file(), ( + "templates/commands/fix.md must exist for scaffold loop inclusion" + ) + + +# --------------------------------------------------------------------------- +# 5. ask.md — grounded Q&A command +# --------------------------------------------------------------------------- + +_ASK_CMD = _REPO_ROOT / "templates" / "commands" / "ask.md" + + +class TestAskCommand: + """Validate the /speckit.ask command template.""" + + @pytest.fixture(scope="class") + def content(self) -> str: + return _ASK_CMD.read_text(encoding="utf-8") + + @pytest.fixture(scope="class") + def frontmatter(self, content) -> dict: + fm, _ = _parse_frontmatter(content) + return fm + + @pytest.fixture(scope="class") + def body(self, content) -> str: + _, b = _parse_frontmatter(content) + return b + + # --- File & frontmatter --- + + def test_file_exists(self): + assert _ASK_CMD.is_file(), "templates/commands/ask.md is missing" + + def test_frontmatter_parseable(self, content): + assert content.startswith("---"), "ask.md must start with YAML frontmatter" + fm, _ = _parse_frontmatter(content) + assert isinstance(fm, dict) + + def test_has_nonempty_description(self, frontmatter): + assert frontmatter.get("description"), "ask.md frontmatter must have a non-empty 'description'" + + def test_has_scripts_sh_and_ps(self, frontmatter): + scripts = frontmatter.get("scripts", {}) or {} + assert "sh" in scripts, "ask.md frontmatter must have 'scripts.sh'" + assert "ps" in scripts, "ask.md frontmatter must have 'scripts.ps'" + + def test_scripts_reference_check_prerequisites(self, frontmatter): + scripts = frontmatter.get("scripts", {}) or {} + assert "check-prerequisites.sh" in scripts.get("sh", "") + assert "check-prerequisites.ps1" in scripts.get("ps", "") + + def test_has_arguments_placeholder(self, body): + assert "$ARGUMENTS" in body, "ask.md must contain $ARGUMENTS placeholder" + + def test_no_toml_double_brace_leak(self, body): + assert "{{args}}" not in body + + # --- Phase 0: question classification --- + + def test_phase_zero_present(self, body): + assert "Phase 0" in body, "ask.md must contain Phase 0 (question classification)" + + def test_classification_table_covers_workflow(self, body): + assert "workflow" in body, "ask.md Phase 0 must cover 'workflow' question category" + + def test_classification_table_covers_spec(self, body): + assert "spec" in body.lower(), "ask.md Phase 0 must cover 'spec' question category" + + def test_classification_table_covers_constitution(self, body): + assert "constitution" in body, "ask.md Phase 0 must cover 'constitution' question category" + + def test_fast_redirect_to_fix(self, body): + """Error questions must be immediately redirected to /speckit.fix.""" + assert "speckit.fix" in body, "ask.md must redirect error questions to /speckit.fix" + + def test_fast_redirect_to_specify(self, body): + """Feature-gap questions must be immediately redirected to /speckit.specify.""" + assert "speckit.specify" in body, "ask.md must redirect feature requests to /speckit.specify" + + # --- Phase 2: structured answer block --- + + def test_answer_block_has_question_field(self, body): + assert "QUESTION" in body, "ask.md Phase 2 answer block must contain QUESTION field" + + def test_answer_block_has_category_field(self, body): + assert "CATEGORY" in body, "ask.md Phase 2 answer block must contain CATEGORY field" + + def test_answer_block_has_grounded_in_field(self, body): + assert "GROUNDED IN" in body, "ask.md Phase 2 answer block must contain GROUNDED IN field" + + def test_answer_block_has_confidence_field(self, body): + assert "CONFIDENCE" in body, "ask.md Phase 2 answer block must contain CONFIDENCE field" + + def test_constitution_read_when_decision_touched(self, body): + """constitution.md must be loaded when the answer touches a project principle.""" + assert "constitution" in body.lower(), ( + "ask.md must instruct loading constitution.md when architectural decisions are involved" + ) + + # --- Phase 3: routing --- + + def test_routing_section_present(self, body): + assert "SUGGESTED NEXT" in body or "Phase 3" in body, ( + "ask.md must contain a routing phase (Phase 3 / SUGGESTED NEXT)" + ) + + def test_routing_covers_clarify(self, body): + assert "speckit.clarify" in body + + def test_routing_covers_plan(self, body): + assert "speckit.plan" in body + + def test_routing_covers_analyze(self, body): + assert "speckit.analyze" in body + + def test_routing_covers_tasks(self, body): + assert "speckit.tasks" in body + + def test_routing_covers_implement(self, body): + assert "speckit.implement" in body, ( + "ask.md routing must include /speckit.implement for when tasks are ready to execute" + ) + + def test_routing_covers_taskstoissues(self, body): + assert "speckit.taskstoissues" in body, ( + "ask.md routing must include /speckit.taskstoissues for edge-case tracking" + ) + + def test_routing_requires_reason_per_suggestion(self, body): + """Each routing suggestion must be accompanied by a reason (no blind suggestions).""" + assert "reason" in body.lower() or "why" in body.lower() or "warranted" in body.lower(), ( + "ask.md must require that every routing suggestion includes a reason" + ) + + # --- Phase 4: confidence check --- + + def test_low_confidence_triggers_clarification(self, body): + assert "low" in body.lower() and "CONFIDENCE" in body, ( + "ask.md must handle low-confidence answers with a clarification block" + ) + + def test_max_two_clarifying_questions(self, body): + assert "2" in body or "two" in body.lower(), ( + "ask.md must cap clarifying questions at 2 when confidence is low" + ) + + # --- Bundle inclusion --- + + def test_ask_stem_in_commands_directory(self): + commands_dir = _REPO_ROOT / "templates" / "commands" + assert (commands_dir / "ask.md").is_file(), ( + "templates/commands/ask.md must exist for scaffold loop inclusion" + ) + + def test_fix_command_map_includes_ask(self): + """fix.md command map must list speckit.ask so agents know it exists.""" + fix_text = _FIX_CMD.read_text(encoding="utf-8") + assert "speckit.ask" in fix_text, ( + "fix.md command map must reference /speckit.ask" + )