From 563f0ebeacc3b7a21672a547cb341c572e9c683c Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Fri, 6 Nov 2026 09:33:34 +0100 Subject: [PATCH 1/5] chore(lint): exclude the vendored spec-kit scripts from ruff and ty The agent-context extension that Spec Kit 1.2 ships contains Python that does not follow the project's rules. `.specify` is in the ruff and ty exclude lists. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6fbb79a93..868e7a0d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,11 @@ addopts = "-vs --strict-markers --cov-report term-missing --cov-report xml --dis [tool.ty] +[tool.ty.src] +exclude = [ + ".specify/**", +] + [tool.ty.environment] python-version = "3.10" @@ -304,6 +309,7 @@ exclude = [ "dist", "examples", "tests/fixtures/unit/test_graphql_plugin", + ".specify", ] From e577bda764101edc61151a704d45310cfe9f5285 Mon Sep 17 00:00:00 2001 From: Pol Michel Date: Fri, 6 Nov 2026 09:35:36 +0100 Subject: [PATCH 2/5] chore(speckit): upgrade Spec Kit to 1.2.5 and opsmill-speckit to 1.4.1 Follows the "Upgrading spec-kit to 1.2 or later" steps of opsmill-speckit v1.4.1, as in opsmill/infrahub#11376: - remove the review extension, which does not install on Spec Kit 1.x - upgrade the Claude integration to Spec Kit 1.2.5 - reinstall opsmill from the v1.4.1 tag - add the checkpoint extension and the critique-opsmill preset - replace the infrahub Jira preset with jira-branch, and delete dev/spec-kit/presets/infrahub - update agent-context to 1.0.2, the version Spec Kit 1.2.5 ships, and keep its CLAUDE.md configuration No file that the integration manages had local changes, so the forced upgrade loses nothing. Also deletes the command files that Spec Kit 0.x generated in .agents/commands. Spec Kit 1.2 uses the skills in .agents/skills instead. Co-Authored-By: Claude Opus 5.5 --- .../commands/speckit.agent-context.update.md | 30 - .agents/commands/speckit.opsmill.auto.md | 76 -- .agents/commands/speckit.opsmill.extract.md | 344 -------- .agents/commands/speckit.opsmill.implement.md | 130 --- .agents/commands/speckit.opsmill.prep.md | 154 ---- .../commands/speckit.opsmill.retrospect.md | 179 ----- .agents/commands/speckit.opsmill.summary.md | 144 ---- .agents/commands/speckit.review.code.md | 60 -- .agents/commands/speckit.review.comments.md | 89 --- .agents/commands/speckit.review.errors.md | 147 ---- .agents/commands/speckit.review.run.md | 178 ----- .agents/commands/speckit.review.simplify.md | 65 -- .agents/commands/speckit.review.tests.md | 86 -- .agents/commands/speckit.review.types.md | 127 --- .../speckit-agent-context-update/SKILL.md | 6 +- .agents/skills/speckit-analyze/SKILL.md | 10 +- .agents/skills/speckit-checklist/SKILL.md | 24 +- .../skills/speckit-checkpoint-commit/SKILL.md | 58 ++ .agents/skills/speckit-clarify/SKILL.md | 18 +- .agents/skills/speckit-constitution/SKILL.md | 71 +- .agents/skills/speckit-converge/SKILL.md | 16 +- .agents/skills/speckit-critique-run/SKILL.md | 7 +- .agents/skills/speckit-git-commit/SKILL.md | 53 ++ .../skills/speckit-git-feature/SKILL.md | 34 +- .../skills/speckit-git-initialize/SKILL.md | 54 ++ .agents/skills/speckit-git-remote/SKILL.md | 50 ++ .agents/skills/speckit-git-validate/SKILL.md | 54 ++ .agents/skills/speckit-implement/SKILL.md | 41 +- .agents/skills/speckit-opsmill-auto/SKILL.md | 15 +- .../skills/speckit-opsmill-extract/SKILL.md | 22 +- .../skills/speckit-opsmill-implement/SKILL.md | 67 +- .agents/skills/speckit-opsmill-prep/SKILL.md | 43 +- .agents/skills/speckit-opsmill-qa/SKILL.md | 371 +++++++++ .../speckit-opsmill-retrospect/SKILL.md | 7 +- .../skills/speckit-opsmill-summary/SKILL.md | 6 +- .agents/skills/speckit-plan/SKILL.md | 20 +- .agents/skills/speckit-review-code/SKILL.md | 59 -- .../skills/speckit-review-comments/SKILL.md | 88 --- .agents/skills/speckit-review-errors/SKILL.md | 146 ---- .agents/skills/speckit-review-run/SKILL.md | 177 ----- .../skills/speckit-review-simplify/SKILL.md | 64 -- .agents/skills/speckit-review-tests/SKILL.md | 85 -- .agents/skills/speckit-review-types/SKILL.md | 126 --- .agents/skills/speckit-specify/SKILL.md | 40 +- .agents/skills/speckit-tasks/SKILL.md | 17 +- .agents/skills/speckit-taskstoissues/SKILL.md | 20 +- .specify/.gitignore | 14 + .specify/extensions.yml | 26 +- .specify/extensions/.registry | 143 +--- .specify/extensions/agent-context/README.md | 69 +- .../agent-context/agent-context-defaults.json | 44 ++ .../commands/speckit.agent-context.update.md | 4 +- .../extensions/agent-context/extension.yml | 2 +- .../scripts/bash/update-agent-context.sh | 176 ++++- .../powershell/update-agent-context.ps1 | 100 ++- .../scripts/python/update_agent_context.py | 368 +++++++++ .../extensions/{review => checkpoint}/LICENSE | 2 +- .specify/extensions/checkpoint/README.md | 68 ++ .../extensions/checkpoint/commands/commit.md | 55 ++ .specify/extensions/checkpoint/extension.yml | 23 + .specify/extensions/opsmill/CHANGELOG.md | 160 +++- .specify/extensions/opsmill/README.md | 257 +++++- .specify/extensions/opsmill/commands/auto.md | 9 +- .../extensions/opsmill/commands/extract.md | 17 +- .../extensions/opsmill/commands/implement.md | 62 +- .specify/extensions/opsmill/commands/prep.md | 38 +- .specify/extensions/opsmill/commands/qa.md | 369 +++++++++ .../extensions/opsmill/commands/summary.md | 4 +- .specify/extensions/opsmill/extension.yml | 8 +- .../extensions/opsmill/presets/CHANGELOG.md | 46 -- .../presets/taskstoissues-jira/README.md | 57 -- .../commands/speckit.taskstoissues.md | 190 ----- .../presets/taskstoissues-jira/preset.yml | 28 - .../templates/jira.example.yml | 38 - .../opsmill/scripts/bash/python-env.sh | 34 + .../opsmill/scripts/bash/python-run.sh | 43 + .../opsmill/scripts/bash/route-codex.sh | 3 + .../opsmill/scripts/python/route_codex.py | 71 ++ .../review/.github/workflows/ci.yml | 243 ------ .specify/extensions/review/.gitignore | 46 -- .specify/extensions/review/.gitmodules | 9 - .specify/extensions/review/CHANGELOG.md | 31 - .specify/extensions/review/README.md | 185 ----- .specify/extensions/review/commands/code.md | 56 -- .../extensions/review/commands/comments.md | 85 -- .specify/extensions/review/commands/errors.md | 143 ---- .specify/extensions/review/commands/run.md | 174 ---- .../extensions/review/commands/simplify.md | 61 -- .specify/extensions/review/commands/tests.md | 82 -- .specify/extensions/review/commands/types.md | 123 --- .../extensions/review/config-template.yml | 17 - .specify/extensions/review/extension.yml | 81 -- .../scripts/bash/detect-changed-files.sh | 243 ------ .../powershell/detect-changed-files.ps1 | 228 ------ .../tests/bats/detect-changed-files.bats | 650 --------------- .../review/tests/bats/test_helper.bash | 90 --- .../pester/detect-changed-files.Tests.ps1 | 740 ------------------ .specify/init-options.json | 4 +- .specify/integration.json | 2 +- .specify/integrations/claude.manifest.json | 24 +- .specify/integrations/speckit.manifest.json | 18 +- .specify/presets/.registry | 24 +- .specify/presets/critique-opsmill/LICENSE | 21 + .specify/presets/critique-opsmill/README.md | 20 + .../commands/speckit.critique.run.md | 20 +- .specify/presets/critique-opsmill/preset.yml | 27 + .specify/presets/infrahub/preset.yml | 26 - .specify/presets/jira-branch/README.md | 17 + .../commands/speckit.git.feature.md} | 27 +- .specify/presets/jira-branch/preset.yml | 27 + .specify/scripts/bash/check-prerequisites.sh | 90 ++- .specify/scripts/bash/common.sh | 575 +++++++++++--- .specify/scripts/bash/create-new-feature.sh | 301 +++++-- .specify/scripts/bash/resolve-template.sh | 57 ++ .specify/scripts/bash/setup-plan.sh | 49 +- .specify/scripts/bash/setup-tasks.sh | 11 +- .specify/templates/checklist-template.md | 15 +- .specify/templates/plan-template.md | 2 +- dev/spec-kit/presets/infrahub/preset.yml | 27 - 119 files changed, 3910 insertions(+), 6967 deletions(-) delete mode 100644 .agents/commands/speckit.agent-context.update.md delete mode 100644 .agents/commands/speckit.opsmill.auto.md delete mode 100644 .agents/commands/speckit.opsmill.extract.md delete mode 100644 .agents/commands/speckit.opsmill.implement.md delete mode 100644 .agents/commands/speckit.opsmill.prep.md delete mode 100644 .agents/commands/speckit.opsmill.retrospect.md delete mode 100644 .agents/commands/speckit.opsmill.summary.md delete mode 100644 .agents/commands/speckit.review.code.md delete mode 100644 .agents/commands/speckit.review.comments.md delete mode 100644 .agents/commands/speckit.review.errors.md delete mode 100644 .agents/commands/speckit.review.run.md delete mode 100644 .agents/commands/speckit.review.simplify.md delete mode 100644 .agents/commands/speckit.review.tests.md delete mode 100644 .agents/commands/speckit.review.types.md create mode 100644 .agents/skills/speckit-checkpoint-commit/SKILL.md create mode 100644 .agents/skills/speckit-git-commit/SKILL.md rename dev/spec-kit/presets/infrahub/commands/speckit.infrahub.feature.md => .agents/skills/speckit-git-feature/SKILL.md (54%) create mode 100644 .agents/skills/speckit-git-initialize/SKILL.md create mode 100644 .agents/skills/speckit-git-remote/SKILL.md create mode 100644 .agents/skills/speckit-git-validate/SKILL.md create mode 100644 .agents/skills/speckit-opsmill-qa/SKILL.md delete mode 100644 .agents/skills/speckit-review-code/SKILL.md delete mode 100644 .agents/skills/speckit-review-comments/SKILL.md delete mode 100644 .agents/skills/speckit-review-errors/SKILL.md delete mode 100644 .agents/skills/speckit-review-run/SKILL.md delete mode 100644 .agents/skills/speckit-review-simplify/SKILL.md delete mode 100644 .agents/skills/speckit-review-tests/SKILL.md delete mode 100644 .agents/skills/speckit-review-types/SKILL.md create mode 100644 .specify/.gitignore create mode 100644 .specify/extensions/agent-context/agent-context-defaults.json create mode 100644 .specify/extensions/agent-context/scripts/python/update_agent_context.py rename .specify/extensions/{review => checkpoint}/LICENSE (96%) create mode 100644 .specify/extensions/checkpoint/README.md create mode 100644 .specify/extensions/checkpoint/commands/commit.md create mode 100644 .specify/extensions/checkpoint/extension.yml create mode 100644 .specify/extensions/opsmill/commands/qa.md delete mode 100644 .specify/extensions/opsmill/presets/CHANGELOG.md delete mode 100644 .specify/extensions/opsmill/presets/taskstoissues-jira/README.md delete mode 100644 .specify/extensions/opsmill/presets/taskstoissues-jira/commands/speckit.taskstoissues.md delete mode 100644 .specify/extensions/opsmill/presets/taskstoissues-jira/preset.yml delete mode 100644 .specify/extensions/opsmill/presets/taskstoissues-jira/templates/jira.example.yml create mode 100755 .specify/extensions/opsmill/scripts/bash/python-env.sh create mode 100755 .specify/extensions/opsmill/scripts/bash/python-run.sh create mode 100755 .specify/extensions/opsmill/scripts/bash/route-codex.sh create mode 100644 .specify/extensions/opsmill/scripts/python/route_codex.py delete mode 100644 .specify/extensions/review/.github/workflows/ci.yml delete mode 100644 .specify/extensions/review/.gitignore delete mode 100644 .specify/extensions/review/.gitmodules delete mode 100644 .specify/extensions/review/CHANGELOG.md delete mode 100644 .specify/extensions/review/README.md delete mode 100644 .specify/extensions/review/commands/code.md delete mode 100644 .specify/extensions/review/commands/comments.md delete mode 100644 .specify/extensions/review/commands/errors.md delete mode 100644 .specify/extensions/review/commands/run.md delete mode 100644 .specify/extensions/review/commands/simplify.md delete mode 100644 .specify/extensions/review/commands/tests.md delete mode 100644 .specify/extensions/review/commands/types.md delete mode 100644 .specify/extensions/review/config-template.yml delete mode 100644 .specify/extensions/review/extension.yml delete mode 100644 .specify/extensions/review/scripts/bash/detect-changed-files.sh delete mode 100644 .specify/extensions/review/scripts/powershell/detect-changed-files.ps1 delete mode 100644 .specify/extensions/review/tests/bats/detect-changed-files.bats delete mode 100644 .specify/extensions/review/tests/bats/test_helper.bash delete mode 100644 .specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1 create mode 100644 .specify/presets/critique-opsmill/LICENSE create mode 100644 .specify/presets/critique-opsmill/README.md rename {.agents => .specify/presets/critique-opsmill}/commands/speckit.critique.run.md (91%) create mode 100644 .specify/presets/critique-opsmill/preset.yml delete mode 100644 .specify/presets/infrahub/preset.yml create mode 100644 .specify/presets/jira-branch/README.md rename .specify/presets/{infrahub/commands/speckit.infrahub.feature.md => jira-branch/commands/speckit.git.feature.md} (60%) create mode 100644 .specify/presets/jira-branch/preset.yml create mode 100755 .specify/scripts/bash/resolve-template.sh delete mode 100644 dev/spec-kit/presets/infrahub/preset.yml diff --git a/.agents/commands/speckit.agent-context.update.md b/.agents/commands/speckit.agent-context.update.md deleted file mode 100644 index aa935bfd4..000000000 --- a/.agents/commands/speckit.agent-context.update.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: Refresh the managed Spec Kit section in coding agent context file(s) ---- - - - - -# Update Coding Agent Context - -Refresh the managed Spec Kit section inside the active coding agent's context/instruction file (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`). - -## Behavior - -The script reads the agent-context extension config at -`.specify/extensions/agent-context/agent-context-config.yml` to discover: - -- `context_file` — the path of the coding agent context file to manage. -- `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`. -- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `` and `` when the field is missing. - -It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs//plan.md`). - -If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected. - -## Execution - -- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]` -- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]` - -When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`. \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.auto.md b/.agents/commands/speckit.opsmill.auto.md deleted file mode 100644 index 25014ac14..000000000 --- a/.agents/commands/speckit.opsmill.auto.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -description: Run the full speckit workflow end-to-end — specify, plan, critique, tasks, - implement, review — making all decisions autonomously. Stops before extract; run - that manually. ---- - - - - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Outline - -You are running the **full speckit pipeline** end-to-end. The user's input (above) is the feature description that will seed the specification phase. - -This skill is a thin orchestrator over two sibling skills: - -- `speckit-opsmill-prep` — Specify → Plan → Critique → Tasks (+ spec/ask alignment check) -- `speckit-opsmill-implement` — Implement → Review → Final report - -Run them back-to-back, making all decisions autonomously. Do not stop to ask the user for input between phases — if a sub-phase requires choices (clarification questions in specify, research decisions in plan, drift remediation in alignment), use your best judgment and proceed. The user expects a hands-off, one-shot execution. Extraction (`speckit-opsmill-extract`) is **not** run by this command — the user invokes it manually after reviewing the implementation report. - -> Each phase below is executed by invoking the named skill (e.g. via the agent's Skill tool). Skills are agent-agnostic, so this workflow runs identically across any harness that supports skill discovery — not only those exposing speckit slash commands. - -### Phase A — Preparation (delegated to `speckit-opsmill-prep`) - -Invoke the `speckit-opsmill-prep` skill with the user's feature description (`$ARGUMENTS`) verbatim. - -- This covers Specify → Plan → Critique → Tasks **and** the trailing spec/ask alignment check. -- The sub-skill commits its own artifacts after each phase via `speckit-checkpoint-commit`. -- If the alignment check inside prep loops (re-running plan/critique/tasks to fix spec drift), let it run to completion. Do not interfere. - -**Read prep's status line.** `speckit-opsmill-prep` ends its output with a literal final line: - -`STATUS: | SPEC_DIR: | REASON: <...>` - -Parse that line — do **not** infer success from the prose summary: - -- Take **`SPEC_DIR`** from this line as the spec directory to pass to Phase B. This is the deterministic hand-off; do not scrape the path out of free text. -- If **`STATUS: BLOCKED`** (e.g., alignment never converged within its retry budget, or a phase could not complete), surface the reason to the user and **stop**. Do **not** proceed to implementation on a misaligned or incomplete spec. -- Only proceed to Phase B when `STATUS: READY`. - -### Phase B — Implementation tail (delegated to `speckit-opsmill-implement`) - -Invoke the `speckit-opsmill-implement` skill with the `SPEC_DIR` path from Phase A as its argument. Because you pass an explicit spec-dir path, the sub-skill runs in **autonomous-parent** mode: its Phase 0 stop-conditions abort with a `STATUS: BLOCKED` line instead of pausing for a user. - -- This covers Phase 0 (Preflight) → Phase 5 (Implement loop in clean-context subagents) → Phase 6 (Review) → Phase 7 (Final report). -- The sub-skill writes `/opsmill-implement-report.md` and commits it. - -**Read implement's status line.** `speckit-opsmill-implement` ends its output with a literal final line: - -`STATUS: | SPEC_DIR: <...> | REASON: <...>` - -- `STATUS: BLOCKED` — a Phase 0 stop-condition aborted before any implementation (no report written). Surface the reason in the final summary; do not retry from this orchestrator. -- `STATUS: INCOMPLETE` — blocked tasks or missing local-pass evidence; capture that for the final summary. The user decides whether to re-run. -- `STATUS: DONE` — all chunks completed with evidence. - -In every case, do **not** retry from this orchestrator. - -## Completion - -After both phases are complete, provide a brief summary: - -- Feature name and spec directory -- Whether the alignment check inside auto-prep required retries (and how many) -- Number of tasks completed (from the auto-implement report) -- Any review findings that were fixed inline vs. deferred -- Any notable decisions you or the sub-skills made autonomously - -Do **not** open a PR, push, run extraction, or start a new feature. The user takes it from there — extraction (`/speckit.opsmill.extract`) is intentionally left as a manual follow-up so the user can review the implementation report first. \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.extract.md b/.agents/commands/speckit.opsmill.extract.md deleted file mode 100644 index f723402cb..000000000 --- a/.agents/commands/speckit.opsmill.extract.md +++ /dev/null @@ -1,344 +0,0 @@ ---- -description: Extract knowledge, guidelines, and ADRs from one or more completed spec - directories into the project documentation system. ---- - - - - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Outline - -Goal: Analyze one or more completed spec directories and extract durable knowledge into the project's documentation system (`dev/knowledge/`, `dev/guidelines/`, `dev/adr/`), then mark each spec as extracted. - -This command accepts multiple specs as input (space-separated) and processes them sequentially. It operationalizes the documentation lifecycle: `specs/ → knowledge/ or guidelines/` (see `dev/guidelines/markdown.md`). - -## Phase 0: Setup & Validation - - -First, resolve all spec directories from the user's arguments and validate each one contains the expected artifacts. - - -1. **Parse arguments** — split `$ARGUMENTS` into individual spec identifiers (space-separated). If `$ARGUMENTS` is empty, list available spec directories and ask the user to pick one or more. - -2. **Resolve each spec directory**: - - For each identifier in the arguments: - - If it matches `specs/NNN-*` or `NNN-*`, resolve to `REPO_ROOT/specs/NNN-*` - - If it is a bare name like `graphql-name-lookup`, search `specs/` for a matching directory - - If no match found for an identifier, report it and continue resolving the rest - - If **no** identifiers could be resolved, list available spec directories and ask the user to pick - -3. **Validate each resolved spec**: - - `spec.md` MUST exist — skip that spec with an error if missing - - `research.md` SHOULD exist — warn if missing ("No ADRs can be extracted without research.md") but continue - -4. **Check extraction status** for each spec: - - If `EXTRACTED.md` exists in the spec directory, warn the user that this spec was previously extracted - - Also check if the spec already lives under `specs/archive/` — if so, it was previously extracted and archived - - Ask for confirmation before re-extracting — default is abort - - The user can choose to skip individual specs from the batch - -5. **Summarize resolved specs** — before proceeding, print the list of specs that will be processed: - ``` - Processing N spec(s): - 1. specs/ - 2. specs/ - ... - ``` - -6. **Load existing documentation index** (once, shared across all specs): - - List all files in `dev/knowledge/` (recursively) - - List all files in `dev/guidelines/` (recursively) - - List all files in `dev/adr/` to determine the next ADR number - - Read the first 20 lines of each knowledge and guidelines file to build a topic index (title + overview section) - -7. **Load spec artifacts** — for each spec, read all available files from its directory: - - `research.md` (primary source for ADRs) - - `spec.md` (context, scope decisions) - - `data-model.md` (schema changes, new methods — if exists) - - All files in `contracts/` (API changes — if exists) - - `plan.md` (architectural context — if exists) - -## Phase 1: Analysis & Classification - - -For each spec, I need to parse each source and classify content into three buckets: ADR, Knowledge, Guideline. I should also identify content to skip with a clear reason. I'll process specs sequentially and tag each finding with its source spec. - - -Repeat the following steps for **each resolved spec directory**. Tag every finding with the source spec name so the extraction plan in Phase 2 groups items by spec. - -### Step 1: Extract ADR candidates from research.md - -Parse each `## R#` section in `research.md`. For each entry: - -1. Extract: Decision, Rationale, Alternatives considered, Implementation pattern -2. Determine if it is ADR-worthy using this heuristic: - - **ADR-worthy**: Affects system structure, API contracts, data models, error handling strategy, or establishes a pattern used across multiple components - - **Not ADR-worthy**: One-off implementation detail with no broader implications (e.g., a local variable naming choice, a single method signature) -3. Draft an ADR title (concise, decision-focused — e.g., "Dual identifier resolution via service-layer helpers") - -Also scan `spec.md` for scope decisions or trade-offs in the Clarifications or Assumptions sections that represent architectural choices worth recording. - -### Step 2: Extract knowledge candidates - -Scan for content that describes **how the system works after the feature**: - -| Source | What to look for | Target file | -|--------|-----------------|-------------| -| `data-model.md` | New entities, fields, relationships, constraints | `dev/knowledge/backend/data-models.md` | -| `contracts/` | New or changed GraphQL queries, mutations, types | `dev/knowledge/backend/api.md` | -| `research.md` | New service patterns, architectural patterns | `dev/knowledge/backend/services.md` or relevant file | -| `spec.md` | New concepts or domain terminology | `dev/knowledge/backend/overview.md` or relevant file | - -For each finding, map it to a specific existing knowledge file and section. If no existing file fits, propose a new file with a name following `kebab-case.md` convention. - -### Step 3: Extract guideline candidates - -Scan `research.md` for implementation patterns that establish **repeatable, prescriptive conventions** for future code: - -- New parameter patterns → `dev/guidelines/backend/python.md` or `dev/guidelines/cyclopts.md` -- New error handling conventions → `dev/guidelines/backend/python.md` -- New API design conventions → `dev/guidelines/backend/graphql.md` (create if needed) -- New testing patterns → relevant guidelines file - -Only extract patterns that are **prescriptive** (should be followed in future code). Do NOT extract patterns that are merely **descriptive** of how this specific feature works — those belong in knowledge. - -### Step 4: Identify skipped content - -For each piece of spec content not classified above, note it with a reason: -- Execution artifacts (`plan.md`, `quickstart.md`) — no durable knowledge -- Implementation details already captured in code — no need to duplicate -- One-off decisions with no broader applicability - -## Phase 2: Interactive Review - -Present findings in a structured extraction plan. When processing multiple specs, group the plan by spec. Use a **global numbering scheme** across all specs so that item numbers are unique (e.g., spec 1 items are 1–4, spec 2 items are 5–8). - -Use this format: - -```markdown -## Extraction Plan - -### specs/ - -#### ADRs to Create () - -| # | Source | Title | Target File | -|---|--------|-------|-------------| -| 1 | R1 | | dev/adr/nnnn-<slug>.md | -| 2 | R2 | <title> | dev/adr/nnnn-<slug>.md | - -#### Knowledge Updates (<count>) - -| # | Target File | Section | Change | Summary | -|---|-------------|---------|--------|---------| -| 3 | dev/knowledge/backend/api.md | Queries | UPDATE | <what changes> | - -#### Guidelines Updates (<count>) - -| # | Target File | Section | Change | Summary | -|---|-------------|---------|--------|---------| -| 4 | dev/guidelines/backend/python.md | Error Handling | UPDATE | <what changes> | - -#### Skipped (with reasons) - -| Source | Reason | -|--------|--------| -| plan.md | Execution artifact — no durable knowledge | - ---- - -### specs/<spec-name-2> - -#### ADRs to Create (<count>) - -| # | Source | Title | Target File | -|---|--------|-------|-------------| -| 5 | R1 | <title> | dev/adr/nnnn-<slug>.md | - -... -``` - -When processing a single spec, omit the per-spec grouping headers and use the simpler flat format (same as the multi-spec table structure but without the `### specs/<name>` wrapper). - -After presenting, tell the user: - -> **Actions:** -> - `approve all` — proceed with all extractions across all specs -> - `approve spec <name>` — approve all items for a specific spec -> - `approve adrs` / `approve knowledge` / `approve guidelines` — approve by category (across all specs) -> - `skip N` — skip a specific numbered item -> - `edit N` — modify a specific item before writing -> - `group N,M` — merge multiple ADR items into a single ADR - -Wait for user response before proceeding. Do NOT write any files until the user approves. - -## Phase 3: Write Extractions - -For each approved item across all specs, write the content. ADR numbering is sequential across all specs (i.e., if spec 1 creates `0005` and `0006`, spec 2 starts at `0007`). - -### ADRs - -Create new files in `dev/adr/` using this format: - -```markdown -# N. <Title> - -**Status**: Accepted -**Date**: <today's date YYYY-MM-DD> -**Source**: specs/archive/<spec-name>/research.md (R#) - -## Context - -<What is the issue that motivates this decision? Derive from the feature context in spec.md and the specific problem the R# entry addresses.> - -## Decision - -<What was decided. Taken from the Decision field of the R# entry.> - -## Consequences - -<What becomes easier or harder as a result. Synthesize from the Rationale and any implementation notes.> - -## Alternatives Considered - -<What other options were evaluated and why they were rejected. Taken from the Alternatives considered field.> -``` - -**Numbering**: Read existing files in `dev/adr/` to find the highest existing ADR number. Start new ADRs at the next sequential number. Zero-pad the **filename** sequence to **4 digits** (e.g., `0001`, `0012`); the H1 heading uses the un-padded number (e.g., `# 1.`, `# 12.`). - -**File naming**: canonical MADR `nnnn-kebab-case-short-title.md` — 4-digit zero-padded sequence, lowercase kebab title, **no** `adr-`/`ADR-` prefix (e.g., `0001-schema-changes-without-migrations.md`). - -### Knowledge updates - -For each knowledge update: -1. Read the full target file -2. Find the appropriate section (match by heading) -3. Add or update content within that section -4. If the section does not exist, create it in a logical position -5. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` - -### Guidelines updates - -Same approach as knowledge updates: -1. Read the full target file (or create a new one if needed) -2. Find the appropriate section -3. Add the prescriptive pattern with code examples where relevant -4. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` - -If creating a new guidelines file, follow the standard structure: -```markdown -# <Topic> Guidelines - -## Overview - -<Brief description of what this document covers.> - -## <Sections...> -``` - -## Phase 4: Cleanup & Report - -Perform the following steps for **each spec** that had approved extractions. - -### 1. Create extraction record - -Write `EXTRACTED.md` in each spec directory: - -```markdown -# Extraction Record - -**Extracted on**: <YYYY-MM-DD> -**Extracted by**: speckit.opsmill.extract - -## ADRs Created - -- <path to ADR file> (from R#) -- ... - -## Knowledge Updated - -- <path to knowledge file> (<section name>) -- ... - -## Guidelines Updated - -- <path to guidelines file> (<section name>) -- ... - -## Archive - -Spec directory moved to `specs/archive/<spec-name>/` as a historical record. -``` - -### 2. Update spec status and archive - -For each spec: -1. In `spec.md`, update or add the status field to: `**Status**: Extracted` -2. Move the entire spec directory into `specs/archive/`: - ```bash - mkdir -p specs/archive - mv specs/<spec-name> specs/archive/<spec-name> - ``` - This makes it immediately clear which specs have been processed and which are still active. - -### 3. Update dev/README.md - -After **all** specs have been processed, update `dev/README.md` once with all new files: -- If any **new** files were created in `dev/knowledge/`, `dev/guidelines/`, or `dev/adr/`: - - Read `dev/README.md` - - Add new entries to the appropriate section (Current Guidelines, Current Knowledge, or a new Current ADRs section) - - Follow the existing link format: `- [filename.md](path/to/filename.md) - Brief description` -- If `dev/adr/` now has content and there is no "Current ADRs" section in the README, create one following the pattern of the existing sections. - -### 4. Report - -Print a combined summary covering all processed specs: - -```markdown -## Extraction Complete - -**Date**: YYYY-MM-DD -**Specs processed**: N - -### Per-Spec Summary - -| Spec | ADRs | Knowledge | Guidelines | Status | -|------|------|-----------|------------|--------| -| specs/<spec-name-1> | N | N | N | archived | -| specs/<spec-name-2> | N | N | N | archived | - -### Totals -- N ADR(s) created in dev/adr/ -- N knowledge file(s) updated -- N guideline file(s) updated - -### Archived -- specs/<spec-name-1> → specs/archive/<spec-name-1> -- specs/<spec-name-2> → specs/archive/<spec-name-2> - -### Files Created/Modified -- <list each file path> - -### Next Steps -- Review the created ADRs for accuracy -- Verify knowledge and guideline updates read well in context -``` - - -## Behavior Rules - -- **Never write files without user approval** in Phase 2 -- If analysis finds nothing to extract (empty across all categories), report cleanly: "No extractable content found in this spec." and skip the review phase -- If a knowledge file section already contains similar content, flag it for user review rather than duplicating -- Do not reformat or restructure existing content in knowledge/guidelines files beyond the specific additions -- Always create `specs/archive/` before moving if it does not exist -- ADR `Source` paths must reference the archive location (`specs/archive/<spec-name>/`) since the spec will be moved there -- For single quotes in bash args, use escape syntax: e.g. `'I'\''m Groot'` (or double-quote if possible: `"I'm Groot"`) \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.implement.md b/.agents/commands/speckit.opsmill.implement.md deleted file mode 100644 index 49fd2ec70..000000000 --- a/.agents/commands/speckit.opsmill.implement.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -description: Run the speckit implementation phase chunk-by-chunk in clean-context - subagents, then review, then produce a final report. Picks up from an existing tasks.md. ---- - - -<!-- Extension: opsmill --> -<!-- Config: .specify/extensions/opsmill/ --> -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). `$ARGUMENTS` is optional and may name the spec directory to operate on (e.g. `specs/008-foo`). If empty, operate on the most recently modified spec directory under `specs/` that has a `tasks.md`. - -## Outline - -You are running the **implementation + review tail** of the speckit pipeline. This command picks up where `speckit-opsmill-prep` leaves off: it expects a feature directory under `specs/` containing `spec.md`, `plan.md`, and `tasks.md`. It does **not** generate or modify those documents — it executes them. - -The implementation phase runs as a **loop** over chunks of `tasks.md`, with each chunk implemented inside a **clean-context subagent**. The orchestrator (you) never edits feature code directly — it dispatches, integrates, and reports. - -After all chunks are complete, you run a single review pass across the whole change set, fix any high-severity findings, and emit a final report. - -> Each phase below is executed by invoking the named skill (e.g. via the agent's Skill tool). Skills are agent-agnostic, so this workflow runs identically across any harness that supports skill discovery — not only those exposing speckit slash commands. - -### Phase 0 — Preflight - -**Invocation context.** This command runs in one of two modes, and the stop-conditions below behave differently in each: - -- **Interactive** (a user invoked it directly). On a stop-condition you may pause and ask the user, as written. -- **Autonomous parent** (invoked as Phase B of `speckit-opsmill-auto`). There is no user to resume a pause, so you **MUST NOT** pause or wait. Treat every stop-condition below as a hard abort: do **not** start the implement loop, and end your run with the `STATUS: BLOCKED` status line defined in **Completion** so the parent orchestrator can detect it and stop cleanly. Never proceed on a dirty tree or missing docs — doing so contaminates the `HEAD@start..HEAD@now` review diff. You can tell you are running under an autonomous parent because you were dispatched with a resolved spec-dir path and no interactive session. - -Before any work begins: - -1. Resolve the target spec directory (from `$ARGUMENTS` or the most recently modified `specs/<feature>/` with a `tasks.md`). Record the absolute path; you will pass this to every subagent. -2. Verify `spec.md`, `plan.md`, and `tasks.md` all exist. If any are missing: interactive → abort with a clear error directing the user to run `speckit.opsmill.prep` first; autonomous parent → abort with `STATUS: BLOCKED` (reason: missing prep artifacts). -3. Read `tasks.md` end-to-end and identify the chunking boundaries (see "Chunking strategy" below). Build a numbered list of chunks with their task IDs. -4. Verify the working tree is clean (or only contains expected prep artifacts). If it is dirty in unrelated ways: interactive → surface that and pause; autonomous parent → abort with `STATUS: BLOCKED` (reason: unexpectedly dirty working tree). Do not attempt to stash or clean it yourself. -5. Note the current `HEAD` commit — you will diff against it for the review and report. - -### Phase 5 — Implement (looped, clean-context subagents) - -> **Phase numbering.** This file is the implementation tail; phases jump 0 → 5 → 6 → 7 on purpose. Phases 1–4 (Specify → Plan → Critique → Tasks) run in `speckit-opsmill-prep`. They are not missing or skipped here. - -Loop over the chunks identified in Phase 0. For **each chunk**: - -1. **Dispatch a subagent with a clean context.** Use your harness's subagent / Task tool (in Claude Code: the `Agent` tool with `subagent_type=general-purpose`). The subagent **must not** inherit the orchestrator's conversation history — every chunk starts fresh. - -2. **Brief the subagent self-contained.** The orchestrator has read context the subagent does not. The prompt to the subagent must include: - - Absolute path to the spec directory. - - Absolute path to the repository root. - - Pointers to project context files the subagent should read first, when present at the repo root or in their conventional locations: `AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`, and the project constitution (commonly `.specify/memory/constitution.md`, `dev/constitution.md`, or `constitution.md`). The orchestrator should resolve which of these actually exist before dispatch and pass the resolved absolute paths — do not ask the subagent to guess. If none exist, say so explicitly so the subagent does not waste a turn searching. - - The **exact chunk** of tasks to implement, copied verbatim from `tasks.md` (task IDs, descriptions, and any `[P]` parallel markers). - - An explicit instruction to invoke the `speckit-implement` skill scoped to **only those task IDs** — not the full `tasks.md`. - - The commit policy: the subagent should run formatters/linters and commit its own work via the `speckit-checkpoint-commit` skill before returning. - - **The local-pass policy.** Every test the chunk adds or modifies — unit, integration, **and E2E when the project supports running them locally** — MUST be observed to PASS locally at least once before the chunk is reported as done. Marker-clean / lint-clean / type-clean is NOT a substitute. If a test class cannot be executed locally in this project (e.g. an E2E suite that requires infrastructure the developer machine cannot provide), the subagent MUST state that explicitly and explain why, rather than silently skipping. For every test that was run, the subagent MUST return the evidence specified in the response contract below; if it cannot produce the evidence for a test it claims to have written, the chunk's outcome line for that task MUST be `⚠️ partial` or `❌ blocked` (not ✅ done) with the reason. - - **Discovering how tests run.** Before executing tests, the subagent should determine the project's test conventions by reading `AGENTS.md` / `CLAUDE.md`, then the repo's standard locations (`Makefile`, `package.json` scripts, `pyproject.toml`, `Cargo.toml`, `go.mod`, CI config, `tasks.md` itself). Use whatever runner the project actually uses — `pytest`, `go test`, `cargo test`, `npm test`, `vitest`, `make test`, etc. Do not assume a specific framework. - - A short response contract: return (a) a one-line outcome per task ID (✅ done / ⚠️ partial / ❌ blocked + reason), (b) the commit SHA(s) it produced, (c) any decisions worth flagging upward, (d) overall lint/format status, (e) **for every new or modified test: the test identifier (node ID, function name, or file::test as the runner reports it), the exact command used to run it, an ISO 8601 wall-clock timestamp of the passing run, any environment context that matters for reproducibility (e.g. cluster id + kubeconfig path, container/compose service, browser version, DB fixture name) — or `n/a` if the runner needs none — and the verbatim pass line from the runner's output (e.g. `PASSED ...`, `ok`, `--- PASS:`, `✓ ...`)**. If the chunk added or modified no tests, the subagent MUST say so explicitly so the orchestrator can record "n/a" rather than an omission. If an E2E test was added but not run locally because the project does not support local E2E execution, the subagent MUST state that as a separate line and include any CI-side runbook / command that will exercise it instead. - - Length cap on the report (e.g. "under 250 words", excluding the test evidence block which has no cap) so the orchestrator's context does not balloon. - -3. **Wait for the subagent to return**, then: - - Pull its outcome lines into your running ledger of chunk results. - - Verify `tasks.md` checkboxes for that chunk are now `[X]` (re-read the file). If the subagent forgot to tick them, do it yourself and add a **fresh fixup commit** — do **not** `--amend` the subagent's commit. Its SHA is already recorded in the Phase 7 chunk ledger (§2) and may be referenced elsewhere; amending would rewrite it and make the final report cite a commit that no longer exists. - - If the subagent reports ❌ blocked: do not auto-retry blindly. Decide between (i) re-dispatching with sharper instructions, (ii) splitting the chunk smaller and retrying, or (iii) recording the block and moving on. State which you chose and why. - - Do **not** invoke `speckit-checkpoint-commit` again here — the subagent already committed. Only commit yourself if you applied a fixup (e.g. ticking missed checkboxes). - -4. Move to the next chunk. **Never run two implementation subagents in parallel** — chunks may share files and conflicting writes are far more expensive than the wall-clock savings. - -#### Chunking strategy - -Prefer the natural phase headings already present in `tasks.md` (`### Phase 3.1: Setup`, `### Phase 3.2: Tests First (TDD)`, `### Phase 3.3: Core Implementation`, etc.). Each phase becomes one chunk. - -If a phase contains more than ~10 tasks or touches > ~15 files, split it further along cohesive seams (e.g. one chunk per module, one chunk per contract test). If two adjacent small phases together total < ~5 tasks, do **not** merge them — keep the boundary; small chunks help review and reduce blast radius if a subagent goes off the rails. - -Respect explicit dependencies in `tasks.md`. Sequential `[P]`-free tasks must stay in their original order across chunks; `[P]` markers within a chunk are fine for the subagent to parallelise internally. - -### Phase 6 — Review - -Once **all** chunks have completed (including any retries), invoke the `speckit-review-run` skill once across the full diff (`HEAD`-at-start..`HEAD`-now). - -- For findings rated **high severity or above**, fix them inline. Prefer fixing them yourself if the change is small and localised; dispatch a fresh clean-context subagent (same protocol as Phase 5) if the fix spans multiple files or needs significant code understanding. -- For lower-severity findings, record them in the report (do not block). -- Commit any review-driven fixes via `speckit-checkpoint-commit`. - -### Phase 7 — Final Report - -Write a markdown report to `<spec-dir>/opsmill-implement-report.md` and also print it to the user. The report **must** contain: - -1. **Header** — feature name, spec dir, base commit, head commit, total wall-clock time if known. -2. **Chunk-by-chunk ledger** — for each chunk dispatched, in order: - - Chunk name (phase heading from `tasks.md`). - - Number of tasks in the chunk. - - Outcome counts (✅ / ⚠️ / ❌). - - Commit SHA(s) produced by the subagent. - - Any decisions or surprises the subagent flagged upward. -3. **Tasks not completed** — task IDs still `[ ]` in `tasks.md`, with the reason from the relevant subagent's report. Empty section if everything is `[X]`. -4. **Local-pass evidence (REQUIRED).** A table of every test added or modified by this run — unit, integration, and E2E — with one row per test: - - | Test id | Type (unit/integration/e2e) | Run command | Passed at (ISO 8601) | Environment context | Verbatim pass line | - |---------|-----------------------------|-------------|----------------------|---------------------|--------------------| - - Aggregate the rows from each chunk subagent's response. If a chunk added/modified a test but did not return evidence, the row's "Passed at" cell MUST be `MISSING — see chunk <N>` and the test MUST also appear in §3 "Tasks not completed" as a blocker — never silently elide it. - - If an E2E test was added but not executable locally in this project, record it as a separate row with `Passed at` = `deferred — local E2E not supported` and put the CI-side command in `Run command`. Call this out in §6 "Autonomous decisions" so the user can confirm the call. - - If no tests were added or modified by the whole run, write `n/a — no new or modified tests in this implementation` instead of the table and state it explicitly so reviewers can verify the claim against the diff. -5. **Review findings** — table of severity / file / one-line summary. Mark which were fixed inline and which were deferred. -6. **Autonomous decisions** — any judgment calls the orchestrator made that the user might want to revisit (chunk splits, blocked-task handling, review-finding triage choices). -7. **Suggested next steps** — e.g. "open a PR", "rerun `speckit.opsmill.implement` to retry the 2 blocked tasks", "address the deferred review findings". - -**Blocking rule.** If §4 contains any `MISSING` row, the report header MUST mark the run `INCOMPLETE` and §7 MUST list "produce local-pass evidence for the listed tests" as the first next step. Do not declare the spec done while local-pass evidence is missing. (`deferred — local E2E not supported` rows do NOT trigger this rule, but they MUST be flagged in §6.) - -Commit the report via `speckit-checkpoint-commit` as the final action. - -## Completion - -Print a 4-6 line summary mirroring the report header + outcome counts so the user does not need to open the file to know the result. - -**Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: - -`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` - -- `STATUS: DONE` — all chunks completed and §4 local-pass evidence has no `MISSING` rows. -- `STATUS: INCOMPLETE` — the run finished but the report is marked `INCOMPLETE` (e.g. missing local-pass evidence, or blocked tasks recorded). -- `STATUS: BLOCKED` — a Phase 0 stop-condition aborted the run before the implement loop (missing prep artifacts, unexpectedly dirty tree). In this case no report is written; emit only the summary explaining why, then this line. - -The parent `speckit-opsmill-auto` parses this line; keep it as the literal last line, unwrapped. - -Then stop — do **not** open a PR, do **not** push, do **not** start a new feature. The user will take it from there. \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.prep.md b/.agents/commands/speckit.opsmill.prep.md deleted file mode 100644 index 12dfca142..000000000 --- a/.agents/commands/speckit.opsmill.prep.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -description: Run the speckit preparation phases end-to-end — specify, plan, critique, - tasks — making all decisions autonomously. Stops before implementation. ---- - - -<!-- Extension: opsmill --> -<!-- Config: .specify/extensions/opsmill/ --> -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Outline - -You are running the **preparation phases of the speckit pipeline** end-to-end. The user's input (above) is the feature description that will seed the specification phase. - -This skill is the prefix of `speckit-opsmill-auto`: it covers Specify → Plan → Critique → Tasks → Spec/Ask Alignment Check and **stops before implementation**. Use it when you want a feature fully designed and broken down into a `tasks.md` ready for review, but you do not want the agent to execute the tasks autonomously. - -Execute every phase below **in order**, making all decisions autonomously. Do not stop to ask the user for input between phases — if a phase requires choices (e.g., clarification questions in specify, research decisions in plan), use your best judgment and proceed. The user expects a hands-off, one-shot execution up through `tasks.md`. - -After **each phase**, invoke the `speckit-checkpoint-commit` skill to commit the artifacts produced by that phase before moving on. - -> Each phase below is executed by invoking the named skill (e.g. via the agent's Skill tool). Skills are agent-agnostic, so this workflow runs identically across any harness that supports skill discovery — not only those exposing speckit slash commands. - -### Phase 1 — Specify - -Invoke the `speckit-specify` skill with the user's feature description (`$ARGUMENTS`). - -- Complete the full specify workflow: generate a short name, create the spec directory, write `spec.md`, run quality checks. -- If clarification questions arise, answer them yourself based on context and best judgment — do **not** pause for user input. -- Commit the spec artifacts. - -### Phase 2 — Plan - -Invoke the `speckit-plan` skill. - -- Complete the full plan workflow: research unknowns, generate `plan.md`, `research.md`, `data-model.md`, API contracts, `quickstart.md`. -- Make all design decisions autonomously. -- Commit the plan artifacts. - -### Phase 3 — Critique - -Invoke the `speckit-critique-run` skill. - -- Run the dual-lens (Product + Engineering) critique against `spec.md` and `plan.md` before any tasks are generated. -- For any 🎯 **Must-Address** findings, apply the suggested fixes to `spec.md` / `plan.md` autonomously and commit them — do not pause for user approval. -- For 💡 Recommendations, apply them when the fix is clear and low-risk; otherwise note and move on. -- 🤔 Questions: resolve with your best judgment based on context (same rule as the Specify phase). -- If the verdict is 🛑 **RETHINK**, loop back to `speckit-plan` (or `speckit-specify` if the spec itself is the problem), re-run the critique, then continue. -- Commit the critique report and any spec/plan updates before moving on. - -### Phase 4 — Tasks - -Invoke the `speckit-tasks` skill. - -- Generate the full `tasks.md` with dependency-ordered, actionable tasks. -- Skip the optional `speckit-analyze` skill unless something looks inconsistent — use your judgment. -- Commit the tasks artifact. - -### Phase 5 — Spec/Ask Alignment Check - -The earlier phases can drift from a detailed PRD: clarifications get over-applied, scope creeps in from research, or requirements get rephrased away. This phase catches that before the user sees `tasks.md`. - -#### 5a. Decide whether to run the check - -Inspect `$ARGUMENTS`. The check is **only meaningful when the user provided a substantive PRD** — either inline or by reference. Decide based on the following, in order: - -1. **`$ARGUMENTS` contains one or more URLs** (links to a Notion / Confluence / Google Doc / GitHub issue / Linear ticket / shared Markdown / PDF). Treat each URL as a likely PRD location. **Run the check.** -2. **`$ARGUMENTS` itself looks like a detailed PRD** — multiple paragraphs with structure such as headings, bullet lists of requirements, explicit goals/non-goals, acceptance criteria, or > ~400 characters of substantive description. **Run the check.** -3. **`$ARGUMENTS` is a one-line description, vague brief, or empty.** **Skip the check** — there is no source-of-truth document to align against. Write a short note in your final summary stating you skipped this phase and why, and proceed to Completion. - -#### 5b. Resolve the source-of-truth PRD - -If you decided to run the check: - -- For each URL in `$ARGUMENTS`, fetch its content using the harness's web fetch tool (in Claude Code: the `WebFetch` tool). Extract the requirement-bearing sections. -- **If the harness has no web-fetch capability at all** (the tool is absent, not merely failing): you cannot resolve URL-only PRDs. Fall back to any inline PRD content in `$ARGUMENTS`. If the only source was a URL and there is no inline content, you cannot run a meaningful check — skip the rest of Phase 5, record `verdict: ⚠️ SKIPPED — source PRD unreachable (no web-fetch tool)` in the alignment report, and note it prominently in the Completion summary so the user knows alignment was not verified. -- If a URL is gated (auth required, 404, network error), record that and fall back to whatever PRD content is in `$ARGUMENTS` itself. If neither is usable, skip the rest of Phase 5 with a note. -- Concatenate the fetched / inline PRD content into a single "source PRD" view that you will compare against `spec.md`. - -#### 5c. Compare source PRD to `spec.md` - -Read the current `spec.md` end-to-end and judge it against the source PRD. You are looking for **significant** drift, not stylistic differences. Significant means: - -- A requirement, goal, or explicit non-goal in the PRD is **missing** from the spec. -- The spec **adds** a requirement, scope item, or constraint that does **not** appear in the PRD and was not a necessary clarification. -- A requirement's semantics have been **changed** (e.g., "users can opt out" became "admins can opt users out"; "must support 10k req/s" became "should scale"). -- Acceptance criteria in the PRD are **dropped** or **softened** in the spec. -- The spec contradicts a stated PRD constraint (compatibility, deadline, dependency, etc.). - -Cosmetic, structural, or expansion-of-detail differences are **not** drift. The spec is allowed to be longer, more precise, and to flesh out implicit requirements. - -#### 5d. Write the alignment report - -Write `<spec-dir>/alignment-check.md` with: - -1. **Source** — which URL(s) and / or inline ask were used as the source PRD. -2. **Verdict** — one of `✅ ALIGNED`, `⚠️ MINOR DRIFT (proceeding)`, or `🛑 SIGNIFICANT DRIFT (re-running prep)`. -3. **Findings** — table of `Severity | Category (missing / added / changed / dropped / contradicted) | PRD reference | Spec reference | Description`. -4. **Action** — what you decided to do (proceed, or re-run which phases). - -Commit the report via `speckit-checkpoint-commit`. - -#### 5e. Remediate significant drift (with retry budget) - -This step is the **loop body**. Initialize a remediation counter to `0` before the first entry. Enter this step whenever the most recent 5d verdict is `🛑 SIGNIFICANT DRIFT`. - -While the latest verdict is `🛑 SIGNIFICANT DRIFT`: - -1. **Check the budget first.** If the counter is already `2`, do **not** start another pass — go to "budget exhausted" below. -2. **Increment the counter** (this pass now counts against the budget of 2). -3. **Update `spec.md`** by re-invoking the `speckit-specify` skill with an augmented input that includes: - - The original `$ARGUMENTS` content (and fetched PRD body if from URL). - - An explicit list of the drift findings the spec must fix (missing requirements to add, off-scope items to remove, semantic changes to revert). - The skill will update the existing `spec.md` in place. -4. **Re-run Phase 2 (Plan)** to refresh `plan.md` against the corrected spec. -5. **Re-run Phase 3 (Critique)** to validate the corrected spec/plan. -6. **Re-run Phase 4 (Tasks)** to regenerate `tasks.md` against the corrected plan. -7. **Re-run Phase 5c–5d** (do **not** re-resolve the source PRD — keep the same source) to produce a fresh verdict, then return to the top of this loop and re-evaluate it. - -**Loop exits:** - -- **Aligned/minor** — a pass produces `✅ ALIGNED` or `⚠️ MINOR DRIFT`: accept it and proceed to Completion. -- **Budget exhausted** — the counter reached `2` and the verdict is still `🛑 SIGNIFICANT DRIFT`: stop. Update `alignment-check.md` with verdict `🛑 UNRESOLVED — manual review required`, list the remaining findings, and surface this prominently in the Completion summary. Do **not** silently proceed. - -**Retry budget**: at most **2 remediation passes**, tracked by the counter above. - -Commit each remediation pass's artifacts as it runs (the inner skills handle their own commits; you only need to commit the updated `alignment-check.md` at the end of each pass). - -## Completion - -After all five phases are complete, provide a brief summary: - -- Feature name and spec directory -- Number of tasks generated in `tasks.md` -- Any critique findings that were addressed inline -- **Alignment check status** — verdict, source PRD reference, number of remediation passes used (if any), and any unresolved drift -- Any notable decisions you made autonomously - -**Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: - -`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` - -- `STATUS: READY` — `tasks.md` was generated **and** the alignment outcome is `✅ ALIGNED`, `⚠️ MINOR DRIFT`, or `⚠️ SKIPPED`. Only in this state is the spec safe to implement. -- `STATUS: BLOCKED` — alignment is `🛑 UNRESOLVED` after the retry budget, or any phase could not complete. This is the explicit failure signal the parent `speckit-opsmill-auto` checks before deciding whether to start implementation; do not dress an unresolved run up as a success. -- `SPEC_DIR` MUST be the absolute path to the spec directory, so the parent can hand it to the implement phase without parsing prose. - -Keep this as the literal last line, unwrapped. - -Do **not** proceed to implementation, review, or extraction. The user will run those phases (or `speckit-opsmill-auto` from scratch) when ready. \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.retrospect.md b/.agents/commands/speckit.opsmill.retrospect.md deleted file mode 100644 index 8a1a48057..000000000 --- a/.agents/commands/speckit.opsmill.retrospect.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -description: Run a session retrospective that surfaces context-management gaps and - routes them to approved follow-up actions. ---- - - -<!-- Extension: opsmill --> -<!-- Config: .specify/extensions/opsmill/ --> -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). Treat it as optional scope guidance, such as a focus area, a pasted transcript summary, or a preferred disposition. - -## Outline - -Run a retrospective on the current agent session while the work is still fresh in context. The goal is to identify concrete improvements to the repository's context-management surface area: `AGENTS.md`, `CLAUDE.md`, `.claude/settings.json`, `.agents/skills/`, `.agents/commands/`, `.specify/templates/`, `dev/knowledge/`, `dev/guides/`, `dev/guidelines/`, `dev/adr/`, and product or architecture decisions that caused avoidable friction. - -This command closes the loop from "the agent struggled with X" to "X is captured as an approved repo improvement, dedicated PR, GitHub issue, or local-only note." - -## Operating Constraints - -- Stay read-only until the user approves the report and any disposition buckets. Do not edit files, commit, push, or create GitHub issues before approval. -- Present the full retrospective before taking action. Ask for explicit approval per disposition bucket: `fix-now`, `open-pr`, `github-issue`, and `local-only`. -- For `fix-now`, present a diff preview or precise patch plan before writing files. Apply only the approved changes. -- For `github-issue`, defer to the existing `opsmill-dev-creating-issues` skill when available; if it is unavailable, use `gh issue create` only after the user approves the exact issue content. -- Respect `AGENTS.md` guardrails. Ask First topics, including database migrations, GraphQL schema changes, new dependencies, CI/CD workflow changes, and authentication or authorization changes, are never auto-applied. Route them to `open-pr` or `github-issue`. -- Never edit generated files. If a finding points at generated output, identify the source template or generator instead. -- Do not treat personal preferences as repo policy. Use `local-only` for non-project-specific workflow preferences. - -## Phase 1: Resolve Session Context - -1. Identify the repository root and current branch: - - Run `git rev-parse --show-toplevel`. - - Run `git branch --show-current`. - - Run `git status --short` to understand whether there are existing edits. -2. Determine whether the session is inside an active spec-kit feature: - - Try `.specify/scripts/bash/check-prerequisites.sh --json --paths-only`. - - If it succeeds, record `FEATURE_DIR` and use `FEATURE_DIR/retrospective.md` as the report path. - - If it fails or the project is not on a spec-kit feature branch, continue as an ad-hoc session and use `.claude/retrospectives/YYYYMMDD-HHMMSS-<short-branch-or-session-slug>.md` as the report path. -3. Load only the context needed to classify findings: - - Root `AGENTS.md` and any area-specific `AGENTS.md` touched this session. - - `CLAUDE.md` and `.claude/settings.json` if present. - - Relevant skill prompts in `.agents/skills/` and commands in `.agents/commands/`. - - Relevant spec-kit templates in `.specify/templates/`. - - Relevant `dev/knowledge/`, `dev/guides/`, `dev/guidelines/`, or `dev/adr/` pages. - - Current diff and recent commands from the conversation context. - -## Phase 2: Identify Findings - -Review the current conversation and repository context for friction that was avoidable through better shared context. Organize findings into exactly these categories: - -1. **Instructions / Configuration Gaps** - - Missing, unclear, duplicated, or contradictory guidance in `AGENTS.md`, `CLAUDE.md`, `.claude/settings.json`, skill prompts, commands, or spec-kit templates. - - Required repo workflows that the agent had to infer or rediscover. -2. **Documentation Gaps** - - Missing or stale content in `dev/knowledge/`, `dev/guides/`, `dev/guidelines/`, or `dev/adr/`. - - Docs that contradicted current code or left out required verification steps. -3. **Architectural Friction** - - Product, architecture, or code choices that forced repeated workarounds. - - Candidates for ADRs, refactors, or product follow-up. -4. **Mistakes & Corrections** - - Wrong turns taken during the session. - - The guardrail, instruction, template change, or test that would have prevented the mistake. - -Prefer concrete findings over broad observations. Each finding must cite the session event or repository location that motivated it. - -## Phase 3: Assign Dispositions - -For each finding, propose one disposition: - -| Disposition | Use When | Action After Approval | -|-------------|----------|-----------------------| -| `fix-now` | Small, low-risk repo context updates that belong on the current branch and do not touch Ask First topics. | Show diff preview, then edit files only after approval. | -| `open-pr` | Context or harness changes that should be reviewed separately from the current feature branch, or anything touching Ask First topics. | Draft a branch/PR plan. Do not branch, commit, push, or open a PR unless explicitly approved. | -| `github-issue` | Larger work, ambiguous ownership, product/architecture debt, ADR candidates needing human authorship, or work outside current scope. | Use the `opsmill-dev-creating-issues` skill to draft each issue; create only after approval. | -| `local-only` | Personal workflow preferences or notes that should not become repo policy. | Record only in approved local .specify/memory/report locations supported by the runtime. | - -Disposition rules: - -- If a finding involves database schema or migration changes, GraphQL schema changes, new dependencies, CI/CD workflows, authentication, authorization, or generated files, do not assign `fix-now`. -- If a finding needs product or architecture ownership, prefer `github-issue` unless the user explicitly asks for a dedicated PR. -- If a finding is already fully covered by existing docs or instructions, do not include it as a finding; mention it only in a brief "No action" note if helpful. - -## Phase 4: Produce Review Report - -Before writing any files, present the retrospective in chat using this structure: - -```markdown -## Session Retrospective - -**Scope**: <current feature, branch, or ad-hoc session> -**Proposed report path**: <path> - -### Findings - -| ID | Category | Evidence | Improvement | Disposition | -|----|----------|----------|-------------|-------------| -| R1 | Instructions / Configuration Gaps | <specific session event or file path> | <concrete change> | fix-now | - -### Disposition Buckets - -#### fix-now - -- R1: <target file and change summary> - -#### open-pr - -- R2: <branch or PR scope> - -#### github-issue - -- R3: <issue title> - -#### local-only - -- R4: <memory/report note> - -### Approval Request - -Reply with one or more approved buckets, for example: - -- `approve report only` -- `approve fix-now` -- `approve github-issue R3` -- `approve all except open-pr` -``` - -The report must include concrete findings in all four categories. If a category genuinely has no findings, include the category with `None found` and a one-sentence reason. - -## Phase 5: Save Report - -Save the retrospective report only after the user approves `report only`, a disposition bucket, or `all`. - -Report path: - -- Active spec-kit feature: `specs/<current-feature>/retrospective.md`. -- Ad-hoc session: `.claude/retrospectives/YYYYMMDD-HHMMSS-<short-branch-or-session-slug>.md`. - -If the report path already exists, append a numeric suffix rather than overwriting it. - -## Phase 6: Execute Approved Dispositions - -Process only the buckets the user approved. - -### fix-now - -1. Show a diff preview or exact patch plan for the approved findings. -2. Wait for confirmation if the preview changes the original scope. -3. Apply edits with normal repository editing rules. -4. Run relevant validation. For prompt, command, and Markdown-only changes, at minimum verify paths exist and the command can be discovered from the extension manifest and runtime command file. Run broader lint or tests when touched files are covered by existing automation. - -### open-pr - -1. Draft the proposed branch name, PR title, PR body summary, and changed file list. -2. Ask for explicit approval before creating a branch, committing, pushing, or opening a PR. -3. If approved, use the repository's normal PR workflow. - -### github-issue - -1. For each approved finding, invoke the `opsmill-dev-creating-issues` skill with the finding as source material. -2. Present the exact issue title/body/labels for approval. -3. Create issues only after approval. - -### local-only - -1. Record the note only in runtime-supported local memory or in the approved retrospective report. -2. Do not add local-only preferences to repo files unless the user explicitly reclassifies them as repo policy. - -## Completion - -After approved actions are complete, provide a brief summary: - -- Report path -- Actions taken -- Validation run -- Any remaining unapproved buckets \ No newline at end of file diff --git a/.agents/commands/speckit.opsmill.summary.md b/.agents/commands/speckit.opsmill.summary.md deleted file mode 100644 index 95b8ba635..000000000 --- a/.agents/commands/speckit.opsmill.summary.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -description: Produce a flow-level summary of the current Claude Code session — executive - summary, chronological timeline, and outcomes — written into the active feature - directory next to spec.md / plan.md. -scripts: - sh: .specify/scripts/bash/check-prerequisites.sh --json --paths-only - ps: .specify/scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly ---- - - -<!-- Extension: opsmill --> -<!-- Config: .specify/extensions/opsmill/ --> -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -Supported argument: `--since <commit|time>` to bound the summary window. -Examples: - -- `--since HEAD~10` — limit the timeline to phases that began after the - 10th-most-recent commit on this branch. -- `--since 14:00` — only include phases that occurred after 2 PM local time - in the active session. - -If no argument is given, summarize the **entire live session**. - -## Goal - -Produce a single markdown document that lets a teammate (or future-you) -understand the *flow* of the session in under 60 seconds. The unit of -output is the working session, not any one artifact. Different -granularity from `speckit.archive` (per-feature, permanent) and -`speckit.reconcile.run` (drift-fixing). - -## Operating Constraints - -**STAY AT THE FLOW LEVEL.** This summary is a narrative, not a -transcript. Do **NOT** include any of the following: - -- Per-tool-call narration ("called Read on X.py, then called Edit on...") -- Diffs or hunks of any size -- Line numbers -- Code snippets longer than a single line -- File-by-file change logs (those belong in PR descriptions and `git log`) -- Step-by-step reproductions of debugging -- Quotes from the conversation buffer longer than one short clause - -**STRICTLY ADDITIVE.** Do not modify `spec.md`, `plan.md`, `tasks.md`, -or any source files. The only file this command writes is the session -summary itself. - -**IDEMPOTENT IN SPIRIT.** A second invocation in the same session must -produce a comparable summary, not a summary of summaries. Ignore -previously generated session files when composing the new one. - -## Outline - -1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root and parse `FEATURE_DIR` from the JSON - output. All paths must be absolute. - - - If `FEATURE_DIR` is missing, empty, or the resolved directory does - not exist, **fail fast** with a clear error such as: - `No feature directory resolved from current branch — /speckit.opsmill.summary requires an active feature branch.` - Suggest the user switch to a feature branch and re-run. - -2. Parse `$ARGUMENTS` for an optional `--since <value>`. Treat the - value as opaque — pass it through to `git log --since=<value>` if it - parses as a date/time, otherwise treat it as a git revision and use - `git log <value>..HEAD`. Do not attempt to interpret it any further. - -3. **Gather context** for the summary, in this order of preference: - - a. The live conversation context (primary source — the actual - session is what is being summarized). - b. `git log` on the current branch, optionally bounded by `--since`, - for outcomes that already landed as commits. - c. `FEATURE_DIR/spec.md`, `plan.md`, `tasks.md` if they exist — only - to anchor terminology, not to copy content. - - Do **not** read every changed file. Do **not** generate diffs. Stay - at the level of "what phase of work was happening, and what did it - produce". - -4. **Compose the summary** with exactly these three sections, in this - order: - - ### Executive Summary - - One paragraph (2–4 sentences). What was this session about? What was - the working theme? Reading just this paragraph should answer "what - did they spend their time on today?". - - ### Timeline - - A chronological bullet list. Each entry is a single short line in - the form `HH:MM — <human-readable phase>`, e.g.: - - - `10:42 — investigated failing E2E test in deployments_create.py` - - `11:05 — narrowed cause to driver init order` - - `11:30 — applied fix and re-ran suite` - - Aim for 5–15 entries. Collapse near-duplicate phases (e.g. three - consecutive "ran tests" lines into one). If exact timestamps are not - recoverable, use relative ordering with no `HH:MM` prefix and a - leading dash only. - - ### Outcomes - - A short bullet list of concrete results from the session. Reference - paths and PRs only — no diffs. Examples: - - - Opened PR #161 (speckit-extensions cleanup). - - Materially changed `.specify/extensions/auto/`, `tinyspec/`. - - Decided: extensions invoke skills, not slash commands. - - Deferred: rewriting the auto extension's hook condition DSL. - -5. **Write the file.** - - - Path: `FEATURE_DIR/sessions/session-YYYY-MM-DD-HHMM.md`, where - `YYYY-MM-DD-HHMM` is the local time at invocation. - - Create `FEATURE_DIR/sessions/` if it does not exist. - - If a file with the same `HHMM` already exists, append a numeric - suffix (`-2`, `-3`, …) rather than overwriting. - -6. **Print the resolved path** back to the user as the final message, - plus a one-line reminder that the summary is intentionally - high-level and the conversation transcript / `git log` remain the - sources of truth for detail. - -## Quality Bar - -A reader who was not in the session should, after under 60 seconds with -the document, be able to answer: - -- What was the session trying to accomplish? -- What was the rough order of work? -- What concretely shipped, what was decided, what was deferred? - -If the draft cannot pass that bar, tighten it before writing. If it -exceeds ~80 lines of markdown, you are too detailed — cut. \ No newline at end of file diff --git a/.agents/commands/speckit.review.code.md b/.agents/commands/speckit.review.code.md deleted file mode 100644 index 99bf3ae41..000000000 --- a/.agents/commands/speckit.review.code.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: General code quality review — project guideline compliance, bug detection, - code quality analysis. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives. - -## Review Scope - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Review Responsibilities - -**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions. - -**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems. - -**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage. - -## Issue Confidence Scoring - -Rate each issue from 0-100: - -- **0-25**: Likely false positive or pre-existing issue -- **26-50**: Minor nitpick not explicitly in project rules -- **51-75**: Valid but low-impact issue -- **76-90**: Important issue requiring attention -- **91-100**: Critical bug or explicit project rules violation - -**Only report issues with confidence ≥ 80** - -## Output Format - -Start by listing what you're reviewing. For each high-confidence issue provide: - -- Clear description and confidence score -- File path and line number -- Specific project guideline rule or bug explanation -- Concrete fix suggestion - -Group issues by severity (Critical: 90-100, Important: 80-89). - -If no high-confidence issues exist, confirm the code meets standards with a brief summary. - -Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter. \ No newline at end of file diff --git a/.agents/commands/speckit.review.comments.md b/.agents/commands/speckit.review.comments.md deleted file mode 100644 index 18d01c121..000000000 --- a/.agents/commands/speckit.review.comments.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -description: Code comment accuracy verification, documentation completeness assessment, - comment rot detection. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time. - -Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Comments Framework:** - -When analyzing comments, you will: - -1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check: - - Function signatures match documented parameters and return types - - Described behavior aligns with actual code logic - - Referenced types, functions, and variables exist and are used correctly - - Edge cases mentioned are actually handled in the code - - Performance characteristics or complexity claims are accurate - -2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant: - - Critical assumptions or preconditions are documented - - Non-obvious side effects are mentioned - - Important error conditions are described - - Complex algorithms have their approach explained - - Business logic rationale is captured when not self-evident - -3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime: - - Comments that merely restate obvious code should be flagged for removal - - Comments explaining 'why' are more valuable than those explaining 'what' - - Comments that will become outdated with likely code changes should be reconsidered - - Comments should be written for the least experienced future maintainer - - Avoid comments that reference temporary states or transitional implementations - -4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted: - - Ambiguous language that could have multiple meanings - - Outdated references to refactored code - - Assumptions that may no longer hold true - - Examples that don't match current implementation - - TODOs or FIXMEs that may have already been addressed - -5. **Suggest Improvements**: Provide specific, actionable feedback: - - Rewrite suggestions for unclear or inaccurate portions - - Recommendations for additional context where needed - - Clear rationale for why comments should be removed - - Alternative approaches for conveying the same information - -Your analysis output should be structured as: - -**Summary**: Brief overview of the comment analysis scope and findings - -**Critical Issues**: Comments that are factually incorrect or highly misleading -- Location: [file:line] -- Issue: [specific problem] -- Suggestion: [recommended fix] - -**Improvement Opportunities**: Comments that could be enhanced -- Location: [file:line] -- Current state: [what's lacking] -- Suggestion: [how to improve] - -**Recommended Removals**: Comments that add no value or create confusion -- Location: [file:line] -- Rationale: [why it should be removed] - -**Positive Findings**: Well-written comments that serve as good examples (if any) - -Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value. - -IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement. \ No newline at end of file diff --git a/.agents/commands/speckit.review.errors.md b/.agents/commands/speckit.review.errors.md deleted file mode 100644 index 3b19f0df8..000000000 --- a/.agents/commands/speckit.review.errors.md +++ /dev/null @@ -1,147 +0,0 @@ ---- -description: Error handling review — silent failure detection, catch block analysis, - error logging. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable. - -## Determine Changed Files - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Principles - -You operate under these non-negotiable rules: - -1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect -2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it -3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems -4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible -5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems - -## Your Review Process - -When examining a PR, you will: - -### 1. Identify All Error Handling Code - -Systematically locate: -- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.) -- All error callbacks and error event handlers -- All conditional branches that handle error states -- All fallback logic and default values used on failure -- All places where errors are logged but execution continues -- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors - -### 2. Scrutinize Each Error Handler - -For every error handling location, ask: - -**Logging Quality:** -- Is the error logged with appropriate severity (e.g., warn vs. error)? -- Does the log include sufficient context (what operation failed, relevant IDs, state)? -- Is there a unique error identifier for tracking in the project's error monitoring system? -- Would this log help someone debug the issue 6 months from now? - -**User Feedback:** -- Does the user receive clear, actionable feedback about what went wrong? -- Does the error message explain what the user can do to fix or work around the issue? -- Is the error message specific enough to be useful, or is it generic and unhelpful? -- Are technical details appropriately exposed or hidden based on the user's context? - -**Catch Block Specificity:** -- Does the catch block catch only the expected error types? -- Could this catch block accidentally suppress unrelated errors? -- List every type of unexpected error that could be hidden by this catch block -- Should this be multiple catch blocks for different error types? - -**Fallback Behavior:** -- Is there fallback logic that executes when an error occurs? -- Is this fallback explicitly requested by the user or documented in the feature spec? -- Does the fallback behavior mask the underlying problem? -- Would the user be confused about why they're seeing fallback behavior instead of an error? -- Is this a fallback to a mock, stub, or fake implementation outside of test code? - -**Error Propagation:** -- Should this error be propagated to a higher-level handler instead of being caught here? -- Is the error being swallowed when it should bubble up? -- Does catching here prevent proper cleanup or resource management? - -### 3. Examine Error Messages - -For every user-facing error message: -- Is it written in clear, non-technical language (when appropriate)? -- Does it explain what went wrong in terms the user understands? -- Does it provide actionable next steps? -- Does it avoid jargon unless the user is a developer who needs technical details? -- Is it specific enough to distinguish this error from similar errors? -- Does it include relevant context (file names, operation names, etc.)? - -### 4. Check for Hidden Failures - -Look for patterns that hide errors: -- Empty catch blocks (absolutely forbidden) -- Catch blocks that only log and continue -- Returning null/nil/None/default values on error without logging -- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail -- Fallback chains that try multiple approaches without explaining why -- Retry logic that exhausts attempts without informing the user - -### 5. Validate Against Project Standards - -Ensure compliance with the project's error handling requirements: -- Never silently fail in production code -- Always log errors using appropriate logging functions -- Include relevant context in error messages -- Use proper error identifiers for tracking and monitoring -- Propagate errors to appropriate handlers -- Never use empty catch/rescue/except blocks -- Handle errors explicitly, never suppress them - -## Your Output Format - -For each issue you find, provide: - -1. **Location**: File path and line number(s) -2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific) -3. **Issue Description**: What's wrong and why it's problematic -4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden -5. **User Impact**: How this affects the user experience and debugging -6. **Recommendation**: Specific code changes needed to fix the issue -7. **Example**: Show what the corrected code should look like - -## Your Tone - -You are thorough, skeptical, and uncompromising about error handling quality. You: -- Call out every instance of inadequate error handling, no matter how minor -- Explain the debugging nightmares that poor error handling creates -- Provide specific, actionable recommendations for improvement -- Acknowledge when error handling is done well (rare but important) -- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..." -- Are constructively critical - your goal is to improve the code, not to criticize the developer - -## Special Considerations - -Be aware of any project-specific conventions: -- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics) -- Verify that error identifiers follow any project-defined catalog or registry -- The project may explicitly forbid silent failures in production code -- Empty catch/rescue/except blocks are never acceptable -- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them - -Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed. \ No newline at end of file diff --git a/.agents/commands/speckit.review.run.md b/.agents/commands/speckit.review.run.md deleted file mode 100644 index 746c8ab82..000000000 --- a/.agents/commands/speckit.review.run.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -description: Comprehensive code review using specialized agents — orchestrates code, - comments, tests, errors, types, and simplify agents sequentially. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -# Comprehensive PR Review - -Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality. - -**Review Aspects (optional):** "$ARGUMENTS" - -## Review Workflow: - -1. **Load Configuration** - - Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists). - - If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`. - - Extract the `agents` map — each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle. - - Agents set to `false` **MUST** be excluded from this run. Do not launch them. - -2. **Determine Review Scope** - - Parse arguments to see if user requested specific review aspects. - - If specific aspects were requested, run exactly those — config toggles do **not** apply (explicit user request overrides config). - - Default (no arguments): Run all applicable reviews that are enabled in config. - -3. **Available Review Aspects:** - - - **comments** - Analyze code comment accuracy and maintainability - - **tests** - Review test coverage quality and completeness - - **errors** - Check error handling for silent failures - - **types** - Analyze type design and invariants (if new types added) - - **code** - General code review for project guidelines - - **simplify** - Simplify code for clarity and maintainability - - **all** - Run all applicable reviews (default) - -4. **Identify Changed Files** - - - If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - - Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. - - The script automatically picks the best detection mode: - - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. - - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). - - JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` - - **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -5. **Determine Applicable Reviews** - - Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`): - - **Always applicable** (if enabled): `/speckit.review.code` (general quality) - - **If test files changed** (if enabled): `/speckit.review.tests` - - **If comments/docs added** (if enabled): `/speckit.review.comments` - - **If error handling changed** (if enabled): `/speckit.review.errors` - - **If types added/modified** (if enabled): `/speckit.review.types` - - **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine) - - If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)"). - -6. **Launch Review Agents** - - **Sequential approach** (one at a time): - - Easier to understand and act on - - Each report is complete before next - - Good for interactive review - - **Parallel approach** (user can request): - - Launch all agents simultaneously - - Faster for comprehensive review - - Results come back together - -7. **Aggregate Results** - - After agents complete, summarize: - - **Critical Issues** (must fix before merge) - - **Important Issues** (should fix) - - **Suggestions** (nice to have) - - **Positive Observations** (what's good) - -8. **Provide Action Plan** - - Organize findings: - ```markdown - # PR Review Summary - - ## Critical Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Important Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Suggestions (X found) - - [agent-name]: Suggestion [file:line] - - ## Strengths - - What's well-done in this PR - - ## Recommended Action - 1. Fix critical issues first - 2. Address important issues - 3. Consider suggestions - 4. Re-run review after fixes - ``` - -## Usage Examples: - -**Full review (default):** -``` -/speckit.review.run -``` - -**Specific aspects:** -``` -/speckit.review.run tests errors -# Reviews only test coverage and error handling - -/speckit.review.run comments -# Reviews only code comments - -/speckit.review.run simplify -# Simplifies code after passing review -``` - -**Parallel review:** -``` -/speckit.review.run all parallel -# Launches all agents in parallel -``` - -## Agent Descriptions: - -**comment**: -- Verifies comment accuracy vs code -- Identifies comment rot -- Checks documentation completeness - -**tests**: -- Reviews behavioral test coverage -- Identifies critical gaps -- Evaluates test quality - -**errors**: -- Finds silent failures -- Reviews catch blocks -- Checks error logging - -**types**: -- Analyzes type encapsulation -- Reviews invariant expression -- Rates type design quality - -**code**: -- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance -- Detects bugs and issues -- Reviews general code quality - -**simplify**: -- Simplifies complex code -- Improves clarity and readability -- Applies project standards -- Preserves functionality - -## Tips: - -- **Run early**: Before creating PR, not after -- **Focus on changes**: Agents analyze diff by default -- **Address critical first**: Fix high-priority issues before lower priority -- **Re-run after fixes**: Verify issues are resolved -- **Use specific reviews**: Target specific aspects when you know the concern - -## Notes: - -- Agents run autonomously and return detailed reports -- Each agent focuses on its specialty for deep analysis -- Results are actionable with specific file:line references -- Agents use appropriate models for their complexity \ No newline at end of file diff --git a/.agents/commands/speckit.review.simplify.md b/.agents/commands/speckit.review.simplify.md deleted file mode 100644 index f9be92483..000000000 --- a/.agents/commands/speckit.review.simplify.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -description: Code simplification suggestions — clarity, unnecessary complexity, redundant - abstractions. Advisory only. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Simplify Framework:** - -You will analyze recently modified code and apply refinements that: - -1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact. - -2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent). - -3. **Enhance Clarity**: Simplify code structure by: - - - Reducing unnecessary complexity and nesting - - Eliminating redundant code and abstractions - - Improving readability through clear variable and function names - - Consolidating related logic - - Removing unnecessary comments that describe obvious code - - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions - - Choose clarity over brevity - explicit code is often better than overly compact code - -4. **Maintain Balance**: Avoid over-simplification that could: - - - Reduce code clarity or maintainability - - Create overly clever solutions that are hard to understand - - Combine too many concerns into single functions or components - - Remove helpful abstractions that improve code organization - - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) - - Make the code harder to debug or extend - -5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope. - -Your refinement process: - -1. Identify the recently modified code sections -2. Analyze for opportunities to improve elegance and consistency -3. Apply project-specific best practices and coding standards -4. Ensure all functionality remains unchanged -5. Verify the refined code is simpler and more maintainable -6. Document only significant changes that affect understanding - -You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality. \ No newline at end of file diff --git a/.agents/commands/speckit.review.tests.md b/.agents/commands/speckit.review.tests.md deleted file mode 100644 index 6de773557..000000000 --- a/.agents/commands/speckit.review.tests.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -description: Test coverage quality analysis — behavioral coverage, critical gap identification, - test resilience evaluation. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Your Core Responsibilities:** - -1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions. - -2. **Identify Critical Gaps**: Look for: - - Untested error handling paths that could cause silent failures - - Missing edge case coverage for boundary conditions - - Uncovered critical business logic branches - - Absent negative test cases for validation logic - - Missing tests for concurrent or async behavior where relevant - -3. **Evaluate Test Quality**: Assess whether tests: - - Test behavior and contracts rather than implementation details - - Would catch meaningful regressions from future code changes - - Are resilient to reasonable refactoring - - Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity - -4. **Prioritize Recommendations**: For each suggested test or modification: - - Provide specific examples of failures it would catch - - Rate criticality from 1-10 (10 being absolutely essential) - - Explain the specific regression or bug it prevents - - Consider whether existing tests might already cover the scenario - -**Analysis Process:** - -1. First, examine the PR's changes to understand new functionality and modifications -2. Review the accompanying tests to map coverage to functionality -3. Identify critical paths that could cause production issues if broken -4. Check for tests that are too tightly coupled to implementation -5. Look for missing negative cases and error scenarios -6. Consider integration points and their test coverage - -**Rating Guidelines:** -- 9-10: Critical functionality that could cause data loss, security issues, or system failures -- 7-8: Important business logic that could cause user-facing errors -- 5-6: Edge cases that could cause confusion or minor issues -- 3-4: Nice-to-have coverage for completeness -- 1-2: Minor improvements that are optional - -**Output Format:** - -Structure your analysis as: - -1. **Summary**: Brief overview of test coverage quality -2. **Critical Gaps** (if any): Tests rated 8-10 that must be added -3. **Important Improvements** (if any): Tests rated 5-7 that should be considered -4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation -5. **Positive Observations**: What's well-tested and follows best practices - -**Important Considerations:** - -- Focus on tests that prevent real bugs, not academic completeness -- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available -- Remember that some code paths may be covered by existing integration tests -- Avoid suggesting tests for trivial getters/setters unless they contain logic -- Consider the cost/benefit of each suggested test -- Be specific about what each test should verify and why it matters -- Note when tests are testing implementation rather than behavior - -You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change. \ No newline at end of file diff --git a/.agents/commands/speckit.review.types.md b/.agents/commands/speckit.review.types.md deleted file mode 100644 index adad492e4..000000000 --- a/.agents/commands/speckit.review.types.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -description: Type design analysis — encapsulation, invariant expression, usefulness, - and enforcement. -scripts: - sh: .specify/scripts/bash/detect-changed-files.sh - ps: .specify/scripts/powershell/detect-changed-files.ps1 ---- - - -<!-- Extension: review --> -<!-- Config: .specify/extensions/review/ --> -You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants. - -**Your Core Mission:** -You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Analysis Framework:** - -When analyzing a type, you will: - -1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for: - - Data consistency requirements - - Valid state transitions - - Relationship constraints between fields - - Business logic rules encoded in the type - - Preconditions and postconditions - -2. **Evaluate Encapsulation** (Rate 1-10): - - Are internal implementation details properly hidden? - - Can the type's invariants be violated from outside? - - Are there appropriate access modifiers? - - Is the interface minimal and complete? - -3. **Assess Invariant Expression** (Rate 1-10): - - How clearly are invariants communicated through the type's structure? - - Are invariants enforced at compile-time where possible? - - Is the type self-documenting through its design? - - Are edge cases and constraints obvious from the type definition? - -4. **Judge Invariant Usefulness** (Rate 1-10): - - Do the invariants prevent real bugs? - - Are they aligned with business requirements? - - Do they make the code easier to reason about? - - Are they neither too restrictive nor too permissive? - -5. **Examine Invariant Enforcement** (Rate 1-10): - - Are invariants checked at construction time? - - Are all mutation points guarded? - - Is it impossible to create invalid instances? - - Are runtime checks appropriate and comprehensive? - -**Output Format:** - -Provide your analysis in this structure: - -``` -## Type: [TypeName] - -### Invariants Identified -- [List each invariant with a brief description] - -### Ratings -- **Encapsulation**: X/10 - [Brief justification] - -- **Invariant Expression**: X/10 - [Brief justification] - -- **Invariant Usefulness**: X/10 - [Brief justification] - -- **Invariant Enforcement**: X/10 - [Brief justification] - -### Strengths -[What the type does well] - -### Concerns -[Specific issues that need attention] - -### Recommended Improvements -[Concrete, actionable suggestions that won't overcomplicate the codebase] -``` - -**Key Principles:** - -- Prefer compile-time guarantees over runtime checks when feasible -- Value clarity and expressiveness over cleverness -- Consider the maintenance burden of suggested improvements -- Recognize that perfect is the enemy of good - suggest pragmatic improvements -- Types should make illegal states unrepresentable -- Constructor validation is crucial for maintaining invariants -- Immutability often simplifies invariant maintenance - -**Common Anti-patterns to Flag:** - -- Anemic domain models with no behavior -- Types that expose mutable internals -- Invariants enforced only through documentation -- Types with too many responsibilities -- Missing validation at construction boundaries -- Inconsistent enforcement across mutation methods -- Types that rely on external code to maintain invariants - -**When Suggesting Improvements:** - -Always consider: -- The complexity cost of your suggestions -- Whether the improvement justifies potential breaking changes -- The skill level and conventions of the existing codebase -- Performance implications of additional validation -- The balance between safety and usability - -Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity. \ No newline at end of file diff --git a/.agents/skills/speckit-agent-context-update/SKILL.md b/.agents/skills/speckit-agent-context-update/SKILL.md index cd26a414f..46688e047 100644 --- a/.agents/skills/speckit-agent-context-update/SKILL.md +++ b/.agents/skills/speckit-agent-context-update/SKILL.md @@ -3,7 +3,7 @@ name: speckit-agent-context-update description: Refresh the managed Spec Kit section in coding agent context file(s) compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: spec-kit-core source: agent-context:commands/speckit.agent-context.update.md --- @@ -20,7 +20,7 @@ The script reads the agent-context extension config at - `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`. - `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing. -It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`). +It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (any `plan.md` under `specs/`, including nested scoped layouts such as `specs/<scope>/<feature>/plan.md`). If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected. @@ -29,4 +29,4 @@ If `context_files` and `context_file` are empty, the command reports nothing to - **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]` - **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]` -When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`. \ No newline at end of file +When `plan_path` is omitted, the script auto-detects the most recently modified `specs/**/plan.md` (searched recursively, so nested scoped layouts are discovered). \ No newline at end of file diff --git a/.agents/skills/speckit-analyze/SKILL.md b/.agents/skills/speckit-analyze/SKILL.md index 83cd91be4..a13a1d78c 100644 --- a/.agents/skills/speckit-analyze/SKILL.md +++ b/.agents/skills/speckit-analyze/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before analysis)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_analyze` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -69,7 +69,7 @@ Identify inconsistencies, duplications, ambiguities, and underspecified items ac ### 1. Initialize Analysis Context -Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: +Run `.specify/scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: - SPEC = FEATURE_DIR/spec.md - PLAN = FEATURE_DIR/plan.md @@ -211,12 +211,12 @@ Ask the user: "Would you like me to suggest concrete remediation edits for the t After reporting, check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_analyze` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` diff --git a/.agents/skills/speckit-checklist/SKILL.md b/.agents/skills/speckit-checklist/SKILL.md index 927ef4aa8..2aadfbdfc 100644 --- a/.agents/skills/speckit-checklist/SKILL.md +++ b/.agents/skills/speckit-checklist/SKILL.md @@ -32,6 +32,15 @@ disable-model-invocation: false **Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works. +**Ownership and checkbox lifecycle**: + +- Custom checklists generated by this command are reviewer-owned requirements-quality review artifacts. +- `[x]` means the reviewer determined the requirements-quality criterion is satisfied. +- `[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-specify` and `/speckit-clarify`; do not treat that exception as applying to custom checklists generated here. + ## User Input ```text @@ -45,12 +54,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before checklist generation)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_checklist` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -78,7 +87,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Execution Steps -1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. +1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json --template checklist-template` from repo root and parse JSON for FEATURE_DIR, AVAILABLE_DOCS list, and TEMPLATE_CONTENT. - All file paths must be absolute. - 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"). @@ -133,7 +142,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Use progressive disclosure: add follow-on retrieval only if gaps detected - If source docs are large, generate interim summary items instead of embedding raw text -6. **Generate checklist** - Create "Unit Tests for Requirements": +6. **Generate checklist** - Use TEMPLATE_CONTENT as the structural template and create "Unit Tests for Requirements": - Create `FEATURE_DIR/checklists/` directory if it doesn't exist - Generate unique checklist filename: - Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`) @@ -142,6 +151,7 @@ You **MUST** consider the user input before proceeding (if not empty). - If file does NOT exist: Create new file and number items starting from CHK001 - If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016) - Never delete or replace existing checklist content - always preserve and append + - Leave every newly generated item unchecked (`[ ]`); checkbox state belongs to the reviewer **CORE PRINCIPLE - Test the Requirements, Not the Implementation**: Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for: @@ -251,7 +261,7 @@ You **MUST** consider the user input before proceeding (if not empty). - ✅ "Are [edge cases/scenarios] addressed in requirements?" - ✅ "Does the spec define [missing aspect]?" -7. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001. +7. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, ownership note, notes section, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, an ownership note explaining that `[x]` means reviewer approval of requirements quality, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001, and notes that `/speckit-implement` reads checklist state but does not modify markers. 8. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize: - Focus areas selected @@ -346,12 +356,12 @@ Sample items: **Check for extension hooks (after checklist generation)**: Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_checklist` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` diff --git a/.agents/skills/speckit-checkpoint-commit/SKILL.md b/.agents/skills/speckit-checkpoint-commit/SKILL.md new file mode 100644 index 000000000..9dd09a8aa --- /dev/null +++ b/.agents/skills/speckit-checkpoint-commit/SKILL.md @@ -0,0 +1,58 @@ +--- +name: speckit-checkpoint-commit +description: Commit changes at meaningful checkpoints throughout the workflow +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: Aaron Sun + source: checkpoint:commands/commit.md +--- + +# /speckit.checkpoint.commit + +## Description + +Stages all current changes and creates a commit with a descriptive message reflecting the work just completed. This command should be called by the agent at two points during the spec-kit workflow: + +1. **After each pre-implementation step** (constitution, specify, clarify, plan, analyze, tasks) — to capture the spec artifacts produced by that step. +2. **During implementation** — at natural checkpoints, such as after completing a task or a logical group of related tasks from the task list. + +## When to Commit + +### Pre-Implementation Steps + +Commit immediately after each of these steps completes: + +- After **constitution** — commit the project principles and guidelines +- After **specify** — commit the requirements and user stories +- After **clarify** — commit the resolved clarifications +- After **plan** — commit the technical implementation plan +- After **analyze** — commit the consistency and coverage analysis +- After **tasks** — commit the task breakdown + +### During Implementation + +Use the task list generated during the tasks step as a reference for when to commit. Good checkpoints include: + +- Completing a single task from the task list +- Completing a logical group of closely related tasks +- Finishing a self-contained unit of work (e.g., a new component, a service layer, a set of tests) + +The agent should use judgment — the goal is to produce commits that are meaningful and reviewable, not to commit after every single file change. + +## Workflow + +1. Determine a clear, concise commit message based on the work just completed +2. Stage all current changes (`git add -A`) +3. Commit with the message (`git commit -m "<message>"`) + +## Commit Message Guidelines + +- For pre-implementation steps, use the step name as a prefix (e.g., `constitution: establish project principles`, `tasks: break down implementation into actionable items`) +- For implementation commits, describe what was built (e.g., `implement user authentication service`, `add product listing page and API routes`) +- Keep messages concise but descriptive enough to understand the scope of the commit + +## Exit Criteria + +- All current changes are staged and committed +- The commit message accurately reflects the work completed +- The working tree is clean after the commit \ No newline at end of file diff --git a/.agents/skills/speckit-clarify/SKILL.md b/.agents/skills/speckit-clarify/SKILL.md index 1715752e7..ee5b5f12f 100644 --- a/.agents/skills/speckit-clarify/SKILL.md +++ b/.agents/skills/speckit-clarify/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before clarification)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_clarify` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -126,7 +126,7 @@ Execution steps: For each category with Partial or Missing status, add a candidate question opportunity unless: - Clarification would not materially change implementation or validation strategy - - Information is better deferred to planning phase (note internally) + - The item is specifically about implementation method, tech-stack comparison, or task breakdown (note internally) 4. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: - Maximum of 5 total questions across the whole session. @@ -141,6 +141,12 @@ Execution steps: 5. Sequential questioning loop (interactive): - Present EXACTLY ONE question at a time. + - **Question writing quality (applies to every question, MC or short-answer):** + - Lead with `**Question:**` followed by a full interrogative that ends with `?`. The question text before the `?` must make sense on its own. + - NEVER use a topic label, section heading, or requirement id as the question itself. For example, `Acceptance device/runtime matrix (FR-023)` is INVALID — it is a label, not a question. + - After the `?`, the only permitted suffix is an optional parenthesized requirement/question id. Exact format: `**Question:** <interrogative>?` or `**Question:** <interrogative>? (FR-023)`. Never put the id before the `?`, and never use the id (alone or with a topic label) as the whole prompt. + - Immediately after the question line, add one plain-language "Why it matters" sentence (the stake for acceptance or shipping) before the recommendation/options. + - Use everyday wording; introduce jargon only if defined in the same sentence. Self-check: a reader who does not know Spec Kit must be able to answer from the Question line alone. Terse is fine; cryptic labels are not. - For multiple‑choice questions: - **Analyze all options** and determine the **most suitable option** based on: - Best practices for the project type @@ -241,12 +247,12 @@ Context for prioritization: $ARGUMENTS Check if `.specify/extensions.yml` exists in the project root. - If it does not exist, or no hooks are registered under `hooks.after_clarify`, skip to the Completion Report. - If it exists, read it and look for entries under the `hooks.after_clarify` key. -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report. +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue to the Completion Report. - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` @@ -276,7 +282,7 @@ Report completion (after questioning loop ends or early termination): - Path to updated spec. - Sections touched (list names). - Spec quality checklist status (if `FEATURE_DIR/checklists/requirements.md` was re-validated): show before/after pass counts (e.g., "Spec Quality Checklist: 12/16 → 15/16 items passing") and list any items that changed state — both newly checked (unchecked → checked) and any regressions (checked → unchecked). If any items remain unchecked, list them as areas needing attention. -- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact). +- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota, or remaining item is specifically implementation method, tech-stack comparison, or task breakdown), Clear (already sufficient), Outstanding (still Partial/Missing but low impact). - If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit-plan` or run `/speckit-clarify` again later post-plan. - Suggested next command. diff --git a/.agents/skills/speckit-constitution/SKILL.md b/.agents/skills/speckit-constitution/SKILL.md index ea8e9d97e..a29582970 100644 --- a/.agents/skills/speckit-constitution/SKILL.md +++ b/.agents/skills/speckit-constitution/SKILL.md @@ -1,6 +1,6 @@ --- name: "speckit-constitution" -description: "Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync." +description: "Create or update the project constitution from interactive or provided principle inputs." argument-hint: "Principles or values for the project constitution" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: @@ -19,17 +19,36 @@ $ARGUMENTS You **MUST** consider the user input before proceeding (if not empty). +## Scope Guard + +This command's own work is limited to updating the project constitution itself. Dependent templates +and commands read the constitution at runtime and are not modified here. + +- Classify every part of the user input as either constitution content or a separate, + non-governance intent. +- If the input includes feature implementation, code generation, refactoring, building, or + deployment requests, you **MUST NOT** execute them. Extract them as deferred intents instead. +- You **MUST NOT** create, modify, or delete application source files, feature routes, + components, tests, deployment files, or other artifacts unrelated to the constitution + workflow. +- If it is unclear whether an instruction is constitution content, ask for clarification before + making changes. +- After completing the constitution update, include a `Next Actions` section for each deferred + intent. List the original intent and suggest the appropriate follow-up Spec Kit command, such + as `/speckit-specify`, without invoking it. +- If there are no non-governance intents, omit the `Next Actions` section. + ## Pre-Execution Checks **Check for extension hooks (before constitution update)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_constitution` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -57,13 +76,22 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline -You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts. - -**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first. +You are updating the project constitution at `.specify/memory/constitution.md`. The active +constitution scaffold is resolved at command time from `constitution-template` through the Spec Kit +preset/template resolution stack. Follow this execution flow: -1. Load the existing constitution at `.specify/memory/constitution.md`. +1. Run `.specify/scripts/bash/resolve-template.sh constitution-template --json` from the repository root and parse `TEMPLATE_CONTENT` as the active template. + - The shared resolver applies project overrides, composing preset layers, and extension layers + before the core template fallback. It MUST succeed before continuing. + - If it fails, stop and report the resolution error; do not continue with only one contributing + template layer. + - If `.specify/memory/constitution.md` exists, load it as the source of current project-specific + values and amendments. Preserve information that is still applicable when applying the newly + resolved scaffold. + - If it does not exist, use the resolved template as the initial document. + - Do not write back to any versioned template layer. - Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`. **IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly. @@ -77,39 +105,34 @@ Follow this execution flow: - PATCH: Clarifications, wording, typo fixes, non-semantic refinements. - If version bump type ambiguous, propose reasoning before finalizing. -3. Draft the updated constitution content: +3. Draft the updated constitution content using the resolved template as the required structure: - Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left). - Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance. - Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing non‑negotiable rules, explicit rationale if not obvious. - Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations. -4. Consistency propagation checklist (convert prior checklist into active validations): - - Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles. - - Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints. - - Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline). - - Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required. - - Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed. - -5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update): +4. Produce a Sync Impact Report as an HTML comment at the top of the constitution file after update. + This report is temporary scratch material for human review of the amendment, not governance + content; it is expected to be removed before the amended constitution file is committed. - Version change: old → new - List of modified principles (old title → new title if renamed) - Added sections - Removed sections - - Templates requiring updates (✅ updated / ⚠ pending) with file paths - Follow-up TODOs if any placeholders intentionally deferred. -6. Validation before final output: +5. Validation before final output: - No remaining unexplained bracket tokens. - Version line matches report. - Dates ISO format YYYY-MM-DD. - Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate). -7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite). +6. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite). -8. Output a final summary to the user with: +7. Output a final summary to the user with: - New version and bump rationale. - - Any files flagged for manual follow-up. + - Any TODO placeholders or deferred items requiring manual follow-up. - Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`). + - A `Next Actions` section for any deferred non-governance intents. Formatting & Style Requirements: @@ -122,19 +145,19 @@ If the user supplies partial updates (e.g., only one principle revision), still If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items. -Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file. +Write only `.specify/memory/constitution.md`; do not create or modify template source files. ## Post-Execution Checks **Check for extension hooks (after constitution update)**: Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_constitution` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` diff --git a/.agents/skills/speckit-converge/SKILL.md b/.agents/skills/speckit-converge/SKILL.md index 61c82417a..4147af865 100644 --- a/.agents/skills/speckit-converge/SKILL.md +++ b/.agents/skills/speckit-converge/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_converge` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): @@ -96,7 +96,7 @@ skip constitution checks gracefully rather than failing. ### 1. Initialize Convergence Context -Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: +Run `.specify/scripts/bash/check-prerequisites.sh --json --require-spec --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: - SPEC = FEATURE_DIR/spec.md - PLAN = FEATURE_DIR/plan.md @@ -149,6 +149,12 @@ Create an internal model (do not echo raw artifacts): ### 4. Assess the Codebase and Classify Findings +Include every existing task in the intent inventory, regardless of checkbox state or +Convergence phase: completion claims are not evidence. Verify current behavior against +the spec, plan, tasks, and constitution; for corrective task chains, assess the resulting +behavior, not superseded implementation details. Check both unmet obligations and +implementation that contradicts, exceeds, or falls outside the stated intent. + For each item in the intent inventory, inspect the current code in scope and produce a `Finding` only where there is a gap. Classify every finding by **gap type**: @@ -243,14 +249,14 @@ Append to the **end** of `tasks.md`, per the append contract: After producing the result, check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_converge` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation - Report the convergence outcome (`converged` or `tasks_appended`) in-session before listing any hooks, so users can decide whether to run optional follow-up commands. -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): diff --git a/.agents/skills/speckit-critique-run/SKILL.md b/.agents/skills/speckit-critique-run/SKILL.md index 60fa93ebe..0ce7e4811 100644 --- a/.agents/skills/speckit-critique-run/SKILL.md +++ b/.agents/skills/speckit-critique-run/SKILL.md @@ -1,11 +1,10 @@ --- name: speckit-critique-run -description: Perform a dual-lens critical review of the specification and plan from - both product strategy and engineering risk perspectives before implementation. +description: Perform a dual-lens critical review of the specification and plan from both product strategy and engineering risk perspectives before implementation. compatibility: Requires spec-kit project structure with .specify/ directory metadata: author: github-spec-kit - source: critique:commands/run.md + source: critique-opsmill:commands/speckit.critique.run.md --- ## User Input @@ -175,7 +174,7 @@ Challenge the specification and implementation plan through two distinct expert - 🤔 **Question**: Ambiguity or assumption that needs stakeholder input. Cannot be resolved by the development team alone. 7. **Generate Critique Report**: - Ensure the directory `FEATURE_DIR/critiques/` exists (create it if necessary), then create the critique report at `FEATURE_DIR/critiques/critique-{timestamp}.md` using `.specify/templates/critique-template.md` as the required structure. The report must include: + Ensure the directory `FEATURE_DIR/critiques/` exists (create it if necessary), then create the critique report at `FEATURE_DIR/critiques/critique-{timestamp}.md` using `.specify/extensions/critique/commands/critique-template.md` as the required structure. The report must include: - **Executive Summary**: Overall assessment and readiness to proceed - **Product Lens Findings**: Organized by subcategory (3a-3e) diff --git a/.agents/skills/speckit-git-commit/SKILL.md b/.agents/skills/speckit-git-commit/SKILL.md new file mode 100644 index 000000000..177b864f3 --- /dev/null +++ b/.agents/skills/speckit-git-commit/SKILL.md @@ -0,0 +1,53 @@ +--- +name: speckit-git-commit +description: Auto-commit changes after a Spec Kit command completes +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: spec-kit-core + source: git:commands/speckit.git.commit.md +--- + +# Auto-Commit Changes + +Automatically stage and commit all changes after a Spec Kit command completes. + +## Behavior + +This command is invoked as a hook after (or before) core commands. It: + +1. Determines the event name from the hook context (e.g., if invoked as an `after_specify` hook, the event is `after_specify`; if `before_plan`, the event is `before_plan`) +2. Checks `.specify/extensions/git/git-config.yml` for the `auto_commit` section +3. Looks up the specific event key to see if auto-commit is enabled +4. Falls back to `auto_commit.default` if no event-specific key exists +5. Uses the per-command `message` if configured, otherwise a default message +6. If enabled and there are uncommitted changes, runs `git add .` + `git commit` + +## Execution + +Determine the event name from the hook that triggered this command, then run the script: + +- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>` + +Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`). + +## Configuration + +In `.specify/extensions/git/git-config.yml`: + +```yaml +auto_commit: + default: false # Global toggle — set true to enable for all commands + after_specify: + enabled: true # Override per-command + message: "[Spec Kit] Add specification" + after_plan: + enabled: false + message: "[Spec Kit] Add implementation plan" +``` + +## Graceful Degradation + +- If Git is not available or the current directory is not a repository: skips with a warning +- If no config file exists: skips (disabled by default) +- If no changes to commit: skips with a message \ No newline at end of file diff --git a/dev/spec-kit/presets/infrahub/commands/speckit.infrahub.feature.md b/.agents/skills/speckit-git-feature/SKILL.md similarity index 54% rename from dev/spec-kit/presets/infrahub/commands/speckit.infrahub.feature.md rename to .agents/skills/speckit-git-feature/SKILL.md index 16724fae0..ed8f85286 100644 --- a/dev/spec-kit/presets/infrahub/commands/speckit.infrahub.feature.md +++ b/.agents/skills/speckit-git-feature/SKILL.md @@ -1,10 +1,15 @@ --- -description: "Validate Jira/JPD ticket reference and create a feature branch" +name: speckit-git-feature +description: Validate Jira/JPD ticket reference and create a feature branch +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: github-spec-kit + source: jira-branch:commands/speckit.git.feature.md --- -# Create Infrahub Feature Branch +# Create Feature Branch -Create and switch to a new git feature branch for the given specification, enforcing an Infrahub Jira or JPD ticket reference as the branch name suffix. +Create and switch to a new git feature branch for the given specification, enforcing a Jira or JPD ticket reference as the branch name suffix. ## User Input @@ -43,28 +48,21 @@ Generate a concise short name (2-4 words) from the feature description: ## Step 3: Create Branch -Construct the branch name as `<short-name>-<ticket-id>` (e.g., `user-auth-infp-460`), then pass it as `GIT_BRANCH_NAME` to bypass the script's automatic numbering: +Construct the branch name as `<short-name>-<ticket-id>` (e.g., `user-auth-infp-460`): the ticket ID is the suffix, and the name must be 244 bytes or fewer. -```bash -GIT_BRANCH_NAME="<short-name>-<ticket-id>" .specify/extensions/git/scripts/bash/create-new-feature.sh --json "<feature description>" -``` - -Example: +Create the branch and switch to it with one plain git command, written out, without an environment variable before it, so that permission rules can match it: ```bash -GIT_BRANCH_NAME="user-auth-infp-460" .specify/extensions/git/scripts/bash/create-new-feature.sh --json "Add user authentication" +git checkout -b "<short-name>-<ticket-id>" ``` -**IMPORTANT**: +If the branch already exists, do not switch to it and do not create another one: stop and tell the user. -- Always construct `GIT_BRANCH_NAME` as `<short-name>-<ticket-id>` — ticket ID is the suffix -- Always include `--json` so output can be parsed reliably -- Run this script exactly once per feature -- For single quotes in args, use escape syntax: `'I'\''m Groot'` +The git extension's branch script is not used: it only takes an exact branch name through the `GIT_BRANCH_NAME` environment variable, which keeps permission rules from matching the command. ## Output -The script outputs JSON with: +Report, as JSON: -- `BRANCH_NAME`: The branch name (e.g., `user-auth-infp-460`) -- `FEATURE_NUM`: The ticket ID +- `BRANCH_NAME`: the branch name (e.g., `user-auth-infp-460`) +- `FEATURE_NUM`: the ticket ID resolved in Step 1 (e.g., `infp-460`) \ No newline at end of file diff --git a/.agents/skills/speckit-git-initialize/SKILL.md b/.agents/skills/speckit-git-initialize/SKILL.md new file mode 100644 index 000000000..989324ac4 --- /dev/null +++ b/.agents/skills/speckit-git-initialize/SKILL.md @@ -0,0 +1,54 @@ +--- +name: speckit-git-initialize +description: Initialize a Git repository with an initial commit +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: spec-kit-core + source: git:commands/speckit.git.initialize.md +--- + +# Initialize Git Repository + +Initialize a Git repository in the current project directory if one does not already exist. + +## Execution + +Run the appropriate script from the project root: + +- **Bash**: `.specify/extensions/git/scripts/bash/initialize-repo.sh` +- **PowerShell**: `.specify/extensions/git/scripts/powershell/initialize-repo.ps1` + +If the extension scripts are not found, fall back to: +- **Bash**: `git init && git add . && git commit -m "Initial commit from Specify template"` +- **PowerShell**: `git init; git add .; git commit -m "Initial commit from Specify template"` + +The script handles all checks internally: +- Skips if Git is not available +- Skips if already inside a Git repository +- Runs `git init`, `git add .`, and `git commit` with an initial commit message + +## Customization + +Replace the script to add project-specific Git initialization steps: +- Custom `.gitignore` templates +- Default branch naming (`git config init.defaultBranch`) +- Git LFS setup +- Git hooks installation +- Commit signing configuration +- Git Flow initialization + +## Output + +On success: +- `✓ Git repository initialized` + +## Graceful Degradation + +If Git is not installed: +- Warn the user +- Skip repository initialization +- The project continues to function without Git (specs can still be created under `specs/`) + +If Git is installed but `git init`, `git add .`, or `git commit` fails: +- Surface the error to the user +- Stop this command rather than continuing with a partially initialized repository \ No newline at end of file diff --git a/.agents/skills/speckit-git-remote/SKILL.md b/.agents/skills/speckit-git-remote/SKILL.md new file mode 100644 index 000000000..c6361dc34 --- /dev/null +++ b/.agents/skills/speckit-git-remote/SKILL.md @@ -0,0 +1,50 @@ +--- +name: speckit-git-remote +description: Detect Git remote URL for GitHub integration +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: spec-kit-core + source: git:commands/speckit.git.remote.md +--- + +# Detect Git Remote URL + +Detect the Git remote URL for integration with GitHub services (e.g., issue creation). + +## Prerequisites + +- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` +- If Git is not available, output a warning and return empty: + ``` + [specify] Warning: Git repository not detected; cannot determine remote URL + ``` + +## Execution + +Run the following command to get the remote URL: + +```bash +git config --get remote.origin.url +``` + +## Output + +Parse the remote URL and determine: + +1. **Repository owner**: Extract from the URL (e.g., `github` from `https://github.com/github/spec-kit.git`) +2. **Repository name**: Extract from the URL (e.g., `spec-kit` from `https://github.com/github/spec-kit.git`) +3. **Is GitHub**: Whether the remote points to a GitHub repository + +Supported URL formats: +- HTTPS: `https://github.com/<owner>/<repo>.git` +- SSH: `git@github.com:<owner>/<repo>.git` + +> [!CAUTION] +> ONLY report a GitHub repository if the remote URL actually points to github.com. +> Do NOT assume the remote is GitHub if the URL format doesn't match. + +## Graceful Degradation + +If Git is not installed, the directory is not a Git repository, or no remote is configured: +- Return an empty result +- Do NOT error — other workflows should continue without Git remote information \ No newline at end of file diff --git a/.agents/skills/speckit-git-validate/SKILL.md b/.agents/skills/speckit-git-validate/SKILL.md new file mode 100644 index 000000000..9eb6240b0 --- /dev/null +++ b/.agents/skills/speckit-git-validate/SKILL.md @@ -0,0 +1,54 @@ +--- +name: speckit-git-validate +description: Validate current branch follows feature branch naming conventions +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: spec-kit-core + source: git:commands/speckit.git.validate.md +--- + +# Validate Feature Branch + +Validate that the current Git branch follows the expected feature branch naming conventions. + +## Prerequisites + +- Check if Git is available by running `git rev-parse --is-inside-work-tree 2>/dev/null` +- If Git is not available, output a warning and skip validation: + ``` + [specify] Warning: Git repository not detected; skipped branch validation + ``` + +## Validation Rules + +Get the current branch name: + +```bash +git rev-parse --abbrev-ref HEAD +``` + +The branch name must match one of these patterns: + +1. **Sequential**: `^[0-9]{3,}-` (e.g., `001-feature-name`, `042-fix-bug`, `1000-big-feature`) +2. **Timestamp**: `^[0-9]{8}-[0-9]{6}-` (e.g., `20260319-143022-feature-name`) + +## Execution + +If on a feature branch (matches either pattern): +- Output: `✓ On feature branch: <branch-name>` +- Check if the corresponding spec directory exists under `specs/`: + - For sequential branches, look for `specs/<prefix>-*` where prefix matches the numeric portion + - For timestamp branches, look for `specs/<prefix>-*` where prefix matches the `YYYYMMDD-HHMMSS` portion +- If spec directory exists: `✓ Spec directory found: <path>` +- If spec directory missing: `⚠ No spec directory found for prefix <prefix>` + +If NOT on a feature branch: +- Output: `✗ Not on a feature branch. Current branch: <branch-name>` +- Output: `Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name` + +## Graceful Degradation + +If Git is not installed or the directory is not a Git repository: +- Check the `SPECIFY_FEATURE` environment variable as a fallback +- If set, validate that value against the naming patterns +- If not set, skip validation with a warning \ No newline at end of file diff --git a/.agents/skills/speckit-implement/SKILL.md b/.agents/skills/speckit-implement/SKILL.md index 94ea0176c..97aa13491 100644 --- a/.agents/skills/speckit-implement/SKILL.md +++ b/.agents/skills/speckit-implement/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before implementation)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_implement` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -49,7 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty). **Automatic Pre-Hook**: {extension} Executing: `/{command}` EXECUTE_COMMAND: {command} - + Wait for the result of the hook command before proceeding to the Outline. ``` After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. @@ -60,33 +60,36 @@ You **MUST** consider the user input before proceeding (if not empty). 1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 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. **Check checklists status** (if FEATURE_DIR/checklists/ exists): + - Treat checklist markers as a read-only gate: scan checkbox state, report status, and ask before proceeding when needed; do NOT modify checklist files or markers + - `checklists/requirements.md` is the built-in spec-quality checklist maintained by `/speckit-specify` and `/speckit-clarify`; custom checklists generated by `/speckit-checklist` are reviewer-owned requirements-quality review artifacts + - For custom checklists, `[x]` means the reviewer determined the requirements-quality criterion is satisfied; it does NOT mean implementation work is complete - Scan all checklist files in the checklists/ directory - For each checklist, count: - Total items: All lines matching `- [ ]` or `- [X]` or `- [x]` - - Completed items: Lines matching `- [X]` or `- [x]` - - Incomplete items: Lines matching `- [ ]` + - Checked items: Lines matching `- [X]` or `- [x]` + - Unchecked items: Lines matching `- [ ]` - Create a status table: ```text - | Checklist | Total | Completed | Incomplete | Status | - |-----------|-------|-----------|------------|--------| - | ux.md | 12 | 12 | 0 | ✓ PASS | - | test.md | 8 | 5 | 3 | ✗ FAIL | - | security.md | 6 | 6 | 0 | ✓ PASS | + | Checklist | Total | Checked | Unchecked | Status | + |-----------|-------|---------|-----------|--------| + | ux.md | 12 | 12 | 0 | ✓ PASS | + | test.md | 8 | 5 | 3 | ✗ FAIL | + | security.md | 6 | 6 | 0 | ✓ PASS | ``` - Calculate overall status: - - **PASS**: All checklists have 0 incomplete items - - **FAIL**: One or more checklists have incomplete items + - **PASS**: All checklists have 0 unchecked items + - **FAIL**: One or more checklists have unchecked items - - **If any checklist is incomplete**: - - Display the table with incomplete item counts - - **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)" + - **If any checklist has unchecked items**: + - Display the table with unchecked item counts + - **STOP** and ask: "Some checklists have unchecked items. Do you want to proceed with implementation anyway? (yes/no)" - Wait for user response before continuing - If user says "no" or "wait" or "stop", halt execution - If user says "yes" or "proceed" or "continue", proceed to step 3 - - **If all checklists are complete**: + - **If all checklists are checked**: - Display the table showing all checklists passed - Automatically proceed to step 3 @@ -151,7 +154,7 @@ You **MUST** consider the user input before proceeding (if not empty). 6. Execute implementation following the task plan: - **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 + - **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together - **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks - **File-based coordination**: Tasks affecting the same files must run sequentially - **Validation checkpoints**: Verify each phase completion before proceeding @@ -186,12 +189,12 @@ Note: This command assumes a complete task breakdown exists in tasks.md. If task Check if `.specify/extensions.yml` exists in the project root. - If it does not exist, or no hooks are registered under `hooks.after_implement`, skip to the Completion Report. - If it exists, read it and look for entries under the `hooks.after_implement` key. -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report. +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue to the Completion Report. - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` diff --git a/.agents/skills/speckit-opsmill-auto/SKILL.md b/.agents/skills/speckit-opsmill-auto/SKILL.md index 71cbc6762..23188584b 100644 --- a/.agents/skills/speckit-opsmill-auto/SKILL.md +++ b/.agents/skills/speckit-opsmill-auto/SKILL.md @@ -1,11 +1,9 @@ --- name: speckit-opsmill-auto -description: Run the full speckit workflow end-to-end — specify, plan, critique, tasks, - implement, review — making all decisions autonomously. Stops before extract; run - that manually. +description: Run the full speckit workflow end-to-end — specify, plan, critique, tasks, implement, review — making all decisions autonomously. Stops before extract; run that manually. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/auto.md --- @@ -40,12 +38,13 @@ Invoke the `speckit-opsmill-prep` skill with the user's feature description (`$A **Read prep's status line.** `speckit-opsmill-prep` ends its output with a literal final line: -`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <...>` +`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | CRITIQUE: <extension|fallback|none|n/a> | REASON: <...>` Parse that line — do **not** infer success from the prose summary: - Take **`SPEC_DIR`** from this line as the spec directory to pass to Phase B. This is the deterministic hand-off; do not scrape the path out of free text. - If **`STATUS: BLOCKED`** (e.g., alignment never converged within its retry budget, or a phase could not complete), surface the reason to the user and **stop**. Do **not** proceed to implementation on a misaligned or incomplete spec. +- Take **`CRITIQUE`** as the critique coverage of the run. Anything other than `extension` or `n/a` means the critique extension was unavailable and prep either ran a reduced fallback critique or none at all. `n/a` is not a coverage claim — it means prep aborted before Phase 3 ever resolved a provider, so read it together with `STATUS`. This does **not** block Phase B — carry it into the final summary. - Only proceed to Phase B when `STATUS: READY`. ### Phase B — Implementation tail (delegated to `speckit-opsmill-implement`) @@ -57,11 +56,12 @@ Invoke the `speckit-opsmill-implement` skill with the `SPEC_DIR` path from Phase **Read implement's status line.** `speckit-opsmill-implement` ends its output with a literal final line: -`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <...> | REASON: <...>` +`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <...> | REVIEW: <plugin|command|fallback|none|n/a> | REASON: <...>` - `STATUS: BLOCKED` — a Phase 0 stop-condition aborted before any implementation (no report written). Surface the reason in the final summary; do not retry from this orchestrator. -- `STATUS: INCOMPLETE` — blocked tasks or missing local-pass evidence; capture that for the final summary. The user decides whether to re-run. +- `STATUS: INCOMPLETE` — blocked tasks, missing local-pass evidence, or `REVIEW: none`; capture that for the final summary. The user decides whether to re-run. - `STATUS: DONE` — all chunks completed with evidence. +- Take **`REVIEW`** as the review coverage of the run. A missing reviewer does not stop the implement phase: `REVIEW: plugin` and `REVIEW: command` are full reviews, `REVIEW: fallback` means the sub-skill ran a reduced built-in review, `REVIEW: none` means no review happened at all and forces `STATUS: INCOMPLETE`. `REVIEW: n/a` is not a coverage claim either — it means implement aborted before Phase 0 resolved a provider, and it pairs with `STATUS: BLOCKED`. Never treat a completed implement phase as a reviewed change set without checking this field. In every case, do **not** retry from this orchestrator. @@ -73,6 +73,7 @@ After both phases are complete, provide a brief summary: - Whether the alignment check inside auto-prep required retries (and how many) - Number of tasks completed (from the auto-implement report) - Any review findings that were fixed inline vs. deferred +- **Coverage of the quality gates (REQUIRED).** State the `CRITIQUE` value from Phase A and the `REVIEW` value from Phase B explicitly. If `CRITIQUE` is not `extension`, or `REVIEW` is not `plugin` or `command`, lead the summary with it, name what is missing, and give the install command (`specify extension add critique` / `/plugin install pr-review-toolkit@claude-plugins-official`). An autonomous run that quietly skipped a quality gate is the one outcome the user must never have to discover by opening a report file - Any notable decisions you or the sub-skills made autonomously Do **not** open a PR, push, run extraction, or start a new feature. The user takes it from there — extraction (`/speckit.opsmill.extract`) is intentionally left as a manual follow-up so the user can review the implementation report first. \ No newline at end of file diff --git a/.agents/skills/speckit-opsmill-extract/SKILL.md b/.agents/skills/speckit-opsmill-extract/SKILL.md index 3635b2b75..cfd98276f 100644 --- a/.agents/skills/speckit-opsmill-extract/SKILL.md +++ b/.agents/skills/speckit-opsmill-extract/SKILL.md @@ -1,10 +1,9 @@ --- name: speckit-opsmill-extract -description: Extract knowledge, guidelines, and ADRs from one or more completed spec - directories into the project documentation system. +description: Extract knowledge, guidelines, and ADRs from one or more completed spec directories into the project documentation system. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/extract.md --- @@ -221,11 +220,22 @@ Create new files in `dev/adr/` using this format: ### Knowledge updates For each knowledge update: -1. Read the full target file +1. Read the full target file (or create a new one if needed) 2. Find the appropriate section (match by heading) 3. Add or update content within that section 4. If the section does not exist, create it in a logical position -5. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` +5. Add a source marker: `<!-- Extracted from specs/archive/<spec-name> on YYYY-MM-DD -->` + +If creating a new knowledge file, follow the standard structure: +```markdown +# <Topic> + +## Overview + +<Brief description of what this document covers.> + +## <Sections...> +``` ### Guidelines updates @@ -233,7 +243,7 @@ Same approach as knowledge updates: 1. Read the full target file (or create a new one if needed) 2. Find the appropriate section 3. Add the prescriptive pattern with code examples where relevant -4. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` +4. Add a source marker: `<!-- Extracted from specs/archive/<spec-name> on YYYY-MM-DD -->` If creating a new guidelines file, follow the standard structure: ```markdown diff --git a/.agents/skills/speckit-opsmill-implement/SKILL.md b/.agents/skills/speckit-opsmill-implement/SKILL.md index 853ce4990..78c393ec0 100644 --- a/.agents/skills/speckit-opsmill-implement/SKILL.md +++ b/.agents/skills/speckit-opsmill-implement/SKILL.md @@ -1,10 +1,9 @@ --- name: speckit-opsmill-implement -description: Run the speckit implementation phase chunk-by-chunk in clean-context - subagents, then review, then produce a final report. Picks up from an existing tasks.md. +description: Run the speckit implementation phase chunk-by-chunk in clean-context subagents, then review, then produce a final report. Picks up from an existing tasks.md. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/implement.md --- @@ -40,6 +39,18 @@ Before any work begins: 3. Read `tasks.md` end-to-end and identify the chunking boundaries (see "Chunking strategy" below). Build a numbered list of chunks with their task IDs. 4. Verify the working tree is clean (or only contains expected prep artifacts). If it is dirty in unrelated ways: interactive → surface that and pause; autonomous parent → abort with `STATUS: BLOCKED` (reason: unexpectedly dirty working tree). Do not attempt to stash or clean it yourself. 5. Note the current `HEAD` commit — you will diff against it for the review and report. +6. **Resolve the review provider.** Decide how Phase 6 will review the change set and record the answer as `REVIEW_MODE`. Take the first row that applies, deciding from the subagent types and commands your harness actually lists, not from memory: + + | Reviewer available | `REVIEW_MODE` | + |--------------------|---------------| + | The `pr-review-toolkit` plugin: its agents (`pr-review-toolkit:code-reviewer`, `pr-review-toolkit:pr-test-analyzer`, …) are among the subagent types you can dispatch | `plugin` | + | The `code-review` skill of Claude Code (`/code-review`) | `command` | + | Neither, and you can dispatch clean-context subagents | `fallback` | + | Neither, and no subagent dispatch is available | `none` | + + This is a starting point: if Phase 6 cannot use the provider, it moves to the next row and reports the mode that actually ran. The spec-kit `review` extension is not used: it does not install on spec-kit 1.x. + + A missing reviewer is **not** a stop-condition, in either invocation mode. The Phase 0 aborts above exist because a dirty tree or missing prep artifacts contaminate the `HEAD@start..HEAD@now` diff; a missing reviewer does not corrupt anything, it only lowers assurance, and Phase 6 has a defined fallback for it. Carry `REVIEW_MODE` forward to Phase 6, Phase 7 §5, and the status line. ### Phase 5 — Implement (looped, clean-context subagents) @@ -79,7 +90,33 @@ Respect explicit dependencies in `tasks.md`. Sequential `[P]`-free tasks must st ### Phase 6 — Review -Once **all** chunks have completed (including any retries), invoke the `speckit-review-run` skill once across the full diff (`HEAD`-at-start..`HEAD`-now). +Once **all** chunks have completed (including any retries), review the full diff (`HEAD`-at-start..`HEAD`-now). How you *produce* the findings depends on the `REVIEW_MODE` resolved in Phase 0. What you *do* with them does not. + +**6a — `REVIEW_MODE: plugin`.** Dispatch five `pr-review-toolkit` agents in parallel, each in a clean context: `code-reviewer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer` and `comment-analyzer`. Brief each one with the repository root, the diff range (`HEAD`-at-start..`HEAD`-now), `spec.md` and `tasks.md` as the statement of intent, and the project context files resolved by the same rule as Phase 5. Ask each for **findings only**, without editing any file: a severity (`high` / `medium` / `low`), the file, the line where known, and a one-line summary. + +Once all five have returned, run `code-simplifier` alone, with the same briefing. It is built to edit code rather than report, so its edits are never kept: + +- Before it runs, check that the working tree is clean (`git status --porcelain` prints nothing). If it is not, skip `code-simplifier` and say so in the report. +- After it ends, turn every change it made into a `low` finding: read them with `git diff` and `git status --porcelain`, one finding per simplification, with the file and line. +- Then discard its changes: `git restore .` for edited files, and delete the files it created (listed as `??`). The working tree must be clean again before the finding handling below. + +**6b — `REVIEW_MODE: command`.** Run the `code-review` skill (`/code-review`) on the change set, given as a Git revision range (`<HEAD-at-start>..HEAD`). The command reviews in the background: wait for its findings before you go on, and collect them in the same shape as 6a. + +If 6a or 6b cannot run — the agents or the command are not found, or they fail before they finish — move to the next mode and run it. A review that finishes with no findings has run: keep its mode and go on with nothing to fix. Record the change and its cause in Phase 7 §5 and §6: the mode you report is the mode that actually ran. + +**6c — `REVIEW_MODE: fallback`.** No reviewer is available, so run the review yourself. Dispatch **one** clean-context review subagent using the same briefing protocol as Phase 5 (self-contained prompt, no inherited history, resolved absolute paths). Brief it with: + +- Absolute paths to the repository root and the spec directory. +- The diff range (`HEAD`-at-start..`HEAD`-now), and the project context files resolved by the same rule as Phase 5 (`AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`, the constitution). Pass the paths that actually exist; say so explicitly if none do. +- `spec.md` and `tasks.md`, as the statement of intent the diff is reviewed against. +- Instructions to cover these six aspects and nothing else: **code quality** (project guideline compliance, bugs, security), **comments** (accuracy, documentation completeness, comment rot), **tests** (behavioural coverage, critical gaps, test resilience), **error handling** (silent failures, catch blocks, error logging), **type design** (encapsulation, invariants, enforcement), **simplification** (clarity, unnecessary complexity, redundant abstractions). +- The required return shape: a severity-rated finding list (`high` / `medium` / `low`), each finding carrying file, line where known, and a one-line summary. **Findings only** — the subagent must not fix anything, so that the fix decisions below stay with the orchestrator. + +This is a safety net, not a replacement. It does not match the depth of the plugin or the review command, and the report must say so rather than present it as an equivalent pass. + +**6d — `REVIEW_MODE: none`.** No review is possible. Proceed to Phase 7 with an empty finding list and carry `REVIEW_MODE: none` forward. Do **not** continue as though the change set had been reviewed, and do **not** substitute a quick unstructured read of the diff for a review and report it as one. + +**Finding handling is identical in 6a, 6b and 6c:** - For findings rated **high severity or above**, fix them inline. Prefer fixing them yourself if the change is small and localised; dispatch a fresh clean-context subagent (same protocol as Phase 5) if the fix spans multiple files or needs significant code understanding. - For lower-severity findings, record them in the report (do not block). @@ -107,12 +144,25 @@ Write a markdown report to `<spec-dir>/opsmill-implement-report.md` and also pri If an E2E test was added but not executable locally in this project, record it as a separate row with `Passed at` = `deferred — local E2E not supported` and put the CI-side command in `Run command`. Call this out in §6 "Autonomous decisions" so the user can confirm the call. If no tests were added or modified by the whole run, write `n/a — no new or modified tests in this implementation` instead of the table and state it explicitly so reviewers can verify the claim against the diff. -5. **Review findings** — table of severity / file / one-line summary. Mark which were fixed inline and which were deferred. -6. **Autonomous decisions** — any judgment calls the orchestrator made that the user might want to revisit (chunk splits, blocked-task handling, review-finding triage choices). +5. **Review findings** — MUST open with a `Review mode:` line, before the table, stating how the findings were produced: + + - `Review mode: plugin (pr-review-toolkit)` + - `Review mode: command (/code-review)` + - `Review mode: fallback (built-in reviewer, reduced depth — install the pr-review-toolkit plugin for a full pass)` + - `Review mode: none — NO REVIEW WAS PERFORMED` + + In `fallback` and `none` mode, follow that line with the install hint: `/plugin install pr-review-toolkit@claude-plugins-official` in Claude Code. + + Then the table of severity / file / one-line summary, marking which findings were fixed inline and which were deferred. In `none` mode write `n/a — no review performed` in place of the table. +6. **Autonomous decisions** — any judgment calls the orchestrator made that the user might want to revisit (chunk splits, blocked-task handling, review-finding triage choices). A `REVIEW_MODE` other than `plugin` or `command` MUST appear here, with the reason no reviewer was available. 7. **Suggested next steps** — e.g. "open a PR", "rerun `speckit.opsmill.implement` to retry the 2 blocked tasks", "address the deferred review findings". **Blocking rule.** If §4 contains any `MISSING` row, the report header MUST mark the run `INCOMPLETE` and §7 MUST list "produce local-pass evidence for the listed tests" as the first next step. Do not declare the spec done while local-pass evidence is missing. (`deferred — local E2E not supported` rows do NOT trigger this rule, but they MUST be flagged in §6.) +**Review blocking rule.** If `REVIEW_MODE` is `none`, the report header MUST mark the run `INCOMPLETE` and §7 MUST list "install the pr-review-toolkit plugin and re-run the review over this diff" as the first next step. `REVIEW_MODE: fallback` does **not** trigger this rule, because a review did happen, but it MUST be flagged in §5 and §6. + +`REVIEW: none` blocks where `CRITIQUE: none` in `speckit-opsmill-prep` does not, on purpose: an un-critiqued spec is still read by a human before any code exists, so the gap is visible and cheap to close, whereas an unreviewed diff is already-written code that nothing downstream re-checks. See the Completion section of `speckit-opsmill-prep` for the full rationale. + Commit the report via `speckit-checkpoint-commit` as the final action. ## Completion @@ -121,11 +171,12 @@ Print a 4-6 line summary mirroring the report header + outcome counts so the use **Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: -`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` +`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REVIEW: <plugin|command|fallback|none|n/a> | REASON: <short reason or n/a>` - `STATUS: DONE` — all chunks completed and §4 local-pass evidence has no `MISSING` rows. -- `STATUS: INCOMPLETE` — the run finished but the report is marked `INCOMPLETE` (e.g. missing local-pass evidence, or blocked tasks recorded). +- `STATUS: INCOMPLETE` — the run finished but the report is marked `INCOMPLETE` (e.g. missing local-pass evidence, blocked tasks recorded, or `REVIEW: none`). - `STATUS: BLOCKED` — a Phase 0 stop-condition aborted the run before the implement loop (missing prep artifacts, unexpectedly dirty tree). In this case no report is written; emit only the summary explaining why, then this line. +- `REVIEW` mirrors the `REVIEW_MODE` resolved in Phase 0, so the parent never has to infer review coverage from prose. `REVIEW: fallback` is compatible with `STATUS: DONE`. `REVIEW: none` forces `STATUS: INCOMPLETE` per the review blocking rule. Use `REVIEW: n/a` only when the run aborted before Phase 0 resolved a provider. The parent `speckit-opsmill-auto` parses this line; keep it as the literal last line, unwrapped. diff --git a/.agents/skills/speckit-opsmill-prep/SKILL.md b/.agents/skills/speckit-opsmill-prep/SKILL.md index 3b1057e39..cd673e605 100644 --- a/.agents/skills/speckit-opsmill-prep/SKILL.md +++ b/.agents/skills/speckit-opsmill-prep/SKILL.md @@ -1,10 +1,9 @@ --- name: speckit-opsmill-prep -description: Run the speckit preparation phases end-to-end — specify, plan, critique, - tasks — making all decisions autonomously. Stops before implementation. +description: Run the speckit preparation phases end-to-end — specify, plan, critique, tasks — making all decisions autonomously. Stops before implementation. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/prep.md --- @@ -46,9 +45,34 @@ Invoke the `speckit-plan` skill. ### Phase 3 — Critique -Invoke the `speckit-critique-run` skill. +Run the dual-lens (Product + Engineering) critique against `spec.md` and `plan.md` before any tasks are generated. + +**Resolve the critique provider first.** Record the answer as `CRITIQUE_MODE`. Run this check from the repository root and read `CRITIQUE_MODE` off its output — do not decide from memory: + +```bash +if [ -d .specify/extensions/critique ] || grep -q '"critique"' .specify/extensions/.registry 2>/dev/null; then + echo "critique extension: installed" +else + echo "critique extension: missing" +fi +``` + +| Check output | `CRITIQUE_MODE` | +|--------------|-----------------| +| `installed` | `extension` | +| `missing`, and you can dispatch clean-context subagents | `fallback` | +| `missing`, and no subagent dispatch is available | `none` | + +Both signals are written by `specify extension add` itself, and neither is harness-specific — do not key the decision off `.claude/skills/speckit-critique-run/`, which only exists under Claude Code. Resolve this by **checking the filesystem**, not by asking yourself whether you "have" the skill. A missing critique provider never blocks the run; it selects the branch below and it is reported on the status line. + +**3a — `CRITIQUE_MODE: extension`.** Invoke the `speckit-critique-run` skill. If that invocation fails — the skill is not found, or it errors out without returning findings — the extension is present on disk but not usable. Downgrade to `CRITIQUE_MODE: fallback` and run 3b, or to `none` if no subagent dispatch is available, and report the mode that actually ran. + +**3b — `CRITIQUE_MODE: fallback`.** Dispatch **one** clean-context subagent to run the same dual-lens critique. Brief it self-contained: absolute paths to `spec.md`, `plan.md`, and the resolved project context files, plus the required return shape — findings tagged 🎯 Must-Address, 💡 Recommendation, or 🤔 Question, and a verdict of `PROCEED` or `RETHINK`. Findings only; the subagent must not edit the spec or plan. Reduced depth compared with the extension, so flag it in Completion rather than presenting it as an equivalent pass. + +**3c — `CRITIQUE_MODE: none`.** Skip the critique and proceed to Phase 4 with no findings. Never describe the run as critiqued, and do not substitute an unstructured reread of `spec.md` for the critique. + +**Finding handling is identical in 3a and 3b:** -- Run the dual-lens (Product + Engineering) critique against `spec.md` and `plan.md` before any tasks are generated. - For any 🎯 **Must-Address** findings, apply the suggested fixes to `spec.md` / `plan.md` autonomously and commit them — do not pause for user approval. - For 💡 Recommendations, apply them when the fix is clear and low-risk; otherwise note and move on. - 🤔 Questions: resolve with your best judgment based on context (same rule as the Specify phase). @@ -73,7 +97,7 @@ Inspect `$ARGUMENTS`. The check is **only meaningful when the user provided a su 1. **`$ARGUMENTS` contains one or more URLs** (links to a Notion / Confluence / Google Doc / GitHub issue / Linear ticket / shared Markdown / PDF). Treat each URL as a likely PRD location. **Run the check.** 2. **`$ARGUMENTS` itself looks like a detailed PRD** — multiple paragraphs with structure such as headings, bullet lists of requirements, explicit goals/non-goals, acceptance criteria, or > ~400 characters of substantive description. **Run the check.** -3. **`$ARGUMENTS` is a one-line description, vague brief, or empty.** **Skip the check** — there is no source-of-truth document to align against. Write a short note in your final summary stating you skipped this phase and why, and proceed to Completion. +3. **`$ARGUMENTS` is a one-line description, vague brief, or empty.** **Skip the check** — there is no source-of-truth document to align against. Write a short note in your final summary stating you skipped this phase and why, and proceed to Completion. If the input is short (under ~400 characters) but still carries substantive, requirement-bearing detail, use your judgment and treat it as a PRD under rule 2 rather than skipping. #### 5b. Resolve the source-of-truth PRD @@ -139,17 +163,20 @@ After all five phases are complete, provide a brief summary: - Feature name and spec directory - Number of tasks generated in `tasks.md` -- Any critique findings that were addressed inline +- Any critique findings that were addressed inline, and the `CRITIQUE_MODE` used. State a `fallback` or `none` mode plainly, with the install hint `specify extension add critique` - **Alignment check status** — verdict, source PRD reference, number of remediation passes used (if any), and any unresolved drift - Any notable decisions you made autonomously **Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: -`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` +`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | CRITIQUE: <extension|fallback|none|n/a> | REASON: <short reason or n/a>` - `STATUS: READY` — `tasks.md` was generated **and** the alignment outcome is `✅ ALIGNED`, `⚠️ MINOR DRIFT`, or `⚠️ SKIPPED`. Only in this state is the spec safe to implement. - `STATUS: BLOCKED` — alignment is `🛑 UNRESOLVED` after the retry budget, or any phase could not complete. This is the explicit failure signal the parent `speckit-opsmill-auto` checks before deciding whether to start implementation; do not dress an unresolved run up as a success. - `SPEC_DIR` MUST be the absolute path to the spec directory, so the parent can hand it to the implement phase without parsing prose. +- `CRITIQUE` mirrors the `CRITIQUE_MODE` resolved in Phase 3, so the parent never has to infer critique coverage from prose. A degraded mode does **not** make the run `BLOCKED` — the spec is still implementable — but it MUST be reported. Use `CRITIQUE: n/a` only when the run aborted before Phase 3. + +**Why `CRITIQUE: none` reports but does not block, while `REVIEW: none` forces `INCOMPLETE` in `speckit-opsmill-implement`.** The two gates fail differently. A missing critique leaves the spec un-pressure-tested, but `spec.md` and `tasks.md` are read by a human before any code is written, so the gap is visible at the next step and closing it costs a re-run of prep. A missing review leaves already-written code unexamined, and the implement report is the only thing that speaks to whether that code is sound — nothing downstream re-checks it, so a `DONE` there would assert an assurance that was never produced. Prep therefore degrades and reports; `implement` degrades and marks the run `INCOMPLETE`. Keep this as the literal last line, unwrapped. diff --git a/.agents/skills/speckit-opsmill-qa/SKILL.md b/.agents/skills/speckit-opsmill-qa/SKILL.md new file mode 100644 index 000000000..867659ac6 --- /dev/null +++ b/.agents/skills/speckit-opsmill-qa/SKILL.md @@ -0,0 +1,371 @@ +--- +name: speckit-opsmill-qa +description: Produce a manual QA checklist that walks a human tester through verifying the just-implemented feature, written next to spec.md / plan.md in the active feature directory. +compatibility: Requires spec-kit project structure with .specify/ directory +metadata: + author: opsmill + source: opsmill:commands/qa.md +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +Supported argument: `--force` to overwrite an existing `qa-checklist.md` +without prompting. Any other free-form text is treated as additional +scope guidance (for example: `focus on the CLI surface`, or +`skip the GraphQL queries`) and should bias the checklist accordingly. + +## Goal + +After the implementation phase of a spec-kit feature, produce a single +markdown document at `FEATURE_DIR/qa-checklist.md` that a human can +follow, step-by-step, to verify the feature works. The audience is a +teammate (or future-you) running the feature **the way a customer +would** — through the same surface customers touch (CLI, running +service, UI, public SDK, docs page). They need concrete, +copy-pasteable instructions — not a description of what the feature +does. + +The checklist is **manual and customer-facing**. It does not run the +automated test suite, does not assert on internal state, and does not +include unit-test or integration-test invocations. Those belong in +`tasks.md`, CI, and `git log`. Snippets that import the project's +public, documented SDK or library are fine — they *are* the customer +surface for SDK-shaped features. + +## Customer Frame + +Test the feature **the way a customer would actually use it.** The +right surface depends on what kind of product the feature ships into: + +| Product type | Customer surface | +|---|---| +| CLI tool / binary | The shipped command, its flags, its stdout/stderr/exit code | +| Service (SaaS / self-hosted) | Config files an operator edits, the running service's HTTP / GraphQL / gRPC API, UI flows, the logs and metrics it surfaces | +| Library / SDK | The public, documented import path — code a customer would write against the exported API | +| Code generator / template | The generated output, run from the user-facing entry point | +| Docs / examples | The rendered page or the runnable snippet | + +**Writing code is fine — even necessary — when the customer is a +developer using a library or SDK.** A snippet that imports the +published top-level package and calls its public methods is exactly +the experience you ship. What is not fine is reaching past the public +API into internal modules, private helpers, test fixtures, or config +classes that the project does not document as part of its surface. + +A useful test before emitting a step: *would I be comfortable +publishing this snippet verbatim in the feature's README, docs, or +release notes?* If yes, it is customer-shaped. If publishing it would +feel wrong because it touches internals, the step is implementation +testing, not QA. + +## Operating Constraints + +**STRICTLY ADDITIVE.** Do not modify `spec.md`, `plan.md`, `tasks.md`, +`research.md`, `data-model.md`, `contracts/`, or any source files. The +only file this command writes is `FEATURE_DIR/qa-checklist.md`. + +**CUSTOMER SURFACES, NOT IMPLEMENTATION.** Every step must exercise +the feature through something a customer would publicly use. Decide +which product type the feature ships into (table above), then pick the +matching surface. Acceptable: + +- A shipped CLI subcommand or binary (with the exact flags). +- A public HTTP / GraphQL / gRPC request against a *running* service + (exact URL, method, body, expected response shape). +- A UI page or flow (with the exact navigation path and visible cues). +- A configuration file the operator edits, plus the user-visible + signals that follow (logs from the running service, metrics, a + status endpoint, observable behavior change in a public API). +- The public, documented import path of a library / SDK — using the + same package and symbols the docs tell customers to use. +- A rendered docs page or a documented, runnable example. + +Not customer surfaces: + +- Private / internal modules, classes, helpers, validators, or + fixtures that the project does not document as user-facing. +- `pytest` / `vitest` / `cargo test` wrappers presented as scenarios — + CI runs those. +- Source-tree inspection ("open the file and confirm X is exported") + — that is code review, not QA. + +**Two worked examples.** + +*Example A — a config flag on a self-hosted service `myapp`.* Customer +surface = config file + running service. Not customer-shaped: + +```bash +# Touches Settings class — not in the published surface. +python -c "from myapp.config import Settings; print(Settings().new_flag)" +``` + +Customer-shaped: + +1. Set `new_flag = true` in `myapp.toml`. +2. Restart: `docker compose restart myapp`. +3. Hit a public endpoint that reflects the flag: + `curl -s localhost:8080/api/status | jq .new_flag` → `true`. + +*Example B — a new `messages.batch.create()` method on a published +SDK.* Customer surface = the public import path. Customer-shaped: + +```python +# The exact snippet a customer would copy from the docs. +from acme_sdk import Acme +client = Acme(api_key="...") +res = client.messages.batch.create(requests=[...]) +print(res.id, res.status) +``` + +Both are valid QA. The difference is not "code vs. no code" — it is +whether the snippet uses a published, supported surface. + +If the feature truly has **no** customer surface (pure refactor, +internal helper that is not re-exported, generator that produces no +visible change), say so in **Scope** and ship a minimal smoke +checklist (build still succeeds, nearest user-visible flow still +works, docs still render). Do not reach for internals to fill space. + +**ONE FILE, IDEMPOTENT.** Re-running in the same feature directory +should produce a comparable checklist, not a diff of checklists. If +`qa-checklist.md` already exists, ask before overwriting unless +`--force` was passed. + +## Outline + +1. **Resolve the feature directory.** Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root and + parse `FEATURE_DIR` from the JSON output. All paths must be + absolute. + + - If `FEATURE_DIR` is missing, empty, or the resolved directory does + not exist, **fail fast** with: + `No feature directory resolved from current branch — /speckit.opsmill.qa requires an active feature branch.` + Suggest the user switch to a feature branch and re-run. + +2. **Check for an existing checklist.** + + - If `FEATURE_DIR/qa-checklist.md` exists and `--force` was not + passed, print the existing path and ask the user whether to + overwrite, append a timestamped variant, or abort. Default is + abort. + - If `--force` was passed, overwrite without prompting but mention + it in the final report. + +3. **Gather context**, in this order of preference: + + a. `FEATURE_DIR/spec.md` — primary source for user-visible behavior, + acceptance criteria, and scope. + b. `FEATURE_DIR/plan.md` — entry points, surfaces touched (CLI, + GraphQL, REST, UI, generators). + c. `FEATURE_DIR/tasks.md` — what was actually built (use this to + ensure the checklist matches reality, not the original spec). + d. `FEATURE_DIR/quickstart.md` if present — re-use any setup steps + verbatim rather than re-inventing them. + e. `FEATURE_DIR/contracts/` if present — to identify exact endpoint + names, mutation names, query names to exercise. + f. The live conversation context for any clarifications or scope + shifts that happened during implementation but did not make it + back into `spec.md`. + + Do **not** read large swaths of source code. Do **not** generate + diffs. The checklist is anchored to user-visible surfaces, not + internals. + +4. **Identify the customer surfaces to exercise.** From the gathered + context, list the customer-facing entry points the feature touches. + Common surfaces include: + + - CLI commands (with the exact invocation). + - HTTP / GraphQL / gRPC endpoints (with method + path or operation + name). + - UI pages, modals, or flows (with the exact navigation path). + - Public SDK / library import paths — the exact top-level symbols a + customer would `import` and the calls they would write. + - Configuration knobs the operator sets (file or env var) and the + observable signal that follows (log line, metric, status + endpoint, API response). + - Generated outputs and rendered docs pages. + + If the feature is purely internal (refactor, private helper that is + not re-exported, generator with no visible change) and has **no** + customer surface, the checklist must say so explicitly in the + **Scope** section and contain at most a smoke-test scenario (the + nearest user-visible flow still works, build still succeeds, docs + still render). + +5. **Compose the checklist.** Use exactly this structure: + + ````markdown + # QA Checklist — <feature title from spec.md> + + **Generated**: <YYYY-MM-DD HH:MM local> + **Feature**: <FEATURE_DIR relative path> + **Source**: speckit.opsmill.qa + + ## Scope + + <One short paragraph: what the tester is verifying, and what is + intentionally out of scope. Lift terminology from spec.md.> + + ## Prerequisites + + <Concrete environment requirements as a bullet list. Pull from + plan.md / quickstart.md if available. Examples:> + + - [ ] Working copy is on branch `<branch-name>` with the + implementation merged or rebased. + - [ ] `uv sync --all-groups --all-extras` (or the project's + install command) has completed cleanly. + - [ ] Services running: <e.g., `docker compose up infrahub`>. + - [ ] Environment variables set: <list with example values>. + + ## Setup + + <Numbered commands to bring the system to a known-good starting + state. Each command on its own line in a fenced block. Reference + files by absolute path or by the repo-root-relative path the user + would actually type.> + + <**Prefer the project's documented dev-environment entry points** — + `invoke <task>`, `just <task>`, `make <target>`, `task <name>`, + `npm run <script>`, or whatever this project ships — over raw + `docker compose` / `kubectl` / service binaries. Discover them + from project conventions in this order:> + + - <`AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md` at + repo root for an explicit "run locally" / "dev setup" section.> + - <Project memory: feedback memories about the dev environment.> + - <Task runner manifests: `tasks/` or `tasks.py` (invoke), + `justfile`, `Makefile`, `Taskfile.yml`, `package.json` scripts.> + - <The feature's own `quickstart.md` / `plan.md` if it names + specific commands.> + + <Fall back to raw `docker compose` / `kubectl` only if no project + wrapper covers the scenario.> + + ```bash + <command 1> + <command 2> + ``` + + ## Test Scenarios + + <One H3 per scenario. **2–5 scenarios** is the sweet spot — each + one must exercise a *different* user-visible surface or a + *different* acceptance criterion. If you find yourself writing + variants of the same step against the same surface, collapse them + into one scenario and move the variants to **Edge Cases**. Inside + each scenario:> + + ### 1. <Scenario name — what the tester is checking> + + **What this verifies**: <One sentence, in user terms.> + + **Steps**: + + - [ ] Run: `<exact command, or click path, or HTTP call>` + - [ ] Observe: <exact output, screen state, or response shape> + - [ ] Then: `<next step>` + - [ ] Confirm: <final assertion in user-visible terms> + + **Expected result**: <One or two sentences describing what success + looks like. Be specific — exit code, status code, field value, + visible UI element.> + + ### 2. <next scenario> + + ... + + ## Edge Cases + + <Bullet checklist for boundary conditions worth a manual poke. Each + item is a one-liner with the input and the expected behavior. + Examples:> + + - [ ] Empty input: `<command with --foo ""` → expect <behavior>. + - [ ] Duplicate name: re-running creates no second copy; the + existing record is updated. + - [ ] Permission denied: as a read-only user, the mutation returns + <error code> with message containing `<substring>`. + + ## Teardown + + <Commands to return the system to its pre-test state. If none are + needed, say so explicitly: "No teardown required — all changes are + scoped to <ephemeral location>." Otherwise:> + + ```bash + <cleanup commands> + ``` + + ## Sign-off + + - [ ] All scenarios above pass. + - [ ] No unexpected output, warnings, or errors observed. + - [ ] Tester: ______________________ Date: __________ + ```` + +6. **Write the file.** + + - Path: `FEATURE_DIR/qa-checklist.md`. + - If the user chose "append a timestamped variant" in step 2, use + `FEATURE_DIR/qa-checklist-YYYY-MM-DD-HHMM.md` instead. + - Do not create any other files. Do not create a `tests/` + subdirectory. + +7. **Print the resolved path** as the final message, plus a one-line + reminder that this checklist is intentionally manual / user-facing + and does not replace the automated test suite. + +## Quality Bar + +A teammate who has never seen the feature should, after under 10 +minutes with this checklist, be able to: + +- Stand up whatever a customer would stand up (the service, an SDK + install, a CLI binary, a docs preview). +- Exercise the feature exactly as a customer would — through its + shipped, documented surface. +- Decide for themselves whether the feature behaves as `spec.md` + promises. + +If any checklist item reaches into private modules, internal helpers, +test fixtures, or symbols that do not appear in the public docs / SDK +reference, rewrite it to use the customer surface or drop it. +**Hard cap: 150 lines of markdown total.** Aim for ~100. If you go +over, the answer is fewer scenarios, not denser ones. + +## Behavior Rules + +- **Never modify `spec.md`, `plan.md`, `tasks.md`, or source files.** + The checklist is purely additive. +- **Public-API code is fine; internal symbols are not.** A snippet + that imports the project's published top-level package and calls + documented public methods is a valid QA step — that is the customer + surface for SDK-shaped features. Steps that reach past the public + API into internal modules, private packages (`_foo`, `.internal`), + undocumented config classes, or test fixtures are not. See + "Customer Frame" and "Customer surfaces, not implementation" in + Operating Constraints. +- If no customer surface exists, say so plainly in **Scope** and ship + a minimal smoke-test checklist rather than fabricating scenarios by + reaching into internals. +- Prefer commands and inputs that already appear in `quickstart.md` or + `tasks.md` over inventing new ones — the tester should not need to + reverse-engineer setup. +- **Use the project's documented dev-environment wrappers** (`invoke`, + `just`, `make`, `task`, `npm run`, etc.) over raw `docker compose` / + `kubectl` / service binaries. Discover them from `AGENTS.md`, + `CLAUDE.md`, README, project memory, or the task-runner manifests + before defaulting to raw container commands. +- Do not include automated test invocations (`pytest`, `vitest`, + `cargo test`, etc.) as scenarios. Those are out of scope for this + command. +- For single quotes in bash args, use escape syntax: e.g. + `'I'\''m Groot'` (or double-quote if possible: `"I'm Groot"`). \ No newline at end of file diff --git a/.agents/skills/speckit-opsmill-retrospect/SKILL.md b/.agents/skills/speckit-opsmill-retrospect/SKILL.md index 46bbdc572..cee41b3a7 100644 --- a/.agents/skills/speckit-opsmill-retrospect/SKILL.md +++ b/.agents/skills/speckit-opsmill-retrospect/SKILL.md @@ -1,10 +1,9 @@ --- name: speckit-opsmill-retrospect -description: Run a session retrospective that surfaces context-management gaps and - routes them to approved follow-up actions. +description: Run a session retrospective that surfaces context-management gaps and routes them to approved follow-up actions. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/retrospect.md --- @@ -118,7 +117,7 @@ Before writing any files, present the retrospective in chat using this structure #### local-only -- R4: <memory/report note> +- R4: <.specify/memory/report note> ### Approval Request diff --git a/.agents/skills/speckit-opsmill-summary/SKILL.md b/.agents/skills/speckit-opsmill-summary/SKILL.md index 6e6099521..2ee0130e9 100644 --- a/.agents/skills/speckit-opsmill-summary/SKILL.md +++ b/.agents/skills/speckit-opsmill-summary/SKILL.md @@ -1,11 +1,9 @@ --- name: speckit-opsmill-summary -description: Produce a flow-level summary of the current Claude Code session — executive - summary, chronological timeline, and outcomes — written into the active feature - directory next to spec.md / plan.md. +description: Produce a flow-level summary of the current Claude Code session — executive summary, chronological timeline, and outcomes — written into the active feature directory next to spec.md / plan.md. compatibility: Requires spec-kit project structure with .specify/ directory metadata: - author: github-spec-kit + author: opsmill source: opsmill:commands/summary.md --- diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md index bc1b70044..29736901c 100644 --- a/.agents/skills/speckit-plan/SKILL.md +++ b/.agents/skills/speckit-plan/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before planning)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_plan` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -57,7 +57,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline -1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` 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"). +1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, FEATURE_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 `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied). @@ -67,7 +67,6 @@ You **MUST** consider the user input before proceeding (if not empty). - Evaluate gates (ERROR if violations unjustified) - Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION) - Phase 1: Generate data-model.md, contracts/, quickstart.md - - Phase 1: Update agent context by running the agent script - Re-evaluate Constitution Check post-design ## Mandatory Post-Execution Hooks @@ -77,12 +76,12 @@ You **MUST** consider the user input before proceeding (if not empty). Check if `.specify/extensions.yml` exists in the project root. - If it does not exist, or no hooks are registered under `hooks.after_plan`, skip to the Completion Report. - If it exists, read it and look for entries under the `hooks.after_plan` key. -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report. +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue to the Completion Report. - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` @@ -107,7 +106,7 @@ Check if `.specify/extensions.yml` exists in the project root. ## Completion Report -Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts. +Command ends after Phase 1 design. Report branch, IMPL_PLAN path, and generated artifacts. ## Phases @@ -156,14 +155,11 @@ Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generate - Do not include full implementation code, model/service/controller bodies, migrations, or complete test suites - Keep this artifact as a validation/run guide; implementation details belong in `tasks.md` and the implementation phase -4. **Agent context update**: - - Update the plan reference between the `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` markers in `CLAUDE.md` to point to the plan file created in step 1 (the IMPL_PLAN path) - -**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context file +**Output**: data-model.md, /contracts/*, quickstart.md ## Key rules -- Use absolute paths for filesystem operations; use project-relative paths for references in documentation and agent context files +- Use absolute paths for filesystem operations; use project-relative paths for references in documentation - ERROR on gate failures or unresolved clarifications ## Done When diff --git a/.agents/skills/speckit-review-code/SKILL.md b/.agents/skills/speckit-review-code/SKILL.md deleted file mode 100644 index e66fccd97..000000000 --- a/.agents/skills/speckit-review-code/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: speckit-review-code -description: General code quality review — project guideline compliance, bug detection, - code quality analysis. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/code.md ---- - -You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives. - -## Review Scope - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Review Responsibilities - -**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions. - -**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems. - -**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage. - -## Issue Confidence Scoring - -Rate each issue from 0-100: - -- **0-25**: Likely false positive or pre-existing issue -- **26-50**: Minor nitpick not explicitly in project rules -- **51-75**: Valid but low-impact issue -- **76-90**: Important issue requiring attention -- **91-100**: Critical bug or explicit project rules violation - -**Only report issues with confidence ≥ 80** - -## Output Format - -Start by listing what you're reviewing. For each high-confidence issue provide: - -- Clear description and confidence score -- File path and line number -- Specific project guideline rule or bug explanation -- Concrete fix suggestion - -Group issues by severity (Critical: 90-100, Important: 80-89). - -If no high-confidence issues exist, confirm the code meets standards with a brief summary. - -Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter. \ No newline at end of file diff --git a/.agents/skills/speckit-review-comments/SKILL.md b/.agents/skills/speckit-review-comments/SKILL.md deleted file mode 100644 index ab9a9f6d6..000000000 --- a/.agents/skills/speckit-review-comments/SKILL.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: speckit-review-comments -description: Code comment accuracy verification, documentation completeness assessment, - comment rot detection. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/comments.md ---- - -You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time. - -Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Comments Framework:** - -When analyzing comments, you will: - -1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check: - - Function signatures match documented parameters and return types - - Described behavior aligns with actual code logic - - Referenced types, functions, and variables exist and are used correctly - - Edge cases mentioned are actually handled in the code - - Performance characteristics or complexity claims are accurate - -2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant: - - Critical assumptions or preconditions are documented - - Non-obvious side effects are mentioned - - Important error conditions are described - - Complex algorithms have their approach explained - - Business logic rationale is captured when not self-evident - -3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime: - - Comments that merely restate obvious code should be flagged for removal - - Comments explaining 'why' are more valuable than those explaining 'what' - - Comments that will become outdated with likely code changes should be reconsidered - - Comments should be written for the least experienced future maintainer - - Avoid comments that reference temporary states or transitional implementations - -4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted: - - Ambiguous language that could have multiple meanings - - Outdated references to refactored code - - Assumptions that may no longer hold true - - Examples that don't match current implementation - - TODOs or FIXMEs that may have already been addressed - -5. **Suggest Improvements**: Provide specific, actionable feedback: - - Rewrite suggestions for unclear or inaccurate portions - - Recommendations for additional context where needed - - Clear rationale for why comments should be removed - - Alternative approaches for conveying the same information - -Your analysis output should be structured as: - -**Summary**: Brief overview of the comment analysis scope and findings - -**Critical Issues**: Comments that are factually incorrect or highly misleading -- Location: [file:line] -- Issue: [specific problem] -- Suggestion: [recommended fix] - -**Improvement Opportunities**: Comments that could be enhanced -- Location: [file:line] -- Current state: [what's lacking] -- Suggestion: [how to improve] - -**Recommended Removals**: Comments that add no value or create confusion -- Location: [file:line] -- Rationale: [why it should be removed] - -**Positive Findings**: Well-written comments that serve as good examples (if any) - -Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value. - -IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement. \ No newline at end of file diff --git a/.agents/skills/speckit-review-errors/SKILL.md b/.agents/skills/speckit-review-errors/SKILL.md deleted file mode 100644 index 5ece62a91..000000000 --- a/.agents/skills/speckit-review-errors/SKILL.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -name: speckit-review-errors -description: Error handling review — silent failure detection, catch block analysis, - error logging. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/errors.md ---- - -You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable. - -## Determine Changed Files - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Principles - -You operate under these non-negotiable rules: - -1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect -2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it -3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems -4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible -5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems - -## Your Review Process - -When examining a PR, you will: - -### 1. Identify All Error Handling Code - -Systematically locate: -- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.) -- All error callbacks and error event handlers -- All conditional branches that handle error states -- All fallback logic and default values used on failure -- All places where errors are logged but execution continues -- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors - -### 2. Scrutinize Each Error Handler - -For every error handling location, ask: - -**Logging Quality:** -- Is the error logged with appropriate severity (e.g., warn vs. error)? -- Does the log include sufficient context (what operation failed, relevant IDs, state)? -- Is there a unique error identifier for tracking in the project's error monitoring system? -- Would this log help someone debug the issue 6 months from now? - -**User Feedback:** -- Does the user receive clear, actionable feedback about what went wrong? -- Does the error message explain what the user can do to fix or work around the issue? -- Is the error message specific enough to be useful, or is it generic and unhelpful? -- Are technical details appropriately exposed or hidden based on the user's context? - -**Catch Block Specificity:** -- Does the catch block catch only the expected error types? -- Could this catch block accidentally suppress unrelated errors? -- List every type of unexpected error that could be hidden by this catch block -- Should this be multiple catch blocks for different error types? - -**Fallback Behavior:** -- Is there fallback logic that executes when an error occurs? -- Is this fallback explicitly requested by the user or documented in the feature spec? -- Does the fallback behavior mask the underlying problem? -- Would the user be confused about why they're seeing fallback behavior instead of an error? -- Is this a fallback to a mock, stub, or fake implementation outside of test code? - -**Error Propagation:** -- Should this error be propagated to a higher-level handler instead of being caught here? -- Is the error being swallowed when it should bubble up? -- Does catching here prevent proper cleanup or resource management? - -### 3. Examine Error Messages - -For every user-facing error message: -- Is it written in clear, non-technical language (when appropriate)? -- Does it explain what went wrong in terms the user understands? -- Does it provide actionable next steps? -- Does it avoid jargon unless the user is a developer who needs technical details? -- Is it specific enough to distinguish this error from similar errors? -- Does it include relevant context (file names, operation names, etc.)? - -### 4. Check for Hidden Failures - -Look for patterns that hide errors: -- Empty catch blocks (absolutely forbidden) -- Catch blocks that only log and continue -- Returning null/nil/None/default values on error without logging -- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail -- Fallback chains that try multiple approaches without explaining why -- Retry logic that exhausts attempts without informing the user - -### 5. Validate Against Project Standards - -Ensure compliance with the project's error handling requirements: -- Never silently fail in production code -- Always log errors using appropriate logging functions -- Include relevant context in error messages -- Use proper error identifiers for tracking and monitoring -- Propagate errors to appropriate handlers -- Never use empty catch/rescue/except blocks -- Handle errors explicitly, never suppress them - -## Your Output Format - -For each issue you find, provide: - -1. **Location**: File path and line number(s) -2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific) -3. **Issue Description**: What's wrong and why it's problematic -4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden -5. **User Impact**: How this affects the user experience and debugging -6. **Recommendation**: Specific code changes needed to fix the issue -7. **Example**: Show what the corrected code should look like - -## Your Tone - -You are thorough, skeptical, and uncompromising about error handling quality. You: -- Call out every instance of inadequate error handling, no matter how minor -- Explain the debugging nightmares that poor error handling creates -- Provide specific, actionable recommendations for improvement -- Acknowledge when error handling is done well (rare but important) -- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..." -- Are constructively critical - your goal is to improve the code, not to criticize the developer - -## Special Considerations - -Be aware of any project-specific conventions: -- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics) -- Verify that error identifiers follow any project-defined catalog or registry -- The project may explicitly forbid silent failures in production code -- Empty catch/rescue/except blocks are never acceptable -- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them - -Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed. \ No newline at end of file diff --git a/.agents/skills/speckit-review-run/SKILL.md b/.agents/skills/speckit-review-run/SKILL.md deleted file mode 100644 index 0fab44d2d..000000000 --- a/.agents/skills/speckit-review-run/SKILL.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -name: speckit-review-run -description: Comprehensive code review using specialized agents — orchestrates code, - comments, tests, errors, types, and simplify agents sequentially. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/run.md ---- - -# Comprehensive PR Review - -Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality. - -**Review Aspects (optional):** "$ARGUMENTS" - -## Review Workflow: - -1. **Load Configuration** - - Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists). - - If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`. - - Extract the `agents` map — each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle. - - Agents set to `false` **MUST** be excluded from this run. Do not launch them. - -2. **Determine Review Scope** - - Parse arguments to see if user requested specific review aspects. - - If specific aspects were requested, run exactly those — config toggles do **not** apply (explicit user request overrides config). - - Default (no arguments): Run all applicable reviews that are enabled in config. - -3. **Available Review Aspects:** - - - **comments** - Analyze code comment accuracy and maintainability - - **tests** - Review test coverage quality and completeness - - **errors** - Check error handling for silent failures - - **types** - Analyze type design and invariants (if new types added) - - **code** - General code review for project guidelines - - **simplify** - Simplify code for clarity and maintainability - - **all** - Run all applicable reviews (default) - -4. **Identify Changed Files** - - - If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - - Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. - - The script automatically picks the best detection mode: - - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. - - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). - - JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` - - **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -5. **Determine Applicable Reviews** - - Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`): - - **Always applicable** (if enabled): `/speckit.review.code` (general quality) - - **If test files changed** (if enabled): `/speckit.review.tests` - - **If comments/docs added** (if enabled): `/speckit.review.comments` - - **If error handling changed** (if enabled): `/speckit.review.errors` - - **If types added/modified** (if enabled): `/speckit.review.types` - - **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine) - - If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)"). - -6. **Launch Review Agents** - - **Sequential approach** (one at a time): - - Easier to understand and act on - - Each report is complete before next - - Good for interactive review - - **Parallel approach** (user can request): - - Launch all agents simultaneously - - Faster for comprehensive review - - Results come back together - -7. **Aggregate Results** - - After agents complete, summarize: - - **Critical Issues** (must fix before merge) - - **Important Issues** (should fix) - - **Suggestions** (nice to have) - - **Positive Observations** (what's good) - -8. **Provide Action Plan** - - Organize findings: - ```markdown - # PR Review Summary - - ## Critical Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Important Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Suggestions (X found) - - [agent-name]: Suggestion [file:line] - - ## Strengths - - What's well-done in this PR - - ## Recommended Action - 1. Fix critical issues first - 2. Address important issues - 3. Consider suggestions - 4. Re-run review after fixes - ``` - -## Usage Examples: - -**Full review (default):** -``` -/speckit.review.run -``` - -**Specific aspects:** -``` -/speckit.review.run tests errors -# Reviews only test coverage and error handling - -/speckit.review.run comments -# Reviews only code comments - -/speckit.review.run simplify -# Simplifies code after passing review -``` - -**Parallel review:** -``` -/speckit.review.run all parallel -# Launches all agents in parallel -``` - -## Agent Descriptions: - -**comment**: -- Verifies comment accuracy vs code -- Identifies comment rot -- Checks documentation completeness - -**tests**: -- Reviews behavioral test coverage -- Identifies critical gaps -- Evaluates test quality - -**errors**: -- Finds silent failures -- Reviews catch blocks -- Checks error logging - -**types**: -- Analyzes type encapsulation -- Reviews invariant expression -- Rates type design quality - -**code**: -- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance -- Detects bugs and issues -- Reviews general code quality - -**simplify**: -- Simplifies complex code -- Improves clarity and readability -- Applies project standards -- Preserves functionality - -## Tips: - -- **Run early**: Before creating PR, not after -- **Focus on changes**: Agents analyze diff by default -- **Address critical first**: Fix high-priority issues before lower priority -- **Re-run after fixes**: Verify issues are resolved -- **Use specific reviews**: Target specific aspects when you know the concern - -## Notes: - -- Agents run autonomously and return detailed reports -- Each agent focuses on its specialty for deep analysis -- Results are actionable with specific file:line references -- Agents use appropriate models for their complexity \ No newline at end of file diff --git a/.agents/skills/speckit-review-simplify/SKILL.md b/.agents/skills/speckit-review-simplify/SKILL.md deleted file mode 100644 index a69bd8072..000000000 --- a/.agents/skills/speckit-review-simplify/SKILL.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: speckit-review-simplify -description: Code simplification suggestions — clarity, unnecessary complexity, redundant - abstractions. Advisory only. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/simplify.md ---- - -You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Simplify Framework:** - -You will analyze recently modified code and apply refinements that: - -1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact. - -2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent). - -3. **Enhance Clarity**: Simplify code structure by: - - - Reducing unnecessary complexity and nesting - - Eliminating redundant code and abstractions - - Improving readability through clear variable and function names - - Consolidating related logic - - Removing unnecessary comments that describe obvious code - - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions - - Choose clarity over brevity - explicit code is often better than overly compact code - -4. **Maintain Balance**: Avoid over-simplification that could: - - - Reduce code clarity or maintainability - - Create overly clever solutions that are hard to understand - - Combine too many concerns into single functions or components - - Remove helpful abstractions that improve code organization - - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) - - Make the code harder to debug or extend - -5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope. - -Your refinement process: - -1. Identify the recently modified code sections -2. Analyze for opportunities to improve elegance and consistency -3. Apply project-specific best practices and coding standards -4. Ensure all functionality remains unchanged -5. Verify the refined code is simpler and more maintainable -6. Document only significant changes that affect understanding - -You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality. \ No newline at end of file diff --git a/.agents/skills/speckit-review-tests/SKILL.md b/.agents/skills/speckit-review-tests/SKILL.md deleted file mode 100644 index abd19563f..000000000 --- a/.agents/skills/speckit-review-tests/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: speckit-review-tests -description: Test coverage quality analysis — behavioral coverage, critical gap identification, - test resilience evaluation. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/tests.md ---- - -You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Your Core Responsibilities:** - -1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions. - -2. **Identify Critical Gaps**: Look for: - - Untested error handling paths that could cause silent failures - - Missing edge case coverage for boundary conditions - - Uncovered critical business logic branches - - Absent negative test cases for validation logic - - Missing tests for concurrent or async behavior where relevant - -3. **Evaluate Test Quality**: Assess whether tests: - - Test behavior and contracts rather than implementation details - - Would catch meaningful regressions from future code changes - - Are resilient to reasonable refactoring - - Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity - -4. **Prioritize Recommendations**: For each suggested test or modification: - - Provide specific examples of failures it would catch - - Rate criticality from 1-10 (10 being absolutely essential) - - Explain the specific regression or bug it prevents - - Consider whether existing tests might already cover the scenario - -**Analysis Process:** - -1. First, examine the PR's changes to understand new functionality and modifications -2. Review the accompanying tests to map coverage to functionality -3. Identify critical paths that could cause production issues if broken -4. Check for tests that are too tightly coupled to implementation -5. Look for missing negative cases and error scenarios -6. Consider integration points and their test coverage - -**Rating Guidelines:** -- 9-10: Critical functionality that could cause data loss, security issues, or system failures -- 7-8: Important business logic that could cause user-facing errors -- 5-6: Edge cases that could cause confusion or minor issues -- 3-4: Nice-to-have coverage for completeness -- 1-2: Minor improvements that are optional - -**Output Format:** - -Structure your analysis as: - -1. **Summary**: Brief overview of test coverage quality -2. **Critical Gaps** (if any): Tests rated 8-10 that must be added -3. **Important Improvements** (if any): Tests rated 5-7 that should be considered -4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation -5. **Positive Observations**: What's well-tested and follows best practices - -**Important Considerations:** - -- Focus on tests that prevent real bugs, not academic completeness -- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available -- Remember that some code paths may be covered by existing integration tests -- Avoid suggesting tests for trivial getters/setters unless they contain logic -- Consider the cost/benefit of each suggested test -- Be specific about what each test should verify and why it matters -- Note when tests are testing implementation rather than behavior - -You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change. \ No newline at end of file diff --git a/.agents/skills/speckit-review-types/SKILL.md b/.agents/skills/speckit-review-types/SKILL.md deleted file mode 100644 index ec86fd59f..000000000 --- a/.agents/skills/speckit-review-types/SKILL.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: speckit-review-types -description: Type design analysis — encapsulation, invariant expression, usefulness, - and enforcement. -compatibility: Requires spec-kit project structure with .specify/ directory -metadata: - author: github-spec-kit - source: review:commands/types.md ---- - -You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants. - -**Your Core Mission:** -You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Analysis Framework:** - -When analyzing a type, you will: - -1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for: - - Data consistency requirements - - Valid state transitions - - Relationship constraints between fields - - Business logic rules encoded in the type - - Preconditions and postconditions - -2. **Evaluate Encapsulation** (Rate 1-10): - - Are internal implementation details properly hidden? - - Can the type's invariants be violated from outside? - - Are there appropriate access modifiers? - - Is the interface minimal and complete? - -3. **Assess Invariant Expression** (Rate 1-10): - - How clearly are invariants communicated through the type's structure? - - Are invariants enforced at compile-time where possible? - - Is the type self-documenting through its design? - - Are edge cases and constraints obvious from the type definition? - -4. **Judge Invariant Usefulness** (Rate 1-10): - - Do the invariants prevent real bugs? - - Are they aligned with business requirements? - - Do they make the code easier to reason about? - - Are they neither too restrictive nor too permissive? - -5. **Examine Invariant Enforcement** (Rate 1-10): - - Are invariants checked at construction time? - - Are all mutation points guarded? - - Is it impossible to create invalid instances? - - Are runtime checks appropriate and comprehensive? - -**Output Format:** - -Provide your analysis in this structure: - -``` -## Type: [TypeName] - -### Invariants Identified -- [List each invariant with a brief description] - -### Ratings -- **Encapsulation**: X/10 - [Brief justification] - -- **Invariant Expression**: X/10 - [Brief justification] - -- **Invariant Usefulness**: X/10 - [Brief justification] - -- **Invariant Enforcement**: X/10 - [Brief justification] - -### Strengths -[What the type does well] - -### Concerns -[Specific issues that need attention] - -### Recommended Improvements -[Concrete, actionable suggestions that won't overcomplicate the codebase] -``` - -**Key Principles:** - -- Prefer compile-time guarantees over runtime checks when feasible -- Value clarity and expressiveness over cleverness -- Consider the maintenance burden of suggested improvements -- Recognize that perfect is the enemy of good - suggest pragmatic improvements -- Types should make illegal states unrepresentable -- Constructor validation is crucial for maintaining invariants -- Immutability often simplifies invariant maintenance - -**Common Anti-patterns to Flag:** - -- Anemic domain models with no behavior -- Types that expose mutable internals -- Invariants enforced only through documentation -- Types with too many responsibilities -- Missing validation at construction boundaries -- Inconsistent enforcement across mutation methods -- Types that rely on external code to maintain invariants - -**When Suggesting Improvements:** - -Always consider: -- The complexity cost of your suggestions -- Whether the improvement justifies potential breaking changes -- The skill level and conventions of the existing codebase -- Performance implications of additional validation -- The balance between safety and usability - -Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity. \ No newline at end of file diff --git a/.agents/skills/speckit-specify/SKILL.md b/.agents/skills/speckit-specify/SKILL.md index 64bbde008..25daab1ad 100644 --- a/.agents/skills/speckit-specify/SKILL.md +++ b/.agents/skills/speckit-specify/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before specification)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_specify` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -141,28 +141,28 @@ Given that feature description, do this: 7. Identify Key Entities (if data involved) 8. Return: SUCCESS (spec ready for planning) -6. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +7. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -7. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: +8. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: a. **Create Spec Quality Checklist**: Generate a checklist file at `SPECIFY_FEATURE_DIRECTORY/checklists/requirements.md` using the checklist template structure with these validation items: ```markdown # Specification Quality Checklist: [FEATURE NAME] - + **Purpose**: Validate specification completeness and quality before proceeding to planning **Created**: [DATE] **Feature**: [Link to spec.md] - + ## Content Quality - + - [ ] No implementation details (languages, frameworks, APIs) - [ ] Focused on user value and business needs - [ ] Written for non-technical stakeholders - [ ] All mandatory sections completed - + ## Requirement Completeness - + - [ ] No [NEEDS CLARIFICATION] markers remain - [ ] Requirements are testable and unambiguous - [ ] Success criteria are measurable @@ -171,16 +171,16 @@ Given that feature description, do this: - [ ] Edge cases are identified - [ ] Scope is clearly bounded - [ ] Dependencies and assumptions identified - + ## Feature Readiness - + - [ ] All functional requirements have clear acceptance criteria - [ ] User scenarios cover primary flows - [ ] Feature meets measurable outcomes defined in Success Criteria - [ ] No implementation details leak into specification - + ## Notes - + - Items marked incomplete require spec updates before `/speckit-clarify` or `/speckit-plan` ``` @@ -205,20 +205,20 @@ Given that feature description, do this: ```markdown ## Question [N]: [Topic] - + **Context**: [Quote relevant spec section] - + **What we need to know**: [Specific question from NEEDS CLARIFICATION marker] - + **Suggested Answers**: - + | Option | Answer | Implications | |--------|--------|--------------| | A | [First suggested answer] | [What this means for the feature] | | B | [Second suggested answer] | [What this means for the feature] | | C | [Third suggested answer] | [What this means for the feature] | | Custom | Provide your own answer | [Explain how to provide custom input] | - + **Your choice**: _[Wait for user response]_ ``` @@ -242,12 +242,12 @@ Given that feature description, do this: Check if `.specify/extensions.yml` exists in the project root. - If it does not exist, or no hooks are registered under `hooks.after_specify`, skip to the Completion Report. - If it exists, read it and look for entries under the `hooks.after_specify` key. -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report. +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue to the Completion Report. - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` diff --git a/.agents/skills/speckit-tasks/SKILL.md b/.agents/skills/speckit-tasks/SKILL.md index 719f03797..1cd505fbc 100644 --- a/.agents/skills/speckit-tasks/SKILL.md +++ b/.agents/skills/speckit-tasks/SKILL.md @@ -24,12 +24,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before tasks generation)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_tasks` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -49,7 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty). **Automatic Pre-Hook**: {extension} Executing: `/{command}` EXECUTE_COMMAND: {command} - + Wait for the result of the hook command before proceeding to the Outline. ``` After emitting the block above you MUST actually invoke the hook and wait for it to finish before continuing. Run it the same way you would run the command yourself in this agent/session (the invocation may differ from the literal `{command}` id shown above, e.g. a skills-mode agent runs it as `/skill:speckit-...` or `$speckit-...`). Emitting the block alone does not run the hook. @@ -57,7 +57,7 @@ You **MUST** consider the user input before proceeding (if not empty). ## Outline -1. **Setup**: Run `.specify/scripts/bash/setup-tasks.sh --json` from repo root and parse FEATURE_DIR, 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"). +1. **Setup**: Run `.specify/scripts/bash/setup-tasks.sh --json` 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: - **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities) @@ -76,7 +76,7 @@ You **MUST** consider the user input before proceeding (if not empty). - Create parallel execution examples per user story - Validate task completeness (each user story has all needed tasks, independently testable) -4. **Generate tasks.md**: Read the tasks template from TASKS_TEMPLATE (from the JSON output above) and use it as structure. If TASKS_TEMPLATE is empty, fall back to `.specify/templates/tasks-template.md`. Fill with: +4. **Generate tasks.md**: Use TASKS_TEMPLATE_CONTENT (from the JSON output above) as the structure. For compatibility with older setup scripts that omit TASKS_TEMPLATE_CONTENT, read TASKS_TEMPLATE instead. Fill with: - Correct feature name from plan.md - Phase 1: Setup tasks (project initialization) - Phase 2: Foundational tasks (blocking prerequisites for all user stories) @@ -96,12 +96,12 @@ You **MUST** consider the user input before proceeding (if not empty). Check if `.specify/extensions.yml` exists in the project root. - If it does not exist, or no hooks are registered under `hooks.after_tasks`, skip to the Completion Report. - If it exists, read it and look for entries under the `hooks.after_tasks` key. -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue to the Completion Report. +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue to the Completion Report. - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Mandatory hook** (`optional: false`) — **You MUST emit `EXECUTE_COMMAND:` for each mandatory hook**: ``` @@ -160,7 +160,7 @@ Every task MUST strictly follow this format: 4. **[Story] label**: REQUIRED for user story phase tasks only - Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md) - Setup phase: NO story label - - Foundational phase: NO story label + - Foundational phase: NO story label - User Story phases: MUST have story label - Polish phase: NO story label 5. **Description**: Clear action with exact file path @@ -195,6 +195,7 @@ Every task MUST strictly follow this format: - Map each entity to the user story(ies) that need it - If entity serves multiple stories: Put in earliest story or Setup phase - Relationships → service layer tasks in appropriate story phase + - For each field with constraints in data-model.md (max length, nullable/required, enum values, validation rules), quote the constraint verbatim in the task description so it is not left to implementation-time discretion 4. **From Setup/Infrastructure**: - Shared infrastructure → Setup phase (Phase 1) diff --git a/.agents/skills/speckit-taskstoissues/SKILL.md b/.agents/skills/speckit-taskstoissues/SKILL.md index ce9e1a25e..c0a6a9c7f 100644 --- a/.agents/skills/speckit-taskstoissues/SKILL.md +++ b/.agents/skills/speckit-taskstoissues/SKILL.md @@ -1,6 +1,6 @@ --- name: "speckit-taskstoissues" -description: "Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." +description: "Deprecated: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." argument-hint: "Optional filter or label for GitHub issues" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: @@ -11,6 +11,14 @@ disable-model-invocation: false --- +## Deprecation Notice + +You **MUST display the following concise warning first**, as the first step of this command and before the User Input and Pre-Execution Checks sections below: + +> ⚠️ `/speckit-taskstoissues` is deprecated and will be removed in a future minor release. Use the bundled `github` extension instead: run `specify extension add github`, then invoke `/speckit-github-taskstoissues`. + +Continue with the existing workflow unchanged. Do not install or enable the extension automatically, do not stop, and do not run the replacement command. + ## User Input ```text @@ -24,12 +32,12 @@ You **MUST** consider the user input before proceeding (if not empty). **Check for extension hooks (before tasks-to-issues conversion)**: - Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.before_taskstoissues` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` @@ -69,7 +77,7 @@ git config --get remote.origin.url > [!CAUTION] > ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB 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 three digits, e.g. `T001`). 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` (word boundaries so tokens like `ST001` or `T0010` are not matched by mistake; 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. **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-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: <description>`, 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`). - Only create issues for tasks that do not yet have a matching issue. @@ -82,12 +90,12 @@ git config --get remote.origin.url **Check for extension hooks (after tasks-to-issues conversion)**: Check if `.specify/extensions.yml` exists in the project root. - If it exists, read it and look for entries under the `hooks.after_taskstoissues` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- If the YAML cannot be parsed or is invalid, do not skip silently: tell the user that `.specify/extensions.yml` could not be read (include the parser error) and that no hooks were checked, including any mandatory (`optional: false`) hooks registered there, then continue normally - Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. - For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. +- When constructing command invocations from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. - For each executable hook, output the following based on its `optional` flag: - **Optional hook** (`optional: true`): ``` diff --git a/.specify/.gitignore b/.specify/.gitignore new file mode 100644 index 000000000..bcd485e2c --- /dev/null +++ b/.specify/.gitignore @@ -0,0 +1,14 @@ +# Machine-local Spec Kit state — not meant to be shared. +# Managed by the Specify CLI; safe to edit (your changes are preserved on refresh). + +# Local pointer to the current feature directory. Rewritten every time you +# switch features, so it is per-checkout state rather than something to share. +feature.json + +# Per-machine extension config overrides. +extensions/*/local-config.yml + +# Executable adapter packages and their per-checkout provenance registry. +# Execution consent is stored separately in the user's local trust registry. +integrations/packages/ +integrations/packages.json diff --git a/.specify/extensions.yml b/.specify/extensions.yml index c04bc15b5..b2f8c59c0 100644 --- a/.specify/extensions.yml +++ b/.specify/extensions.yml @@ -1,10 +1,10 @@ installed: - agent-context +- checkpoint - critique - id: infrahub path: .specify/extensions/infrahub - opsmill -- review settings: auto_execute_hooks: true hooks: @@ -120,14 +120,6 @@ hooks: prompt: Commit plan changes? description: Auto-commit after implementation planning condition: null - - extension: agent-context - command: speckit.agent-context.update - enabled: true - optional: true - priority: 10 - prompt: Execute speckit.agent-context.update? - description: Refresh agent context after planning - condition: null - extension: critique command: speckit.critique.run enabled: true @@ -136,6 +128,14 @@ hooks: prompt: Run spec critique before proceeding to tasks? description: Challenges the spec and plan through product and engineering lenses condition: null + - extension: agent-context + command: speckit.agent-context.update + enabled: true + optional: true + priority: 10 + prompt: Execute speckit.agent-context.update? + description: Refresh agent context after planning + condition: null after_tasks: - extension: git command: speckit.git.commit @@ -152,14 +152,6 @@ hooks: prompt: Commit implementation changes? description: Auto-commit after implementation condition: null - - extension: review - command: speckit.review.run - enabled: true - optional: true - priority: 10 - prompt: Run PR review on implemented changes? - description: Comprehensive review after implementation - condition: null after_checklist: - extension: git command: speckit.git.commit diff --git a/.specify/extensions/.registry b/.specify/extensions/.registry index 60777eae5..d2b5240e3 100644 --- a/.specify/extensions/.registry +++ b/.specify/extensions/.registry @@ -19,164 +19,79 @@ "registered_skills": [], "installed_at": "2026-04-28T07:55:49.511501+00:00" }, - "agent-context": { + "critique": { "version": "1.0.0", "source": "local", - "manifest_hash": "sha256:9a1dc02d2d0139bb03860392ecacef79183be2c442feda2f9ccaa4e5907b1e47", + "manifest_hash": "sha256:0b63a1835415df730cbd487c90c1252b294980176c10c14a3fa9c31a7cb882e0", "enabled": true, "priority": 10, "registered_commands": { "agy": [ - "speckit.agent-context.update" + "speckit.critique.run" ], "amp": [ - "speckit.agent-context.update" + "speckit.critique.run" ], "claude": [ - "speckit.agent-context.update" + "speckit.critique.run" ], "codex": [ - "speckit.agent-context.update" + "speckit.critique.run" ], "zed": [ - "speckit.agent-context.update" + "speckit.critique.run" ] }, "registered_skills": [], - "installed_at": "2026-06-28T09:52:47.461970+00:00" + "installed_at": "2026-07-06T11:06:45.149443+00:00" }, - "opsmill": { - "version": "1.1.0", + "checkpoint": { + "version": "1.0.0", "source": "local", - "manifest_hash": "sha256:e4681b338e7176e0ad5e85af4bcfbd31e6a457432ce3a9fbeedeb6ce62e12e0e", + "manifest_hash": "sha256:f1f8587fcfee109d13e5fd2837df498439695218b32d00083f6e98095fca64c0", "enabled": true, "priority": 10, "registered_commands": { - "agy": [ - "speckit.opsmill.extract", - "speckit.opsmill.retrospect", - "speckit.opsmill.summary", - "speckit.opsmill.auto", - "speckit.opsmill.prep", - "speckit.opsmill.implement" - ], - "amp": [ - "speckit.opsmill.extract", - "speckit.opsmill.retrospect", - "speckit.opsmill.summary", - "speckit.opsmill.auto", - "speckit.opsmill.prep", - "speckit.opsmill.implement" - ], "claude": [ - "speckit.opsmill.extract", - "speckit.opsmill.retrospect", - "speckit.opsmill.summary", - "speckit.opsmill.auto", - "speckit.opsmill.prep", - "speckit.opsmill.implement" - ], - "codex": [ - "speckit.opsmill.extract", - "speckit.opsmill.retrospect", - "speckit.opsmill.summary", - "speckit.opsmill.auto", - "speckit.opsmill.prep", - "speckit.opsmill.implement" - ], - "zed": [ - "speckit.opsmill.extract", - "speckit.opsmill.retrospect", - "speckit.opsmill.summary", - "speckit.opsmill.auto", - "speckit.opsmill.prep", - "speckit.opsmill.implement" + "speckit.checkpoint.commit" ] }, "registered_skills": [], - "installed_at": "2026-07-06T11:06:33.491620+00:00" + "installed_at": "2026-11-06T08:34:33.326906+00:00" }, - "review": { - "version": "1.0.1", + "agent-context": { + "version": "1.0.2", "source": "local", - "manifest_hash": "sha256:3f9eedfc8079662edfb8d1f9c07a161be4e66111ea57621061f1658e91710d83", + "manifest_hash": "sha256:270100175bb742c37b8d549f2233c5b79114869dac754883dbecac2a4813beef", "enabled": true, "priority": 10, "registered_commands": { - "agy": [ - "speckit.review.run", - "speckit.review.code", - "speckit.review.comments", - "speckit.review.tests", - "speckit.review.errors", - "speckit.review.types", - "speckit.review.simplify" - ], - "amp": [ - "speckit.review.run", - "speckit.review.code", - "speckit.review.comments", - "speckit.review.tests", - "speckit.review.errors", - "speckit.review.types", - "speckit.review.simplify" - ], "claude": [ - "speckit.review.run", - "speckit.review.code", - "speckit.review.comments", - "speckit.review.tests", - "speckit.review.errors", - "speckit.review.types", - "speckit.review.simplify" - ], - "codex": [ - "speckit.review.run", - "speckit.review.code", - "speckit.review.comments", - "speckit.review.tests", - "speckit.review.errors", - "speckit.review.types", - "speckit.review.simplify" - ], - "zed": [ - "speckit.review.run", - "speckit.review.code", - "speckit.review.comments", - "speckit.review.tests", - "speckit.review.errors", - "speckit.review.types", - "speckit.review.simplify" + "speckit.agent-context.update" ] }, "registered_skills": [], - "installed_at": "2026-07-06T11:06:42.923444+00:00" + "installed_at": "2026-11-06T08:34:51.384829+00:00" }, - "critique": { - "version": "1.0.0", + "opsmill": { + "version": "1.4.1", "source": "local", - "manifest_hash": "sha256:0b63a1835415df730cbd487c90c1252b294980176c10c14a3fa9c31a7cb882e0", + "manifest_hash": "sha256:8d2d95bcc835b1febff8aef37dc6065e0dc4ad7bbea132adfbdbf4dce17ceabf", "enabled": true, "priority": 10, "registered_commands": { - "agy": [ - "speckit.critique.run" - ], - "amp": [ - "speckit.critique.run" - ], "claude": [ - "speckit.critique.run" - ], - "codex": [ - "speckit.critique.run" - ], - "zed": [ - "speckit.critique.run" + "speckit.opsmill.extract", + "speckit.opsmill.retrospect", + "speckit.opsmill.summary", + "speckit.opsmill.qa", + "speckit.opsmill.auto", + "speckit.opsmill.prep", + "speckit.opsmill.implement" ] }, "registered_skills": [], - "installed_at": "2026-07-06T11:06:45.149443+00:00" + "installed_at": "2026-11-08T18:36:12.155032+00:00" } } } \ No newline at end of file diff --git a/.specify/extensions/agent-context/README.md b/.specify/extensions/agent-context/README.md index 091e2b480..53602c334 100644 --- a/.specify/extensions/agent-context/README.md +++ b/.specify/extensions/agent-context/README.md @@ -2,53 +2,55 @@ This bundled extension manages the **coding agent context/instruction file** (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `GEMINI.md`, …) for the active integration. -It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`). +It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`). For `.mdc` files, it also ensures the YAML frontmatter (the metadata block at the top of the file) contains `alwaysApply: true`. Otherwise, everything outside the managed section is untouched. + +> NOTE: Spec Kit itself never touches your agent context file. This extension is the only thing that does, and it's opt-in: install it if you want the block kept in sync, skip it if you'd rather manage that file yourself. ## Why an extension? -Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Extracting this behavior into a dedicated extension lets users: +Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Keeping this behavior in a dedicated, **opt-in** extension lets users: -- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file. -- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value. +- **Choose whether to install it at all** - `specify init` does **not** install it. Add it explicitly when you want Spec Kit to manage the agent context file; when it is absent, the file is never modified, and when it is disabled, its automatic hooks do not run. +- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` ([agent-context-config.yml](./agent-context-config.yml) in this repo) - the bundled scripts honor the `context_markers` value. - **Synchronize multiple agent anchors** by setting `context_files` when a project intentionally uses more than one coding agent context file, such as `AGENTS.md` and `CLAUDE.md`. -- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`). - -## Commands +- **Refresh on demand** by running the `speckit.agent-context.update` command in your agent, or automatically through the hooks declared in [extension.yml](./extension.yml) (`after_specify`, `after_plan`). -| Command | Description | -|---------|-------------| -| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. | +## Installation -## Configuration +To install the extension, from the root of an initialized Spec Kit project, run: -All configuration flows through the extension's own config file at -`.specify/extensions/agent-context/agent-context-config.yml`: +```bash +specify extension add agent-context +``` -```yaml -# Path to the coding agent context file managed by this extension -context_file: CLAUDE.md +## Disabling -# Optional list of coding agent context files to manage together. -# When non-empty, this takes precedence over context_file. -context_files: - - AGENTS.md - - CLAUDE.md +```bash +specify extension disable agent-context -# Delimiters for the managed Spec Kit section -context_markers: - start: "<!-- SPECKIT START -->" - end: "<!-- SPECKIT END -->" +# Re-enable it +specify extension enable agent-context ``` -- `context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`. -- `context_files` — optional project-relative paths to multiple coding agent context files. When non-empty, the list takes precedence over `context_file`. Absolute paths, backslash separators, and `..` path segments are rejected. -- `context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers. +While this extension is disabled (or not installed), nothing in Spec Kit creates, updates, or removes the managed block - the `__CONTEXT_FILE__` placeholder in any template is left as-is, and the extension's own config is never read. + +## Commands + +| Command | Description | +| ------------------------------ | --------------------------------------------------------------------------------- | +| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. | + +> NOTE: The command ID above is canonical. Invoke it using the syntax for your integration: `/speckit.agent-context.update` for dot-command integrations; `/speckit-agent-context-update` for hyphen/skills integrations (including Forge and Cline); `$speckit-agent-context-update` for Codex or ZCode in skills mode; or `/skill:speckit-agent-context-update` for Kimi. + +## Configuration + +All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml` ([agent-context-config.yml](./agent-context-config.yml) in the repo). ## Requirements The bundled update scripts require **Python 3** with **PyYAML** for YAML/upsert processing (PowerShell can also use `ConvertFrom-Yaml` when available). -PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports *"PyYAML is required … not available in the current Python environment"*, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run: +PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports _"PyYAML is required … not available in the current Python environment"_, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run: ```bash pip install pyyaml @@ -56,11 +58,6 @@ pip install pyyaml /path/to/speckit-python -m pip install pyyaml ``` -## Disable - -```bash -specify extension disable agent-context -``` +## Issues -When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`). -Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out. +For any other issues, please create an issue in the [official GitHub repo](https://github.com/github/spec-kit/issues). diff --git a/.specify/extensions/agent-context/agent-context-defaults.json b/.specify/extensions/agent-context/agent-context-defaults.json new file mode 100644 index 000000000..a00b1d527 --- /dev/null +++ b/.specify/extensions/agent-context/agent-context-defaults.json @@ -0,0 +1,44 @@ +{ + "_comment": "Default coding agent context file per integration, owned by the agent-context extension. Used to self-seed agent-context-config.yml when it declares no context_file/context_files. Keyed by the Spec Kit integration key recorded in .specify/init-options.json. This mapping is independent of the Specify CLI by design.", + "agents": { + "agy": "AGENTS.md", + "alquimia": "ALQUIMIA.md", + "amp": "AGENTS.md", + "auggie": ".augment/rules/specify-rules.md", + "bob": "AGENTS.md", + "claude": "CLAUDE.md", + "cline": ".clinerules/specify-rules.md", + "codebuddy": "CODEBUDDY.md", + "codex": "AGENTS.md", + "copilot": ".github/copilot-instructions.md", + "cursor-agent": ".cursor/rules/specify-rules.mdc", + "devin": "AGENTS.md", + "firebender": ".firebender/rules/specify-rules.mdc", + "forge": "AGENTS.md", + "gemini": "GEMINI.md", + "generic": "AGENTS.md", + "goose": "AGENTS.md", + "grok": "AGENTS.md", + "hermes": "AGENTS.md", + "junie": ".junie/AGENTS.md", + "kilocode": ".kilocode/rules/specify-rules.md", + "kimi": "AGENTS.md", + "kiro-cli": "AGENTS.md", + "lingma": ".lingma/rules/specify-rules.md", + "mcode": "AGENTS.md", + "muse": "AGENTS.md", + "omp": "AGENTS.md", + "opencode": "AGENTS.md", + "pi": "AGENTS.md", + "qodercli": "QODER.md", + "qwen": "QWEN.md", + "rovodev": "AGENTS.md", + "shai": "SHAI.md", + "tabnine": "TABNINE.md", + "trae": ".trae/rules/project_rules.md", + "vibe": "AGENTS.md", + "windsurf": ".windsurf/rules/specify-rules.md", + "zcode": "ZCODE.md", + "zed": "AGENTS.md" + } +} diff --git a/.specify/extensions/agent-context/commands/speckit.agent-context.update.md b/.specify/extensions/agent-context/commands/speckit.agent-context.update.md index a654eb5a0..71c85f5d8 100644 --- a/.specify/extensions/agent-context/commands/speckit.agent-context.update.md +++ b/.specify/extensions/agent-context/commands/speckit.agent-context.update.md @@ -15,7 +15,7 @@ The script reads the agent-context extension config at - `context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`. - `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing. -It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`). +It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (any `plan.md` under `specs/`, including nested scoped layouts such as `specs/<scope>/<feature>/plan.md`). If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected. @@ -24,4 +24,4 @@ If `context_files` and `context_file` are empty, the command reports nothing to - **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]` - **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]` -When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`. +When `plan_path` is omitted, the script auto-detects the most recently modified `specs/**/plan.md` (searched recursively, so nested scoped layouts are discovered). diff --git a/.specify/extensions/agent-context/extension.yml b/.specify/extensions/agent-context/extension.yml index 191069e32..7f9e9867a 100644 --- a/.specify/extensions/agent-context/extension.yml +++ b/.specify/extensions/agent-context/extension.yml @@ -3,7 +3,7 @@ schema_version: "1.0" extension: id: agent-context name: "Coding Agent Context" - version: "1.0.0" + version: "1.0.2" description: "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers" author: spec-kit-core repository: https://github.com/github/spec-kit diff --git a/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh b/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh index 64e1bae89..7fbe3ef49 100755 --- a/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh +++ b/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh @@ -12,7 +12,7 @@ # # When `plan_path` is omitted, the script derives it from `.specify/feature.json` # (written by /speckit-specify). Falls back to the most recently modified -# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet. +# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet. set -euo pipefail @@ -59,7 +59,14 @@ case "$(uname -s 2>/dev/null || true)" in esac # Parse extension config once; emit context files as JSON, followed by marker strings. -if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" <<'PY' +# +# NOTE (bash 3.2 / macOS portability): the embedded Python heredocs below run +# inside $(...) command substitution. bash 3.2 (the system /bin/bash on macOS) +# mis-parses a single-quote/apostrophe in a heredoc body nested in $(...), +# failing with "unexpected EOF while looking for matching `''". Keep these +# $(...)-nested heredoc bodies free of apostrophes (use double quotes in Python +# string literals and avoid contractions in comments). +if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" "$PROJECT_ROOT" <<'PY' import json import sys try: @@ -95,24 +102,67 @@ def get_str(obj, *keys): context_files = [] seen_context_files = set() case_insensitive = sys.argv[2] == "1" or sys.platform.startswith(("win32", "cygwin")) +def add_context_file(value): + if not isinstance(value, str): + return + candidate = value.strip() + if not candidate: + return + key = candidate.casefold() if case_insensitive else candidate + if key in seen_context_files: + return + context_files.append(candidate) + seen_context_files.add(key) raw_files = data.get("context_files") if isinstance(raw_files, list): for value in raw_files: - if not isinstance(value, str): - continue - candidate = value.strip() - if not candidate: - continue - key = candidate.casefold() if case_insensitive else candidate - if key in seen_context_files: - continue - context_files.append(candidate) - seen_context_files.add(key) + add_context_file(value) +if not context_files: + add_context_file(get_str(data, "context_file")) if not context_files: - raw_file = get_str(data, "context_file") - candidate = raw_file.strip() - if candidate: - context_files.append(candidate) + # Self-seed: the agent-context extension manages its own lifecycle, so when + # its config declares no target, it derives one from the active integration + # recorded in init-options.json, mapped through the bundled + # agent-context-defaults.json file. This is independent of the Specify CLI + # by design; nothing here imports specify_cli. + project_root = sys.argv[3] if len(sys.argv) > 3 else "." + integration_key = "" + try: + with open( + f"{project_root}/.specify/init-options.json", "r", encoding="utf-8" + ) as fh: + opts = json.load(fh) + if isinstance(opts, dict): + value = opts.get("integration") or opts.get("ai") or "" + integration_key = value if isinstance(value, str) else "" + except Exception: + integration_key = "" + if integration_key: + defaults_path = ( + f"{project_root}/.specify/extensions/agent-context/" + "agent-context-defaults.json" + ) + mapping = {} + try: + with open(defaults_path, "r", encoding="utf-8") as fh: + loaded = json.load(fh) + agents = loaded.get("agents", {}) if isinstance(loaded, dict) else {} + mapping = agents if isinstance(agents, dict) else {} + except Exception: + print( + "agent-context: unable to read %s; cannot self-seed the context " + "file. Set context_file in the extension config." % defaults_path, + file=sys.stderr, + ) + mapping = {} + add_context_file(mapping.get(integration_key, "") or "") + if not context_files: + print( + "agent-context: no default context file is known for integration " + "%s. Set context_file in the extension config to choose one." + % integration_key, + file=sys.stderr, + ) print(json.dumps(context_files)) print(get_str(data, "context_markers", "start")) print(get_str(data, "context_markers", "end")) @@ -126,13 +176,18 @@ _opts_lines=() while IFS= read -r _line || [[ -n "$_line" ]]; do _opts_lines+=("$_line") done < <(printf '%s\n' "$_raw_opts") -if (( ${#_opts_lines[@]} < 3 )); then - echo "agent-context: malformed config parser output; expected 3 lines (context_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2 +if (( ${#_opts_lines[@]} < 1 )); then + echo "agent-context: malformed config parser output; expected at least the context_files line, got ${#_opts_lines[@]}; skipping update." >&2 exit 0 fi +# The marker lines may be absent: the $(...) capture above strips trailing +# newlines, so blank markers (the config omitting context_markers and relying on +# defaults) collapse the 3-line output to fewer lines. Default them to empty here +# and let the DEFAULT_START/END substitution below fill them in, matching the +# Python and PowerShell ports. CONTEXT_FILES_JSON="${_opts_lines[0]}" -MARKER_START="${_opts_lines[1]}" -MARKER_END="${_opts_lines[2]}" +MARKER_START="${_opts_lines[1]:-}" +MARKER_END="${_opts_lines[2]:-}" if ! _context_files_raw="$("$_python" - "$CONTEXT_FILES_JSON" <<'PY' import json @@ -257,16 +312,28 @@ import sys from pathlib import Path root = Path(sys.argv[1]).resolve() specs = root / "specs" -plans = sorted( - specs.glob("*/plan.md"), - key=lambda p: p.stat().st_mtime, - reverse=True, -) -if plans: + +def _resolved_rel(p): + # Resolve symlinks before checking containment: relative_to() is lexical + # and would otherwise accept a plan reached through a specs/ symlink that + # points outside the project, emitting an in-project-looking path for an + # out-of-project file (or picking it as "most recent"). try: - print(plans[0].relative_to(root).as_posix()) - except ValueError: - print("") + return p.resolve().relative_to(root) + except (OSError, ValueError): + return None + +# Recurse (rather than the old one-level specs/*/plan.md glob) so scoped layouts +# created via SPECIFY_FEATURE_DIRECTORY, e.g. specs/<scope>/<feature>/plan.md, +# are still discovered when feature.json is absent (#3024). +candidates = [] +for p in specs.rglob("plan.md"): + rel = _resolved_rel(p) + if rel: + candidates.append((p, rel)) +candidates.sort(key=lambda pr: pr[0].stat().st_mtime, reverse=True) +if candidates: + print(candidates[0][1].as_posix()) else: print("") PY @@ -295,11 +362,58 @@ for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do mkdir -p "$(dirname "$CTX_PATH")" "$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY' -import sys, os +import os +import re +import sys + ctx_path, start, end, section_path = sys.argv[1:5] with open(section_path, "r", encoding="utf-8") as fh: section = fh.read().rstrip("\n") + "\n" + +def ensure_mdc_frontmatter(content): + """Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``. + + Cursor only auto-loads ``.mdc`` rule files that carry frontmatter with + ``alwaysApply: true``. Prepend it when missing, or repair the value while + preserving any existing frontmatter comments/formatting. + """ + leading_ws = len(content) - len(content.lstrip()) + leading = content[:leading_ws] + stripped = content[leading_ws:] + + if not stripped.startswith("---"): + return "---\nalwaysApply: true\n---\n\n" + content + + match = re.match( + r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)", + stripped, + re.DOTALL, + ) + if not match: + return "---\nalwaysApply: true\n---\n\n" + content + + opening, fm_text, closing, sep, rest = match.groups() + newline = "\r\n" if "\r\n" in opening else "\n" + + if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text): + return content + + if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text): + fm_text = re.sub( + r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$", + r"\1alwaysApply: true\2", + fm_text, + count=1, + ) + elif fm_text.strip(): + fm_text = fm_text + newline + "alwaysApply: true" + else: + fm_text = "alwaysApply: true" + + return f"{leading}{opening}{fm_text}{closing}{sep}{rest}" + + if os.path.exists(ctx_path): with open(ctx_path, "r", encoding="utf-8-sig") as fh: content = fh.read() @@ -329,6 +443,8 @@ else: new_content = section new_content = new_content.replace("\r\n", "\n").replace("\r", "\n") +if ctx_path.casefold().endswith(".mdc"): + new_content = ensure_mdc_frontmatter(new_content) with open(ctx_path, "wb") as fh: fh.write(new_content.encode("utf-8")) PY diff --git a/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 b/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 index da9ff443c..91d067cc4 100644 --- a/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +++ b/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 @@ -12,7 +12,7 @@ # # When `plan_path` is omitted, the script derives it from `.specify/feature.json` # (written by /speckit-specify). Falls back to the most recently modified -# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet. +# `specs/**/plan.md` only when feature.json is absent or its plan does not exist yet. [CmdletBinding()] param( @@ -20,6 +20,56 @@ param( [string]$PlanPath ) +function Add-MdcFrontmatter { + <# + Ensure .mdc content has YAML frontmatter with alwaysApply: true. + + Cursor only auto-loads .mdc rule files that carry frontmatter with + alwaysApply: true. Prepend it when missing, or repair the value while + preserving any existing frontmatter comments/formatting. + #> + param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content) + + $leading = '' + $stripped = $Content + $m = [regex]::Match($Content, '^\s*') + if ($m.Success) { + $leading = $m.Value + $stripped = $Content.Substring($m.Length) + } + + if (-not $stripped.StartsWith('---')) { + return "---`nalwaysApply: true`n---`n`n" + $Content + } + + $fm = [regex]::Match($stripped, '^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)', [System.Text.RegularExpressions.RegexOptions]::Singleline) + if (-not $fm.Success) { + return "---`nalwaysApply: true`n---`n`n" + $Content + } + + $opening = $fm.Groups[1].Value + $fmText = $fm.Groups[2].Value + $closing = $fm.Groups[3].Value + $sep = $fm.Groups[4].Value + $rest = $fm.Groups[5].Value + $newline = if ($opening.Contains("`r`n")) { "`r`n" } else { "`n" } + + if ([regex]::IsMatch($fmText, '(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$')) { + return $Content + } + + if ([regex]::IsMatch($fmText, '(?m)^[ \t]*alwaysApply[ \t]*:')) { + $alwaysApplyRegex = [regex]'(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$' + $fmText = $alwaysApplyRegex.Replace($fmText, '${1}alwaysApply: true${2}', 1) + } elseif ($fmText.Trim()) { + $fmText = $fmText + $newline + 'alwaysApply: true' + } else { + $fmText = 'alwaysApply: true' + } + + return "$leading$opening$fmText$closing$sep$rest" +} + function Get-ConfigValue { param( [AllowNull()][object]$Object, @@ -250,6 +300,43 @@ foreach ($ContextFile in $ContextFiles) { } } $ContextFiles = $dedupedContextFiles +if ($ContextFiles.Count -eq 0) { + # Self-seed: the agent-context extension owns its lifecycle, so when its + # own config declares no target it derives one from the active integration + # recorded in init-options.json, using the extension's OWN bundled mapping + # (agent-context-defaults.json). Independent of the Specify CLI by design. + $initOptionsPath = Join-Path $ProjectRoot '.specify/init-options.json' + if (Test-Path -LiteralPath $initOptionsPath) { + try { + $initOpts = Get-Content -LiteralPath $initOptionsPath -Raw | ConvertFrom-Json -ErrorAction Stop + $integrationKey = $null + if ($initOpts.PSObject.Properties['integration'] -and $initOpts.integration) { + $integrationKey = [string]$initOpts.integration + } elseif ($initOpts.PSObject.Properties['ai'] -and $initOpts.ai) { + $integrationKey = [string]$initOpts.ai + } + if ($integrationKey) { + $defaultsPath = Join-Path $ProjectRoot '.specify/extensions/agent-context/agent-context-defaults.json' + if (Test-Path -LiteralPath $defaultsPath) { + $defaults = Get-Content -LiteralPath $defaultsPath -Raw | ConvertFrom-Json -ErrorAction Stop + $derived = $null + if ($defaults.PSObject.Properties['agents'] -and $defaults.agents.PSObject.Properties[$integrationKey]) { + $derived = [string]$defaults.agents.PSObject.Properties[$integrationKey].Value + } + if ($derived -and -not [string]::IsNullOrWhiteSpace($derived)) { + $ContextFiles += $derived.Trim() + } else { + Write-Warning ("agent-context: no default context file is known for integration '{0}'; set 'context_file' in the extension config to choose one." -f $integrationKey) + } + } else { + Write-Warning ("agent-context: unable to read {0}; cannot self-seed the context file. Set 'context_file' in the extension config." -f $defaultsPath) + } + } + } catch { + # Non-fatal: fall through to the nothing-to-do guard below. + } + } +} if ($ContextFiles.Count -eq 0) { Write-Warning 'agent-context: context_files/context_file not set in extension config; nothing to do.' exit 0 @@ -339,9 +426,11 @@ if (-not $PlanPath) { if (-not $PlanPath) { try { $specsDir = Join-Path $ProjectRoot 'specs' - $candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue | - ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } | - Where-Object { $_ } | + # Recurse (rather than the old one-level specs/*/plan.md scan) so scoped + # layouts created via SPECIFY_FEATURE_DIRECTORY, e.g. + # specs/<scope>/<feature>/plan.md, are still discovered when + # feature.json is absent (#3024). + $candidate = Get-ChildItem -Path $specsDir -Filter 'plan.md' -File -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($candidate) { @@ -411,6 +500,9 @@ foreach ($ContextFile in $ContextFiles) { } $newContent = $newContent.Replace("`r`n", "`n").Replace("`r", "`n") + if ($ContextFile -match '\.mdc$') { + $newContent = Add-MdcFrontmatter -Content $newContent + } [System.IO.File]::WriteAllText($CtxPath, $newContent, (New-Object System.Text.UTF8Encoding($false))) Write-Host "agent-context: updated $ContextFile" diff --git a/.specify/extensions/agent-context/scripts/python/update_agent_context.py b/.specify/extensions/agent-context/scripts/python/update_agent_context.py new file mode 100644 index 000000000..669ec5bf9 --- /dev/null +++ b/.specify/extensions/agent-context/scripts/python/update_agent_context.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 +"""Refresh the managed Spec Kit section in the coding agent's context file(s). + +Python port of ``update-agent-context.sh`` / ``update-agent-context.ps1``. + +Reads ``context_files`` or ``context_file``, plus ``context_markers.{start,end}``, +from the agent-context extension config: + .specify/extensions/agent-context/agent-context-config.yml + +Usage: update_agent_context.py [plan_path] + +When ``plan_path`` is omitted, the script derives it from +``.specify/feature.json`` (written by /speckit-specify). Falls back to the most +recently modified ``plan.md`` found anywhere under ``specs/`` — scoped layouts +nest it as ``specs/<scope>/<feature>/plan.md`` — only when feature.json is +absent or its plan does not exist yet. +""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + +DEFAULT_START = "<!-- SPECKIT START -->" +DEFAULT_END = "<!-- SPECKIT END -->" + + +def _err(message: str) -> None: + print(message, file=sys.stderr) + + +def _get_str(obj: object, *keys: str) -> str: + node = obj + for key in keys: + if isinstance(node, dict) and key in node: + node = node[key] + else: + return "" + return node if isinstance(node, str) else "" + + +def _collect_context_files(data: dict, project_root: str) -> list[str]: + """Resolve the managed context files from config, mirroring the bash logic.""" + context_files: list[str] = [] + seen: set[str] = set() + case_insensitive = sys.platform.startswith(("win32", "cygwin", "msys")) + + def add(value: object) -> None: + if not isinstance(value, str): + return + candidate = value.strip() + if not candidate: + return + key = candidate.casefold() if case_insensitive else candidate + if key in seen: + return + context_files.append(candidate) + seen.add(key) + + raw_files = data.get("context_files") + if isinstance(raw_files, list): + for value in raw_files: + add(value) + if not context_files: + add(_get_str(data, "context_file")) + if not context_files: + # Self-seed: when the config declares no target, derive one from the + # active integration recorded in init-options.json, mapped through the + # bundled agent-context-defaults.json file. Independent of the Specify + # CLI by design. + integration_key = "" + try: + with open( + f"{project_root}/.specify/init-options.json", "r", encoding="utf-8" + ) as fh: + opts = json.load(fh) + if isinstance(opts, dict): + value = opts.get("integration") or opts.get("ai") or "" + integration_key = value if isinstance(value, str) else "" + except Exception: + integration_key = "" + if integration_key: + defaults_path = ( + f"{project_root}/.specify/extensions/agent-context/" + "agent-context-defaults.json" + ) + mapping = {} + try: + with open(defaults_path, "r", encoding="utf-8") as fh: + loaded = json.load(fh) + agents = loaded.get("agents", {}) if isinstance(loaded, dict) else {} + mapping = agents if isinstance(agents, dict) else {} + except Exception: + _err( + "agent-context: unable to read %s; cannot self-seed the context " + "file. Set context_file in the extension config." % defaults_path + ) + mapping = {} + add(mapping.get(integration_key, "") or "") + if not context_files: + _err( + "agent-context: no default context file is known for integration " + "%s. Set context_file in the extension config to choose one." + % integration_key + ) + return context_files + + +def _validate_context_file(project_root: str, context_file: str) -> str | None: + """Return an error message when the path escapes the project root.""" + if context_file.startswith("/") or re.match(r"^[A-Za-z]:", context_file): + return ( + "agent-context: context files must be project-relative paths; " + f"got '{context_file}'." + ) + if "\\" in context_file: + return ( + "agent-context: context files must not contain backslash separators; " + f"got '{context_file}'." + ) + if ".." in context_file.split("/"): + return ( + "agent-context: context files must not contain '..' path segments; " + f"got '{context_file}'." + ) + root = Path(project_root).resolve() + target = (root / context_file).resolve() + try: + target.relative_to(root) + except ValueError: + return ( + "agent-context: context file path resolves outside the project root; " + f"got '{context_file}'." + ) + return None + + +def _resolve_plan_path(project_root: str) -> str: + """Derive the plan path: feature.json first, then the mtime fallback.""" + plan_path = "" + feature_json = Path(project_root) / ".specify" / "feature.json" + if feature_json.is_file(): + feature_dir = "" + try: + with open(feature_json, "r", encoding="utf-8") as fh: + data = json.load(fh) + value = data.get("feature_directory", "") + feature_dir = value if isinstance(value, str) else "" + except Exception: + feature_dir = "" + # Normalize backslashes (written by PS on Windows) before path ops. + feature_dir = feature_dir.replace("\\", "/").rstrip("/") + if feature_dir: + # feature_directory may be relative or absolute (absolute paths + # outside the project root are preserved as-is), including + # drive-qualified paths (C:/...) written by PowerShell on Windows. + if feature_dir.startswith("/") or re.match(r"^[A-Za-z]:/", feature_dir): + candidate = Path(feature_dir) / "plan.md" + else: + candidate = Path(project_root) / feature_dir / "plan.md" + if candidate.is_file(): + # Resolve symlinks before comparing so paths like /var/… vs + # /private/var/… (macOS) are treated as equivalent. + root = Path(project_root).resolve() + resolved = candidate.resolve() + try: + plan_path = resolved.relative_to(root).as_posix() + except ValueError: + plan_path = resolved.as_posix() + + if not plan_path: + root = Path(project_root).resolve() + specs = root / "specs" + + def _resolved_rel(p: Path) -> Path | None: + # Resolve symlinks before checking containment: relative_to() is + # lexical and would otherwise accept a plan reached through a specs/ + # symlink that points outside the project, emitting an + # in-project-looking path for an out-of-project file (or picking it + # as "most recent"). + try: + return p.resolve().relative_to(root) + except (OSError, ValueError): + return None + + # Recurse (rather than the old one-level specs/*/plan.md glob) so scoped + # layouts created via SPECIFY_FEATURE_DIRECTORY, e.g. + # specs/<scope>/<feature>/plan.md, are still discovered when + # feature.json is absent (#3024). Mirrors the bash and PowerShell twins. + candidates = [] + for p in specs.rglob("plan.md"): + rel = _resolved_rel(p) + if rel is not None: + candidates.append((p, rel)) + candidates.sort(key=lambda pr: pr[0].stat().st_mtime, reverse=True) + if candidates: + plan_path = candidates[0][1].as_posix() + return plan_path + + +def _build_section(marker_start: str, marker_end: str, plan_path: str) -> str: + lines = [ + marker_start, + "For additional context about technologies to be used, project structure,", + "shell commands, and other important information, read the current plan", + ] + if plan_path: + lines.append(f"at {plan_path}") + lines.append(marker_end) + return "\n".join(lines) + "\n" + + +def ensure_mdc_frontmatter(content: str) -> str: + """Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``. + + Cursor only auto-loads ``.mdc`` rule files that carry frontmatter with + ``alwaysApply: true``. Prepend it when missing, or repair the value while + preserving any existing frontmatter comments/formatting. + """ + leading_ws = len(content) - len(content.lstrip()) + leading = content[:leading_ws] + stripped = content[leading_ws:] + + if not stripped.startswith("---"): + return "---\nalwaysApply: true\n---\n\n" + content + + match = re.match( + r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)", + stripped, + re.DOTALL, + ) + if not match: + return "---\nalwaysApply: true\n---\n\n" + content + + opening, fm_text, closing, sep, rest = match.groups() + newline = "\r\n" if "\r\n" in opening else "\n" + + if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text): + return content + + if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text): + fm_text = re.sub( + r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$", + r"\1alwaysApply: true\2", + fm_text, + count=1, + ) + elif fm_text.strip(): + fm_text = fm_text + newline + "alwaysApply: true" + else: + fm_text = "alwaysApply: true" + + return f"{leading}{opening}{fm_text}{closing}{sep}{rest}" + + +def _upsert_section( + ctx_path: str, marker_start: str, marker_end: str, section: str +) -> None: + """Insert or replace the managed section, then normalize and write.""" + if os.path.exists(ctx_path): + with open(ctx_path, "r", encoding="utf-8-sig") as fh: + content = fh.read() + s = content.find(marker_start) + e = content.find(marker_end, s if s != -1 else 0) + if s != -1 and e != -1 and e > s: + end_of_marker = e + len(marker_end) + if end_of_marker < len(content) and content[end_of_marker] == "\r": + end_of_marker += 1 + if end_of_marker < len(content) and content[end_of_marker] == "\n": + end_of_marker += 1 + new_content = content[:s] + section + content[end_of_marker:] + elif s != -1: + new_content = content[:s] + section + elif e != -1: + end_of_marker = e + len(marker_end) + if end_of_marker < len(content) and content[end_of_marker] == "\r": + end_of_marker += 1 + if end_of_marker < len(content) and content[end_of_marker] == "\n": + end_of_marker += 1 + new_content = section + content[end_of_marker:] + else: + if content and not content.endswith("\n"): + content += "\n" + new_content = (content + "\n" + section) if content else section + else: + new_content = section + + new_content = new_content.replace("\r\n", "\n").replace("\r", "\n") + if ctx_path.casefold().endswith(".mdc"): + new_content = ensure_mdc_frontmatter(new_content) + with open(ctx_path, "wb") as fh: + fh.write(new_content.encode("utf-8")) + + +def main(argv: list[str] | None = None) -> int: + args = sys.argv[1:] if argv is None else argv + project_root = os.getcwd() + ext_config = ( + f"{project_root}/.specify/extensions/agent-context/agent-context-config.yml" + ) + + if not os.path.isfile(ext_config): + _err(f"agent-context: {ext_config} not found; nothing to do.") + return 0 + + try: + import yaml + except ImportError: + _err( + "agent-context: PyYAML is required to parse extension config but is " + "not available in the current Python environment.\n" + " To resolve: pip install pyyaml (or install it into the environment " + "used by python3).\n" + " Context file will not be updated until PyYAML is importable." + ) + _err("agent-context: skipping update (see above for details).") + return 0 + + try: + with open(ext_config, "r", encoding="utf-8") as fh: + data = yaml.safe_load(fh) + except Exception as exc: + _err( + f"agent-context: unable to parse {ext_config} ({exc}); " + "cannot update context." + ) + _err("agent-context: skipping update (see above for details).") + return 0 + if not isinstance(data, dict): + data = {} + + context_files = _collect_context_files(data, project_root) + if not context_files: + _err( + "agent-context: context_files/context_file not set in extension config; " + "nothing to do." + ) + return 0 + + for context_file in context_files: + error = _validate_context_file(project_root, context_file) + if error: + _err(error) + return 1 + + marker_start = _get_str(data, "context_markers", "start") or DEFAULT_START + marker_end = _get_str(data, "context_markers", "end") or DEFAULT_END + + plan_path = args[0] if args else "" + if not plan_path: + plan_path = _resolve_plan_path(project_root) + + section = _build_section(marker_start, marker_end, plan_path) + + for context_file in context_files: + ctx_path = os.path.join(project_root, context_file) + os.makedirs(os.path.dirname(ctx_path) or ".", exist_ok=True) + _upsert_section(ctx_path, marker_start, marker_end, section) + print(f"agent-context: updated {context_file}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.specify/extensions/review/LICENSE b/.specify/extensions/checkpoint/LICENSE similarity index 96% rename from .specify/extensions/review/LICENSE rename to .specify/extensions/checkpoint/LICENSE index 8e4e888a9..539d42b5d 100644 --- a/.specify/extensions/review/LICENSE +++ b/.specify/extensions/checkpoint/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Ismael Jimenez +Copyright (c) 2026 Aaron Sun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/.specify/extensions/checkpoint/README.md b/.specify/extensions/checkpoint/README.md new file mode 100644 index 000000000..f101658d7 --- /dev/null +++ b/.specify/extensions/checkpoint/README.md @@ -0,0 +1,68 @@ +# spec-kit-checkpoint + +A [spec-kit](https://github.com/github/spec-kit) extension that introduces incremental commits throughout the agent's workflow, so you don't end up with one massive commit at the very end. + +## What Problem Does This Solve? + +By default, the agent either makes no commits during implementation or bundles everything into a single commit at the end. This leaves you with a wall of changed files and no clear picture of: + +- **How** the code was implemented step by step +- **Why** certain files were changed together +- **What order** things were built in + +spec-kit-checkpoint fixes this by instructing the agent to commit at meaningful checkpoints — after each workflow step and during implementation as tasks are completed. + +## How It Works + +The extension provides a single command, `/speckit.checkpoint.commit`, which the agent calls to stage and commit changes at two key points in the workflow: + +### 1. After Each Pre-Implementation Step + +The agent commits after completing each of the following steps, capturing the spec artifacts produced: + +- **Constitution** — project principles and guidelines +- **Specify** — requirements and user stories +- **Clarify** — resolved ambiguities and clarifications +- **Plan** — technical implementation plan +- **Analyze** — cross-artifact consistency analysis +- **Tasks** — actionable task breakdown + +### 2. During Implementation + +While executing the implementation, the agent commits at natural checkpoints. A good reference point for when to commit is the task list generated during the tasks step — the agent can commit after completing each task or a logical group of related tasks. This is not a rigid rule; the agent should use judgment to commit whenever a meaningful, self-contained unit of work is done. + +## Quick Start + +### Prerequisites + +- [spec-kit](https://github.com/github/spec-kit) >= 0.1.0 + +### Installation + +Add `spec-kit-checkpoint` to your spec-kit extensions: + +``` +aaronrsun/spec-kit-checkpoint +``` + +## Usage + +Once installed, the `/speckit.checkpoint.commit` command is available for the agent to call whenever it needs to commit staged changes with a descriptive message. + +``` +/speckit.checkpoint.commit +``` + +The agent will determine the appropriate commit message based on the work just completed (e.g., the step name or the task description). + +## Project Structure + +``` +extension.yml # Extension metadata and command registration +commands/ + commit.md # Command definition for committing changes +``` + +## License + +[MIT](LICENSE) diff --git a/.specify/extensions/checkpoint/commands/commit.md b/.specify/extensions/checkpoint/commands/commit.md new file mode 100644 index 000000000..fce25e655 --- /dev/null +++ b/.specify/extensions/checkpoint/commands/commit.md @@ -0,0 +1,55 @@ +--- +id: "speckit.checkpoint.commit" +name: "Checkpoint Commit" +description: "Commit changes at meaningful checkpoints throughout the workflow" +--- + +# /speckit.checkpoint.commit + +## Description + +Stages all current changes and creates a commit with a descriptive message reflecting the work just completed. This command should be called by the agent at two points during the spec-kit workflow: + +1. **After each pre-implementation step** (constitution, specify, clarify, plan, analyze, tasks) — to capture the spec artifacts produced by that step. +2. **During implementation** — at natural checkpoints, such as after completing a task or a logical group of related tasks from the task list. + +## When to Commit + +### Pre-Implementation Steps + +Commit immediately after each of these steps completes: + +- After **constitution** — commit the project principles and guidelines +- After **specify** — commit the requirements and user stories +- After **clarify** — commit the resolved clarifications +- After **plan** — commit the technical implementation plan +- After **analyze** — commit the consistency and coverage analysis +- After **tasks** — commit the task breakdown + +### During Implementation + +Use the task list generated during the tasks step as a reference for when to commit. Good checkpoints include: + +- Completing a single task from the task list +- Completing a logical group of closely related tasks +- Finishing a self-contained unit of work (e.g., a new component, a service layer, a set of tests) + +The agent should use judgment — the goal is to produce commits that are meaningful and reviewable, not to commit after every single file change. + +## Workflow + +1. Determine a clear, concise commit message based on the work just completed +2. Stage all current changes (`git add -A`) +3. Commit with the message (`git commit -m "<message>"`) + +## Commit Message Guidelines + +- For pre-implementation steps, use the step name as a prefix (e.g., `constitution: establish project principles`, `tasks: break down implementation into actionable items`) +- For implementation commits, describe what was built (e.g., `implement user authentication service`, `add product listing page and API routes`) +- Keep messages concise but descriptive enough to understand the scope of the commit + +## Exit Criteria + +- All current changes are staged and committed +- The commit message accurately reflects the work completed +- The working tree is clean after the commit diff --git a/.specify/extensions/checkpoint/extension.yml b/.specify/extensions/checkpoint/extension.yml new file mode 100644 index 000000000..b14845560 --- /dev/null +++ b/.specify/extensions/checkpoint/extension.yml @@ -0,0 +1,23 @@ +schema_version: "1.0" + +extension: + id: "checkpoint" + name: "Spec Checkpoint" + version: "1.0.0" + description: "An extension to commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end." + author: "Aaron Sun" + repository: "https://github.com/aaronrsun/spec-kit-checkpoint" + license: "MIT" + +requires: + speckit_version: ">=0.1.0" + +provides: + commands: + - name: "speckit.checkpoint.commit" + file: "commands/commit.md" + description: "Commit changes" + +tags: + - "checkpoint" + - "commit" diff --git a/.specify/extensions/opsmill/CHANGELOG.md b/.specify/extensions/opsmill/CHANGELOG.md index f69fe9d62..135f9a689 100644 --- a/.specify/extensions/opsmill/CHANGELOG.md +++ b/.specify/extensions/opsmill/CHANGELOG.md @@ -5,6 +5,160 @@ Release history for the `opsmill` spec-kit extension (`extension.yml`). The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this artifact adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.1] - 2026-11-06 + +The extension ships the agent hooks that point spec-kit's scripts at the +project's Python environment, which provides PyYAML. A repo no longer copies +them. README: [Agent hooks for PyYAML](README.md#agent-hooks-for-pyyaml). + +### Added +- `scripts/bash/python-env.sh`: a Claude Code session start hook that sets + `SPECKIT_PYTHON_EXECUTABLE` and `SPECKIT_PYTHON` to the project's Python + environment when it can import PyYAML. +- `scripts/bash/route-codex.sh` and `scripts/python/route_codex.py`: a Codex + hook that runs `setup-plan.sh`, `setup-tasks.sh`, `resolve-template.sh` and + spec-kit's own `create-new-feature.sh` through `scripts/bash/python-run.sh`, + which sets the same variables. The copies in opsmill/infrahub#11376 + rerouted only the first two: `/speckit-constitution` runs + `resolve-template.sh`, and agents fall back to `create-new-feature.sh` when + the git extension cannot create the branch. + The hook rewrites only a command that is exactly one call of these + scripts in the current repository, because Codex runs a rewritten command + without asking for approval. +- Both hooks use the main checkout's environment in a linked git worktree + that has none of its own. +- CI: the install check runs both hooks in the installed project, and unit + tests cover the Codex hook's command matching. + +### Changed +- `extension.version` bumped `1.4.0` → `1.4.1`. +- README: step 6 of the spec-kit 1.2 upgrade adds the hooks to the repo. + +## [1.4.0] - 2026-10-27 + +The current workflow runs on spec-kit 1.2, and still on 0.8.0 and later. +README: [Upgrading spec-kit to 1.2 or later](README.md#upgrading-spec-kit-to-12-or-later). + +### Changed +- `extension.version` bumped `1.3.2` → `1.4.0`. +- `implement` reviews with the `pr-review-toolkit` Claude Code plugin when it is + installed, else with `/code-review`, else with its built-in + reviewer. The spec-kit `review` extension is no longer used: it does not + install on spec-kit 1.x. The `REVIEW:` field of the status line now reads + `plugin`, `command`, `fallback`, `none` or `n/a`, instead of `extension`, + `fallback`, `none` or `n/a`. Shipped as a minor bump, as in 1.3.0, whose + consumer audit found `auto` (updated here) to be the only reader of it. +- README: the `checkpoint` extension, which provides the + `speckit-checkpoint-commit` skill that `prep`, `implement` and `auto` commit + with, is listed with the other companions. +- `.extensionignore` also excludes `.github/` and `.git`, so the repo's CI files + and, for a `--dev` install from a clone, its git history are not copied into + `.specify/extensions/opsmill/`. + +### Fixed +- `summary` and `qa` find `check-prerequisites.sh` again on Spec Kit 1.2, which + resolves a command's script path from the extension directory. + +## [1.3.2] - 2026-10-23 + +### Added +- `.extensionignore` excludes `presets/` from the installed extension. A tag + archive holds the whole repo, so `specify extension add` copied the presets + into `.specify/extensions/opsmill/`, where spec-kit never reads them. + +### Fixed +- README: installs pin a release tag instead of `main`. The preset installs + used `specify preset add --from … --subdir`, an option no spec-kit release + has; they now install with `--dev` from a clone of the tag. +- README: the `critique` and `review` companion extensions move from Requires + to an optional section, since `prep`, `auto`, and `implement` fall back to a + built-in pass without them. + +### Changed +- `extension.version` bumped `1.3.1` → `1.3.2`. + +## [1.3.1] - 2026-10-23 + +### Fixed +- `extract`: knowledge and guideline source markers now point to the archived + spec path (`specs/archive/<spec-name>`), matching the ADR rule, so references + no longer break after Phase 4 archives the spec. The Knowledge updates + section gains a "create a new one if needed" branch, for parity with + Guidelines updates (opsmill/opsmill-speckit#10). + +### Changed +- `extension.version` bumped `1.3.0` → `1.3.1`. + +## [1.3.0] - 2026-09-12 + +### Added +- **Companion-extension fallbacks.** `implement` Phase 0 now resolves a + review provider (`REVIEW_MODE`), and `prep` Phase 3 resolves a critique + provider (`CRITIQUE_MODE`), by running a literal shell check for + `.specify/extensions/<id>/` and the installer's `.specify/extensions/.registry` + record — both installer-written and harness-independent. When a companion + extension is missing, the phase runs a reduced built-in reviewer/critic in a + clean-context subagent instead of silently skipping the gate. Where no + subagent dispatch exists either, the mode is `none` and the run says so. +- A half-installed extension (files on disk, command never registered) + downgrades at invocation time: if `speckit-review-run` / `speckit-critique-run` + cannot be invoked, the phase drops to the fallback branch and reports the mode + that actually ran, rather than reporting `extension` for a pass that did not + happen. +- `implement` Phase 7 §5 now opens with a required `Review mode:` line, and + §6 must record any non-`extension` mode as an autonomous decision. +- `implement` gains a review blocking rule: `REVIEW_MODE: none` marks the run + `INCOMPLETE` and makes "install the review extension and re-run the review" + the first next step, mirroring the existing local-pass-evidence rule. +- `auto` Completion must now state the `CRITIQUE` and `REVIEW` coverage of the + run, leading with it when either gate was degraded. +- README: companion extensions documented as soft dependencies, plus a + Troubleshooting section covering the upstream `review` install failure on + spec-kit 1.0.x (opsmill/opsmill-speckit#16) and the `REVIEW: none` state. + +### Changed +- `extension.version` bumped `1.2.0` → `1.3.0` (opsmill/opsmill-speckit#17). +- **Status-line contract — field order changed.** `prep` now emits a `CRITIQUE:` + field and `implement` a `REVIEW:` field, both inserted **before** `REASON:`: + - `STATUS: <READY|BLOCKED> | SPEC_DIR: <path> | CRITIQUE: <extension|fallback|none|n/a> | REASON: <...>` + - `STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <path> | REVIEW: <extension|fallback|none|n/a> | REASON: <...>` + + `STATUS` and `SPEC_DIR` keep their position and meaning, but `REASON` moves + from the third field to the fourth, so anything reading it by index needs + updating. Shipped as a minor bump rather than a major one (review call on + opsmill/opsmill-speckit#17) because the consumer audit found no such reader: + `auto` in this extension (updated here) is the only parser, `infrahub-speckit` + does not read these lines at all — it ships three route commands and no status + parsing — and an org-wide code search for `SPEC_DIR` returns only vendored + copies of this extension. + +## [1.2.0] - 2026-08-19 + +### Added +- `speckit.opsmill.qa` — produces a manual QA checklist at + `FEATURE_DIR/qa-checklist.md` that walks a human tester through + verifying the just-implemented feature (scope, prerequisites, setup, + test scenarios, edge cases, teardown, sign-off). Manual / user-facing + only; automated test suites are intentionally out of scope. Not wired + to any hook by default — run it manually, or re-wire it at + `after_implement` on the consumer side (snippet in README) + (opsmill/opsmill-speckit#5). +- `tags` gains `qa` for registry discoverability. +- README: the `critique` and `review` companion extensions, whose + `speckit-critique-run` and `speckit-review-run` skills `prep`, `auto` and + `implement` invoke, are listed under Requires with install commands + (opsmill/opsmill-speckit#9). + +### Changed +- `extension.version` bumped `1.1.0` → `1.2.0`. +- `extension.description` updated to cover the QA checklist command. + +### Fixed +- `retrospect`: the issue-creation skill reference follows the skill renames + in opsmill-skills, first `create-issue` → `creating-issues` + (opsmill/opsmill-speckit#8), then `creating-issues` → + `opsmill-dev-creating-issues` (opsmill/opsmill-speckit#11). + ## [1.1.0] - 2026-06-19 ### Added @@ -32,6 +186,10 @@ and this artifact adheres to [Semantic Versioning](https://semver.org/spec/v2.0. ### Changed - `extension.version` bumped `1.0.0` → `1.1.0`. - `extension.description` updated to cover the autonomous end-to-end workflow. +- `extract` writes ADRs in the MADR layout used by infrahub and infrahub-mcp: + file `nnnn-kebab-case-title.md` (4 digits, no `adr-` prefix, previously + `adr-NNN-title.md`) and heading `# N. <Title>` (previously `# ADR-NNN:`) + (opsmill/opsmill-speckit#6). ## [1.0.0] - 2026-05-11 @@ -71,4 +229,4 @@ Command bodies are lifted from `opsmill/styrmin/.specify/extensions/`: - `speckit.opsmill.retrospect` — Run a session retrospective that surfaces context-management gaps and routes them to approved follow-up actions. - `speckit.opsmill.summary` — Produce a flow-level timeline of the current Claude Code session in the active feature directory. - Discovery command used: `specify extension list` -- Command files verified in scratch project at `.specify/extensions/opsmill/commands/{extract,retrospect,summary}.md`. \ No newline at end of file +- Command files verified in scratch project at `.specify/extensions/opsmill/commands/{extract,retrospect,summary}.md`. diff --git a/.specify/extensions/opsmill/README.md b/.specify/extensions/opsmill/README.md index 81bebf700..2fa713d1a 100644 --- a/.specify/extensions/opsmill/README.md +++ b/.specify/extensions/opsmill/README.md @@ -3,7 +3,7 @@ OpsMill house [spec-kit](https://github.com/github/spec-kit) repo. Ships two independently installable artifacts: -1. **Extension `opsmill`** — six workflow commands under the `opsmill` +1. **Extension `opsmill`** — seven workflow commands under the `opsmill` namespace: - `/speckit.opsmill.auto` — run the full pipeline end-to-end (prep + implement) autonomously, making all decisions without pausing. Stops @@ -22,44 +22,62 @@ independently installable artifacts: - `/speckit.opsmill.summary` — produce a flow-level timeline of the current Claude Code session next to `spec.md` / `plan.md` in the active feature directory. + - `/speckit.opsmill.qa` — produce a manual QA checklist (`qa-checklist.md`) + next to `spec.md` / `plan.md` so a human tester can verify the + just-implemented feature step-by-step. -2. **Presets** — drop-in overrides for native spec-kit commands. Each preset - is installed independently of the extension. Currently one ships: +2. **Presets** — drop-in overrides for spec-kit commands (native or + extension-provided). Each preset is installed independently of the + extension. Currently four ship: - [`taskstoissues-jira`](presets/taskstoissues-jira/README.md) — overrides `/speckit.taskstoissues` with a Jira-flavored implementation that fans `tasks.md` out into Jira issues under a single Epic (one issue per `## Phase N:` block) via the Atlassian MCP. + - [`reconcile-opsmill`](presets/reconcile-opsmill/README.md) — overrides + `/speckit.reconcile.run` from the + [stn1slv/spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) + extension with an OpsMill-adapted command body: remediation tasks + stay inside `## Phase <N>:` blocks, `[P]` keeps its core + "parallelizable" meaning, the compliance gate reads + `dev/guidelines/` + `dev/adr/`, and the report is + Jira-aware. Requires the `reconcile` extension to be + installed in the consumer repo. + - [`critique-opsmill`](presets/critique-opsmill/README.md) — overrides + `/speckit.critique.run`, whose upstream version looks for its script in + the wrong folder. Requires the `critique` extension. + - [`jira-branch`](presets/jira-branch/README.md) — overrides + `/speckit.git.feature` so that a feature branch needs a JPD or Jira + reference. Requires the git extension. ## Requires -- spec-kit `>=0.8.0` +- spec-kit `>=0.8.0` (`requires.speckit_version` in `extension.yml`). CI + installs every component on that version for each pull request. - `check-prerequisites.sh` (shipped by spec-kit core; present at `.specify/scripts/bash/check-prerequisites.sh` in any spec-kit-initialized repo). Used by the `summary` command via the `{SCRIPT}` placeholder. -- **Two companion extensions.** The `prep`, `auto`, and `implement` commands - invoke skills provided by separate extensions: - - [`critique`](https://github.com/arunt14/spec-kit-critique) — provides the - `speckit-critique-run` skill used by `prep` and `auto`. - - [`review`](https://github.com/ismaelJimenez/spec-kit-review) — provides the - `speckit-review-run` skill used by `implement` and `auto`. +- PyYAML for the Python that spec-kit's scripts run: `python3`, or the one set + in `SPECKIT_PYTHON_EXECUTABLE`. spec-kit needs it to read the presets' + manifests; without it, `/speckit.plan` cannot prepare `plan.md`. In a repo + whose Python environment provides PyYAML, the extension's + [agent hooks](#agent-hooks-for-pyyaml) point spec-kit at that environment. - Install both before using `prep`, `auto`, or `implement` (see below). +Recommended: the [companions](#companion-extensions) `checkpoint`, `critique` +and the `pr-review-toolkit` Claude Code plugin, which give `prep`, `auto`, and +`implement` their commits and their full critique and review passes. ## Install -Latest `main`: +Install a tagged release, never `main`. Releases are listed in +[`CHANGELOG.md`](CHANGELOG.md): ```bash specify extension add opsmill \ - --from https://github.com/opsmill/opsmill-speckit/archive/refs/heads/main.zip + --from https://github.com/opsmill/opsmill-speckit/archive/refs/tags/v1.4.0.zip ``` -Pinned release: - -```bash -specify extension add opsmill \ - --from https://github.com/opsmill/opsmill-speckit/archive/refs/tags/v1.0.0.zip -``` +The URL is not in a spec-kit catalog, so `specify` asks you to confirm the +source before installing. Local development install (from a working tree): @@ -69,17 +87,62 @@ specify extension add --dev /path/to/opsmill-speckit ### Companion extensions -The `prep`, `auto`, and `implement` commands depend on two other extensions. -Install both: +The `prep`, `auto`, and `implement` commands rely on these companions: -```bash -specify extension add review \ - --from https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip +- [`checkpoint`](https://github.com/aaronrsun/spec-kit-checkpoint) — provides + the `speckit-checkpoint-commit` skill that commits each phase and each chunk. +- [`critique`](https://github.com/arunt14/spec-kit-critique) — provides the + `speckit-critique-run` skill used by `prep` and `auto`. Install the + [`critique-opsmill`](presets/critique-opsmill/README.md) preset with it. +- [`pr-review-toolkit`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/pr-review-toolkit) + — a Claude Code plugin whose review agents `implement` runs: five in + parallel, then `code-simplifier` alone, whose edits become findings and are + discarded. + Without it, `implement` uses `/code-review` if available. +```bash +specify extension add checkpoint \ + --from https://github.com/aaronrsun/spec-kit-checkpoint/archive/refs/tags/v1.0.0.zip specify extension add critique \ --from https://github.com/arunt14/spec-kit-critique/archive/refs/tags/v1.0.0.zip ``` +In Claude Code: + +```text +/plugin install pr-review-toolkit@claude-plugins-official +``` + +When the critique or review provider is missing, `prep`, `auto`, and +`implement` fall back to a reduced built-in critique or review subagent. The +fallback is always reported on the machine-readable status line (`CRITIQUE:` / +`REVIEW:`) and in the run summary, so a degraded run is never silent. + +## Upgrading spec-kit to 1.2 or later + +From `v1.4.0`, the workflow runs on spec-kit 1.2 and later. In a repo that +uses an older spec-kit: + +1. Remove the `review` extension, which does not install on spec-kit 1.x: + `specify extension remove review`. +2. Upgrade the CLI: + `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@v1.2.3`, + then `specify integration upgrade claude`. +3. Reinstall `opsmill` from the `v1.4.0` tag (see [Install](#install)): + `specify extension remove opsmill`, then `specify extension add opsmill --from …`. +4. Install the [companions](#companion-extensions) that the repo does not have + yet (`specify extension list` shows them): `checkpoint`, `critique`, and the + `pr-review-toolkit` plugin. Then add the `critique-opsmill` preset, also + when `critique` was already installed. +5. If the repo has the + [`infrahub` preset](https://github.com/opsmill/infrahub/tree/4d9fd0f0a469217aada697cb5ef306f7ef485f78/dev/spec-kit/presets/infrahub) + copied from opsmill/infrahub, which checks branch names against Jira, + replace it with [`jira-branch`](presets/jira-branch/README.md). +6. Add the [agent hooks](#agent-hooks-for-pyyaml) that point spec-kit's + scripts at the project's Python environment. In a repo without a Python + environment that provides PyYAML, each developer makes PyYAML available to + the Python that spec-kit's scripts run instead (see [Requires](#requires)). + ## Commands ### `/speckit.opsmill.extract` @@ -110,20 +173,56 @@ feature directory next to `spec.md` / `plan.md`. Supports `--since <commit|time>` to bound the summary window. +### `/speckit.opsmill.qa` + +Produces a manual QA checklist at `FEATURE_DIR/qa-checklist.md` that walks +a human tester through verifying the just-implemented feature. Scope is +**manual / user-facing only** — exact commands, URLs, UI paths, and the +outputs to look for. Automated test suites are intentionally out of scope. + +The checklist is organized into Scope, Prerequisites, Setup, Test Scenarios, +Edge Cases, Teardown, and Sign-off sections. Re-running on the same feature +prompts before overwriting; pass `--force` to skip the prompt, or any other +free-form text as scope guidance (e.g. `focus on the CLI surface`). + ### `/speckit.taskstoissues` (preset override) Provided by the [`taskstoissues-jira`](presets/taskstoissues-jira/README.md) preset, not the extension. Install separately: ```bash -specify preset add taskstoissues-jira \ - --from https://github.com/opsmill/opsmill-speckit/archive/refs/heads/main.zip \ - --subdir presets/taskstoissues-jira +git clone -b v1.4.0 https://github.com/opsmill/opsmill-speckit +specify preset add --dev opsmill-speckit/presets/taskstoissues-jira ``` See [`presets/taskstoissues-jira/README.md`](presets/taskstoissues-jira/README.md) for config (`dev/jira.yml`) and failure-mode details. +### `/speckit.reconcile.run` (preset override) + +The `/speckit.reconcile.run` command comes from the +[stn1slv/spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) +extension; the [`reconcile-opsmill`](presets/reconcile-opsmill/README.md) +preset overrides its body. Install the extension first (it must be +present in the consumer repo), then the preset: + +```bash +specify extension add reconcile \ + --from https://github.com/stn1slv/spec-kit-reconcile/archive/886f1dd.zip + +git clone -b v1.4.0 https://github.com/opsmill/opsmill-speckit +specify preset add --dev opsmill-speckit/presets/reconcile-opsmill +``` + +See [`presets/reconcile-opsmill/README.md`](presets/reconcile-opsmill/README.md) +for provenance and behavior. + +### `/speckit.critique.run` and `/speckit.git.feature` (preset overrides) + +See [`critique-opsmill`](presets/critique-opsmill/README.md) and +[`jira-branch`](presets/jira-branch/README.md) for what they change and how to +install them. + ## Hooks (auto-fire during SDD) The extension registers one opt-in hook at install time. It prompts before @@ -133,16 +232,16 @@ running (`optional: true`): |---|---|---| | `after_taskstoissues` | `/speckit.opsmill.summary` | Capture the session timeline at the moment of handoff to the issue tracker. | -`/speckit.opsmill.extract` and `/speckit.opsmill.retrospect` are not wired by -default — they remain manual commands. Extract is intentionally manual so the -user can review the implementation report before promoting content into -`dev/`. +`/speckit.opsmill.extract`, `/speckit.opsmill.retrospect`, and +`/speckit.opsmill.qa` are not wired by default — they remain manual commands. +Extract is intentionally manual so the user can review the implementation +report before promoting content into `dev/`. The `extension.yml` `hooks:` schema accepts one command per event. To fire -additional commands at the same event (or to re-wire `extract` to fire -automatically after implement if you prefer that workflow), append entries -to your repo's `.specify/extensions.yml` registry. Example: fire `extract` -at `after_implement` on the consumer side: +additional commands at the same event (or to re-wire `extract` or `qa` to +fire automatically after implement if you prefer that workflow), append +entries to your repo's `.specify/extensions.yml` registry. Example: fire +`extract` and `qa` at `after_implement` on the consumer side: ```yaml # .specify/extensions.yml (consumer-side, snippet) @@ -153,8 +252,96 @@ hooks: enabled: true optional: true prompt: "Extract knowledge, guidelines, and ADRs from the completed spec?" + - extension: opsmill + command: speckit.opsmill.qa + enabled: true + optional: true + prompt: "Create QA testing checklist?" ``` +## Agent hooks for PyYAML + +spec-kit 1.2 reads the presets' manifests with PyYAML in `setup-plan.sh`, +`setup-tasks.sh`, `resolve-template.sh` and its own `create-new-feature.sh`. These scripts run `python3` from +the PATH, which often lacks PyYAML, unless `SPECKIT_PYTHON_EXECUTABLE` names +another interpreter. Until spec-kit stops needing PyYAML in these scripts +([github/spec-kit#4674](https://github.com/github/spec-kit/pull/4674)), the +extension ships hooks that point them at the project's Python environment: +`.venv`, or the directory in `UV_PROJECT_ENVIRONMENT`. +In a linked git worktree that has no such environment, the hooks use the +main checkout's. + +| Script in `.specify/extensions/opsmill/scripts/` | Agent | What it does | +|---|---|---| +| `bash/python-env.sh` | Claude Code, at session start | Sets `SPECKIT_PYTHON_EXECUTABLE` and `SPECKIT_PYTHON` for the session, when the environment can import PyYAML. Does nothing otherwise. | +| `bash/route-codex.sh` | Codex, before each shell command | Runs a command that is exactly one call of the four scripts above, from the repository root or by its absolute path, through `bash/python-run.sh`, which sets the same variables for that command. Leaves other commands unchanged. | + +A repo adds them once. The next `opsmill` upgrade updates the scripts, and +the entries below keep pointing at them. + +`.claude/settings.json`: + +```json +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.specify/extensions/opsmill/scripts/bash/python-env.sh" + } + ] + } + ] + } +} +``` + +`.codex/hooks.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "command", + "command": "sh -c '\"$(git rev-parse --show-toplevel)\"/.specify/extensions/opsmill/scripts/bash/route-codex.sh'" + } + ] + } + ] + } +} +``` + +Codex runs project hooks only after each developer trusts them. + +Codex runs a rewritten command without asking for approval. So the Codex hook +leaves a compound command unchanged, such as `cd specs && ../.specify/...` or +`OUT=$(...)`, and that command runs with `python3` from the PATH. + +The extension does not declare these hooks as spec-kit `events:`, which would +write the Claude Code entry automatically. spec-kit writes such an entry with +the interpreter found when the extension is installed: `.venv/bin/python`, as +a path relative to the directory the agent starts in, when a `.venv` exists. +That entry fails for a developer without a `.venv`, or who starts the agent +in a subdirectory. spec-kit writes Codex entries only in repos that install +its Codex integration. + +## Troubleshooting + +### `implement` or `auto` reports `REVIEW: none` / `CRITIQUE: none` + +No critique or review provider was available **and** the harness offered no +subagent dispatch, so no fallback was possible. `REVIEW: none` marks the run +`INCOMPLETE` by design. Install the missing companion and re-run the gate over +the same diff. + ## Provenance The `extract`, `retrospect`, and `summary` command bodies originated as lifts diff --git a/.specify/extensions/opsmill/commands/auto.md b/.specify/extensions/opsmill/commands/auto.md index 06192167b..9f18836c2 100644 --- a/.specify/extensions/opsmill/commands/auto.md +++ b/.specify/extensions/opsmill/commands/auto.md @@ -33,12 +33,13 @@ Invoke the `speckit-opsmill-prep` skill with the user's feature description (`$A **Read prep's status line.** `speckit-opsmill-prep` ends its output with a literal final line: -`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <...>` +`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | CRITIQUE: <extension|fallback|none|n/a> | REASON: <...>` Parse that line — do **not** infer success from the prose summary: - Take **`SPEC_DIR`** from this line as the spec directory to pass to Phase B. This is the deterministic hand-off; do not scrape the path out of free text. - If **`STATUS: BLOCKED`** (e.g., alignment never converged within its retry budget, or a phase could not complete), surface the reason to the user and **stop**. Do **not** proceed to implementation on a misaligned or incomplete spec. +- Take **`CRITIQUE`** as the critique coverage of the run. Anything other than `extension` or `n/a` means the critique extension was unavailable and prep either ran a reduced fallback critique or none at all. `n/a` is not a coverage claim — it means prep aborted before Phase 3 ever resolved a provider, so read it together with `STATUS`. This does **not** block Phase B — carry it into the final summary. - Only proceed to Phase B when `STATUS: READY`. ### Phase B — Implementation tail (delegated to `speckit-opsmill-implement`) @@ -50,11 +51,12 @@ Invoke the `speckit-opsmill-implement` skill with the `SPEC_DIR` path from Phase **Read implement's status line.** `speckit-opsmill-implement` ends its output with a literal final line: -`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <...> | REASON: <...>` +`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <...> | REVIEW: <plugin|command|fallback|none|n/a> | REASON: <...>` - `STATUS: BLOCKED` — a Phase 0 stop-condition aborted before any implementation (no report written). Surface the reason in the final summary; do not retry from this orchestrator. -- `STATUS: INCOMPLETE` — blocked tasks or missing local-pass evidence; capture that for the final summary. The user decides whether to re-run. +- `STATUS: INCOMPLETE` — blocked tasks, missing local-pass evidence, or `REVIEW: none`; capture that for the final summary. The user decides whether to re-run. - `STATUS: DONE` — all chunks completed with evidence. +- Take **`REVIEW`** as the review coverage of the run. A missing reviewer does not stop the implement phase: `REVIEW: plugin` and `REVIEW: command` are full reviews, `REVIEW: fallback` means the sub-skill ran a reduced built-in review, `REVIEW: none` means no review happened at all and forces `STATUS: INCOMPLETE`. `REVIEW: n/a` is not a coverage claim either — it means implement aborted before Phase 0 resolved a provider, and it pairs with `STATUS: BLOCKED`. Never treat a completed implement phase as a reviewed change set without checking this field. In every case, do **not** retry from this orchestrator. @@ -66,6 +68,7 @@ After both phases are complete, provide a brief summary: - Whether the alignment check inside auto-prep required retries (and how many) - Number of tasks completed (from the auto-implement report) - Any review findings that were fixed inline vs. deferred +- **Coverage of the quality gates (REQUIRED).** State the `CRITIQUE` value from Phase A and the `REVIEW` value from Phase B explicitly. If `CRITIQUE` is not `extension`, or `REVIEW` is not `plugin` or `command`, lead the summary with it, name what is missing, and give the install command (`specify extension add critique` / `/plugin install pr-review-toolkit@claude-plugins-official`). An autonomous run that quietly skipped a quality gate is the one outcome the user must never have to discover by opening a report file - Any notable decisions you or the sub-skills made autonomously Do **not** open a PR, push, run extraction, or start a new feature. The user takes it from there — extraction (`/speckit.opsmill.extract`) is intentionally left as a manual follow-up so the user can review the implementation report first. diff --git a/.specify/extensions/opsmill/commands/extract.md b/.specify/extensions/opsmill/commands/extract.md index 36a388ade..95ac9102e 100644 --- a/.specify/extensions/opsmill/commands/extract.md +++ b/.specify/extensions/opsmill/commands/extract.md @@ -215,11 +215,22 @@ Create new files in `dev/adr/` using this format: ### Knowledge updates For each knowledge update: -1. Read the full target file +1. Read the full target file (or create a new one if needed) 2. Find the appropriate section (match by heading) 3. Add or update content within that section 4. If the section does not exist, create it in a logical position -5. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` +5. Add a source marker: `<!-- Extracted from specs/archive/<spec-name> on YYYY-MM-DD -->` + +If creating a new knowledge file, follow the standard structure: +```markdown +# <Topic> + +## Overview + +<Brief description of what this document covers.> + +## <Sections...> +``` ### Guidelines updates @@ -227,7 +238,7 @@ Same approach as knowledge updates: 1. Read the full target file (or create a new one if needed) 2. Find the appropriate section 3. Add the prescriptive pattern with code examples where relevant -4. Add a source marker: `<!-- Extracted from specs/<spec-name> on YYYY-MM-DD -->` +4. Add a source marker: `<!-- Extracted from specs/archive/<spec-name> on YYYY-MM-DD -->` If creating a new guidelines file, follow the standard structure: ```markdown diff --git a/.specify/extensions/opsmill/commands/implement.md b/.specify/extensions/opsmill/commands/implement.md index e5206ee63..ad3b12f8e 100644 --- a/.specify/extensions/opsmill/commands/implement.md +++ b/.specify/extensions/opsmill/commands/implement.md @@ -34,6 +34,18 @@ Before any work begins: 3. Read `tasks.md` end-to-end and identify the chunking boundaries (see "Chunking strategy" below). Build a numbered list of chunks with their task IDs. 4. Verify the working tree is clean (or only contains expected prep artifacts). If it is dirty in unrelated ways: interactive → surface that and pause; autonomous parent → abort with `STATUS: BLOCKED` (reason: unexpectedly dirty working tree). Do not attempt to stash or clean it yourself. 5. Note the current `HEAD` commit — you will diff against it for the review and report. +6. **Resolve the review provider.** Decide how Phase 6 will review the change set and record the answer as `REVIEW_MODE`. Take the first row that applies, deciding from the subagent types and commands your harness actually lists, not from memory: + + | Reviewer available | `REVIEW_MODE` | + |--------------------|---------------| + | The `pr-review-toolkit` plugin: its agents (`pr-review-toolkit:code-reviewer`, `pr-review-toolkit:pr-test-analyzer`, …) are among the subagent types you can dispatch | `plugin` | + | The `code-review` skill of Claude Code (`/code-review`) | `command` | + | Neither, and you can dispatch clean-context subagents | `fallback` | + | Neither, and no subagent dispatch is available | `none` | + + This is a starting point: if Phase 6 cannot use the provider, it moves to the next row and reports the mode that actually ran. The spec-kit `review` extension is not used: it does not install on spec-kit 1.x. + + A missing reviewer is **not** a stop-condition, in either invocation mode. The Phase 0 aborts above exist because a dirty tree or missing prep artifacts contaminate the `HEAD@start..HEAD@now` diff; a missing reviewer does not corrupt anything, it only lowers assurance, and Phase 6 has a defined fallback for it. Carry `REVIEW_MODE` forward to Phase 6, Phase 7 §5, and the status line. ### Phase 5 — Implement (looped, clean-context subagents) @@ -73,7 +85,33 @@ Respect explicit dependencies in `tasks.md`. Sequential `[P]`-free tasks must st ### Phase 6 — Review -Once **all** chunks have completed (including any retries), invoke the `speckit-review-run` skill once across the full diff (`HEAD`-at-start..`HEAD`-now). +Once **all** chunks have completed (including any retries), review the full diff (`HEAD`-at-start..`HEAD`-now). How you *produce* the findings depends on the `REVIEW_MODE` resolved in Phase 0. What you *do* with them does not. + +**6a — `REVIEW_MODE: plugin`.** Dispatch five `pr-review-toolkit` agents in parallel, each in a clean context: `code-reviewer`, `pr-test-analyzer`, `silent-failure-hunter`, `type-design-analyzer` and `comment-analyzer`. Brief each one with the repository root, the diff range (`HEAD`-at-start..`HEAD`-now), `spec.md` and `tasks.md` as the statement of intent, and the project context files resolved by the same rule as Phase 5. Ask each for **findings only**, without editing any file: a severity (`high` / `medium` / `low`), the file, the line where known, and a one-line summary. + +Once all five have returned, run `code-simplifier` alone, with the same briefing. It is built to edit code rather than report, so its edits are never kept: + +- Before it runs, check that the working tree is clean (`git status --porcelain` prints nothing). If it is not, skip `code-simplifier` and say so in the report. +- After it ends, turn every change it made into a `low` finding: read them with `git diff` and `git status --porcelain`, one finding per simplification, with the file and line. +- Then discard its changes: `git restore .` for edited files, and delete the files it created (listed as `??`). The working tree must be clean again before the finding handling below. + +**6b — `REVIEW_MODE: command`.** Run the `code-review` skill (`/code-review`) on the change set, given as a Git revision range (`<HEAD-at-start>..HEAD`). The command reviews in the background: wait for its findings before you go on, and collect them in the same shape as 6a. + +If 6a or 6b cannot run — the agents or the command are not found, or they fail before they finish — move to the next mode and run it. A review that finishes with no findings has run: keep its mode and go on with nothing to fix. Record the change and its cause in Phase 7 §5 and §6: the mode you report is the mode that actually ran. + +**6c — `REVIEW_MODE: fallback`.** No reviewer is available, so run the review yourself. Dispatch **one** clean-context review subagent using the same briefing protocol as Phase 5 (self-contained prompt, no inherited history, resolved absolute paths). Brief it with: + +- Absolute paths to the repository root and the spec directory. +- The diff range (`HEAD`-at-start..`HEAD`-now), and the project context files resolved by the same rule as Phase 5 (`AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`, the constitution). Pass the paths that actually exist; say so explicitly if none do. +- `spec.md` and `tasks.md`, as the statement of intent the diff is reviewed against. +- Instructions to cover these six aspects and nothing else: **code quality** (project guideline compliance, bugs, security), **comments** (accuracy, documentation completeness, comment rot), **tests** (behavioural coverage, critical gaps, test resilience), **error handling** (silent failures, catch blocks, error logging), **type design** (encapsulation, invariants, enforcement), **simplification** (clarity, unnecessary complexity, redundant abstractions). +- The required return shape: a severity-rated finding list (`high` / `medium` / `low`), each finding carrying file, line where known, and a one-line summary. **Findings only** — the subagent must not fix anything, so that the fix decisions below stay with the orchestrator. + +This is a safety net, not a replacement. It does not match the depth of the plugin or the review command, and the report must say so rather than present it as an equivalent pass. + +**6d — `REVIEW_MODE: none`.** No review is possible. Proceed to Phase 7 with an empty finding list and carry `REVIEW_MODE: none` forward. Do **not** continue as though the change set had been reviewed, and do **not** substitute a quick unstructured read of the diff for a review and report it as one. + +**Finding handling is identical in 6a, 6b and 6c:** - For findings rated **high severity or above**, fix them inline. Prefer fixing them yourself if the change is small and localised; dispatch a fresh clean-context subagent (same protocol as Phase 5) if the fix spans multiple files or needs significant code understanding. - For lower-severity findings, record them in the report (do not block). @@ -101,12 +139,25 @@ Write a markdown report to `<spec-dir>/opsmill-implement-report.md` and also pri If an E2E test was added but not executable locally in this project, record it as a separate row with `Passed at` = `deferred — local E2E not supported` and put the CI-side command in `Run command`. Call this out in §6 "Autonomous decisions" so the user can confirm the call. If no tests were added or modified by the whole run, write `n/a — no new or modified tests in this implementation` instead of the table and state it explicitly so reviewers can verify the claim against the diff. -5. **Review findings** — table of severity / file / one-line summary. Mark which were fixed inline and which were deferred. -6. **Autonomous decisions** — any judgment calls the orchestrator made that the user might want to revisit (chunk splits, blocked-task handling, review-finding triage choices). +5. **Review findings** — MUST open with a `Review mode:` line, before the table, stating how the findings were produced: + + - `Review mode: plugin (pr-review-toolkit)` + - `Review mode: command (/code-review)` + - `Review mode: fallback (built-in reviewer, reduced depth — install the pr-review-toolkit plugin for a full pass)` + - `Review mode: none — NO REVIEW WAS PERFORMED` + + In `fallback` and `none` mode, follow that line with the install hint: `/plugin install pr-review-toolkit@claude-plugins-official` in Claude Code. + + Then the table of severity / file / one-line summary, marking which findings were fixed inline and which were deferred. In `none` mode write `n/a — no review performed` in place of the table. +6. **Autonomous decisions** — any judgment calls the orchestrator made that the user might want to revisit (chunk splits, blocked-task handling, review-finding triage choices). A `REVIEW_MODE` other than `plugin` or `command` MUST appear here, with the reason no reviewer was available. 7. **Suggested next steps** — e.g. "open a PR", "rerun `speckit.opsmill.implement` to retry the 2 blocked tasks", "address the deferred review findings". **Blocking rule.** If §4 contains any `MISSING` row, the report header MUST mark the run `INCOMPLETE` and §7 MUST list "produce local-pass evidence for the listed tests" as the first next step. Do not declare the spec done while local-pass evidence is missing. (`deferred — local E2E not supported` rows do NOT trigger this rule, but they MUST be flagged in §6.) +**Review blocking rule.** If `REVIEW_MODE` is `none`, the report header MUST mark the run `INCOMPLETE` and §7 MUST list "install the pr-review-toolkit plugin and re-run the review over this diff" as the first next step. `REVIEW_MODE: fallback` does **not** trigger this rule, because a review did happen, but it MUST be flagged in §5 and §6. + +`REVIEW: none` blocks where `CRITIQUE: none` in `speckit-opsmill-prep` does not, on purpose: an un-critiqued spec is still read by a human before any code exists, so the gap is visible and cheap to close, whereas an unreviewed diff is already-written code that nothing downstream re-checks. See the Completion section of `speckit-opsmill-prep` for the full rationale. + Commit the report via `speckit-checkpoint-commit` as the final action. ## Completion @@ -115,11 +166,12 @@ Print a 4-6 line summary mirroring the report header + outcome counts so the use **Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: -`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` +`STATUS: <DONE|INCOMPLETE|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REVIEW: <plugin|command|fallback|none|n/a> | REASON: <short reason or n/a>` - `STATUS: DONE` — all chunks completed and §4 local-pass evidence has no `MISSING` rows. -- `STATUS: INCOMPLETE` — the run finished but the report is marked `INCOMPLETE` (e.g. missing local-pass evidence, or blocked tasks recorded). +- `STATUS: INCOMPLETE` — the run finished but the report is marked `INCOMPLETE` (e.g. missing local-pass evidence, blocked tasks recorded, or `REVIEW: none`). - `STATUS: BLOCKED` — a Phase 0 stop-condition aborted the run before the implement loop (missing prep artifacts, unexpectedly dirty tree). In this case no report is written; emit only the summary explaining why, then this line. +- `REVIEW` mirrors the `REVIEW_MODE` resolved in Phase 0, so the parent never has to infer review coverage from prose. `REVIEW: fallback` is compatible with `STATUS: DONE`. `REVIEW: none` forces `STATUS: INCOMPLETE` per the review blocking rule. Use `REVIEW: n/a` only when the run aborted before Phase 0 resolved a provider. The parent `speckit-opsmill-auto` parses this line; keep it as the literal last line, unwrapped. diff --git a/.specify/extensions/opsmill/commands/prep.md b/.specify/extensions/opsmill/commands/prep.md index 2eb58164f..a96d775b1 100644 --- a/.specify/extensions/opsmill/commands/prep.md +++ b/.specify/extensions/opsmill/commands/prep.md @@ -40,9 +40,34 @@ Invoke the `speckit-plan` skill. ### Phase 3 — Critique -Invoke the `speckit-critique-run` skill. +Run the dual-lens (Product + Engineering) critique against `spec.md` and `plan.md` before any tasks are generated. + +**Resolve the critique provider first.** Record the answer as `CRITIQUE_MODE`. Run this check from the repository root and read `CRITIQUE_MODE` off its output — do not decide from memory: + +```bash +if [ -d .specify/extensions/critique ] || grep -q '"critique"' .specify/extensions/.registry 2>/dev/null; then + echo "critique extension: installed" +else + echo "critique extension: missing" +fi +``` + +| Check output | `CRITIQUE_MODE` | +|--------------|-----------------| +| `installed` | `extension` | +| `missing`, and you can dispatch clean-context subagents | `fallback` | +| `missing`, and no subagent dispatch is available | `none` | + +Both signals are written by `specify extension add` itself, and neither is harness-specific — do not key the decision off `.claude/skills/speckit-critique-run/`, which only exists under Claude Code. Resolve this by **checking the filesystem**, not by asking yourself whether you "have" the skill. A missing critique provider never blocks the run; it selects the branch below and it is reported on the status line. + +**3a — `CRITIQUE_MODE: extension`.** Invoke the `speckit-critique-run` skill. If that invocation fails — the skill is not found, or it errors out without returning findings — the extension is present on disk but not usable. Downgrade to `CRITIQUE_MODE: fallback` and run 3b, or to `none` if no subagent dispatch is available, and report the mode that actually ran. + +**3b — `CRITIQUE_MODE: fallback`.** Dispatch **one** clean-context subagent to run the same dual-lens critique. Brief it self-contained: absolute paths to `spec.md`, `plan.md`, and the resolved project context files, plus the required return shape — findings tagged 🎯 Must-Address, 💡 Recommendation, or 🤔 Question, and a verdict of `PROCEED` or `RETHINK`. Findings only; the subagent must not edit the spec or plan. Reduced depth compared with the extension, so flag it in Completion rather than presenting it as an equivalent pass. + +**3c — `CRITIQUE_MODE: none`.** Skip the critique and proceed to Phase 4 with no findings. Never describe the run as critiqued, and do not substitute an unstructured reread of `spec.md` for the critique. + +**Finding handling is identical in 3a and 3b:** -- Run the dual-lens (Product + Engineering) critique against `spec.md` and `plan.md` before any tasks are generated. - For any 🎯 **Must-Address** findings, apply the suggested fixes to `spec.md` / `plan.md` autonomously and commit them — do not pause for user approval. - For 💡 Recommendations, apply them when the fix is clear and low-risk; otherwise note and move on. - 🤔 Questions: resolve with your best judgment based on context (same rule as the Specify phase). @@ -67,7 +92,7 @@ Inspect `$ARGUMENTS`. The check is **only meaningful when the user provided a su 1. **`$ARGUMENTS` contains one or more URLs** (links to a Notion / Confluence / Google Doc / GitHub issue / Linear ticket / shared Markdown / PDF). Treat each URL as a likely PRD location. **Run the check.** 2. **`$ARGUMENTS` itself looks like a detailed PRD** — multiple paragraphs with structure such as headings, bullet lists of requirements, explicit goals/non-goals, acceptance criteria, or > ~400 characters of substantive description. **Run the check.** -3. **`$ARGUMENTS` is a one-line description, vague brief, or empty.** **Skip the check** — there is no source-of-truth document to align against. Write a short note in your final summary stating you skipped this phase and why, and proceed to Completion. +3. **`$ARGUMENTS` is a one-line description, vague brief, or empty.** **Skip the check** — there is no source-of-truth document to align against. Write a short note in your final summary stating you skipped this phase and why, and proceed to Completion. If the input is short (under ~400 characters) but still carries substantive, requirement-bearing detail, use your judgment and treat it as a PRD under rule 2 rather than skipping. #### 5b. Resolve the source-of-truth PRD @@ -133,17 +158,20 @@ After all five phases are complete, provide a brief summary: - Feature name and spec directory - Number of tasks generated in `tasks.md` -- Any critique findings that were addressed inline +- Any critique findings that were addressed inline, and the `CRITIQUE_MODE` used. State a `fallback` or `none` mode plainly, with the install hint `specify extension add critique` - **Alignment check status** — verdict, source PRD reference, number of remediation passes used (if any), and any unresolved drift - Any notable decisions you made autonomously **Machine-readable status line (REQUIRED).** The **final line** of your output MUST be exactly: -`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | REASON: <short reason or n/a>` +`STATUS: <READY|BLOCKED> | SPEC_DIR: <absolute spec-dir path> | CRITIQUE: <extension|fallback|none|n/a> | REASON: <short reason or n/a>` - `STATUS: READY` — `tasks.md` was generated **and** the alignment outcome is `✅ ALIGNED`, `⚠️ MINOR DRIFT`, or `⚠️ SKIPPED`. Only in this state is the spec safe to implement. - `STATUS: BLOCKED` — alignment is `🛑 UNRESOLVED` after the retry budget, or any phase could not complete. This is the explicit failure signal the parent `speckit-opsmill-auto` checks before deciding whether to start implementation; do not dress an unresolved run up as a success. - `SPEC_DIR` MUST be the absolute path to the spec directory, so the parent can hand it to the implement phase without parsing prose. +- `CRITIQUE` mirrors the `CRITIQUE_MODE` resolved in Phase 3, so the parent never has to infer critique coverage from prose. A degraded mode does **not** make the run `BLOCKED` — the spec is still implementable — but it MUST be reported. Use `CRITIQUE: n/a` only when the run aborted before Phase 3. + +**Why `CRITIQUE: none` reports but does not block, while `REVIEW: none` forces `INCOMPLETE` in `speckit-opsmill-implement`.** The two gates fail differently. A missing critique leaves the spec un-pressure-tested, but `spec.md` and `tasks.md` are read by a human before any code is written, so the gap is visible at the next step and closing it costs a re-run of prep. A missing review leaves already-written code unexamined, and the implement report is the only thing that speaks to whether that code is sound — nothing downstream re-checks it, so a `DONE` there would assert an assurance that was never produced. Prep therefore degrades and reports; `implement` degrades and marks the run `INCOMPLETE`. Keep this as the literal last line, unwrapped. diff --git a/.specify/extensions/opsmill/commands/qa.md b/.specify/extensions/opsmill/commands/qa.md new file mode 100644 index 000000000..811814c2c --- /dev/null +++ b/.specify/extensions/opsmill/commands/qa.md @@ -0,0 +1,369 @@ +--- +description: Produce a manual QA checklist that walks a human tester through verifying the just-implemented feature, written next to spec.md / plan.md in the active feature directory. +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). + +Supported argument: `--force` to overwrite an existing `qa-checklist.md` +without prompting. Any other free-form text is treated as additional +scope guidance (for example: `focus on the CLI surface`, or +`skip the GraphQL queries`) and should bias the checklist accordingly. + +## Goal + +After the implementation phase of a spec-kit feature, produce a single +markdown document at `FEATURE_DIR/qa-checklist.md` that a human can +follow, step-by-step, to verify the feature works. The audience is a +teammate (or future-you) running the feature **the way a customer +would** — through the same surface customers touch (CLI, running +service, UI, public SDK, docs page). They need concrete, +copy-pasteable instructions — not a description of what the feature +does. + +The checklist is **manual and customer-facing**. It does not run the +automated test suite, does not assert on internal state, and does not +include unit-test or integration-test invocations. Those belong in +`tasks.md`, CI, and `git log`. Snippets that import the project's +public, documented SDK or library are fine — they *are* the customer +surface for SDK-shaped features. + +## Customer Frame + +Test the feature **the way a customer would actually use it.** The +right surface depends on what kind of product the feature ships into: + +| Product type | Customer surface | +|---|---| +| CLI tool / binary | The shipped command, its flags, its stdout/stderr/exit code | +| Service (SaaS / self-hosted) | Config files an operator edits, the running service's HTTP / GraphQL / gRPC API, UI flows, the logs and metrics it surfaces | +| Library / SDK | The public, documented import path — code a customer would write against the exported API | +| Code generator / template | The generated output, run from the user-facing entry point | +| Docs / examples | The rendered page or the runnable snippet | + +**Writing code is fine — even necessary — when the customer is a +developer using a library or SDK.** A snippet that imports the +published top-level package and calls its public methods is exactly +the experience you ship. What is not fine is reaching past the public +API into internal modules, private helpers, test fixtures, or config +classes that the project does not document as part of its surface. + +A useful test before emitting a step: *would I be comfortable +publishing this snippet verbatim in the feature's README, docs, or +release notes?* If yes, it is customer-shaped. If publishing it would +feel wrong because it touches internals, the step is implementation +testing, not QA. + +## Operating Constraints + +**STRICTLY ADDITIVE.** Do not modify `spec.md`, `plan.md`, `tasks.md`, +`research.md`, `data-model.md`, `contracts/`, or any source files. The +only file this command writes is `FEATURE_DIR/qa-checklist.md`. + +**CUSTOMER SURFACES, NOT IMPLEMENTATION.** Every step must exercise +the feature through something a customer would publicly use. Decide +which product type the feature ships into (table above), then pick the +matching surface. Acceptable: + +- A shipped CLI subcommand or binary (with the exact flags). +- A public HTTP / GraphQL / gRPC request against a *running* service + (exact URL, method, body, expected response shape). +- A UI page or flow (with the exact navigation path and visible cues). +- A configuration file the operator edits, plus the user-visible + signals that follow (logs from the running service, metrics, a + status endpoint, observable behavior change in a public API). +- The public, documented import path of a library / SDK — using the + same package and symbols the docs tell customers to use. +- A rendered docs page or a documented, runnable example. + +Not customer surfaces: + +- Private / internal modules, classes, helpers, validators, or + fixtures that the project does not document as user-facing. +- `pytest` / `vitest` / `cargo test` wrappers presented as scenarios — + CI runs those. +- Source-tree inspection ("open the file and confirm X is exported") + — that is code review, not QA. + +**Two worked examples.** + +*Example A — a config flag on a self-hosted service `myapp`.* Customer +surface = config file + running service. Not customer-shaped: + +```bash +# Touches Settings class — not in the published surface. +python -c "from myapp.config import Settings; print(Settings().new_flag)" +``` + +Customer-shaped: + +1. Set `new_flag = true` in `myapp.toml`. +2. Restart: `docker compose restart myapp`. +3. Hit a public endpoint that reflects the flag: + `curl -s localhost:8080/api/status | jq .new_flag` → `true`. + +*Example B — a new `messages.batch.create()` method on a published +SDK.* Customer surface = the public import path. Customer-shaped: + +```python +# The exact snippet a customer would copy from the docs. +from acme_sdk import Acme +client = Acme(api_key="...") +res = client.messages.batch.create(requests=[...]) +print(res.id, res.status) +``` + +Both are valid QA. The difference is not "code vs. no code" — it is +whether the snippet uses a published, supported surface. + +If the feature truly has **no** customer surface (pure refactor, +internal helper that is not re-exported, generator that produces no +visible change), say so in **Scope** and ship a minimal smoke +checklist (build still succeeds, nearest user-visible flow still +works, docs still render). Do not reach for internals to fill space. + +**ONE FILE, IDEMPOTENT.** Re-running in the same feature directory +should produce a comparable checklist, not a diff of checklists. If +`qa-checklist.md` already exists, ask before overwriting unless +`--force` was passed. + +## Outline + +1. **Resolve the feature directory.** Run `{SCRIPT}` from repo root and + parse `FEATURE_DIR` from the JSON output. All paths must be + absolute. + + - If `FEATURE_DIR` is missing, empty, or the resolved directory does + not exist, **fail fast** with: + `No feature directory resolved from current branch — /speckit.opsmill.qa requires an active feature branch.` + Suggest the user switch to a feature branch and re-run. + +2. **Check for an existing checklist.** + + - If `FEATURE_DIR/qa-checklist.md` exists and `--force` was not + passed, print the existing path and ask the user whether to + overwrite, append a timestamped variant, or abort. Default is + abort. + - If `--force` was passed, overwrite without prompting but mention + it in the final report. + +3. **Gather context**, in this order of preference: + + a. `FEATURE_DIR/spec.md` — primary source for user-visible behavior, + acceptance criteria, and scope. + b. `FEATURE_DIR/plan.md` — entry points, surfaces touched (CLI, + GraphQL, REST, UI, generators). + c. `FEATURE_DIR/tasks.md` — what was actually built (use this to + ensure the checklist matches reality, not the original spec). + d. `FEATURE_DIR/quickstart.md` if present — re-use any setup steps + verbatim rather than re-inventing them. + e. `FEATURE_DIR/contracts/` if present — to identify exact endpoint + names, mutation names, query names to exercise. + f. The live conversation context for any clarifications or scope + shifts that happened during implementation but did not make it + back into `spec.md`. + + Do **not** read large swaths of source code. Do **not** generate + diffs. The checklist is anchored to user-visible surfaces, not + internals. + +4. **Identify the customer surfaces to exercise.** From the gathered + context, list the customer-facing entry points the feature touches. + Common surfaces include: + + - CLI commands (with the exact invocation). + - HTTP / GraphQL / gRPC endpoints (with method + path or operation + name). + - UI pages, modals, or flows (with the exact navigation path). + - Public SDK / library import paths — the exact top-level symbols a + customer would `import` and the calls they would write. + - Configuration knobs the operator sets (file or env var) and the + observable signal that follows (log line, metric, status + endpoint, API response). + - Generated outputs and rendered docs pages. + + If the feature is purely internal (refactor, private helper that is + not re-exported, generator with no visible change) and has **no** + customer surface, the checklist must say so explicitly in the + **Scope** section and contain at most a smoke-test scenario (the + nearest user-visible flow still works, build still succeeds, docs + still render). + +5. **Compose the checklist.** Use exactly this structure: + + ````markdown + # QA Checklist — <feature title from spec.md> + + **Generated**: <YYYY-MM-DD HH:MM local> + **Feature**: <FEATURE_DIR relative path> + **Source**: speckit.opsmill.qa + + ## Scope + + <One short paragraph: what the tester is verifying, and what is + intentionally out of scope. Lift terminology from spec.md.> + + ## Prerequisites + + <Concrete environment requirements as a bullet list. Pull from + plan.md / quickstart.md if available. Examples:> + + - [ ] Working copy is on branch `<branch-name>` with the + implementation merged or rebased. + - [ ] `uv sync --all-groups --all-extras` (or the project's + install command) has completed cleanly. + - [ ] Services running: <e.g., `docker compose up infrahub`>. + - [ ] Environment variables set: <list with example values>. + + ## Setup + + <Numbered commands to bring the system to a known-good starting + state. Each command on its own line in a fenced block. Reference + files by absolute path or by the repo-root-relative path the user + would actually type.> + + <**Prefer the project's documented dev-environment entry points** — + `invoke <task>`, `just <task>`, `make <target>`, `task <name>`, + `npm run <script>`, or whatever this project ships — over raw + `docker compose` / `kubectl` / service binaries. Discover them + from project conventions in this order:> + + - <`AGENTS.md`, `CLAUDE.md`, `README.md`, `CONTRIBUTING.md` at + repo root for an explicit "run locally" / "dev setup" section.> + - <Project memory: feedback memories about the dev environment.> + - <Task runner manifests: `tasks/` or `tasks.py` (invoke), + `justfile`, `Makefile`, `Taskfile.yml`, `package.json` scripts.> + - <The feature's own `quickstart.md` / `plan.md` if it names + specific commands.> + + <Fall back to raw `docker compose` / `kubectl` only if no project + wrapper covers the scenario.> + + ```bash + <command 1> + <command 2> + ``` + + ## Test Scenarios + + <One H3 per scenario. **2–5 scenarios** is the sweet spot — each + one must exercise a *different* user-visible surface or a + *different* acceptance criterion. If you find yourself writing + variants of the same step against the same surface, collapse them + into one scenario and move the variants to **Edge Cases**. Inside + each scenario:> + + ### 1. <Scenario name — what the tester is checking> + + **What this verifies**: <One sentence, in user terms.> + + **Steps**: + + - [ ] Run: `<exact command, or click path, or HTTP call>` + - [ ] Observe: <exact output, screen state, or response shape> + - [ ] Then: `<next step>` + - [ ] Confirm: <final assertion in user-visible terms> + + **Expected result**: <One or two sentences describing what success + looks like. Be specific — exit code, status code, field value, + visible UI element.> + + ### 2. <next scenario> + + ... + + ## Edge Cases + + <Bullet checklist for boundary conditions worth a manual poke. Each + item is a one-liner with the input and the expected behavior. + Examples:> + + - [ ] Empty input: `<command with --foo ""` → expect <behavior>. + - [ ] Duplicate name: re-running creates no second copy; the + existing record is updated. + - [ ] Permission denied: as a read-only user, the mutation returns + <error code> with message containing `<substring>`. + + ## Teardown + + <Commands to return the system to its pre-test state. If none are + needed, say so explicitly: "No teardown required — all changes are + scoped to <ephemeral location>." Otherwise:> + + ```bash + <cleanup commands> + ``` + + ## Sign-off + + - [ ] All scenarios above pass. + - [ ] No unexpected output, warnings, or errors observed. + - [ ] Tester: ______________________ Date: __________ + ```` + +6. **Write the file.** + + - Path: `FEATURE_DIR/qa-checklist.md`. + - If the user chose "append a timestamped variant" in step 2, use + `FEATURE_DIR/qa-checklist-YYYY-MM-DD-HHMM.md` instead. + - Do not create any other files. Do not create a `tests/` + subdirectory. + +7. **Print the resolved path** as the final message, plus a one-line + reminder that this checklist is intentionally manual / user-facing + and does not replace the automated test suite. + +## Quality Bar + +A teammate who has never seen the feature should, after under 10 +minutes with this checklist, be able to: + +- Stand up whatever a customer would stand up (the service, an SDK + install, a CLI binary, a docs preview). +- Exercise the feature exactly as a customer would — through its + shipped, documented surface. +- Decide for themselves whether the feature behaves as `spec.md` + promises. + +If any checklist item reaches into private modules, internal helpers, +test fixtures, or symbols that do not appear in the public docs / SDK +reference, rewrite it to use the customer surface or drop it. +**Hard cap: 150 lines of markdown total.** Aim for ~100. If you go +over, the answer is fewer scenarios, not denser ones. + +## Behavior Rules + +- **Never modify `spec.md`, `plan.md`, `tasks.md`, or source files.** + The checklist is purely additive. +- **Public-API code is fine; internal symbols are not.** A snippet + that imports the project's published top-level package and calls + documented public methods is a valid QA step — that is the customer + surface for SDK-shaped features. Steps that reach past the public + API into internal modules, private packages (`_foo`, `.internal`), + undocumented config classes, or test fixtures are not. See + "Customer Frame" and "Customer surfaces, not implementation" in + Operating Constraints. +- If no customer surface exists, say so plainly in **Scope** and ship + a minimal smoke-test checklist rather than fabricating scenarios by + reaching into internals. +- Prefer commands and inputs that already appear in `quickstart.md` or + `tasks.md` over inventing new ones — the tester should not need to + reverse-engineer setup. +- **Use the project's documented dev-environment wrappers** (`invoke`, + `just`, `make`, `task`, `npm run`, etc.) over raw `docker compose` / + `kubectl` / service binaries. Discover them from `AGENTS.md`, + `CLAUDE.md`, README, project memory, or the task-runner manifests + before defaulting to raw container commands. +- Do not include automated test invocations (`pytest`, `vitest`, + `cargo test`, etc.) as scenarios. Those are out of scope for this + command. +- For single quotes in bash args, use escape syntax: e.g. + `'I'\''m Groot'` (or double-quote if possible: `"I'm Groot"`). diff --git a/.specify/extensions/opsmill/commands/summary.md b/.specify/extensions/opsmill/commands/summary.md index 5368b3490..3ae74acf1 100644 --- a/.specify/extensions/opsmill/commands/summary.md +++ b/.specify/extensions/opsmill/commands/summary.md @@ -1,8 +1,8 @@ --- description: Produce a flow-level summary of the current Claude Code session — executive summary, chronological timeline, and outcomes — written into the active feature directory next to spec.md / plan.md. scripts: - sh: scripts/bash/check-prerequisites.sh --json --paths-only - ps: scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly + sh: ../../scripts/bash/check-prerequisites.sh --json --paths-only + ps: ../../scripts/powershell/check-prerequisites.ps1 -Json -PathsOnly --- ## User Input diff --git a/.specify/extensions/opsmill/extension.yml b/.specify/extensions/opsmill/extension.yml index 650b19191..6c0fb7bec 100644 --- a/.specify/extensions/opsmill/extension.yml +++ b/.specify/extensions/opsmill/extension.yml @@ -3,8 +3,8 @@ schema_version: "1.0" extension: id: "opsmill" name: "OpsMill Speckit Workflow" - version: "1.1.0" - description: "OpsMill house spec-kit commands: autonomous end-to-end workflow (auto/prep/implement), knowledge extraction, session retrospective, and session summary." + version: "1.4.1" + description: "OpsMill house spec-kit commands: autonomous end-to-end workflow (auto/prep/implement), knowledge extraction, session retrospective, session summary, and QA checklist." author: "opsmill" repository: "https://github.com/opsmill/opsmill-speckit" license: "Apache-2.0" @@ -26,6 +26,9 @@ provides: - name: "speckit.opsmill.summary" file: "commands/summary.md" description: "Produce a flow-level timeline of the current Claude Code session in the active feature directory." + - name: "speckit.opsmill.qa" + file: "commands/qa.md" + description: "Produce a manual QA checklist next to spec.md / plan.md that walks a human tester through verifying the implemented feature." - name: "speckit.opsmill.auto" file: "commands/auto.md" description: "Run the speckit workflow end-to-end (prep + implement) autonomously. Stops before extract; run that manually." @@ -48,6 +51,7 @@ tags: - "extract" - "retrospect" - "summary" + - "qa" - "workflow" - "auto" - "prep" diff --git a/.specify/extensions/opsmill/presets/CHANGELOG.md b/.specify/extensions/opsmill/presets/CHANGELOG.md deleted file mode 100644 index 1c498b905..000000000 --- a/.specify/extensions/opsmill/presets/CHANGELOG.md +++ /dev/null @@ -1,46 +0,0 @@ -# Changelog — preset collection - -Release history for the `presets/` directory, taken as a unit. - -The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this collection adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.0.0] - 2026-05-20 - -### Added -- **`taskstoissues-jira`** (preset version `1.0.0`) — initial release of - the collection's first preset. Spec-kit preset - (`schema_version: "1.0"`, `requires.speckit_version: ">=0.8.0"`) that - declares one command override under `provides.templates`: - `type: command`, `name: speckit.taskstoissues`, - `file: commands/speckit.taskstoissues.md`, - `replaces: speckit.taskstoissues` (default `replace` strategy). - Installable independently via: - - ```bash - specify preset add taskstoissues-jira \ - --from https://github.com/opsmill/opsmill-speckit/archive/refs/heads/main.zip \ - --subdir presets/taskstoissues-jira - ``` - - Overrides the native `/speckit.taskstoissues` to fan `tasks.md` out - into Jira issues under a single Epic — one issue per `## Phase N:` - block, with `Blocks` links derived from `T<NNN>` mentions - (transitively reduced). Talks to Atlassian through the Atlassian MCP. - Project config lives at `dev/jira.yml` in the consumer repo; the - assignee is the user authenticated to the Atlassian MCP (resolved via - `atlassianUserInfo`). See - [`taskstoissues-jira/README.md`](taskstoissues-jira/README.md) for the - full configuration model and behavior. - -### Provenance -`taskstoissues-jira` is ported from the Infrahub preset in -[opsmill/infrahub#9208](https://github.com/opsmill/infrahub/pull/9208). -Generalized for cross-repo reuse: -- Project key + Epic key regex driven by `default_project_key` from - config rather than hardcoded `IFC`. -- Custom field IDs reduced to placeholders (`customfield_XXXXX`); - operator resolves real IDs via `getJiraIssueTypeMetaWithFields`. -- Preset id renamed from `infrahub` to `taskstoissues-jira` so the - install path reads as a portable Jira-flavored override of - `speckit.taskstoissues` rather than a single-product preset. diff --git a/.specify/extensions/opsmill/presets/taskstoissues-jira/README.md b/.specify/extensions/opsmill/presets/taskstoissues-jira/README.md deleted file mode 100644 index ebc181e06..000000000 --- a/.specify/extensions/opsmill/presets/taskstoissues-jira/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# taskstoissues-jira - -A spec-kit preset that **overrides** the native `speckit.taskstoissues` command with a Jira-flavored implementation. One Jira issue is created per `## Phase N:` block in `tasks.md` (not per task line); inter-task `T<NNN>` mentions are resolved at phase granularity and emitted as `Blocks` issue links between phase issues (transitively reduced). All Atlassian traffic goes through the Atlassian MCP. - -## Install - -```bash -specify preset add taskstoissues-jira \ - --from https://github.com/opsmill/opsmill-speckit/archive/refs/heads/main.zip \ - --subdir presets/taskstoissues-jira -``` - -Local development install (from a working tree): - -```bash -specify preset add --dev ./presets/taskstoissues-jira -``` - -After install, the preset's files live at `.specify/presets/taskstoissues-jira/` in the consumer repo, and `/speckit.taskstoissues` resolves to this preset's command body. - -## Configuration model - -One file: `dev/jira.yml` at the consumer repo root, committed. It holds every Jira parameter the preset reads — `cloud`, `default_project_key`, `default_issue_type`, `custom_fields.*`, `team.name` / `team.id`, `labels_default`. The assignee for every created issue is the user authenticated to the Atlassian MCP, resolved via `atlassianUserInfo`. - -## One-time setup (per consumer repo) - -1. Copy the project template into place: - - ```bash - mkdir -p dev - cp .specify/presets/taskstoissues-jira/templates/jira.example.yml dev/jira.yml - ``` - -2. Edit `dev/jira.yml` and fill in every REQUIRED field: - - - `cloud` — your Atlassian site URL (e.g. `https://opsmill.atlassian.net/`). Matched against `getAccessibleAtlassianResources` to resolve `cloudId`. The shipped example points at `https://opsmill.atlassian.net/`; replace if your tenant differs. - - `default_project_key` — your repo's Jira project key. The shipped placeholder `PROJ` aborts on purpose. - - `default_issue_type` — issue type for created phase issues (e.g. `Task`, `Story`). - - `custom_fields.epic_link` + `custom_fields.team` — real custom field IDs for your Jira instance. Resolve with `mcp__claude_ai_Atlassian__getJiraIssueTypeMetaWithFields` and replace each `customfield_XXXXX` placeholder. - - `team.name` — the Atlassian Team for every created issue. Leave `team.id` empty on first run; the command resolves it from `name` and writes the UUID back. Expect a `dev/jira.yml` diff after the first run — commit it. - - `labels_default` — labels stamped on every created issue (e.g. `[spec-kit]`). - -3. Commit `dev/jira.yml`. The whole repo shares it. - -Re-running `specify preset add taskstoissues-jira` only touches `.specify/presets/taskstoissues-jira/`, so `dev/jira.yml` is never clobbered by preset updates. - -## Epic resolution - -The command parses the current branch name for `<default_project_key>-\d+` (case-insensitive), falling back to `$ARGUMENTS`, then to an interactive prompt. The matched key is validated as an Epic via `getJiraIssue`. - -## Failure mode - -The run stops at the first `createJiraIssue` / `createIssueLink` error and prints the partial `phase_number → IssueKey` map. The command is **not** idempotent — delete the listed issues in Jira manually before retrying. - -## Provenance - -Ported from the Infrahub preset introduced in [opsmill/infrahub#9208](https://github.com/opsmill/infrahub/pull/9208). Generalized for cross-repo reuse: the Infrahub-specific project key (`IFC`) and custom field IDs become placeholders consumers fill in via `dev/jira.yml`. diff --git a/.specify/extensions/opsmill/presets/taskstoissues-jira/commands/speckit.taskstoissues.md b/.specify/extensions/opsmill/presets/taskstoissues-jira/commands/speckit.taskstoissues.md deleted file mode 100644 index eea719ef5..000000000 --- a/.specify/extensions/opsmill/presets/taskstoissues-jira/commands/speckit.taskstoissues.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -description: Convert tasks.md into Jira issues under a single Epic (one Jira issue per phase). ---- - - -## User Input - -```text -$ARGUMENTS -``` - -You **MUST** consider the user input before proceeding (if not empty). - -## Pre-Execution Checks - -**Check for extension hooks (before tasks-to-issues conversion)**: -- Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.before_taskstoissues` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Pre-Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Pre-Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - - Wait for the result of the hook command before proceeding to the Outline. - ``` -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently - -## Outline - -1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse `FEATURE_DIR` and `AVAILABLE_DOCS` list. All paths must be absolute. Extract the absolute path to `tasks.md` from `FEATURE_DIR`. 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 project configuration**: - - Read the project-level Jira config from `dev/jira.yml` at the repo root. Required keys: `cloud`, `default_project_key`, `default_issue_type`, `custom_fields.epic_link`, `custom_fields.team`, `team.name`, `labels_default`. Optional: `team.id` (cached UUID; auto-populated on first run), additional `custom_fields.*` entries (e.g. `sprint`, `story_points`) for future use. - - > [!CAUTION] - > If `dev/jira.yml` does not exist, abort with: `> dev/jira.yml not found. Copy .specify/presets/taskstoissues-jira/templates/jira.example.yml to dev/jira.yml at the repo root, fill in every REQUIRED field, and commit before re-running.` - - > [!CAUTION] - > If any required top-level key is missing from `dev/jira.yml`, abort with: `> dev/jira.yml is missing required key '<key>'. See .specify/presets/taskstoissues-jira/templates/jira.example.yml for the full schema.` Do not silently default. - - > [!CAUTION] - > If `default_project_key` still equals the example placeholder `PROJ`, abort with: `> dev/jira.yml still has placeholder default_project_key 'PROJ'. Set it to your repo's Jira project key before re-running.` - - > [!CAUTION] - > If `custom_fields.epic_link` or `custom_fields.team` equals `customfield_XXXXX`, abort with: `> dev/jira.yml still has placeholder custom field IDs. Resolve real IDs with mcp__claude_ai_Atlassian__getJiraIssueTypeMetaWithFields and update the file before re-running.` Do not invent IDs. - -3. **Resolve Atlassian cloud id**: Call `mcp__claude_ai_Atlassian__getAccessibleAtlassianResources` once and match `cloud` from `dev/jira.yml` against the returned site URLs to obtain `cloudId`. - - > [!CAUTION] - > If `cloudId` cannot be resolved, or `getAccessibleAtlassianResources` returns more than one site without a unique match to the configured `cloud`, abort with: `> Multiple Atlassian sites accessible — pin one in dev/jira.yml under 'cloud' before re-running.` - -4. **Resolve the Epic key**: - 1. Run `git rev-parse --abbrev-ref HEAD`. Build a case-insensitive regex from `default_project_key` of the form `<default_project_key>-\d+` and match it against the branch name; uppercase the project-key portion of the match (e.g. with `default_project_key=IFC`, `pmi-ifc-2521-auto-create-groups` → `IFC-2521`). - 2. If the branch yields no match, test `$ARGUMENTS` against the same pattern. - 3. If still no match, prompt: `> Provide the Jira Epic for these tasks (e.g. <default_project_key>-1234):` and wait for input. Validate the input matches the same `<default_project_key>-\d+` pattern (case-insensitive). - 4. Validate the resolved key by calling `mcp__claude_ai_Atlassian__getJiraIssue` and confirming `fields.issuetype.name == "Epic"`. Abort if it is not an Epic. - -5. **Resolve assignee account id**: Call `mcp__claude_ai_Atlassian__atlassianUserInfo` once and read `account_id` from the response. That is the accountId of the user currently authenticated to the Atlassian MCP — the same user running the command — and becomes the assignee for every issue created in this run. Cache it for the duration of this run. - - > [!CAUTION] - > If `atlassianUserInfo` fails or returns no `account_id`, abort with: `> Atlassian MCP not authenticated. Authenticate the Atlassian MCP server before re-running.` Do not create any issues. - -6. **Resolve team UUID**: From `dev/jira.yml` read `team.name` and `team.id`. Jira's Atlassian Teams picker (`custom_fields.team`) only accepts a UUID — `team.name` is kept in `dev/jira.yml` as a human label, never sent to Jira. - - - If `team.id` is set, use it as-is. - - If `team.id` is empty or missing, resolve it once via Atlassian: - 1. Call `mcp__claude_ai_Atlassian__searchJiraIssuesUsingJql` with: - - `cloudId` from step 3 - - `jql = 'project = <default_project_key> AND "Team[Team]" is not EMPTY ORDER BY created DESC'` - - `fields = ["<custom_fields.team>"]` - - `maxResults = 20` - 2. Scan the returned issues' `<custom_fields.team>.name` for an exact case-insensitive match to `team.name` from `dev/jira.yml`. Extract the matching `<custom_fields.team>.id` (the UUID). - 3. If a match is found, persist the UUID back into `dev/jira.yml` at `team.id` so future runs skip this lookup, and use that UUID for issue creation. - 4. If no match is found, abort with: `> Team '<team.name>' not found in Atlassian (searched recent issues in <default_project_key>). Set team.id explicitly in dev/jira.yml before re-running.` Do not create any issues. - - Pass the resolved value to `createJiraIssue` as a **bare UUID string** (e.g. `"<custom_fields.team>": "079e72e1-..."`). The object form `{"id": "<uuid>"}` and the name form (e.g. `"Backend Team"`) are both rejected by Jira's Teams picker. - -7. **Parse `tasks.md` by phase**: Walk the file top-to-bottom and split it into phases, where a phase starts at a `## Phase <N>: <title>` header and ends at the next `## Phase` header (or end of file). For each phase capture: - - `phase_number` (`<N>`) and `phase_title` (`<title>`) from the header line - - `goal_block`: the `**Goal**:` paragraph immediately under the header, if present - - `independent_test_block`: the `**Independent Test**:` paragraph, if present - - `tasks`: every unchecked `- [ ] T<NNN> …` line inside the phase, preserved verbatim including its `[P]` and `[US<N>]` tags and trailing file paths. Skip tasks already checked (`- [x]`). - - `story`: if the phase header matches `User Story <n>` **or** every child task carries the same `[US<n>]` tag, capture `<n>` as the phase's user-story number; otherwise leave unset. - - `files`: the union of affected file paths across the phase's task lines (trailing the summary or on subsequent indented lines), deduplicated and sorted. - - `exit_criteria`: any trailing `**Exit criteria**` / `**Checkpoint**` paragraph in the phase, if present. - - Skip phases that contain zero unchecked tasks. Build an ordered list `phases: [{phase_number, phase_title, story, goal_block, independent_test_block, tasks, files, exit_criteria}]`. - -8. **Create one Jira issue per phase**: For each parsed phase, call `mcp__claude_ai_Atlassian__createJiraIssue` with: - - `cloudId` from step 3. - - `projectKey` from `default_project_key`. - - `issueTypeName` from `default_issue_type`. - - `summary` = `phase_title` from step 7, with spec-kit-internal taxonomy tags stripped: - - `US<N>` tags — already captured as a `US<N>` label below. - - `P<N>` tags — already captured by the `Blocks` links in step 9. - - Bracketed `[…]` decorations carrying the same data (`[P]`, `[US<N>]`). - - **Keep** meaningful tier markers (`MVP`, `Beta`, `Alpha`, etc.) and the descriptive feature text. Drop surrounding parentheses if their only remaining content is itself dropped (e.g. `(P1 MVP)` → `MVP`, but `(Beta)` stays as `(Beta)`). - - Examples: - - `Phase 3: US1 (P1 MVP) — Auto-create groups` → `"MVP — Auto-create groups"` - - `Phase 1: US2 — Bulk import` → `"Bulk import"` - - `Phase 2: (Beta) Pagination on lists` → `"(Beta) Pagination on lists"` - - `description` composed in this order: - 1. The `goal_block` (if any). - 2. The `independent_test_block` (if any). - 3. A `## Tasks` section containing the phase's task lines as a markdown checklist — each entry is the verbatim `- [ ] T<NNN> …` line. - 4. A `## Files` section listing each affected path from `files` as a bullet. - 5. A `## Exit criteria` section reproducing `exit_criteria` (if any). - 6. A trailing line `_Source:_ <relative path from repo root to tasks.md>`. - - `additional_fields`: - - `assignee`: `{ accountId: <resolved accountId> }` - - `labels`: `labels_default` (from `dev/jira.yml`) + `US<story>` if the phase has a `story` value - - `custom_fields`: - - `<custom_fields.epic_link>`: `<Epic key from step 4>` (e.g. `customfield_10014: "IFC-2521"`) - - `<custom_fields.team>`: the bare UUID string resolved in step 6 (e.g. `customfield_10001: "079e72e1-..."`). Never send the object form `{"id": "<uuid>"}` and never send `team.name`. - - Record `phase_number -> issueKey` in an in-memory map. - - > [!CAUTION] - > UNDER NO CIRCUMSTANCES CREATE ISSUES IN A PROJECT OTHER THAN `default_project_key` FROM `dev/jira.yml`. - - > [!CAUTION] - > If any `createJiraIssue` call fails mid-run, **stop immediately**. Print the partial `phase_number -> issueKey` map and instruct: `> Partial run — delete the issues listed above manually in Jira before re-running. This skill is not idempotent in v1.` Do not retry, do not roll back automatically. - -9. **Create phase-level dependency links**: After every phase issue exists, derive the phase dependency graph from `T<NNN>` mentions inside each phase's task bodies: - 1. Build a `tid -> phase_number` index from the parse in step 7 — every `T<NNN>` owned by a phase maps to that phase's `phase_number`. - 2. For each phase `P`, scan its task bodies for `T<NNN>` mentions whose owning phase is **not** `P` itself. For each such mention, emit a directed edge `(phase_of_mentioned_tid) -> P` (the phase that owns the cited task blocks `P`). - 3. Deduplicate edges, then apply a transitive reduction so only direct edges remain. If the graph has `A -> B`, `B -> C`, and `A -> C`, drop `A -> C`. - 4. For each surviving edge `(blocker_phase) -> (blocked_phase)`, call `mcp__claude_ai_Atlassian__createIssueLink` with: - - `type` = `"Blocks"` - - `inwardIssue` = the blocker phase's Jira key (from the `phase_number -> issueKey` map) - - `outwardIssue` = the blocked phase's Jira key - - Phase headers and `[P]` markers remain sequencing hints only — they are **not** first-class dependencies and must not produce link edges. The link source is exclusively `T<NNN>` mentions. - -10. **Summary output**: Print a markdown table mapping `Phase` → `IssueKey` → `Summary`. Do not edit `tasks.md` automatically; the user can paste the mapping back if they want. - -## Post-Execution Checks - -**Check for extension hooks (after tasks-to-issues conversion)**: -Check if `.specify/extensions.yml` exists in the project root. -- If it exists, read it and look for entries under the `hooks.after_taskstoissues` key -- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally -- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. -- For each remaining hook, do **not** attempt to interpret or evaluate hook `condition` expressions: - - If the hook has no `condition` field, or it is null/empty, treat the hook as executable - - If the hook defines a non-empty `condition`, skip the hook and leave condition evaluation to the HookExecutor implementation -- When constructing slash commands from hook command names, replace dots (`.`) with hyphens (`-`). For example, `speckit.git.commit` → `/speckit-git-commit`. -- For each executable hook, output the following based on its `optional` flag: - - **Optional hook** (`optional: true`): - ``` - ## Extension Hooks - - **Optional Hook**: {extension} - Command: `/{command}` - Description: {description} - - Prompt: {prompt} - To execute: `/{command}` - ``` - - **Mandatory hook** (`optional: false`): - ``` - ## Extension Hooks - - **Automatic Hook**: {extension} - Executing: `/{command}` - EXECUTE_COMMAND: {command} - ``` -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.specify/extensions/opsmill/presets/taskstoissues-jira/preset.yml b/.specify/extensions/opsmill/presets/taskstoissues-jira/preset.yml deleted file mode 100644 index 1a1417efb..000000000 --- a/.specify/extensions/opsmill/presets/taskstoissues-jira/preset.yml +++ /dev/null @@ -1,28 +0,0 @@ -schema_version: "1.0" - -preset: - id: "taskstoissues-jira" - name: "Tasks-to-Issues (Jira)" - version: "1.0.0" - description: "Override speckit.taskstoissues to fan tasks.md out into Jira issues under a single Epic (one issue per phase) via the Atlassian MCP." - author: "opsmill" - repository: "https://github.com/opsmill/opsmill-speckit" - license: "Apache-2.0" - -requires: - speckit_version: ">=0.8.0" - -provides: - templates: - - type: "command" - name: "speckit.taskstoissues" - file: "commands/speckit.taskstoissues.md" - description: "Convert tasks.md into Jira issues under a single Epic via the Atlassian MCP." - replaces: "speckit.taskstoissues" - -tags: - - "taskstoissues" - - "jira" - - "atlassian" - - "workflow" - - "opsmill" diff --git a/.specify/extensions/opsmill/presets/taskstoissues-jira/templates/jira.example.yml b/.specify/extensions/opsmill/presets/taskstoissues-jira/templates/jira.example.yml deleted file mode 100644 index 7aa9cc9a6..000000000 --- a/.specify/extensions/opsmill/presets/taskstoissues-jira/templates/jira.example.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Project-level Jira configuration for this repo. -# -# Copy this file to `dev/jira.yml` at your repo root, fill in real values, -# and commit. Holds every Jira parameter the taskstoissues-jira preset -# reads. The assignee for each created issue is the user authenticated -# to the Atlassian MCP (resolved via atlassianUserInfo). - -# REQUIRED — Atlassian site URL. Matched against -# getAccessibleAtlassianResources to resolve cloudId. -cloud: https://opsmill.atlassian.net/ - -# REQUIRED — Jira project key new issues are created in (e.g. IFC, INFP). -default_project_key: PROJ - -# REQUIRED — issue type for created phase issues. -default_issue_type: Task - -# REQUIRED — custom field IDs for your Jira instance. Retrieve with -# mcp__claude_ai_Atlassian__getJiraIssueTypeMetaWithFields and paste below. -custom_fields: - epic_link: customfield_XXXXX - team: customfield_XXXXX - # Optional / reserved for future use: - # sprint: customfield_XXXXX - # story_points: customfield_XXXXX - -# REQUIRED — Atlassian Team assigned to every created issue. `name` is the -# human label; `id` is the bare UUID Jira's Teams picker requires. Leave -# `id` empty on first run and the command will resolve it from `name` and -# write it back here. -team: - name: "Core" - id: "" - -# REQUIRED — labels applied to every created issue. -labels_default: - - spec-kit diff --git a/.specify/extensions/opsmill/scripts/bash/python-env.sh b/.specify/extensions/opsmill/scripts/bash/python-env.sh new file mode 100755 index 000000000..8ed888186 --- /dev/null +++ b/.specify/extensions/opsmill/scripts/bash/python-env.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Claude Code session start: points the Spec Kit scripts at the project's Python environment for the +# rest of the session, when that environment can import PyYAML. Does nothing otherwise. +# The Spec Kit scripts call python3 from the PATH unless these variables name another interpreter. + +[ -n "${CLAUDE_ENV_FILE:-}" ] || exit 0 +cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0 + +# Read the environment path the way uv does, because running uv here would create an empty +# environment when none exists. +venv="${UV_PROJECT_ENVIRONMENT:-.venv}" +case "$venv" in + /*) ;; + *) venv="$PWD/$venv" ;; +esac +python="$venv/bin/python" + +# A linked worktree might have no environment of its own, so the main checkout's is used instead. +# The Spec Kit scripts only need PyYAML from it. +if ! "$python" -c 'import yaml' >/dev/null 2>&1; then + common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || exit 0 + case "$common" in + */.git) python="${common%/.git}/${UV_PROJECT_ENVIRONMENT:-.venv}/bin/python" ;; + *) exit 0 ;; + esac + "$python" -c 'import yaml' >/dev/null 2>&1 || exit 0 +fi + +# The file is sourced as shell code, so the path is single-quoted to keep spaces, quotes and $ literal. +quoted="'$(printf '%s' "$python" | sed "s/'/'\\\\''/g")'" +{ + echo "export SPECKIT_PYTHON_EXECUTABLE=$quoted" + echo "export SPECKIT_PYTHON=$quoted" +} >> "$CLAUDE_ENV_FILE" diff --git a/.specify/extensions/opsmill/scripts/bash/python-run.sh b/.specify/extensions/opsmill/scripts/bash/python-run.sh new file mode 100755 index 000000000..c70b616db --- /dev/null +++ b/.specify/extensions/opsmill/scripts/bash/python-run.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Runs one shell command with the Spec Kit scripts pointed at the project's Python environment. +# The Codex hook (route-codex.sh) wraps the Spec Kit scripts that need PyYAML in this script. +set -eu + +if [ "$#" -ne 1 ]; then + echo "Usage: python-run.sh <shell command>" >&2 + exit 2 +fi + +# This script is installed at .specify/extensions/opsmill/scripts/bash/. +repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/../../../../.." && pwd) +venv=${UV_PROJECT_ENVIRONMENT:-.venv} +case "$venv" in + /*) ;; + *) venv="$repo_root/$venv" ;; +esac +# A linked worktree might have no environment of its own, so the main checkout's is tried last. +main_venv= +common=$(git -C "$repo_root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || common= +case "$common" in + */.git) main_venv="${common%/.git}/${UV_PROJECT_ENVIRONMENT:-.venv}" ;; +esac + +# Without an interpreter that has PyYAML, the command runs unchanged and Spec Kit falls back to python3 from the PATH. +for python in "${SPECKIT_PYTHON_EXECUTABLE:-}" "${SPECKIT_PYTHON:-}" "$venv/bin/python" "${main_venv:+$main_venv/bin/python}"; do + case "$python" in + "") continue ;; + */*) ;; + *) python=$(command -v "$python") || continue ;; + esac + # The command can change directory, so a relative path is made absolute. + case "$python" in + /*) ;; + *) python="$PWD/$python" ;; + esac + if "$python" -c 'import yaml' >/dev/null 2>&1; then + export SPECKIT_PYTHON_EXECUTABLE="$python" + export SPECKIT_PYTHON="$python" + break + fi +done +exec sh -c "$1" diff --git a/.specify/extensions/opsmill/scripts/bash/route-codex.sh b/.specify/extensions/opsmill/scripts/bash/route-codex.sh new file mode 100755 index 000000000..842151eaa --- /dev/null +++ b/.specify/extensions/opsmill/scripts/bash/route-codex.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# Codex PreToolUse hook entry point. See ../python/route_codex.py. +exec python3 "$(dirname -- "$0")/../python/route_codex.py" diff --git a/.specify/extensions/opsmill/scripts/python/route_codex.py b/.specify/extensions/opsmill/scripts/python/route_codex.py new file mode 100644 index 000000000..9cf30d207 --- /dev/null +++ b/.specify/extensions/opsmill/scripts/python/route_codex.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Codex PreToolUse hook: run the Spec Kit scripts that need PyYAML through python-run.sh. + +Codex has no session environment file, so this hook rewrites each such call to run through +python-run.sh, which points the Spec Kit scripts at the project's Python environment. +Standard library only: it runs with whatever python3 Codex finds. +""" + +import json +import os +import re +import shlex +import sys +from pathlib import Path + +# This script is installed at .specify/extensions/opsmill/scripts/python/. +REPO_ROOT = Path(__file__).resolve().parents[5] +WRAPPER = Path(__file__).resolve().parent.parent / "bash" / "python-run.sh" +# Codex runs a rewritten command without asking, so only one exact call of a Spec Kit core script +# that reads preset manifests with PyYAML is accepted, with nothing that the shell could read +# differently. +SCRIPT_CALL = re.compile( + r"[ \t]*(?:(?:ba)?sh[ \t]+)?" + r"(?P<quote>['\"]?)(?:(?P<root>/[^'\"$`\\\x00-\x1f\x7f]+)/)?" + r"\.specify/scripts/bash/(?:setup-plan|setup-tasks|resolve-template|create-new-feature)\.sh(?P=quote)" + r"(?:[ \t]+[A-Za-z0-9_./:=@+,-]+)*[ \t]*" +) +# Inside quotes, a checkout path may hold spaces and other characters that the shell reads literally there. +UNQUOTED_ROOT = re.compile(r"/[A-Za-z0-9_./-]+") + + +def is_speckit_script_call(command: str, cwd: str) -> bool: + match = SCRIPT_CALL.fullmatch(command) + if match is None: + return False + if match.group("root") and not match.group("quote") and not UNQUOTED_ROOT.fullmatch(match.group("root")): + return False + root = match.group("root") or cwd + if ".." in root.split("/"): + return False + try: + return Path(os.path.realpath(root)) == REPO_ROOT + except (OSError, ValueError): + return False + + +def main() -> None: + hook_input = json.load(sys.stdin) + tool_input = hook_input.get("tool_input", {}) + command = tool_input.get("command") + if hook_input.get("tool_name") != "Bash" or not isinstance(command, str): + return + if not is_speckit_script_call(command, hook_input.get("cwd") or os.getcwd()): + return + + updated_command = f"{shlex.quote(str(WRAPPER))} {shlex.quote(command)}" + print( + json.dumps( + { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow", + "updatedInput": {**tool_input, "command": updated_command}, + } + } + ) + ) + + +if __name__ == "__main__": + main() diff --git a/.specify/extensions/review/.github/workflows/ci.yml b/.specify/extensions/review/.github/workflows/ci.yml deleted file mode 100644 index d1f7a24c4..000000000 --- a/.specify/extensions/review/.github/workflows/ci.yml +++ /dev/null @@ -1,243 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - validate-manifest: - name: Validate Manifest & Commands - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check required files - run: | - files=("README.md" "CHANGELOG.md" "extension.yml" "LICENSE") - for f in "${files[@]}"; do - if [ -f "$f" ]; then - echo "✅ $f exists" - else - echo "❌ $f missing" - exit 1 - fi - done - - - name: Validate extension.yml - run: | - # Validate YAML syntax - python3 -c "import yaml; yaml.safe_load(open('extension.yml'))" || { - echo "❌ extension.yml has invalid YAML syntax" - exit 1 - } - echo "✅ extension.yml has valid YAML syntax" - - # Deep manifest validation - python3 << 'PYEOF' - import yaml, sys, re, os - - with open('extension.yml') as f: - ext = yaml.safe_load(f) - - errors = [] - - # --- Top-level required fields --- - for field in ['schema_version', 'extension']: - if field not in ext: - errors.append(f"Missing required field: {field}") - - if 'extension' not in ext: - for e in errors: - print(f"❌ {e}") - sys.exit(1) - - meta = ext['extension'] - - for field in ['id', 'name', 'version', 'description', 'author']: - if field not in meta: - errors.append(f"Missing extension.{field}") - - # --- Extension ID format (lowercase, alphanumeric, hyphens) --- - ext_id = meta.get('id', '') - if ext_id and not re.match(r'^[a-z0-9-]+$', ext_id): - errors.append(f"Extension ID '{ext_id}' must match ^[a-z0-9-]+$ (lowercase, alphanumeric, hyphens only)") - if ext_id: - print(f"✅ Extension ID '{ext_id}' has valid format") - - # --- SemVer validation --- - version = meta.get('version', '') - if version and not re.match(r'^\d+\.\d+\.\d+$', version): - errors.append(f"Version '{version}' must follow semantic versioning (X.Y.Z)") - if version: - print(f"✅ Version '{version}' is valid SemVer") - - # --- Description length (under 200 chars) --- - desc = meta.get('description', '') - if len(desc) > 200: - errors.append(f"Description is {len(desc)} characters (must be under 200)") - if desc: - print(f"✅ Description is {len(desc)} characters (under 200 limit)") - - # --- requires.speckit_version --- - requires = ext.get('requires', {}) - if not requires or 'speckit_version' not in requires: - errors.append("Missing requires.speckit_version (required)") - else: - print(f"✅ requires.speckit_version: {requires['speckit_version']}") - - # --- provides.commands exists and is non-empty --- - provides = ext.get('provides', {}) - commands = provides.get('commands', []) - if not commands: - errors.append("provides.commands must exist and contain at least one command") - else: - print(f"✅ provides.commands has {len(commands)} command(s)") - - cmd_pattern = f'^speckit\\.{re.escape(ext_id)}\\.[a-z0-9-]+$' - - # --- Command naming convention: speckit.{ext-id}.{command} --- - for cmd in commands: - name = cmd.get('name', '') - if not re.match(cmd_pattern, name): - errors.append(f"Command name '{name}' must match pattern speckit.{ext_id}.<command>") - else: - print(f"✅ Command '{name}' follows naming convention") - - # --- Alias naming convention: speckit.{ext-id}.{alias} --- - for cmd in commands: - aliases = cmd.get('aliases', []) - if aliases is None: - aliases = [] - if not isinstance(aliases, list): - errors.append(f"Aliases for command '{cmd.get('name', '')}' must be a list") - continue - for alias in aliases: - if not isinstance(alias, str): - errors.append(f"Alias in command '{cmd.get('name', '')}' must be a string") - continue - if not re.match(cmd_pattern, alias): - errors.append(f"Alias '{alias}' must match pattern speckit.{ext_id}.<command>") - else: - print(f"✅ Alias '{alias}' follows naming convention") - - # --- Command file existence --- - for cmd in commands: - cmd_file = cmd.get('file', '') - if cmd_file and not os.path.isfile(cmd_file): - errors.append(f"Command file '{cmd_file}' referenced in manifest does not exist") - elif cmd_file: - print(f"✅ Command file '{cmd_file}' exists") - - # --- Config template existence --- - configs = provides.get('config', []) - for cfg in configs: - template = cfg.get('template', '') - if template and not os.path.isfile(template): - errors.append(f"Config template '{template}' referenced in manifest does not exist") - elif template: - print(f"✅ Config template '{template}' exists") - - # --- Report --- - if errors: - print() - for e in errors: - print(f"❌ {e}") - sys.exit(1) - - print() - print(f"✅ All manifest validations passed") - print(f" Extension: {meta['name']} v{meta['version']}") - PYEOF - - - name: Validate command files - run: | - if [ -d commands ]; then - for cmd in commands/*.md; do - if [ -f "$cmd" ]; then - # Check frontmatter exists - if head -1 "$cmd" | grep -q "^---"; then - echo "✅ $cmd has frontmatter" - - # Check description field in frontmatter - if python3 -c " - import yaml, sys - with open('$cmd') as f: - content = f.read() - parts = content.split('---', 2) - if len(parts) >= 3: - fm = yaml.safe_load(parts[1]) - if fm and 'description' in fm: - sys.exit(0) - sys.exit(1) - " 2>/dev/null; then - echo "✅ $cmd has 'description' in frontmatter" - else - echo "❌ $cmd missing 'description' in frontmatter (required)" - exit 1 - fi - else - echo "❌ $cmd missing frontmatter" - exit 1 - fi - fi - done - else - echo "❌ No commands directory found" - exit 1 - fi - - validate-bash: - name: Validate & Test Bash Scripts (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Check Bash scripts syntax - run: | - echo "Bash version: $(bash --version | head -1)" - for script in scripts/bash/*.sh; do - echo "Checking $script..." - bash -n "$script" - echo " ✓ Syntax OK" - done - - - name: Run BATS tests - run: | - echo "Running BATS tests on ${{ matrix.os }}..." - tests/bats/lib/bats-core/bin/bats tests/bats/*.bats - - validate-powershell: - name: Validate & Test PowerShell Scripts - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - name: Check PowerShell scripts syntax - shell: pwsh - run: | - foreach ($script in Get-ChildItem scripts/powershell/*.ps1) { - Write-Host "Checking $($script.Name)..." - $null = [System.Management.Automation.PSParser]::Tokenize( - (Get-Content $script.FullName -Raw), [ref]$null - ) - Write-Host " OK" - } - - - name: Run Pester tests - shell: pwsh - run: | - $config = New-PesterConfiguration - $config.Run.Path = "tests/pester" - $config.Output.Verbosity = "Detailed" - $config.Run.Exit = $true - Invoke-Pester -Configuration $config \ No newline at end of file diff --git a/.specify/extensions/review/.gitignore b/.specify/extensions/review/.gitignore deleted file mode 100644 index 124b18f3d..000000000 --- a/.specify/extensions/review/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Local configuration overrides -*-config.local.yml - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# IDEs -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log - -# Build artifacts -dist/ -build/ -*.egg-info/ - -# Temporary files -*.tmp -.cache/ - -# SDD -.github/agents/copilot-instructions.md -.github/agents/spec* -.github/prompts/spec* -.specify -specs/ \ No newline at end of file diff --git a/.specify/extensions/review/.gitmodules b/.specify/extensions/review/.gitmodules deleted file mode 100644 index 73c7f1641..000000000 --- a/.specify/extensions/review/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "tests/bats/lib/bats-core"] - path = tests/bats/lib/bats-core - url = https://github.com/bats-core/bats-core.git -[submodule "tests/bats/lib/bats-support"] - path = tests/bats/lib/bats-support - url = https://github.com/bats-core/bats-support.git -[submodule "tests/bats/lib/bats-assert"] - path = tests/bats/lib/bats-assert - url = https://github.com/bats-core/bats-assert.git diff --git a/.specify/extensions/review/CHANGELOG.md b/.specify/extensions/review/CHANGELOG.md deleted file mode 100644 index baf2b2d14..000000000 --- a/.specify/extensions/review/CHANGELOG.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -All notable changes to this extension will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.0.1] - 2026-04-04 - -### Fixed - -- Removed invalid alias `speckit.review` (two-segment name); the canonical command `speckit.review.run` is now the only entry point — fixes `Validation Error: Invalid alias` on `specify extension add` -- Added alias naming validation to CI workflow to catch invalid aliases before release - -## [1.0.0] - 2026-03-05 - -### Added - -- Command: `/speckit.review.run` (alias: `/speckit.review`) — coordinator that orchestrates all agents -- Command: `/speckit.review.code` — code quality reviewer (guideline compliance, bugs, security) -- Command: `/speckit.review.comments` — comment accuracy analyzer (documentation, comment rot) -- Command: `/speckit.review.tests` — test coverage analyzer (behavioral coverage, critical gaps) -- Command: `/speckit.review.errors` — error handling reviewer (silent failures, catch blocks) -- Command: `/speckit.review.types` — type design analyzer (encapsulation, invariants) -- Command: `/speckit.review.simplify` — code simplification advisor (clarity, complexity) -- Targeted review via aspect arguments (`/speckit.review.run tests errors`) - -### Requirements - -- Spec Kit: >=0.1.0 -- git: Required for change detection diff --git a/.specify/extensions/review/README.md b/.specify/extensions/review/README.md deleted file mode 100644 index ae03f0764..000000000 --- a/.specify/extensions/review/README.md +++ /dev/null @@ -1,185 +0,0 @@ -# Code review Extension for Spec Kit - -Post-implementation code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification. Orchestrates 6 focused review agents into a single consolidated report with severity-based grouping and actionable remediation guidance. - -## Features - -- **Coordinator** (`/speckit.review.run`): Orchestrates all agents, produces a consolidated report -- **Code Reviewer** (`/speckit.review.code`): Project guideline compliance, bug detection, code quality -- **Comment Analyzer** (`/speckit.review.comments`): Comment accuracy, documentation completeness, comment rot -- **Test Analyzer** (`/speckit.review.tests`): Behavioral coverage, critical gap identification, test resilience -- **Error Handling Reviewer** (`/speckit.review.errors`): Silent failure detection, catch block analysis, error logging -- **Type Design Analyzer** (`/speckit.review.types`): Encapsulation, invariant expression, usefulness, and enforcement -- **Code Simplifier** (`/speckit.review.simplify`): Clarity analysis, unnecessary complexity, redundant abstractions - -## Installation - -```bash -# From community catalog -specify extension add review - -# Or from repository directly -specify extension add review --from https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip - -# Local development -specify extension add --dev /path/to/spec-kit-review -``` - -Verify installation: - -```bash -specify extension list -# Should show: -# ✓ Review Extension (v1.0.1) -# Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification. -# Commands: 7 | Hooks: 1 | Status: Enabled -``` - -## Usage - -### Full Coordinated Review - -Run all specialized agents against your changes and get a consolidated report: - -``` -/speckit.review.run -``` - -All commands (coordinator and individual agents) use the built-in `detect-changed-files` script to automatically identify what to review when no files are specified: -- **Feature branch**: Committed changes since the merge base with the default branch (main/master), plus any uncommitted work -- **Default branch**: Only uncommitted work (staged and unstaged changes) - -You can skip the script entirely by telling the agent what to review: - -``` -/speckit.review.run only staged changes -/speckit.review.run only files in src/utils/ -``` - -### Targeted Review - -Run only specific agents by passing aspect names: - -``` -/speckit.review.run tests errors # Only test and error handling analysis -/speckit.review.run code # Only code quality review -/speckit.review.run comments simplify # Only comment analysis and simplification -``` - -Valid aspects: `code`, `comments`, `tests`, `errors`, `types`, `simplify`, `all` - -### Parallel Review - -By default agents run sequentially so you can act on each report as it arrives. Add `parallel` to launch all agents simultaneously for faster results: - -``` -/speckit.review.run all parallel # Full review, all agents in parallel -/speckit.review.run tests errors parallel # Parallel targeted review -``` - -Parallel mode is useful for comprehensive reviews where you want all findings at once rather than incremental feedback. - -### Direct Agent Invocation - -Run any agent directly for focused, deep analysis: - -``` -/speckit.review.code # Code quality review -/speckit.review.comments # Comment accuracy analysis -/speckit.review.tests # Test coverage analysis -/speckit.review.errors # Error handling review -/speckit.review.types # Type design analysis -/speckit.review.simplify # Code simplification suggestions -``` - -Each agent auto-detects changed files independently when invoked directly. - -## Report Output - -The consolidated report includes: - -- **Critical Issues**: Must-fix issues identified by agents — file, line, description -- **Important Issues**: Should-fix issues — file, line, description -- **Suggestions**: Nice-to-have improvements — file, line, description -- **Strengths**: What's well-done in the PR -- **Recommended Action**: Prioritized remediation steps - -## Configuration - -### Project Guidelines - -If project-specific guidelines exist (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent), agents use them as additional review criteria for project-specific conventions and standards. - -## Environment Requirements - -- **git**: Required for change detection -- **spec-kit**: >= 0.1.0 - -## Token Usage - -> **Heads up:** A full coordinated review (`/speckit.review.run`) dispatches 6 specialized agents, each of which reads the changed files independently. This can be token-intensive on larger PRs. To reduce costs, run targeted reviews (`/speckit.review.run code errors`) instead of the full suite. - -## Recommended Workflow - -``` -1. Implement changes: /speckit.implement -2. Run full review: /speckit.review.run -3. Fix critical issues -4. Re-run targeted review: /speckit.review.run code errors -5. Verify fixes resolved -6. Create PR -``` - -## Integration with Verify Extension - -If you also use the [Verify Extension](https://github.com/ismaelJimenez/spec-kit-verify) (`spec-kit-verify`), the recommended workflow is: - -``` -1. Implement changes: /speckit.implement -2. Verify spec alignment: /speckit.verify.run -3. Run PR review: /speckit.review.run -4. Fix issues and iterate -``` - -The verify extension validates that your implementation matches specification artifacts (spec.md, plan.md, tasks.md). The review extension then performs broader code quality analysis. When both are installed, the verify extension offers a handoff to run the review automatically after verification completes. - -## Troubleshooting - -### Issue: Command not available - -**Solutions:** - -1. Check extension is installed: `specify extension list` -2. Restart AI agent -3. Reinstall extension: `specify extension add review` - -### Issue: `Validation Error: Invalid alias 'speckit.review'` - -**Solution:** Upgrade to v1.0.1 or later and invoke the coordinator with `/speckit.review.run`. Spec Kit now enforces three-segment alias names, so `/speckit.review` is no longer accepted by the validator. - -### Issue: "Not a git repository" error - -**Solution:** The review extension requires git for change detection. Initialize a git repository with `git init` or run commands from within an existing repo. - -### Issue: "No changes detected" - -**Solution:** Make some code changes first. On a feature branch, commit changes. On the default branch, stage or modify files. - -## Acknowledgments - -The first version of this extension was modeled after the [PR Review Toolkit](https://github.com/anthropics/claude-code/tree/main/plugins/pr-review-toolkit) plugin for Claude Code by Anthropic. - -## License - -MIT License — see [LICENSE](LICENSE) file - -## Support - -- Issues: [https://github.com/ismaelJimenez/spec-kit-review/issues](https://github.com/ismaelJimenez/spec-kit-review/issues) -- Spec Kit Docs: [https://github.com/github/spec-kit](https://github.com/github/spec-kit) - -## Changelog - -See [CHANGELOG.md](CHANGELOG.md) for version history. - -Extension Version: 1.0.1 · Spec Kit: >=0.1.0 diff --git a/.specify/extensions/review/commands/code.md b/.specify/extensions/review/commands/code.md deleted file mode 100644 index 7f17aefb9..000000000 --- a/.specify/extensions/review/commands/code.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -description: General code quality review — project guideline compliance, bug detection, code quality analysis. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives. - -## Review Scope - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Review Responsibilities - -**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions. - -**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems. - -**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage. - -## Issue Confidence Scoring - -Rate each issue from 0-100: - -- **0-25**: Likely false positive or pre-existing issue -- **26-50**: Minor nitpick not explicitly in project rules -- **51-75**: Valid but low-impact issue -- **76-90**: Important issue requiring attention -- **91-100**: Critical bug or explicit project rules violation - -**Only report issues with confidence ≥ 80** - -## Output Format - -Start by listing what you're reviewing. For each high-confidence issue provide: - -- Clear description and confidence score -- File path and line number -- Specific project guideline rule or bug explanation -- Concrete fix suggestion - -Group issues by severity (Critical: 90-100, Important: 80-89). - -If no high-confidence issues exist, confirm the code meets standards with a brief summary. - -Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter. diff --git a/.specify/extensions/review/commands/comments.md b/.specify/extensions/review/commands/comments.md deleted file mode 100644 index 8a5b23b88..000000000 --- a/.specify/extensions/review/commands/comments.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -description: Code comment accuracy verification, documentation completeness assessment, comment rot detection. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time. - -Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Comments Framework:** - -When analyzing comments, you will: - -1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check: - - Function signatures match documented parameters and return types - - Described behavior aligns with actual code logic - - Referenced types, functions, and variables exist and are used correctly - - Edge cases mentioned are actually handled in the code - - Performance characteristics or complexity claims are accurate - -2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant: - - Critical assumptions or preconditions are documented - - Non-obvious side effects are mentioned - - Important error conditions are described - - Complex algorithms have their approach explained - - Business logic rationale is captured when not self-evident - -3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime: - - Comments that merely restate obvious code should be flagged for removal - - Comments explaining 'why' are more valuable than those explaining 'what' - - Comments that will become outdated with likely code changes should be reconsidered - - Comments should be written for the least experienced future maintainer - - Avoid comments that reference temporary states or transitional implementations - -4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted: - - Ambiguous language that could have multiple meanings - - Outdated references to refactored code - - Assumptions that may no longer hold true - - Examples that don't match current implementation - - TODOs or FIXMEs that may have already been addressed - -5. **Suggest Improvements**: Provide specific, actionable feedback: - - Rewrite suggestions for unclear or inaccurate portions - - Recommendations for additional context where needed - - Clear rationale for why comments should be removed - - Alternative approaches for conveying the same information - -Your analysis output should be structured as: - -**Summary**: Brief overview of the comment analysis scope and findings - -**Critical Issues**: Comments that are factually incorrect or highly misleading -- Location: [file:line] -- Issue: [specific problem] -- Suggestion: [recommended fix] - -**Improvement Opportunities**: Comments that could be enhanced -- Location: [file:line] -- Current state: [what's lacking] -- Suggestion: [how to improve] - -**Recommended Removals**: Comments that add no value or create confusion -- Location: [file:line] -- Rationale: [why it should be removed] - -**Positive Findings**: Well-written comments that serve as good examples (if any) - -Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value. - -IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement. diff --git a/.specify/extensions/review/commands/errors.md b/.specify/extensions/review/commands/errors.md deleted file mode 100644 index af3c4f30b..000000000 --- a/.specify/extensions/review/commands/errors.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -description: Error handling review — silent failure detection, catch block analysis, error logging. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable. - -## Determine Changed Files - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -## Core Principles - -You operate under these non-negotiable rules: - -1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect -2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it -3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems -4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible -5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems - -## Your Review Process - -When examining a PR, you will: - -### 1. Identify All Error Handling Code - -Systematically locate: -- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.) -- All error callbacks and error event handlers -- All conditional branches that handle error states -- All fallback logic and default values used on failure -- All places where errors are logged but execution continues -- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors - -### 2. Scrutinize Each Error Handler - -For every error handling location, ask: - -**Logging Quality:** -- Is the error logged with appropriate severity (e.g., warn vs. error)? -- Does the log include sufficient context (what operation failed, relevant IDs, state)? -- Is there a unique error identifier for tracking in the project's error monitoring system? -- Would this log help someone debug the issue 6 months from now? - -**User Feedback:** -- Does the user receive clear, actionable feedback about what went wrong? -- Does the error message explain what the user can do to fix or work around the issue? -- Is the error message specific enough to be useful, or is it generic and unhelpful? -- Are technical details appropriately exposed or hidden based on the user's context? - -**Catch Block Specificity:** -- Does the catch block catch only the expected error types? -- Could this catch block accidentally suppress unrelated errors? -- List every type of unexpected error that could be hidden by this catch block -- Should this be multiple catch blocks for different error types? - -**Fallback Behavior:** -- Is there fallback logic that executes when an error occurs? -- Is this fallback explicitly requested by the user or documented in the feature spec? -- Does the fallback behavior mask the underlying problem? -- Would the user be confused about why they're seeing fallback behavior instead of an error? -- Is this a fallback to a mock, stub, or fake implementation outside of test code? - -**Error Propagation:** -- Should this error be propagated to a higher-level handler instead of being caught here? -- Is the error being swallowed when it should bubble up? -- Does catching here prevent proper cleanup or resource management? - -### 3. Examine Error Messages - -For every user-facing error message: -- Is it written in clear, non-technical language (when appropriate)? -- Does it explain what went wrong in terms the user understands? -- Does it provide actionable next steps? -- Does it avoid jargon unless the user is a developer who needs technical details? -- Is it specific enough to distinguish this error from similar errors? -- Does it include relevant context (file names, operation names, etc.)? - -### 4. Check for Hidden Failures - -Look for patterns that hide errors: -- Empty catch blocks (absolutely forbidden) -- Catch blocks that only log and continue -- Returning null/nil/None/default values on error without logging -- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail -- Fallback chains that try multiple approaches without explaining why -- Retry logic that exhausts attempts without informing the user - -### 5. Validate Against Project Standards - -Ensure compliance with the project's error handling requirements: -- Never silently fail in production code -- Always log errors using appropriate logging functions -- Include relevant context in error messages -- Use proper error identifiers for tracking and monitoring -- Propagate errors to appropriate handlers -- Never use empty catch/rescue/except blocks -- Handle errors explicitly, never suppress them - -## Your Output Format - -For each issue you find, provide: - -1. **Location**: File path and line number(s) -2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific) -3. **Issue Description**: What's wrong and why it's problematic -4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden -5. **User Impact**: How this affects the user experience and debugging -6. **Recommendation**: Specific code changes needed to fix the issue -7. **Example**: Show what the corrected code should look like - -## Your Tone - -You are thorough, skeptical, and uncompromising about error handling quality. You: -- Call out every instance of inadequate error handling, no matter how minor -- Explain the debugging nightmares that poor error handling creates -- Provide specific, actionable recommendations for improvement -- Acknowledge when error handling is done well (rare but important) -- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..." -- Are constructively critical - your goal is to improve the code, not to criticize the developer - -## Special Considerations - -Be aware of any project-specific conventions: -- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics) -- Verify that error identifiers follow any project-defined catalog or registry -- The project may explicitly forbid silent failures in production code -- Empty catch/rescue/except blocks are never acceptable -- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them - -Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed. \ No newline at end of file diff --git a/.specify/extensions/review/commands/run.md b/.specify/extensions/review/commands/run.md deleted file mode 100644 index f7a7d87f9..000000000 --- a/.specify/extensions/review/commands/run.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -description: Comprehensive code review using specialized agents — orchestrates code, comments, tests, errors, types, and simplify agents sequentially. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -# Comprehensive PR Review - -Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality. - -**Review Aspects (optional):** "$ARGUMENTS" - -## Review Workflow: - -1. **Load Configuration** - - Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists). - - If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`. - - Extract the `agents` map — each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle. - - Agents set to `false` **MUST** be excluded from this run. Do not launch them. - -2. **Determine Review Scope** - - Parse arguments to see if user requested specific review aspects. - - If specific aspects were requested, run exactly those — config toggles do **not** apply (explicit user request overrides config). - - Default (no arguments): Run all applicable reviews that are enabled in config. - -3. **Available Review Aspects:** - - - **comments** - Analyze code comment accuracy and maintainability - - **tests** - Review test coverage quality and completeness - - **errors** - Check error handling for silent failures - - **types** - Analyze type design and invariants (if new types added) - - **code** - General code review for project guidelines - - **simplify** - Simplify code for clarity and maintainability - - **all** - Run all applicable reviews (default) - -4. **Identify Changed Files** - - - If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - - Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. - - The script automatically picks the best detection mode: - - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. - - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). - - JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` - - **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -5. **Determine Applicable Reviews** - - Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`): - - **Always applicable** (if enabled): `/speckit.review.code` (general quality) - - **If test files changed** (if enabled): `/speckit.review.tests` - - **If comments/docs added** (if enabled): `/speckit.review.comments` - - **If error handling changed** (if enabled): `/speckit.review.errors` - - **If types added/modified** (if enabled): `/speckit.review.types` - - **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine) - - If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)"). - -6. **Launch Review Agents** - - **Sequential approach** (one at a time): - - Easier to understand and act on - - Each report is complete before next - - Good for interactive review - - **Parallel approach** (user can request): - - Launch all agents simultaneously - - Faster for comprehensive review - - Results come back together - -7. **Aggregate Results** - - After agents complete, summarize: - - **Critical Issues** (must fix before merge) - - **Important Issues** (should fix) - - **Suggestions** (nice to have) - - **Positive Observations** (what's good) - -8. **Provide Action Plan** - - Organize findings: - ```markdown - # PR Review Summary - - ## Critical Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Important Issues (X found) - - [agent-name]: Issue description [file:line] - - ## Suggestions (X found) - - [agent-name]: Suggestion [file:line] - - ## Strengths - - What's well-done in this PR - - ## Recommended Action - 1. Fix critical issues first - 2. Address important issues - 3. Consider suggestions - 4. Re-run review after fixes - ``` - -## Usage Examples: - -**Full review (default):** -``` -/speckit.review.run -``` - -**Specific aspects:** -``` -/speckit.review.run tests errors -# Reviews only test coverage and error handling - -/speckit.review.run comments -# Reviews only code comments - -/speckit.review.run simplify -# Simplifies code after passing review -``` - -**Parallel review:** -``` -/speckit.review.run all parallel -# Launches all agents in parallel -``` - -## Agent Descriptions: - -**comment**: -- Verifies comment accuracy vs code -- Identifies comment rot -- Checks documentation completeness - -**tests**: -- Reviews behavioral test coverage -- Identifies critical gaps -- Evaluates test quality - -**errors**: -- Finds silent failures -- Reviews catch blocks -- Checks error logging - -**types**: -- Analyzes type encapsulation -- Reviews invariant expression -- Rates type design quality - -**code**: -- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance -- Detects bugs and issues -- Reviews general code quality - -**simplify**: -- Simplifies complex code -- Improves clarity and readability -- Applies project standards -- Preserves functionality - -## Tips: - -- **Run early**: Before creating PR, not after -- **Focus on changes**: Agents analyze diff by default -- **Address critical first**: Fix high-priority issues before lower priority -- **Re-run after fixes**: Verify issues are resolved -- **Use specific reviews**: Target specific aspects when you know the concern - -## Notes: - -- Agents run autonomously and return detailed reports -- Each agent focuses on its specialty for deep analysis -- Results are actionable with specific file:line references -- Agents use appropriate models for their complexity \ No newline at end of file diff --git a/.specify/extensions/review/commands/simplify.md b/.specify/extensions/review/commands/simplify.md deleted file mode 100644 index 2d87b2fe4..000000000 --- a/.specify/extensions/review/commands/simplify.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -description: Code simplification suggestions — clarity, unnecessary complexity, redundant abstractions. Advisory only. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Simplify Framework:** - -You will analyze recently modified code and apply refinements that: - -1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact. - -2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent). - -3. **Enhance Clarity**: Simplify code structure by: - - - Reducing unnecessary complexity and nesting - - Eliminating redundant code and abstractions - - Improving readability through clear variable and function names - - Consolidating related logic - - Removing unnecessary comments that describe obvious code - - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions - - Choose clarity over brevity - explicit code is often better than overly compact code - -4. **Maintain Balance**: Avoid over-simplification that could: - - - Reduce code clarity or maintainability - - Create overly clever solutions that are hard to understand - - Combine too many concerns into single functions or components - - Remove helpful abstractions that improve code organization - - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) - - Make the code harder to debug or extend - -5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope. - -Your refinement process: - -1. Identify the recently modified code sections -2. Analyze for opportunities to improve elegance and consistency -3. Apply project-specific best practices and coding standards -4. Ensure all functionality remains unchanged -5. Verify the refined code is simpler and more maintainable -6. Document only significant changes that affect understanding - -You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality. \ No newline at end of file diff --git a/.specify/extensions/review/commands/tests.md b/.specify/extensions/review/commands/tests.md deleted file mode 100644 index bfb9e0217..000000000 --- a/.specify/extensions/review/commands/tests.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -description: Test coverage quality analysis — behavioral coverage, critical gap identification, test resilience evaluation. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Your Core Responsibilities:** - -1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions. - -2. **Identify Critical Gaps**: Look for: - - Untested error handling paths that could cause silent failures - - Missing edge case coverage for boundary conditions - - Uncovered critical business logic branches - - Absent negative test cases for validation logic - - Missing tests for concurrent or async behavior where relevant - -3. **Evaluate Test Quality**: Assess whether tests: - - Test behavior and contracts rather than implementation details - - Would catch meaningful regressions from future code changes - - Are resilient to reasonable refactoring - - Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity - -4. **Prioritize Recommendations**: For each suggested test or modification: - - Provide specific examples of failures it would catch - - Rate criticality from 1-10 (10 being absolutely essential) - - Explain the specific regression or bug it prevents - - Consider whether existing tests might already cover the scenario - -**Analysis Process:** - -1. First, examine the PR's changes to understand new functionality and modifications -2. Review the accompanying tests to map coverage to functionality -3. Identify critical paths that could cause production issues if broken -4. Check for tests that are too tightly coupled to implementation -5. Look for missing negative cases and error scenarios -6. Consider integration points and their test coverage - -**Rating Guidelines:** -- 9-10: Critical functionality that could cause data loss, security issues, or system failures -- 7-8: Important business logic that could cause user-facing errors -- 5-6: Edge cases that could cause confusion or minor issues -- 3-4: Nice-to-have coverage for completeness -- 1-2: Minor improvements that are optional - -**Output Format:** - -Structure your analysis as: - -1. **Summary**: Brief overview of test coverage quality -2. **Critical Gaps** (if any): Tests rated 8-10 that must be added -3. **Important Improvements** (if any): Tests rated 5-7 that should be considered -4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation -5. **Positive Observations**: What's well-tested and follows best practices - -**Important Considerations:** - -- Focus on tests that prevent real bugs, not academic completeness -- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available -- Remember that some code paths may be covered by existing integration tests -- Avoid suggesting tests for trivial getters/setters unless they contain logic -- Consider the cost/benefit of each suggested test -- Be specific about what each test should verify and why it matters -- Note when tests are testing implementation rather than behavior - -You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change. \ No newline at end of file diff --git a/.specify/extensions/review/commands/types.md b/.specify/extensions/review/commands/types.md deleted file mode 100644 index a4867ff55..000000000 --- a/.specify/extensions/review/commands/types.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -description: Type design analysis — encapsulation, invariant expression, usefulness, and enforcement. -scripts: - sh: scripts/bash/detect-changed-files.sh - ps: scripts/powershell/detect-changed-files.ps1 ---- - -You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants. - -**Your Core Mission:** -You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems. - -**Determine Changed Files:** - -If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly. - -Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method — always delegate to the script. The script automatically picks the best detection mode: - -> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes. -> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch). -> -> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}` -> -> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it — do not skip it or substitute your own file-detection logic. - -**Analysis Framework:** - -When analyzing a type, you will: - -1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for: - - Data consistency requirements - - Valid state transitions - - Relationship constraints between fields - - Business logic rules encoded in the type - - Preconditions and postconditions - -2. **Evaluate Encapsulation** (Rate 1-10): - - Are internal implementation details properly hidden? - - Can the type's invariants be violated from outside? - - Are there appropriate access modifiers? - - Is the interface minimal and complete? - -3. **Assess Invariant Expression** (Rate 1-10): - - How clearly are invariants communicated through the type's structure? - - Are invariants enforced at compile-time where possible? - - Is the type self-documenting through its design? - - Are edge cases and constraints obvious from the type definition? - -4. **Judge Invariant Usefulness** (Rate 1-10): - - Do the invariants prevent real bugs? - - Are they aligned with business requirements? - - Do they make the code easier to reason about? - - Are they neither too restrictive nor too permissive? - -5. **Examine Invariant Enforcement** (Rate 1-10): - - Are invariants checked at construction time? - - Are all mutation points guarded? - - Is it impossible to create invalid instances? - - Are runtime checks appropriate and comprehensive? - -**Output Format:** - -Provide your analysis in this structure: - -``` -## Type: [TypeName] - -### Invariants Identified -- [List each invariant with a brief description] - -### Ratings -- **Encapsulation**: X/10 - [Brief justification] - -- **Invariant Expression**: X/10 - [Brief justification] - -- **Invariant Usefulness**: X/10 - [Brief justification] - -- **Invariant Enforcement**: X/10 - [Brief justification] - -### Strengths -[What the type does well] - -### Concerns -[Specific issues that need attention] - -### Recommended Improvements -[Concrete, actionable suggestions that won't overcomplicate the codebase] -``` - -**Key Principles:** - -- Prefer compile-time guarantees over runtime checks when feasible -- Value clarity and expressiveness over cleverness -- Consider the maintenance burden of suggested improvements -- Recognize that perfect is the enemy of good - suggest pragmatic improvements -- Types should make illegal states unrepresentable -- Constructor validation is crucial for maintaining invariants -- Immutability often simplifies invariant maintenance - -**Common Anti-patterns to Flag:** - -- Anemic domain models with no behavior -- Types that expose mutable internals -- Invariants enforced only through documentation -- Types with too many responsibilities -- Missing validation at construction boundaries -- Inconsistent enforcement across mutation methods -- Types that rely on external code to maintain invariants - -**When Suggesting Improvements:** - -Always consider: -- The complexity cost of your suggestions -- Whether the improvement justifies potential breaking changes -- The skill level and conventions of the existing codebase -- Performance implications of additional validation -- The balance between safety and usability - -Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity. \ No newline at end of file diff --git a/.specify/extensions/review/config-template.yml b/.specify/extensions/review/config-template.yml deleted file mode 100644 index 92b02b61e..000000000 --- a/.specify/extensions/review/config-template.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Review Extension Configuration -# -# Copy this file to your project as .specify/extensions/review/review-config.yml -# and adjust settings as needed. All settings are optional — defaults apply -# when omitted. - -# ── Agent Toggles ───────────────────────────────────────────────────── -# Control which agents run during a full review (speckit.review.run). -# Set to false to skip an agent. Direct agent commands (e.g., -# speckit.review.errors) always run regardless of this setting. -agents: - code: true - comments: true - tests: true - errors: true - types: true - simplify: true \ No newline at end of file diff --git a/.specify/extensions/review/extension.yml b/.specify/extensions/review/extension.yml deleted file mode 100644 index 0e38bafdf..000000000 --- a/.specify/extensions/review/extension.yml +++ /dev/null @@ -1,81 +0,0 @@ -schema_version: "1.0" - -extension: - id: "review" - name: "Review Extension" - version: "1.0.1" - description: "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification." - author: "ismaelJimenez" - repository: "https://github.com/ismaelJimenez/spec-kit-review" - license: "MIT" - homepage: "https://github.com/ismaelJimenez/spec-kit-review" - -requires: - speckit_version: ">=0.1.0" - - commands: - - "speckit.implement" - -provides: - config: - - name: "review-config.yml" - template: "config-template.yml" - description: "Review extension configuration — confidence threshold and agent toggles" - required: false - - scripts: - - name: "detect-changed-files.sh" - file: "scripts/bash/detect-changed-files.sh" - executable: true - - name: "detect-changed-files.ps1" - file: "scripts/powershell/detect-changed-files.ps1" - executable: true - - commands: - - name: "speckit.review.run" - file: "commands/run.md" - description: "Comprehensive code review using specialized agents" - - name: "speckit.review.code" - file: "commands/code.md" - description: "General code quality review — project guideline compliance, bug detection, code quality" - - name: "speckit.review.comments" - file: "commands/comments.md" - description: "Code comment accuracy verification, documentation completeness, comment rot detection" - - name: "speckit.review.tests" - file: "commands/tests.md" - description: "Test coverage quality analysis — behavioral coverage, critical gap identification, test resilience evaluation" - - name: "speckit.review.errors" - file: "commands/errors.md" - description: "Error handling review — silent failure detection, catch block analysis, error logging" - - name: "speckit.review.types" - file: "commands/types.md" - description: "Type design analysis — encapsulation, invariant expression, usefulness, and enforcement" - - name: "speckit.review.simplify" - file: "commands/simplify.md" - description: "Code simplification suggestions — clarity, unnecessary complexity, redundant abstractions" - -hooks: - after_implement: - command: "speckit.review.run" - optional: true - prompt: "Run PR review on implemented changes?" - description: "Comprehensive review after implementation" - condition: null - -tags: - - "review" - - "code-quality" - - "pr-review" - - "testing" - - "error-handling" - -defaults: - agents: - code: true - comments: true - tests: true - errors: true - types: true - simplify: true - - diff --git a/.specify/extensions/review/scripts/bash/detect-changed-files.sh b/.specify/extensions/review/scripts/bash/detect-changed-files.sh deleted file mode 100644 index cb95648b7..000000000 --- a/.specify/extensions/review/scripts/bash/detect-changed-files.sh +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env bash -# Detect changed files for code review via git diff -# -# Identifies changed files by comparing the current branch against -# the default branch plus any uncommitted work (Mode A — feature branch -# diff + working directory) or by collecting staged + unstaged changes -# (Mode B — working directory changes only). -# -# Usage: ./detect-changed-files.sh [OPTIONS] -# -# OPTIONS: -# --json Output in JSON format (for machine consumption) -# --help, -h Show this help message -# -# EXIT CODES: -# 0 Changed files detected successfully -# 1 Error (git unavailable, not a git repository) -# 2 No changes detected -# -# OUTPUTS: -# Text mode: -# BRANCH: <current-branch> -# DEFAULT_BRANCH: <default-branch> -# MODE: <detection mode description> -# CHANGED_FILES: -# file1 -# file2 -# -# JSON mode: -# {"branch":"...","default_branch":"...","mode":"...","changed_files":["..."]} - -set -e - -# --- Argument parsing --- -JSON_MODE=false - -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --help|-h) - cat << 'EOF' -Usage: detect-changed-files.sh [OPTIONS] - -Detect changed files for code review via git diff. - -OPTIONS: - --json Output in JSON format - --help, -h Show this help message - -EXIT CODES: - 0 Changed files detected successfully - 1 Error (git unavailable, not a git repository) - 2 No changes detected -EOF - exit 0 - ;; - *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;; - esac -done - -# --- Helper: escape a string for safe JSON embedding --- -json_escape() { - local s="$1" - s="${s//\\/\\\\}" # \ → \\ - s="${s//\"/\\\"}" # " → \\" - s="${s//$'\t'/\\t}" # tab → \t - s="${s//$'\n'/\\n}" # newline → \n - s="${s//$'\r'/\\r}" # carriage return → \r - printf '%s' "$s" -} - -# --- Helper: format bash array as JSON array --- -fmt_array() { - local arr=("$@") - if [[ ${#arr[@]} -eq 0 ]]; then echo "[]"; return; fi - local first=true - local result="[" - for item in "${arr[@]}"; do - if $first; then first=false; else result+=","; fi - result+="\"$(json_escape "$item")\"" - done - result+="]" - echo "$result" -} - -# --- Helper: output error and exit --- -error_exit() { - local message="$1" - local code="${2:-1}" - if $JSON_MODE; then - printf '{"error":"%s"}\n' "$(json_escape "$message")" - else - echo "Error: $message" >&2 - fi - exit "$code" -} - -# --- 1a. Verify Git Availability --- -if ! command -v git >/dev/null 2>&1; then - error_exit "git is not available. The review extension requires git to identify changed files." 1 -fi - -if ! git rev-parse --git-dir >/dev/null 2>&1; then - error_exit "Not a git repository. The review extension requires git to identify changed files." 1 -fi - -# --- 1b. Detect Branch Context --- - -# Get current branch (empty string if detached HEAD) -CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "") - -# Determine default branch -DEFAULT_BRANCH="" - -# Try symbolic-ref first -symref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "") -if [[ -n "$symref" ]]; then - DEFAULT_BRANCH="${symref##refs/remotes/origin/}" -fi - -# Fallback: check origin/main -if [[ -z "$DEFAULT_BRANCH" ]]; then - if git rev-parse --verify origin/main >/dev/null 2>&1; then - DEFAULT_BRANCH="main" - fi -fi - -# Fallback: check origin/master -if [[ -z "$DEFAULT_BRANCH" ]]; then - if git rev-parse --verify origin/master >/dev/null 2>&1; then - DEFAULT_BRANCH="master" - fi -fi - -# --- 1c. Get Changed Files --- - -CHANGED_FILES=() -MODE="" - -if [[ -n "$CURRENT_BRANCH" && -n "$DEFAULT_BRANCH" && "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then - # Mode A — Feature Branch - MERGE_BASE=$(git merge-base "origin/$DEFAULT_BRANCH" HEAD 2>/dev/null || echo "") - - if [[ -n "$MERGE_BASE" ]]; then - # Committed changes since merge-base - COMMITTED=() - while IFS= read -r -d '' line; do - [[ -n "$line" ]] && COMMITTED+=("$line") - done < <(git diff --name-only -z --diff-filter=ACMR "${MERGE_BASE}...HEAD" 2>/dev/null) - - # Staged (index) changes - STAGED=() - while IFS= read -r -d '' line; do - [[ -n "$line" ]] && STAGED+=("$line") - done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null) - - # Unstaged (working tree) changes - UNSTAGED=() - while IFS= read -r -d '' line; do - [[ -n "$line" ]] && UNSTAGED+=("$line") - done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null) - - # Combine and deduplicate (bash 3 compatible — no associative arrays) - CHANGED_FILES=() - for f in "${COMMITTED[@]}" "${STAGED[@]}" "${UNSTAGED[@]}"; do - [[ -z "$f" ]] && continue - _dup=false - for existing in "${CHANGED_FILES[@]}"; do - if [[ "$existing" == "$f" ]]; then - _dup=true - break - fi - done - if ! $_dup; then - CHANGED_FILES+=("$f") - fi - done - - MODE="Feature branch diff (${DEFAULT_BRANCH}...HEAD) + uncommitted changes" - else - # merge-base failed — fall through to Mode B - DEFAULT_BRANCH="" - fi -fi - -if [[ -z "$MODE" ]]; then - # Mode B — Working Directory Changes - STAGED=() - while IFS= read -r -d '' line; do - [[ -n "$line" ]] && STAGED+=("$line") - done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null) - - UNSTAGED=() - while IFS= read -r -d '' line; do - [[ -n "$line" ]] && UNSTAGED+=("$line") - done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null) - - # Combine and deduplicate (bash 3 compatible — no associative arrays) - CHANGED_FILES=() - for f in "${STAGED[@]}" "${UNSTAGED[@]}"; do - [[ -z "$f" ]] && continue - _dup=false - for existing in "${CHANGED_FILES[@]}"; do - if [[ "$existing" == "$f" ]]; then - _dup=true - break - fi - done - if ! $_dup; then - CHANGED_FILES+=("$f") - fi - done - - MODE="Working directory changes (staged + unstaged)" - [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="(unknown)" -fi - -# --- 1d. Validate Changed Files --- -if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then - if $JSON_MODE; then - printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":[],"message":"No changes detected. Nothing to review."}\n' \ - "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")" - else - echo "No changes detected. Nothing to review." - fi - exit 2 -fi - -# --- Output --- -if $JSON_MODE; then - printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":%s}\n' \ - "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")" "$(fmt_array "${CHANGED_FILES[@]}")" -else - echo "BRANCH: $CURRENT_BRANCH" - echo "DEFAULT_BRANCH: $DEFAULT_BRANCH" - echo "MODE: $MODE" - echo "CHANGED_FILES:" - for f in "${CHANGED_FILES[@]}"; do - echo " $f" - done -fi - -exit 0 diff --git a/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1 b/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1 deleted file mode 100644 index cd71c6de0..000000000 --- a/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1 +++ /dev/null @@ -1,228 +0,0 @@ -<# -.SYNOPSIS - Detect changed files for code review via git diff. - -.DESCRIPTION - Identifies changed files by comparing the current branch against - the default branch plus any uncommitted work (Mode A - feature branch - diff + working directory) or by collecting staged + unstaged changes - (Mode B - working directory changes only). - -.PARAMETER Json - Output in JSON format (for machine consumption). - -.PARAMETER Help - Show help message and exit. - -.EXAMPLE - .\detect-changed-files.ps1 - # Text output of changed files - -.EXAMPLE - .\detect-changed-files.ps1 -Json - # JSON output of changed files - -.NOTES - EXIT CODES: - 0 Changed files detected successfully - 1 Error (git unavailable, not a git repository) - 2 No changes detected -#> - -[CmdletBinding()] -param( - [switch]$Json, - [Alias("h")] - [switch]$Help -) - -$ErrorActionPreference = 'Stop' - -# --- Help --- -if ($Help) { - @" -Usage: detect-changed-files.ps1 [OPTIONS] - -Detect changed files for code review via git diff. - -OPTIONS: - -Json Output in JSON format - -Help, -h Show this help message - -EXIT CODES: - 0 Changed files detected successfully - 1 Error (git unavailable, not a git repository) - 2 No changes detected -"@ - exit 0 -} - -# --- Helper: output error and exit --- -function Write-ErrorAndExit { - param( - [string]$Message, - [int]$Code = 1 - ) - if ($Json) { - [PSCustomObject]@{ error = $Message } | ConvertTo-Json -Compress - } else { - Write-Error "Error: $Message" - } - exit $Code -} - -# --- 1a. Verify Git Availability --- -$gitCmd = Get-Command git -ErrorAction SilentlyContinue -if (-not $gitCmd) { - Write-ErrorAndExit "git is not available. The review extension requires git to identify changed files." 1 -} - -$gitDir = git rev-parse --git-dir 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-ErrorAndExit "Not a git repository. The review extension requires git to identify changed files." 1 -} - -# --- 1b. Detect Branch Context --- - -# Get current branch (empty string if detached HEAD) -$CurrentBranch = "" -try { - $CurrentBranch = (git branch --show-current 2>$null) | Out-String - $CurrentBranch = $CurrentBranch.Trim() -} catch { - $CurrentBranch = "" -} - -# Determine default branch -$DefaultBranch = "" - -# Try symbolic-ref first -try { - $symref = (git symbolic-ref refs/remotes/origin/HEAD 2>$null) | Out-String - $symref = $symref.Trim() - if ($symref -match "refs/remotes/origin/(.+)$") { - $DefaultBranch = $Matches[1] - } -} catch {} - -# Fallback: check origin/main -if (-not $DefaultBranch) { - $null = git rev-parse --verify origin/main 2>$null - if ($LASTEXITCODE -eq 0) { - $DefaultBranch = "main" - } -} - -# Fallback: check origin/master -if (-not $DefaultBranch) { - $null = git rev-parse --verify origin/master 2>$null - if ($LASTEXITCODE -eq 0) { - $DefaultBranch = "master" - } -} - -# --- 1c. Get Changed Files --- - -$ChangedFiles = @() -$Mode = "" - -if ($CurrentBranch -and $DefaultBranch -and ($CurrentBranch -ne $DefaultBranch)) { - # Mode A - Feature Branch - $MergeBase = "" - try { - $MergeBase = (git merge-base "origin/$DefaultBranch" HEAD 2>$null) | Out-String - $MergeBase = $MergeBase.Trim() - } catch {} - - if ($MergeBase) { - # Committed changes since merge-base - $diffRaw = git diff --name-only -z --diff-filter=ACMR "$MergeBase...HEAD" 2>$null - $committedFiles = @() - if ($diffRaw) { - $diffJoined = ($diffRaw -join "`n") - $committedFiles = @($diffJoined -split "`0" | Where-Object { $_ -ne "" }) - } - - # Staged (index) changes - $stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null - $stagedFiles = @() - if ($stagedRaw) { - $sJoined = ($stagedRaw -join "`n") - $stagedFiles = @($sJoined -split "`0" | Where-Object { $_ -ne "" }) - } - - # Unstaged (working tree) changes - $unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null - $unstagedFiles = @() - if ($unstagedRaw) { - $uJoined = ($unstagedRaw -join "`n") - $unstagedFiles = @($uJoined -split "`0" | Where-Object { $_ -ne "" }) - } - - # Combine and deduplicate - $ChangedFiles = @($committedFiles + $stagedFiles + $unstagedFiles | Sort-Object -Unique) - - $Mode = "Feature branch diff ($DefaultBranch...HEAD) + uncommitted changes" - } else { - # merge-base failed - fall through to Mode B - $DefaultBranch = "" - } -} - -if (-not $Mode) { - # Mode B - Working Directory Changes - $stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null - $unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null - - $allFiles = @() - if ($stagedRaw) { - $sJoined = ($stagedRaw -join "`n") - $allFiles += @($sJoined -split "`0" | Where-Object { $_ -ne "" }) - } - if ($unstagedRaw) { - $uJoined = ($unstagedRaw -join "`n") - $allFiles += @($uJoined -split "`0" | Where-Object { $_ -ne "" }) - } - - # Deduplicate - $ChangedFiles = @($allFiles | Sort-Object -Unique) - - $Mode = "Working directory changes (staged + unstaged)" - if (-not $DefaultBranch) { $DefaultBranch = "(unknown)" } -} - -# --- 1d. Validate Changed Files --- -if ($ChangedFiles.Count -eq 0) { - if ($Json) { - [PSCustomObject]@{ - branch = $CurrentBranch - default_branch = $DefaultBranch - mode = $Mode - changed_files = @() - message = "No changes detected. Nothing to review." - } | ConvertTo-Json -Compress - } else { - Write-Output "No changes detected. Nothing to review." - } - exit 2 -} - -# --- Output --- -if ($Json) { - [PSCustomObject]@{ - branch = $CurrentBranch - default_branch = $DefaultBranch - mode = $Mode - changed_files = $ChangedFiles - } | ConvertTo-Json -Compress -} else { - Write-Output "BRANCH: $CurrentBranch" - Write-Output "DEFAULT_BRANCH: $DefaultBranch" - Write-Output "MODE: $Mode" - Write-Output "CHANGED_FILES:" - foreach ($f in $ChangedFiles) { - Write-Output " $f" - } -} - -exit 0 diff --git a/.specify/extensions/review/tests/bats/detect-changed-files.bats b/.specify/extensions/review/tests/bats/detect-changed-files.bats deleted file mode 100644 index 4457eeaaf..000000000 --- a/.specify/extensions/review/tests/bats/detect-changed-files.bats +++ /dev/null @@ -1,650 +0,0 @@ -load "test_helper" - -setup() { - setup_temp_dir -} - -teardown() { - teardown_temp_dir -} - -# ────────────────────────────────────────────── -# 1a. Git Availability Errors -# ────────────────────────────────────────────── - -@test "fails when not in a git repository" { - cd "$TEST_TEMP_DIR" - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_failure - [ "$status" -eq 1 ] - assert_output --partial "Not a git repository" -} - -@test "fails with JSON error when not in a git repository" { - cd "$TEST_TEMP_DIR" - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_failure - [ "$status" -eq 1 ] - assert_output --partial '"error"' - assert_output --partial "Not a git repository" -} - -# ────────────────────────────────────────────── -# Help -# ────────────────────────────────────────────── - -@test "--help shows usage information" { - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --help - assert_success - assert_output --partial "Usage" - assert_output --partial "detect-changed-files" -} - -@test "-h shows usage information" { - run bash "$SCRIPTS_DIR/detect-changed-files.sh" -h - assert_success - assert_output --partial "Usage" -} - -@test "unknown option fails with error" { - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --bogus - assert_failure - assert_output --partial "Unknown option" -} - -# ────────────────────────────────────────────── -# No Changes Detected (exit code 2) -# ────────────────────────────────────────────── - -@test "exit code 2 when no changes in clean repo" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - [ "$status" -eq 2 ] - assert_output --partial "No changes detected" -} - -@test "exit code 2 with JSON output when no changes" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - [ "$status" -eq 2 ] - assert_valid_json "$output" - local msg=$(json_field "$output" "message") - [[ "$msg" == *"No changes detected"* ]] -} - -# ────────────────────────────────────────────── -# Mode B — Working Directory: Unstaged Changes -# ────────────────────────────────────────────── - -@test "detects unstaged changes (Mode B)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create and commit a file, then modify it - echo "initial" > tracked.txt - git add tracked.txt - git commit --quiet -m "Add tracked file" - echo "modified" > tracked.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "tracked.txt" - assert_output --partial "Working directory changes" -} - -@test "detects unstaged changes with --json (Mode B)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "initial" > tracked.txt - git add tracked.txt - git commit --quiet -m "Add tracked file" - echo "modified" > tracked.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - local mode=$(json_field "$output" "mode") - [[ "$mode" == "Working directory changes (staged + unstaged)" ]] - assert_output --partial '"tracked.txt"' -} - -# ────────────────────────────────────────────── -# Mode B — Working Directory: Staged Changes -# ────────────────────────────────────────────── - -@test "detects staged changes (Mode B)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "new file" > staged.txt - git add staged.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "staged.txt" - assert_output --partial "Working directory changes" -} - -# ────────────────────────────────────────────── -# Mode B — Staged + Unstaged Deduplication -# ────────────────────────────────────────────── - -@test "deduplicates staged and unstaged changes (Mode B)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a file, commit, stage a change, then modify again (unstaged) - echo "v1" > both.txt - git add both.txt - git commit --quiet -m "Add both.txt" - echo "v2" > both.txt - git add both.txt - echo "v3" > both.txt - - # Also add a new staged-only file - echo "new" > only-staged.txt - git add only-staged.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # both.txt should appear only once - local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('both.txt'))") - [ "$count" -eq 1 ] - - # only-staged.txt should also be present - assert_output --partial '"only-staged.txt"' -} - -# ────────────────────────────────────────────── -# Mode A — Feature Branch Diff -# ────────────────────────────────────────────── - -@test "detects feature branch changes via merge-base (Mode A)" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a feature branch with a new file - git checkout --quiet -b feature-branch - echo "feature code" > feature.txt - git add feature.txt - git commit --quiet -m "Add feature file" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "feature.txt" - assert_output --partial "Feature branch diff" -} - -@test "Mode A --json has correct structure" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - git checkout --quiet -b feature-branch - echo "feature code" > feature.txt - git add feature.txt - git commit --quiet -m "Add feature file" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - local branch=$(json_field "$output" "branch") - [ "$branch" = "feature-branch" ] - - local mode=$(json_field "$output" "mode") - [[ "$mode" == "Feature branch diff"* ]] - - assert_output --partial '"feature.txt"' -} - -@test "Mode A excludes deleted files (diff-filter=ACMR)" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Add a file on main and push - echo "to delete" > delete-me.txt - git add delete-me.txt - git commit --quiet -m "Add file to delete" - git push --quiet origin main - - # Create feature branch, delete the file, add another - git checkout --quiet -b feature-delete - git rm --quiet delete-me.txt - echo "keep me" > keep.txt - git add keep.txt - git commit --quiet -m "Delete and add" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - # delete-me.txt should NOT appear (it was deleted) - local files=$(json_array_field "$output" "changed_files") - ! echo "$files" | grep -q "delete-me.txt" - - # keep.txt should appear - assert_output --partial '"keep.txt"' -} - -@test "Mode A detects multiple changed files" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - git checkout --quiet -b multi-files - echo "a" > file-a.txt - echo "b" > file-b.txt - mkdir -p sub - echo "c" > sub/file-c.txt - git add . - git commit --quiet -m "Add multiple files" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_output --partial '"file-a.txt"' - assert_output --partial '"file-b.txt"' - assert_output --partial '"sub/file-c.txt"' -} - -# ────────────────────────────────────────────── -# Mode A — Feature Branch + Uncommitted Changes -# ────────────────────────────────────────────── - -@test "Mode A includes staged uncommitted files" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - git checkout --quiet -b feature-staged - echo "committed" > committed.txt - git add committed.txt - git commit --quiet -m "Add committed file" - - # Stage a new file without committing - echo "staged" > staged-only.txt - git add staged-only.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # Both committed and staged files should appear - assert_output --partial '"committed.txt"' - assert_output --partial '"staged-only.txt"' - - local mode=$(json_field "$output" "mode") - [[ "$mode" == *"uncommitted"* ]] -} - -@test "Mode A includes unstaged uncommitted files" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a file on main, push it, then modify on feature branch - echo "original" > existing.txt - git add existing.txt - git commit --quiet -m "Add existing file" - git push --quiet origin main - - git checkout --quiet -b feature-unstaged - echo "committed on branch" > committed.txt - git add committed.txt - git commit --quiet -m "Add committed file" - - # Modify existing file without staging - echo "modified" > existing.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # Both committed diff and unstaged modification should appear - assert_output --partial '"committed.txt"' - assert_output --partial '"existing.txt"' -} - -@test "Mode A deduplicates committed and uncommitted files" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - git checkout --quiet -b feature-dedup - echo "v1" > shared.txt - git add shared.txt - git commit --quiet -m "Add shared file" - - # Modify the same file (unstaged) — it appears in both committed diff and unstaged - echo "v2" > shared.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # shared.txt should appear exactly once - local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('shared.txt'))") - [ "$count" -eq 1 ] -} - -# ────────────────────────────────────────────── -# Default Branch Detection Fallbacks -# ────────────────────────────────────────────── - -@test "detects origin/main as default branch" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Unset symbolic-ref to force fallback - git remote set-head origin --delete 2>/dev/null || true - - git checkout --quiet -b test-branch - echo "test" > test-file.txt - git add test-file.txt - git commit --quiet -m "Add test file" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - local default=$(json_field "$output" "default_branch") - [ "$default" = "main" ] -} - -@test "detects origin/master as default branch when no origin/main" { - # Create a bare repo with master as default - local bare="${TEST_TEMP_DIR}/_bare_master" - mkdir -p "$bare" - git -C "$bare" init --bare --quiet - git -C "$bare" symbolic-ref HEAD refs/heads/master - - # Clone-like setup - cd "$TEST_TEMP_DIR" - git init --quiet - git config user.email "test@example.com" - git config user.name "Test" - git remote add origin "$bare" - - # Create initial commit on master and push - touch .gitkeep - git add . - git checkout -b master --quiet 2>/dev/null || true - git commit --quiet -m "Initial commit" - git push --quiet origin master - - # Remove symbolic-ref to force fallback - git remote set-head origin --delete 2>/dev/null || true - - # Create feature branch - git checkout --quiet -b test-branch - echo "test" > test-file.txt - git add test-file.txt - git commit --quiet -m "Add test file" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - local default=$(json_field "$output" "default_branch") - [ "$default" = "master" ] -} - -@test "falls back to Mode B when no remote default branch found" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # No remote configured, so no origin/* branches - echo "change" > new-file.txt - git add new-file.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - local mode=$(json_field "$output" "mode") - [[ "$mode" == "Working directory changes"* ]] -} - -# ────────────────────────────────────────────── -# Detached HEAD -# ────────────────────────────────────────────── - -@test "detached HEAD falls back to Mode B" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a commit and detach HEAD - echo "file" > detached.txt - git add detached.txt - git commit --quiet -m "Add file" - local commit_hash=$(git rev-parse HEAD) - git checkout --quiet "$commit_hash" - - # Make a change - echo "modified" > detached.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "Working directory changes" - assert_output --partial "detached.txt" -} - -# ────────────────────────────────────────────── -# JSON Output Validation -# ────────────────────────────────────────────── - -@test "--json output is valid JSON with all required keys" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "content" > new.txt - git add new.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # Verify all expected keys exist - echo "$output" | python3 -c " -import json, sys -data = json.load(sys.stdin) -assert 'branch' in data, 'missing branch key' -assert 'default_branch' in data, 'missing default_branch key' -assert 'mode' in data, 'missing mode key' -assert 'changed_files' in data, 'missing changed_files key' -assert isinstance(data['changed_files'], list), 'changed_files should be a list' -print('All keys present and correct types') -" -} - -@test "text mode output has correct format" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "content" > formatted.txt - git add formatted.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "BRANCH:" - assert_output --partial "DEFAULT_BRANCH:" - assert_output --partial "MODE:" - assert_output --partial "CHANGED_FILES:" - assert_output --partial "formatted.txt" -} - -# ────────────────────────────────────────────── -# Edge Cases -# ────────────────────────────────────────────── - -@test "handles files with spaces in names" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "content" > "file with spaces.txt" - git add "file with spaces.txt" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" - assert_success - assert_output --partial "file with spaces.txt" -} - -@test "handles nested directory changes" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - mkdir -p deep/nested/path - echo "deep" > deep/nested/path/file.txt - git add . - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_output --partial '"deep/nested/path/file.txt"' -} - -@test "only reports ACMR files (Added, Copied, Modified, Renamed)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Add and commit files - echo "keep" > keep.txt - echo "remove" > remove.txt - git add . - git commit --quiet -m "Add files" - - # Delete one, modify another — only staged - git rm --quiet remove.txt - echo "modified" > keep.txt - echo "added" > added.txt - git add . - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - # keep.txt (Modified) and added.txt (Added) should be present - assert_output --partial '"keep.txt"' - assert_output --partial '"added.txt"' - - # remove.txt (Deleted) should NOT be present - local files=$(json_array_field "$output" "changed_files") - [[ ! "$files" == *"remove.txt"* ]] -} - -@test "Mode A: branch field matches current branch name" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - git checkout --quiet -b my-feature-123 - echo "x" > x.txt - git add x.txt - git commit --quiet -m "commit" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - local branch=$(json_field "$output" "branch") - [ "$branch" = "my-feature-123" ] -} - -@test "Mode B: branch field shows current branch on default" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "change" > file.txt - git add file.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - - # Branch should be main (or master depending on git default) - local branch=$(json_field "$output" "branch") - [[ "$branch" == "main" || "$branch" == "master" ]] -} - -# ────────────────────────────────────────────── -# Special Characters in Filenames -# ────────────────────────────────────────────── - -@test "handles filenames with double quotes in JSON mode" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a file with a double quote in its name - local fname='file"quote.txt' - echo "content" > "$fname" - git add "$fname" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # The filename should be properly escaped in JSON - local files=$(json_array_field "$output" "changed_files") - [[ "$files" == *'file"quote.txt'* ]] -} - -@test "handles filenames with backslashes in JSON mode" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create a file with a backslash in its name - local fname='file\slash.txt' - echo "content" > "$fname" - git add "$fname" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" -} - -@test "handles filenames with special characters in JSON mode" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - # Create files with various special characters - echo "a" > "file (1).txt" - echo "b" > "file's.txt" - echo "c" > "file&more.txt" - git add . - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_valid_json "$output" - - # All three files should be present - local count=$(echo "$output" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['changed_files']))") - [ "$count" -eq 3 ] -} - -# ────────────────────────────────────────────── -# Renamed Files -# ────────────────────────────────────────────── - -@test "Mode A detects renamed files (diff-filter includes R)" { - init_git_repo_with_remote "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "content" > original.txt - git add original.txt - git commit --quiet -m "Add original" - git push --quiet origin main - - git checkout --quiet -b feature-rename - git mv original.txt renamed.txt - git commit --quiet -m "Rename file" - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_output --partial '"renamed.txt"' -} - -@test "Mode B detects renamed files (staged)" { - init_git_repo "$TEST_TEMP_DIR" - cd "$TEST_TEMP_DIR" - - echo "content" > original.txt - git add original.txt - git commit --quiet -m "Add original" - - git mv original.txt renamed.txt - - run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json - assert_success - assert_output --partial '"renamed.txt"' -} diff --git a/.specify/extensions/review/tests/bats/test_helper.bash b/.specify/extensions/review/tests/bats/test_helper.bash deleted file mode 100644 index c9ac12a12..000000000 --- a/.specify/extensions/review/tests/bats/test_helper.bash +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env bash -# Shared test helper for BATS tests - -# Load bats libraries -BATS_LIB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/lib" && pwd)" -load "${BATS_LIB_DIR}/bats-support/load" -load "${BATS_LIB_DIR}/bats-assert/load" - -# Project root (repo root) -PROJECT_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" - -# Scripts under test -SCRIPTS_DIR="${PROJECT_ROOT}/scripts/bash" - -# Create a temporary working directory for each test -setup_temp_dir() { - TEST_TEMP_DIR="$(mktemp -d)" - export TEST_TEMP_DIR -} - -# Clean up the temporary directory -teardown_temp_dir() { - if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then - rm -rf "$TEST_TEMP_DIR" - fi -} - -# Initialize a git repo in the temp directory -init_git_repo() { - local dir="${1:-$TEST_TEMP_DIR}" - git -C "$dir" init --quiet -b main - git -C "$dir" config user.email "test@example.com" - git -C "$dir" config user.name "Test" - # Create initial commit so git diff works - touch "$dir/.gitkeep" - git -C "$dir" add . - git -C "$dir" commit --quiet -m "Initial commit" -} - -# Initialize a git repo with a bare remote (for origin/* refs) -init_git_repo_with_remote() { - local dir="${1:-$TEST_TEMP_DIR}" - local bare_dir="${dir}/_bare_remote" - - # Create a bare repo to act as origin (explicitly use main) - mkdir -p "$bare_dir" - git -C "$bare_dir" init --bare --quiet - git -C "$bare_dir" symbolic-ref HEAD refs/heads/main - - # Create the working repo - git -C "$dir" init --quiet -b main - git -C "$dir" config user.email "test@example.com" - git -C "$dir" config user.name "Test" - git -C "$dir" remote add origin "$bare_dir" - - # Create initial commit and push to establish origin/main - touch "$dir/.gitkeep" - git -C "$dir" add . - git -C "$dir" commit --quiet -m "Initial commit" - git -C "$dir" push --quiet origin main - - # Set origin/HEAD so symbolic-ref works - git -C "$dir" remote set-head origin --auto 2>/dev/null || true -} - -# Validate that output is valid JSON -assert_valid_json() { - local output="$1" - echo "$output" | python3 -m json.tool > /dev/null 2>&1 \ - || fail "Invalid JSON: $output" -} - -# Extract a JSON field value (simple top-level string/bool/number) -json_field() { - local json="$1" - local field="$2" - echo "$json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$field',''))" -} - -# Extract a JSON array field as newline-separated values -json_array_field() { - local json="$1" - local field="$2" - echo "$json" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for item in data.get('$field', []): - print(item) -" -} diff --git a/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1 b/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1 deleted file mode 100644 index 4e3576aca..000000000 --- a/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1 +++ /dev/null @@ -1,740 +0,0 @@ -BeforeAll { - $ScriptsDir = Join-Path $PSScriptRoot "..\..\scripts\powershell" - $Script = Join-Path $ScriptsDir "detect-changed-files.ps1" - - function New-TempDir { - $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) - New-Item -ItemType Directory -Path $tmp -Force | Out-Null - return $tmp - } - - function Initialize-GitRepo { - param([string]$Dir) - Push-Location $Dir - git init --quiet -b main - git config user.email "test@example.com" - git config user.name "Test" - New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null - git add . - git commit --quiet -m "Initial commit" - Pop-Location - } - - function Initialize-GitRepoWithRemote { - param([string]$Dir) - $bareDir = Join-Path $Dir "_bare_remote" - New-Item -ItemType Directory -Path $bareDir -Force | Out-Null - Push-Location $bareDir - git init --bare --quiet - git symbolic-ref HEAD refs/heads/main - Pop-Location - - Push-Location $Dir - git init --quiet -b main - git config user.email "test@example.com" - git config user.name "Test" - git remote add origin $bareDir - New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null - git add . - git commit --quiet -m "Initial commit" - git push --quiet origin main - git remote set-head origin --auto 2>$null - Pop-Location - } -} - -Describe "detect-changed-files.ps1" { - - # ────────────────────────────────────────────── - # Help - # ────────────────────────────────────────────── - - Describe "Help" { - It "shows usage with -Help" { - $result = & pwsh -NoProfile -File $Script -Help - $LASTEXITCODE | Should -Be 0 - ($result -join "`n") | Should -Match "Usage" - } - - It "shows usage with -h" { - $result = & pwsh -NoProfile -File $Script -h - $LASTEXITCODE | Should -Be 0 - ($result -join "`n") | Should -Match "Usage" - } - } - - # ────────────────────────────────────────────── - # Git Availability Errors - # ────────────────────────────────────────────── - - Describe "Git availability" { - It "fails when not in a git repository" { - $tmp = New-TempDir - try { - Push-Location $tmp - $result = & pwsh -NoProfile -File $Script 2>&1 - $LASTEXITCODE | Should -Be 1 - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "fails with JSON error when not in a git repository" { - $tmp = New-TempDir - try { - Push-Location $tmp - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 1 - ($result -join "`n") | Should -Match '"error"' - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # No Changes Detected - # ────────────────────────────────────────────── - - Describe "No changes" { - It "exit code 2 when no changes in clean repo" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - & pwsh -NoProfile -File $Script 2>&1 - $LASTEXITCODE | Should -Be 2 - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "exit code 2 with JSON output when no changes" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 2 - $json = $result | ConvertFrom-Json - $json.message | Should -Match "No changes detected" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Mode B — Unstaged Changes - # ────────────────────────────────────────────── - - Describe "Mode B - Unstaged" { - It "detects unstaged changes" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "initial" | Set-Content "tracked.txt" - git add tracked.txt - git commit --quiet -m "Add tracked file" - "modified" | Set-Content "tracked.txt" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.mode | Should -Match "Working directory changes" - $json.changed_files | Should -Contain "tracked.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Mode B — Staged Changes - # ────────────────────────────────────────────── - - Describe "Mode B - Staged" { - It "detects staged changes" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "new file" | Set-Content "staged.txt" - git add staged.txt - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.mode | Should -Match "Working directory changes" - $json.changed_files | Should -Contain "staged.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Mode B — Deduplication - # ────────────────────────────────────────────── - - Describe "Mode B - Deduplication" { - It "deduplicates staged and unstaged changes" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "v1" | Set-Content "both.txt" - git add both.txt - git commit --quiet -m "Add both.txt" - "v2" | Set-Content "both.txt" - git add both.txt - "v3" | Set-Content "both.txt" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - ($json.changed_files | Where-Object { $_ -eq "both.txt" }).Count | Should -Be 1 - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Mode A — Feature Branch Diff - # ────────────────────────────────────────────── - - Describe "Mode A - Feature Branch" { - It "detects feature branch changes via merge-base" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - git checkout --quiet -b feature-branch - "feature code" | Set-Content "feature.txt" - git add feature.txt - git commit --quiet -m "Add feature file" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.branch | Should -Be "feature-branch" - $json.mode | Should -Match "Feature branch diff" - $json.changed_files | Should -Contain "feature.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "excludes deleted files (diff-filter=ACMR)" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - "to delete" | Set-Content "delete-me.txt" - git add delete-me.txt - git commit --quiet -m "Add file to delete" - git push --quiet origin main - - git checkout --quiet -b feature-delete - git rm --quiet delete-me.txt - "keep me" | Set-Content "keep.txt" - git add keep.txt - git commit --quiet -m "Delete and add" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "keep.txt" - $json.changed_files | Should -Not -Contain "delete-me.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "detects multiple changed files" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - git checkout --quiet -b multi-files - "a" | Set-Content "file-a.txt" - "b" | Set-Content "file-b.txt" - $subDir = Join-Path $tmp "sub" - New-Item -ItemType Directory -Path $subDir -Force | Out-Null - "c" | Set-Content (Join-Path $subDir "file-c.txt") - git add . - git commit --quiet -m "Add multiple files" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "file-a.txt" - $json.changed_files | Should -Contain "file-b.txt" - $json.changed_files | Should -Contain "sub/file-c.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "detects renamed files (diff-filter includes R)" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - "content" | Set-Content "original.txt" - git add original.txt - git commit --quiet -m "Add original" - git push --quiet origin main - - git checkout --quiet -b feature-rename - git mv original.txt renamed.txt - git commit --quiet -m "Rename file" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "renamed.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Mode A — Feature Branch + Uncommitted Changes - # ────────────────────────────────────────────── - - Describe "Mode A - Uncommitted changes" { - It "includes staged uncommitted files" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - git checkout --quiet -b feature-staged - "committed" | Set-Content "committed.txt" - git add committed.txt - git commit --quiet -m "Add committed file" - - # Stage a new file without committing - "staged" | Set-Content "staged-only.txt" - git add staged-only.txt - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "committed.txt" - $json.changed_files | Should -Contain "staged-only.txt" - $json.mode | Should -Match "uncommitted" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "includes unstaged uncommitted files" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - "original" | Set-Content "existing.txt" - git add existing.txt - git commit --quiet -m "Add existing file" - git push --quiet origin main - - git checkout --quiet -b feature-unstaged - "committed on branch" | Set-Content "committed.txt" - git add committed.txt - git commit --quiet -m "Add committed file" - - # Modify existing file without staging - "modified" | Set-Content "existing.txt" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "committed.txt" - $json.changed_files | Should -Contain "existing.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "deduplicates committed and uncommitted files" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - git checkout --quiet -b feature-dedup - "v1" | Set-Content "shared.txt" - git add shared.txt - git commit --quiet -m "Add shared file" - - # Modify the same file (unstaged) - "v2" | Set-Content "shared.txt" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - ($json.changed_files | Where-Object { $_ -eq "shared.txt" }).Count | Should -Be 1 - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Default Branch Detection Fallbacks - # ────────────────────────────────────────────── - - Describe "Default branch detection" { - It "detects origin/main as default branch" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - - # Unset symbolic-ref to force fallback - git remote set-head origin --delete 2>$null - - git checkout --quiet -b test-branch - "test" | Set-Content "test-file.txt" - git add test-file.txt - git commit --quiet -m "Add test file" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.default_branch | Should -Be "main" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "falls back to Mode B when no remote default branch found" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "change" | Set-Content "new-file.txt" - git add new-file.txt - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.mode | Should -Match "Working directory changes" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "detects origin/master as default branch when no origin/main" { - $tmp = New-TempDir - try { - # Create a bare repo with master as default - $bareDir = Join-Path $tmp "_bare_master" - New-Item -ItemType Directory -Path $bareDir -Force | Out-Null - Push-Location $bareDir - git init --bare --quiet - git symbolic-ref HEAD refs/heads/master - Pop-Location - - Push-Location $tmp - git init --quiet - git config user.email "test@example.com" - git config user.name "Test" - git remote add origin $bareDir - - New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null - git add . - git checkout -b master --quiet 2>$null - git commit --quiet -m "Initial commit" - git push --quiet origin master 2>$null - - # Remove symbolic-ref to force fallback - git remote set-head origin --delete 2>$null - - # Create feature branch - git checkout --quiet -b test-branch - "test" | Set-Content "test-file.txt" - git add test-file.txt - git commit --quiet -m "Add test file" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.default_branch | Should -Be "master" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # JSON Output Validation - # ────────────────────────────────────────────── - - Describe "JSON output" { - It "has all required keys" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "content" | Set-Content "new.txt" - git add new.txt - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.PSObject.Properties.Name | Should -Contain "branch" - $json.PSObject.Properties.Name | Should -Contain "default_branch" - $json.PSObject.Properties.Name | Should -Contain "mode" - $json.PSObject.Properties.Name | Should -Contain "changed_files" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Edge Cases - # ────────────────────────────────────────────── - - Describe "Edge cases" { - It "handles files with spaces in names" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "content" | Set-Content "file with spaces.txt" - git add "file with spaces.txt" - - $result = & pwsh -NoProfile -File $Script 2>&1 - $LASTEXITCODE | Should -Be 0 - ($result -join "`n") | Should -Match "file with spaces.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "handles nested directory changes" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - $nested = Join-Path $tmp "deep" "nested" "path" - New-Item -ItemType Directory -Path $nested -Force | Out-Null - "deep" | Set-Content (Join-Path $nested "file.txt") - git add . - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "deep/nested/path/file.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "only reports ACMR files" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "keep" | Set-Content "keep.txt" - "remove" | Set-Content "remove.txt" - git add . - git commit --quiet -m "Add files" - - git rm --quiet remove.txt - "modified" | Set-Content "keep.txt" - "added" | Set-Content "added.txt" - git add . - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "keep.txt" - $json.changed_files | Should -Contain "added.txt" - $json.changed_files | Should -Not -Contain "remove.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Detached HEAD - # ────────────────────────────────────────────── - - Describe "Detached HEAD" { - It "falls back to Mode B on detached HEAD" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "file" | Set-Content "detached.txt" - git add detached.txt - git commit --quiet -m "Add file" - $commitHash = git rev-parse HEAD - git checkout --quiet $commitHash - - "modified" | Set-Content "detached.txt" - - $result = & pwsh -NoProfile -File $Script 2>&1 - $LASTEXITCODE | Should -Be 0 - ($result -join "`n") | Should -Match "Working directory changes" - ($result -join "`n") | Should -Match "detached.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Text Output Format Validation - # ────────────────────────────────────────────── - - Describe "Text output format" { - It "text mode output has correct format" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "content" | Set-Content "formatted.txt" - git add formatted.txt - - $result = & pwsh -NoProfile -File $Script 2>&1 - $LASTEXITCODE | Should -Be 0 - $text = $result -join "`n" - $text | Should -Match "BRANCH:" - $text | Should -Match "DEFAULT_BRANCH:" - $text | Should -Match "MODE:" - $text | Should -Match "CHANGED_FILES:" - $text | Should -Match "formatted.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Branch Field Validation - # ────────────────────────────────────────────── - - Describe "Branch field" { - It "branch field matches current branch name in Mode A" { - $tmp = New-TempDir - try { - Initialize-GitRepoWithRemote -Dir $tmp - Push-Location $tmp - git checkout --quiet -b my-feature-123 - "x" | Set-Content "x.txt" - git add x.txt - git commit --quiet -m "commit" - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.branch | Should -Be "my-feature-123" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "branch field shows current branch on default branch (Mode B)" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - "change" | Set-Content "file.txt" - git add file.txt - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.branch | Should -Be "main" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } - - # ────────────────────────────────────────────── - # Special Characters in Filenames - # ────────────────────────────────────────────── - - Describe "Special character filenames" { - It "handles filenames with special characters in JSON mode" { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - - # Create files with special characters - "a" | Set-Content "file (1).txt" - "b" | Set-Content "file's.txt" - "c" | Set-Content "file&more.txt" - git add . - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files | Should -Contain "file (1).txt" - $json.changed_files | Should -Contain "file's.txt" - $json.changed_files | Should -Contain "file&more.txt" - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - - It "handles filenames with double quotes in JSON mode" -Skip:($IsWindows -or $env:OS -eq 'Windows_NT') { - $tmp = New-TempDir - try { - Initialize-GitRepo -Dir $tmp - Push-Location $tmp - - # Create a file with a double quote in its name - # (skipped on Windows — NTFS does not allow " in filenames) - $fname = 'file"quote.txt' - [System.IO.File]::WriteAllText((Join-Path $tmp $fname), "content") - git add . - - $result = & pwsh -NoProfile -File $Script -Json 2>&1 - $LASTEXITCODE | Should -Be 0 - $json = $result | ConvertFrom-Json - $json.changed_files.Count | Should -Be 1 - $json.changed_files[0] | Should -Match 'quote' - } finally { - Pop-Location - Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue - } - } - } -} diff --git a/.specify/init-options.json b/.specify/init-options.json index 03ddd69ef..da7260bca 100644 --- a/.specify/init-options.json +++ b/.specify/init-options.json @@ -5,5 +5,5 @@ "here": true, "integration": "claude", "script": "sh", - "speckit_version": "0.11.9" -} \ No newline at end of file + "speckit_version": "1.2.5" +} diff --git a/.specify/integration.json b/.specify/integration.json index 56ee62661..263f99db9 100644 --- a/.specify/integration.json +++ b/.specify/integration.json @@ -1,5 +1,5 @@ { - "version": "0.11.9", + "version": "1.2.5", "integration_state_schema": 1, "installed_integrations": [ "claude" diff --git a/.specify/integrations/claude.manifest.json b/.specify/integrations/claude.manifest.json index b8af5cfb5..70befb2a9 100644 --- a/.specify/integrations/claude.manifest.json +++ b/.specify/integrations/claude.manifest.json @@ -1,17 +1,17 @@ { "integration": "claude", - "version": "0.11.9", - "installed_at": "2026-06-28T09:52:47.426637+00:00", + "version": "1.2.5", + "installed_at": "2026-11-06T08:34:06.203756+00:00", "files": { - ".agents/skills/speckit-analyze/SKILL.md": "fecd4bf113c3dda58c75d387473c0106fc2dfea97a27bb7c65af94f3f916c188", - ".agents/skills/speckit-clarify/SKILL.md": "c1c2098756ca407530cca11c5b608f517d769962215ddafa013951b81e3e19c5", - ".agents/skills/speckit-constitution/SKILL.md": "ee3972318415a05559c6bf281dcbd2e8deda944e595d64ab5474abeacf558697", - ".agents/skills/speckit-implement/SKILL.md": "823049e49aa983fe398d4bccf6c686ab6afe8f2cd3856e0380c3ef797d78d56d", - ".agents/skills/speckit-converge/SKILL.md": "04226b8443797337624983111546d5e5a48d9993a176c4e6d72a4099a0af50d4", - ".agents/skills/speckit-plan/SKILL.md": "53733c8a4f4fd01685759bb1c68e94c73da4ce90d549139e79e419dec6471510", - ".agents/skills/speckit-checklist/SKILL.md": "946c6bc808891436972a11a423f89f0fbd272a79809bb8fd1d29f481ebe02613", - ".agents/skills/speckit-specify/SKILL.md": "9324dd55d12d420cd581031419fa37eb94ef75ae0bdd53391dd4414bd9d45e02", - ".agents/skills/speckit-tasks/SKILL.md": "cb29fb8247a30aac751be83de88d0399221692589dd26327552ae6f193816fda", - ".agents/skills/speckit-taskstoissues/SKILL.md": "dfe23aaca349cd76e98505dafa9aae1ef4616a0c35a5c79122b9bd881e16b62f" + ".agents/skills/speckit-analyze/SKILL.md": "3237cd827194efaf6bb06ba05ef9bc1a703a223d362f5e34d0558c5a38ae34d0", + ".agents/skills/speckit-clarify/SKILL.md": "164f49ef87cb2e922585316f5281b455a8e1940b1efd25317d517a567acae472", + ".agents/skills/speckit-constitution/SKILL.md": "60c27f3962e78edf204b2b2e7cba91a616980860b61d6bf2051dc6965d483dda", + ".agents/skills/speckit-implement/SKILL.md": "7186d59488f89dcf23abd17baed118b05c242d4f62f1de3f9f44625a2b1728f0", + ".agents/skills/speckit-converge/SKILL.md": "9f93c1d573592f146670202210128ed24a9d75c92133951b19a2bb0e34eac13b", + ".agents/skills/speckit-plan/SKILL.md": "9e97fc0b4ce895c70a1ea720dc0377618c23e27c83ecf4d87b6e0f6c323790a4", + ".agents/skills/speckit-checklist/SKILL.md": "5dc436df0c727e9078bf182dd385e5cb1b60ee67ebe644d3f659ec12e3b78abb", + ".agents/skills/speckit-specify/SKILL.md": "d4d93f2444355136e78bb533988aa2dd560dc4b258f4923f67b1a437e7de38f8", + ".agents/skills/speckit-tasks/SKILL.md": "d148aeda1011260c4465c5f105fa1e045a0b0224ccb3e6e0c1ec32c732cae1ae", + ".agents/skills/speckit-taskstoissues/SKILL.md": "471edc95cb41acdc18d4ab3c0f024334bbc4bac9936f955dde30fc3cb9d77473" } } diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json index dae223a23..99426895d 100644 --- a/.specify/integrations/speckit.manifest.json +++ b/.specify/integrations/speckit.manifest.json @@ -1,17 +1,19 @@ { "integration": "speckit", - "version": "0.11.9", + "version": "1.2.5", "installed_at": "2026-04-28T09:03:28.000533+00:00", "files": { - ".specify/scripts/bash/common.sh": "af8a16f87b4f9084759c42ff9abf35c0b2a2025dffe58c298758ff86de2923b2", - ".specify/scripts/bash/setup-plan.sh": "4eb12c5b00f5c66a7d01b56c90898d320dcef4425d9b96652d57156c84948eda", - ".specify/scripts/bash/check-prerequisites.sh": "afce0aa8db177320d83aa0b8e3619c06b865fd810781894e4a7a3f81664941ce", - ".specify/scripts/bash/create-new-feature.sh": "9ba116b64f0328eb69bc1a195d209074ea38823a73a554160d69df34a74daa65", + ".specify/scripts/bash/common.sh": "53cb1c23e349db4586c272436ce35c6e2f36a9c0b5351a85e0b216d9887a26b7", + ".specify/scripts/bash/setup-plan.sh": "d378ec66ba20d2da01c71dad8935062c1c27f0bba890be9a6d3dda52ea056a4e", + ".specify/scripts/bash/check-prerequisites.sh": "e127735bbc5ac42181e2a3c773a2d639979f865f18e74a1a977cdf53392a0a7d", + ".specify/scripts/bash/create-new-feature.sh": "972f03ed8ca5ee17b7baad6d56d821d46269d77abc337496beafb0fe85a725f2", ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3", - ".specify/templates/checklist-template.md": "c37695297e5d3153d64f82c21223509940b13932046c7961c42d1d669516130c", + ".specify/templates/checklist-template.md": "856532b3cb66171c662cc16f16b31a5856e4655a8666aad1e545bbfc7f603ca1", ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842", ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e", - ".specify/templates/plan-template.md": "cc7f7979cf8d8836ec26492785affd80791d3422a2b745062ec695be8c985ef7", - ".specify/scripts/bash/setup-tasks.sh": "cf21ba2212b4dd5b435c5ea8527500cfd27768b86c0bbc7ebc3207759f118d27" + ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4", + ".specify/scripts/bash/setup-tasks.sh": "4a33dd1e6c32ddc7d570f4b538572c54069192573eed0ec654d9fb826e31a4fe", + ".specify/scripts/bash/resolve-template.sh": "829e227096abc8bf0889889ec9f792f503ca5d395b7836a8a7eb739ee75214e7", + ".specify/.gitignore": "0a61c5378b0bb2e0f85babff99c8b166f2170a2c12378cd17d929aa19296ec88" } } diff --git a/.specify/presets/.registry b/.specify/presets/.registry index 6d6a4ca56..393e40fe0 100644 --- a/.specify/presets/.registry +++ b/.specify/presets/.registry @@ -1,10 +1,24 @@ { "schema_version": "1.0", "presets": { - "infrahub": { + "critique-opsmill": { "version": "1.0.0", "source": "local", - "manifest_hash": "sha256:4c489fa51392b80ba0553462e1d80b344e0d6af997596a42b485d2d021902139", + "manifest_hash": "sha256:0dfb76ba3f29dc941a01ed43cda981777fb1463466a9ebaedc796fd85962ffff", + "enabled": true, + "priority": 10, + "registered_commands": { + "claude": [ + "speckit.critique.run" + ] + }, + "registered_skills": {}, + "installed_at": "2026-11-06T08:34:33.695468+00:00" + }, + "jira-branch": { + "version": "1.0.0", + "source": "local", + "manifest_hash": "sha256:c06329b9c0f45605abdf8513eaf04b6cdf0955e5c9a24b830c0d5da3a335457f", "enabled": true, "priority": 10, "registered_commands": { @@ -12,10 +26,8 @@ "speckit.git.feature" ] }, - "registered_skills": [ - "speckit-git-feature" - ], - "installed_at": "2026-04-28T07:59:23.895326+00:00" + "registered_skills": {}, + "installed_at": "2026-11-06T08:34:33.872666+00:00" } } } \ No newline at end of file diff --git a/.specify/presets/critique-opsmill/LICENSE b/.specify/presets/critique-opsmill/LICENSE new file mode 100644 index 000000000..d9acc4d91 --- /dev/null +++ b/.specify/presets/critique-opsmill/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 arunt14 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.specify/presets/critique-opsmill/README.md b/.specify/presets/critique-opsmill/README.md new file mode 100644 index 000000000..ca34164f7 --- /dev/null +++ b/.specify/presets/critique-opsmill/README.md @@ -0,0 +1,20 @@ +# critique-opsmill + +Overrides `speckit.critique.run` from the [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) extension, whose command looks for `check-prerequisites.sh` in the extension's folder, where it does not exist. The command is upstream `commands/run.md` at `v1.0.0`, with its script paths changed to `../../scripts/` and its report template found where the extension installs it. `speckit.opsmill.prep` runs it as its critique step. + +## Install + +Install the `critique` extension first, otherwise the override is skipped: + +```bash +specify extension add critique \ + --from https://github.com/arunt14/spec-kit-critique/archive/refs/tags/v1.0.0.zip +git clone -b <tag> https://github.com/opsmill/opsmill-speckit +specify preset add --dev opsmill-speckit/presets/critique-opsmill +``` + +`<tag>` is a release that includes this preset (see [`presets/CHANGELOG.md`](../CHANGELOG.md)). If the preset was added before the extension, run `specify preset remove critique-opsmill` and add it again. + +## License + +MIT, from spec-kit-critique: see [`LICENSE`](LICENSE). diff --git a/.agents/commands/speckit.critique.run.md b/.specify/presets/critique-opsmill/commands/speckit.critique.run.md similarity index 91% rename from .agents/commands/speckit.critique.run.md rename to .specify/presets/critique-opsmill/commands/speckit.critique.run.md index fea86445f..c3b8cc87b 100644 --- a/.agents/commands/speckit.critique.run.md +++ b/.specify/presets/critique-opsmill/commands/speckit.critique.run.md @@ -1,14 +1,10 @@ --- -description: Perform a dual-lens critical review of the specification and plan from - both product strategy and engineering risk perspectives before implementation. +description: Perform a dual-lens critical review of the specification and plan from both product strategy and engineering risk perspectives before implementation. scripts: - sh: .specify/scripts/bash/check-prerequisites.sh --json --include-tasks - ps: .specify/scripts/powershell/check-prerequisites.ps1 -Json -IncludeTasks + sh: ../../scripts/bash/check-prerequisites.sh --json --include-tasks + ps: ../../scripts/powershell/check-prerequisites.ps1 -Json -IncludeTasks --- - -<!-- Extension: critique --> -<!-- Config: .specify/extensions/critique/ --> ## User Input ```text @@ -61,16 +57,16 @@ Challenge the specification and implementation plan through two distinct expert **CONSTRUCTIVE CHALLENGE**: The goal is to strengthen the spec and plan, not to block progress. Every critique item must include a constructive suggestion for improvement. -**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) defines non-negotiable principles. Any spec/plan element conflicting with the constitution is automatically a 🎯 Must-Address item. +**Constitution Authority**: The project constitution (`/memory/constitution.md`) defines non-negotiable principles. Any spec/plan element conflicting with the constitution is automatically a 🎯 Must-Address item. ## Outline -1. Run `.specify/scripts/bash/check-prerequisites.sh --json --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 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"). +1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 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 Critique Context**: - **REQUIRED**: Read `spec.md` for requirements, user stories, and acceptance criteria - **REQUIRED**: Read `plan.md` for architecture, tech stack, and implementation phases - - **IF EXISTS**: Read `.specify/memory/constitution.md` for governing principles + - **IF EXISTS**: Read `/memory/constitution.md` for governing principles - **IF EXISTS**: Read `tasks.md` for task breakdown (if already generated) - **IF EXISTS**: Read previous critique reports in FEATURE_DIR/critiques/ for context @@ -176,7 +172,7 @@ Challenge the specification and implementation plan through two distinct expert - 🤔 **Question**: Ambiguity or assumption that needs stakeholder input. Cannot be resolved by the development team alone. 7. **Generate Critique Report**: - Ensure the directory `FEATURE_DIR/critiques/` exists (create it if necessary), then create the critique report at `FEATURE_DIR/critiques/critique-{timestamp}.md` using `.specify/templates/critique-template.md` as the required structure. The report must include: + Ensure the directory `FEATURE_DIR/critiques/` exists (create it if necessary), then create the critique report at `FEATURE_DIR/critiques/critique-{timestamp}.md` using `.specify/extensions/critique/commands/critique-template.md` as the required structure. The report must include: - **Executive Summary**: Overall assessment and readiness to proceed - **Product Lens Findings**: Organized by subcategory (3a-3e) @@ -239,4 +235,4 @@ Suggest next steps based on verdict: Executing: `/{command}` EXECUTE_COMMAND: {command} ``` -- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently \ No newline at end of file +- If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/.specify/presets/critique-opsmill/preset.yml b/.specify/presets/critique-opsmill/preset.yml new file mode 100644 index 000000000..d74d192f0 --- /dev/null +++ b/.specify/presets/critique-opsmill/preset.yml @@ -0,0 +1,27 @@ +schema_version: "1.0" + +preset: + id: "critique-opsmill" + name: "Critique (OpsMill)" + version: "1.0.0" + description: "Override speckit.critique.run (arunt14/spec-kit-critique extension) with its script path fixed." + author: "opsmill" + repository: "https://github.com/opsmill/opsmill-speckit" + license: "MIT" + +requires: + speckit_version: ">=0.8.0" + +provides: + templates: + - type: "command" + name: "speckit.critique.run" + file: "commands/speckit.critique.run.md" + description: "Perform a dual-lens critical review of the specification and plan" + replaces: "speckit.critique.run" + +tags: + - "critique" + - "review" + - "planning" + - "opsmill" diff --git a/.specify/presets/infrahub/preset.yml b/.specify/presets/infrahub/preset.yml deleted file mode 100644 index 2269a39ac..000000000 --- a/.specify/presets/infrahub/preset.yml +++ /dev/null @@ -1,26 +0,0 @@ -schema_version: "1.0" - -preset: - id: "infrahub" - name: "Infrahub Workflow" - version: "1.0.0" - description: "Infrahub-specific SpecKit customizations: enforces Jira/JPD ticket reference on feature branch creation" - author: "opsmill" - license: "MIT" - -requires: - speckit_version: ">=0.6.0" - -provides: - templates: - - type: "command" - name: "speckit.git.feature" - file: "commands/speckit.infrahub.feature.md" - description: "Validate Jira/JPD ticket reference and create a feature branch" - replaces: "speckit.git.feature" - strategy: "replace" - -tags: - - "infrahub" - - "jira" - - "branching" diff --git a/.specify/presets/jira-branch/README.md b/.specify/presets/jira-branch/README.md new file mode 100644 index 000000000..61bd533a4 --- /dev/null +++ b/.specify/presets/jira-branch/README.md @@ -0,0 +1,17 @@ +# jira-branch + +Overrides `speckit.git.feature` from the git extension bundled with spec-kit, so that `/speckit.specify` creates a feature branch only with a JPD (`infp-<n>`) or Jira epic (`ifc-<n>`) reference. The branch is named `<short-name>-<ticket-id>`, for example `user-auth-infp-460`. + +It replaces the [`infrahub` preset](https://github.com/opsmill/infrahub/tree/4d9fd0f0a469217aada697cb5ef306f7ef485f78/dev/spec-kit/presets/infrahub) of opsmill/infrahub. + +## Install + +In a repo that has the `infrahub` preset, remove it first with `specify preset remove infrahub`. If `.specify/extensions/git/` does not exist, install the git extension before the preset, otherwise the override is skipped: + +```bash +specify extension add git +git clone -b <tag> https://github.com/opsmill/opsmill-speckit +specify preset add --dev opsmill-speckit/presets/jira-branch +``` + +`<tag>` is a release that includes this preset (see [`presets/CHANGELOG.md`](../CHANGELOG.md)). diff --git a/.specify/presets/infrahub/commands/speckit.infrahub.feature.md b/.specify/presets/jira-branch/commands/speckit.git.feature.md similarity index 60% rename from .specify/presets/infrahub/commands/speckit.infrahub.feature.md rename to .specify/presets/jira-branch/commands/speckit.git.feature.md index 16724fae0..3b15452d4 100644 --- a/.specify/presets/infrahub/commands/speckit.infrahub.feature.md +++ b/.specify/presets/jira-branch/commands/speckit.git.feature.md @@ -2,9 +2,9 @@ description: "Validate Jira/JPD ticket reference and create a feature branch" --- -# Create Infrahub Feature Branch +# Create Feature Branch -Create and switch to a new git feature branch for the given specification, enforcing an Infrahub Jira or JPD ticket reference as the branch name suffix. +Create and switch to a new git feature branch for the given specification, enforcing a Jira or JPD ticket reference as the branch name suffix. ## User Input @@ -43,28 +43,21 @@ Generate a concise short name (2-4 words) from the feature description: ## Step 3: Create Branch -Construct the branch name as `<short-name>-<ticket-id>` (e.g., `user-auth-infp-460`), then pass it as `GIT_BRANCH_NAME` to bypass the script's automatic numbering: +Construct the branch name as `<short-name>-<ticket-id>` (e.g., `user-auth-infp-460`): the ticket ID is the suffix, and the name must be 244 bytes or fewer. -```bash -GIT_BRANCH_NAME="<short-name>-<ticket-id>" .specify/extensions/git/scripts/bash/create-new-feature.sh --json "<feature description>" -``` - -Example: +Create the branch and switch to it with one plain git command, written out, without an environment variable before it, so that permission rules can match it: ```bash -GIT_BRANCH_NAME="user-auth-infp-460" .specify/extensions/git/scripts/bash/create-new-feature.sh --json "Add user authentication" +git checkout -b "<short-name>-<ticket-id>" ``` -**IMPORTANT**: +If the branch already exists, do not switch to it and do not create another one: stop and tell the user. -- Always construct `GIT_BRANCH_NAME` as `<short-name>-<ticket-id>` — ticket ID is the suffix -- Always include `--json` so output can be parsed reliably -- Run this script exactly once per feature -- For single quotes in args, use escape syntax: `'I'\''m Groot'` +The git extension's branch script is not used: it only takes an exact branch name through the `GIT_BRANCH_NAME` environment variable, which keeps permission rules from matching the command. ## Output -The script outputs JSON with: +Report, as JSON: -- `BRANCH_NAME`: The branch name (e.g., `user-auth-infp-460`) -- `FEATURE_NUM`: The ticket ID +- `BRANCH_NAME`: the branch name (e.g., `user-auth-infp-460`) +- `FEATURE_NUM`: the ticket ID resolved in Step 1 (e.g., `infp-460`) diff --git a/.specify/presets/jira-branch/preset.yml b/.specify/presets/jira-branch/preset.yml new file mode 100644 index 000000000..29d2f0678 --- /dev/null +++ b/.specify/presets/jira-branch/preset.yml @@ -0,0 +1,27 @@ +schema_version: "1.0" + +preset: + id: "jira-branch" + name: "Jira branch" + version: "1.0.0" + description: "Override speckit.git.feature so that a feature branch is created only with a Jira or JPD ticket reference, as <short-name>-<ticket-id>." + author: "opsmill" + repository: "https://github.com/opsmill/opsmill-speckit" + license: "Apache-2.0" + +requires: + speckit_version: ">=0.8.0" + +provides: + templates: + - type: "command" + name: "speckit.git.feature" + file: "commands/speckit.git.feature.md" + description: "Validate Jira/JPD ticket reference and create a feature branch" + replaces: "speckit.git.feature" + +tags: + - "jira" + - "branching" + - "git" + - "opsmill" diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh index 8377d8ec5..430da3ca3 100755 --- a/.specify/scripts/bash/check-prerequisites.sh +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -9,9 +9,11 @@ # # OPTIONS: # --json Output in JSON format +# --require-spec Require spec.md to exist (for analysis phase) # --require-tasks Require tasks.md to exist (for implementation phase) # --include-tasks Include tasks.md in AVAILABLE_DOCS list # --paths-only Only output path variables (no validation) +# --template NAME Include composed template content in JSON output # --help, -h Show help message # # OUTPUTS: @@ -23,15 +25,20 @@ set -e # Parse command line arguments JSON_MODE=false +REQUIRE_SPEC=false REQUIRE_TASKS=false INCLUDE_TASKS=false PATHS_ONLY=false +TEMPLATE_NAME="" -for arg in "$@"; do - case "$arg" in +while [[ $# -gt 0 ]]; do + case "$1" in --json) JSON_MODE=true ;; + --require-spec) + REQUIRE_SPEC=true + ;; --require-tasks) REQUIRE_TASKS=true ;; @@ -41,6 +48,14 @@ for arg in "$@"; do --paths-only) PATHS_ONLY=true ;; + --template) + shift + if [[ $# -eq 0 ]]; then + echo "ERROR: --template requires a template name" >&2 + exit 1 + fi + TEMPLATE_NAME="$1" + ;; --help|-h) cat << 'EOF' Usage: check-prerequisites.sh [OPTIONS] @@ -49,37 +64,46 @@ Consolidated prerequisite checking for Spec-Driven Development workflow. OPTIONS: --json Output in JSON format + --require-spec Require spec.md to exist (for analysis phase) --require-tasks Require tasks.md to exist (for implementation phase) --include-tasks Include tasks.md in AVAILABLE_DOCS list --paths-only Only output path variables (no prerequisite validation) + --template NAME Include composed template content in JSON output --help, -h Show this help message EXAMPLES: # Check task prerequisites (plan.md required) ./check-prerequisites.sh --json - + # Check implementation prerequisites (plan.md + tasks.md required) ./check-prerequisites.sh --json --require-tasks --include-tasks - + # Get feature paths only (no validation) ./check-prerequisites.sh --paths-only - + EOF exit 0 ;; *) - echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 + echo "ERROR: Unknown option '$1'. Use --help for usage information." >&2 exit 1 ;; esac + shift done # Source common functions SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" -# Get feature paths -_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +# Get feature paths. +# In --paths-only mode this is pure resolution, so pass --no-persist to opt out +# of the feature.json write side effect (issue #3025). +if $PATHS_ONLY; then + _paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +else + _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +fi eval "$_paths_output" unset _paths_output @@ -124,6 +148,19 @@ if [[ ! -f "$IMPL_PLAN" ]]; then exit 1 fi +if [[ ! -s "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md is empty in $FEATURE_DIR" >&2 + echo "Populate it, or remove the empty file and rerun /speckit-plan with a valid plan template." >&2 + exit 1 +fi + +# Check for spec.md if required +if $REQUIRE_SPEC && [[ ! -f "$FEATURE_SPEC" ]]; then + echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit-specify first to create the feature specification." >&2 + exit 1 +fi + # Check for tasks.md if required if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 @@ -150,6 +187,16 @@ if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then docs+=("tasks.md") fi +TEMPLATE_CONTENT="" +if [[ -n "$TEMPLATE_NAME" ]]; then + if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then + TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}" + else + echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2 + exit 1 + fi +fi + # Output results if $JSON_MODE; then # Build JSON array of documents @@ -159,10 +206,18 @@ if $JSON_MODE; then else json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .) fi - jq -cn \ - --arg feature_dir "$FEATURE_DIR" \ - --argjson docs "$json_docs" \ - '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}' + if [[ -n "$TEMPLATE_NAME" ]]; then + jq -cn \ + --arg feature_dir "$FEATURE_DIR" \ + --argjson docs "$json_docs" \ + --arg template_content "$TEMPLATE_CONTENT" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TEMPLATE_CONTENT:$template_content}' + else + jq -cn \ + --arg feature_dir "$FEATURE_DIR" \ + --argjson docs "$json_docs" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}' + fi else if [[ ${#docs[@]} -eq 0 ]]; then json_docs="[]" @@ -170,19 +225,24 @@ if $JSON_MODE; then json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done) json_docs="[${json_docs%,}]" fi - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" + if [[ -n "$TEMPLATE_NAME" ]]; then + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TEMPLATE_CONTENT":"%s"}\n' \ + "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "$TEMPLATE_CONTENT")" + else + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs" + fi fi else # Text output echo "FEATURE_DIR:$FEATURE_DIR" echo "AVAILABLE_DOCS:" - + # Show status of each potential document check_file "$RESEARCH" "research.md" check_file "$DATA_MODEL" "data-model.md" check_dir "$CONTRACTS_DIR" "contracts/" check_file "$QUICKSTART" "quickstart.md" - + if $INCLUDE_TASKS; then check_file "$TASKS" "tasks.md" fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh index 70ab89b01..ff18cba4d 100755 --- a/.specify/scripts/bash/common.sh +++ b/.specify/scripts/bash/common.sh @@ -97,17 +97,26 @@ read_feature_json_feature_directory() { local fj="$repo_root/.specify/feature.json" [[ -f "$fj" ]] || { printf '%s' ''; return 0; } + # Try parsers in order (jq -> python3 -> grep/sed), falling through on + # failure. Selection is by *parse success*, not mere availability: on + # Windows `python3` commonly resolves to the Microsoft Store App Execution + # Alias stub, which passes `command -v` but fails at runtime (exit 49), so + # an availability-gated `elif` would pick python3, swallow its failure, and + # never reach the grep/sed fallback -- leaving feature.json unreadable even + # though it is valid (issue #3304). local _fd='' if command -v jq >/dev/null 2>&1; then if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then _fd='' fi - elif command -v python3 >/dev/null 2>&1; then + fi + if [[ -z "$_fd" ]] && command -v python3 >/dev/null 2>&1; then # Use Python so pretty-printed/multi-line JSON still parses correctly. - if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then + if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1], encoding='utf-8')); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then _fd='' fi - else + fi + if [[ -z "$_fd" ]]; then # Last-resort single-line grep/sed fallback. The `|| true` guards against # grep returning 1 (no match) aborting under `set -e` / `pipefail`. _fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \ @@ -152,6 +161,22 @@ _persist_feature_json() { } get_feature_paths() { + # Read-only callers (e.g. check-prerequisites.sh --paths-only) pass + # --no-persist so pure path resolution never writes .specify/feature.json, + # which would dirty the working tree or overwrite a pinned value (issue #3025). + local no_persist=false + if [[ "${1:-}" == "--no-persist" ]]; then + no_persist=true + shift + fi + # SPECIFY_FEATURE_NO_PERSIST is the environment-level equivalent of --no-persist, + # letting an orchestrator (multi-agent runner, CI matrix) guarantee that no + # script invocation in the process tree writes .specify/feature.json, even + # scripts that don't pass --no-persist themselves (#4128). + if [[ "${SPECIFY_FEATURE_NO_PERSIST:-}" == "1" || "${SPECIFY_FEATURE_NO_PERSIST:-}" == "true" ]]; then + no_persist=true + fi + # Split decl/assignment so a SPECIFY_INIT_DIR validation failure in # get_repo_root propagates as a hard error instead of being masked by `local`. local repo_root @@ -168,8 +193,11 @@ get_feature_paths() { feature_dir="$SPECIFY_FEATURE_DIRECTORY" # Normalize relative paths to absolute under repo root [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir" - # Persist to feature.json so future sessions without the env var still work - _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY" + # Persist to feature.json so future sessions without the env var still + # work — unless the caller opted out for read-only resolution (#3025). + if [[ "$no_persist" != true ]]; then + _persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY" + fi elif [[ -f "$repo_root/.specify/feature.json" ]]; then local _fd _fd=$(read_feature_json_feature_directory "$repo_root") @@ -186,6 +214,15 @@ get_feature_paths() { return 1 fi + # When no branch context exists (no SPECIFY_FEATURE, feature resolved via + # SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature + # directory basename so CURRENT_BRANCH is a usable identifier rather than + # an empty, misleading value (issue #3026). + if [[ -z "$current_branch" ]]; then + local feature_dir_trimmed="${feature_dir%/}" + current_branch="${feature_dir_trimmed##*/}" + fi + # Use printf '%q' to safely quote values, preventing shell injection # via crafted branch names or paths containing special characters printf 'REPO_ROOT=%q\n' "$repo_root" @@ -214,21 +251,29 @@ get_invoke_separator() { local integration_json="$repo_root/.specify/integration.json" local separator="." - local parsed_with_jq=0 + local parsed=0 if [[ -f "$integration_json" ]]; then + # Try parsers in order (jq -> python3 -> awk), falling through on + # failure. Selection is by *parse success*, not mere availability: on + # Windows `python3` commonly resolves to the Microsoft Store App + # Execution Alias stub, which passes `command -v` but fails at runtime + # (exit 49). An availability-gated branch would pick python3, swallow + # its failure, and — because this function historically had no text + # fallback — silently return "." even for `-`-separator integrations + # (e.g. forge, cline), yielding wrong command hints (issue #3304). if command -v jq >/dev/null 2>&1; then local jq_separator if jq_separator=$(jq -r '(.default_integration // .integration // "") as $k | if $k == "" then "." else (.integration_settings[$k].invoke_separator // ".") end' "$integration_json" 2>/dev/null); then - parsed_with_jq=1 case "$jq_separator" in - "."|"-") separator="$jq_separator" ;; + "."|"-") separator="$jq_separator"; parsed=1 ;; esac fi fi - if [[ "$parsed_with_jq" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then - if separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null + if [[ "$parsed" -eq 0 ]] && command -v python3 >/dev/null 2>&1; then + local py_separator + if py_separator=$(python3 - "$integration_json" <<'PY' 2>/dev/null import json import sys @@ -244,17 +289,64 @@ try: separator = entry["invoke_separator"] print(separator) except Exception: - print(".") + sys.exit(1) PY ); then - case "$separator" in - "."|"-") ;; - *) separator="." ;; + case "$py_separator" in + "."|"-") separator="$py_separator"; parsed=1 ;; esac - else - separator="." fi fi + + if [[ "$parsed" -eq 0 ]]; then + # Last-resort text fallback for environments with neither jq nor a + # working python3 (e.g. stock Windows + Git Bash). Reads the active + # integration key (default_integration, else integration) and its + # invoke_separator from within the integration_settings object. + # Handles both pretty-printed (the written form) and compact JSON. + # Accumulate all lines into one buffer in END rather than using + # gawk-only whole-file slurp (RS="^$"), so this stays portable to + # the BSD awk on macOS. + local awk_separator + awk_separator=$(awk ' + function keyval(d, name, v) { + if (match(d, "\"" name "\"[ \t\r\n]*:[ \t\r\n]*\"[^\"]*\"")) { + v=substr(d,RSTART,RLENGTH); sub(/^.*:[ \t\r\n]*"/,"",v); sub(/"$/,"",v); return v + } + return "" + } + { doc = doc $0 "\n" } + END { + key=keyval(doc,"default_integration"); if (key=="") key=keyval(doc,"integration") + sep="." + if (key!="") { + settings=doc + if (match(doc, /"integration_settings"[ \t\r\n]*:[ \t\r\n]*[{]/)) { + settings=substr(doc, RSTART+RLENGTH-1) + } + if (match(settings, "\"" key "\"[ \t\r\n]*:[ \t\r\n]*[{]")) { + start=RSTART+RLENGTH-1 + depth=0 + obj="" + for (i=start; i<=length(settings); i++) { + c=substr(settings,i,1) + obj=obj c + if (c=="{") depth++ + else if (c=="}") { depth--; if (depth==0) break } + } + if (match(obj, /"invoke_separator"[ \t\r\n]*:[ \t\r\n]*"[-.]"/)) { + tok=substr(obj,RSTART,RLENGTH); s=substr(tok,length(tok)-1,1) + if (s=="." || s=="-") sep=s + } + } + } + print sep + } + ' "$integration_json" 2>/dev/null) + case "$awk_separator" in + "."|"-") separator="$awk_separator" ;; + esac + fi fi _SPECIFY_INVOKE_SEPARATOR_CACHE_REPO_ROOT="$repo_root" @@ -313,6 +405,110 @@ json_escape() { check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } +_python3_command() { + # SPECKIT_PYTHON_EXECUTABLE is the canonical override; SPECKIT_PYTHON is + # kept as a deprecated fallback (still used by update-agent-context.sh). + local override="${SPECKIT_PYTHON_EXECUTABLE:-${SPECKIT_PYTHON:-}}" + if [[ -n "$override" ]] && command -v "$override" >/dev/null 2>&1 && + "$override" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1 && + "$override" -c 'import yaml' >/dev/null 2>&1; then + printf '%s\n' "$override" + elif command -v python3 >/dev/null 2>&1 && + python3 -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then + printf '%s\n' "python3" + elif command -v python >/dev/null 2>&1 && + python -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then + printf '%s\n' "python" + elif command -v py >/dev/null 2>&1 && + py -3 -c 'import sys' >/dev/null 2>&1; then + printf '%s\n' "py" "-3" + else + return 1 + fi +} + +_sorted_extension_ids() { + local ext_dir="$1" + local -a python_cmd=() + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) + if [ "${#python_cmd[@]}" -gt 0 ]; then + local py_stderr sorted_ids + py_stderr=$(mktemp) + if sorted_ids=$(SPECKIT_EXTENSIONS="$ext_dir" "${python_cmd[@]}" -c " +import json, os, re, sys +from pathlib import Path + +root = Path(os.environ['SPECKIT_EXTENSIONS']) +registered = {} +registry = root / '.registry' +if os.path.lexists(registry): + if not registry.is_file(): + print('registry_invalid: not a regular file', file=sys.stderr) + sys.exit(1) + try: + data = json.loads(registry.read_text(encoding='utf-8')) + except Exception as exc: + print('registry_invalid: ' + str(exc), file=sys.stderr) + sys.exit(1) + if not isinstance(data, dict): + print('registry_invalid: root must be a mapping', file=sys.stderr) + sys.exit(1) + raw_extensions = data.get('extensions', {}) + if not isinstance(raw_extensions, dict): + print('registry_invalid: extensions must be a mapping', file=sys.stderr) + sys.exit(1) + registered = raw_extensions + +def priority(value): + if isinstance(value, bool): + return 10 + try: + parsed = int(value) + return parsed if parsed >= 1 else 10 + except (TypeError, ValueError, OverflowError): + return 10 + +ranked = [] +for ext_id, meta in registered.items(): + if isinstance(ext_id, str) and re.fullmatch(r'[a-z0-9-]+', ext_id) and isinstance(meta, dict) and bool(meta.get('enabled', True)): + ranked.append((priority(meta.get('priority')), ext_id)) +for path in root.iterdir(): + if path.is_dir() and re.fullmatch(r'[a-z0-9-]+', path.name) and path.name not in registered: + ranked.append((10, path.name)) +for _, ext_id in sorted(ranked): + print(ext_id) +" 2>"$py_stderr"); then + rm -f "$py_stderr" + printf '%s\n' "$sorted_ids" + return 0 + else + echo "Error: invalid extension registry $ext_dir/.registry" >&2 + rm -f "$py_stderr" + return 1 + fi + fi + + if [ -e "$ext_dir/.registry" ] || [ -L "$ext_dir/.registry" ]; then + if [ ! -f "$ext_dir/.registry" ] || [ ! -r "$ext_dir/.registry" ]; then + echo "Error: invalid extension registry $ext_dir/.registry" >&2 + return 1 + fi + echo "Error: Python 3 is required to honor the extension registry" >&2 + return 2 + fi + + local ext extension_id + for ext in "$ext_dir"/*/; do + [ -d "$ext" ] || continue + extension_id=$(basename "$ext") + case "$extension_id" in *[!a-z0-9-]*) continue ;; esac + printf '%s\n' "$extension_id" + done +} + # Resolve a template name to a file path using the priority stack: # 1. .specify/templates/overrides/ # 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry) @@ -323,6 +519,8 @@ resolve_template() { local repo_root="$2" local base="$repo_root/.specify/templates" + case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac + # Priority 1: Project overrides local override="$base/overrides/${template_name}.md" [ -f "$override" ] && echo "$override" && return 0 @@ -331,19 +529,32 @@ resolve_template() { local presets_dir="$repo_root/.specify/presets" if [ -d "$presets_dir" ]; then local registry_file="$presets_dir/.registry" - if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then + local -a python_cmd=() + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) + if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then # Read preset IDs sorted by priority (lower number = higher precedence). # The python3 call is wrapped in an if-condition so that set -e does not # abort the function when python3 exits non-zero (e.g. invalid JSON). local sorted_presets="" - if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c " -import json, sys, os + if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c " +import json, re, sys, os try: - with open(os.environ['SPECKIT_REGISTRY']) as f: + with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f: data = json.load(f) presets = data.get('presets', {}) - for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10): - if isinstance(meta, dict) and meta.get('enabled', True) is not False: + def priority(meta): + if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool): + return 10 + try: + value = int(meta.get('priority', 10)) + return value if value >= 1 else 10 + except (TypeError, ValueError, OverflowError): + return 10 + for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])): + if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid): print(pid) except Exception: sys.exit(1) @@ -353,6 +564,8 @@ except Exception: while IFS= read -r preset_id; do local candidate="$presets_dir/$preset_id/templates/${template_name}.md" [ -f "$candidate" ] && echo "$candidate" && return 0 + candidate="$presets_dir/$preset_id/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 done <<< "$sorted_presets" fi # python3 succeeded but registry has no presets — nothing to search @@ -362,6 +575,8 @@ except Exception: [ -d "$preset" ] || continue local candidate="$preset/templates/${template_name}.md" [ -f "$candidate" ] && echo "$candidate" && return 0 + candidate="$preset/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 done fi else @@ -370,6 +585,8 @@ except Exception: [ -d "$preset" ] || continue local candidate="$preset/templates/${template_name}.md" [ -f "$candidate" ] && echo "$candidate" && return 0 + candidate="$preset/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 done fi fi @@ -377,13 +594,17 @@ except Exception: # Priority 3: Extension-provided templates local ext_dir="$repo_root/.specify/extensions" if [ -d "$ext_dir" ]; then - for ext in "$ext_dir"/*/; do - [ -d "$ext" ] || continue - # Skip hidden directories (e.g. .backup, .cache) - case "$(basename "$ext")" in .*) continue;; esac + local sorted_extensions="" + if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then + return 2 + fi + while IFS= read -r extension_id; do + [ -n "$extension_id" ] || continue + local ext="$ext_dir/$extension_id" local candidate="$ext/templates/${template_name}.md" + [ -f "$candidate" ] || candidate="$ext/${template_name}.md" [ -f "$candidate" ] && echo "$candidate" && return 0 - done + done <<< "$sorted_extensions" fi # Priority 4: Core templates @@ -407,6 +628,8 @@ resolve_template_content() { local repo_root="$2" local base="$repo_root/.specify/templates" + case "$template_name" in ""|*[!a-z0-9-]*) return 1 ;; esac + # Collect all layers (highest priority first) local -a layer_paths=() local -a layer_strategies=() @@ -414,133 +637,206 @@ resolve_template_content() { # Priority 1: Project overrides (always "replace") local override="$base/overrides/${template_name}.md" if [ -f "$override" ]; then - layer_paths+=("$override") - layer_strategies+=("replace") + if ! cat "$override"; then + echo "Error: failed to read template layer $override" >&2 + return 2 + fi + return 0 fi + local effective_base_found=false + # Priority 2: Installed presets (sorted by priority from .registry) local presets_dir="$repo_root/.specify/presets" if [ -d "$presets_dir" ]; then local registry_file="$presets_dir/.registry" local sorted_presets="" - if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then - if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c " -import json, sys, os + local registry_parsed=false + local -a python_cmd=() + local _python_cmd_line + while IFS= read -r _python_cmd_line; do + python_cmd+=("$_python_cmd_line") + done < <(_python3_command) + if [ -f "$registry_file" ] && [ "${#python_cmd[@]}" -gt 0 ]; then + if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" "${python_cmd[@]}" -c " +import json, re, sys, os try: - with open(os.environ['SPECKIT_REGISTRY']) as f: + with open(os.environ['SPECKIT_REGISTRY'], encoding='utf-8') as f: data = json.load(f) presets = data.get('presets', {}) - for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10): - if isinstance(meta, dict) and meta.get('enabled', True) is not False: + def priority(meta): + if not isinstance(meta, dict) or isinstance(meta.get('priority'), bool): + return 10 + try: + value = int(meta.get('priority', 10)) + return value if value >= 1 else 10 + except (TypeError, ValueError, OverflowError): + return 10 + for pid, meta in sorted(presets.items(), key=lambda x: (priority(x[1]), x[0])): + if isinstance(meta, dict) and bool(meta.get('enabled', True)) and re.fullmatch(r'[a-z0-9-]+', pid): print(pid) except Exception: sys.exit(1) " 2>/dev/null); then - if [ -n "$sorted_presets" ]; then - local yaml_warned=false - while IFS= read -r preset_id; do - # Read strategy and file path from preset manifest - local strategy="replace" - local manifest_file="" - local manifest="$presets_dir/$preset_id/preset.yml" - if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then - # Requires PyYAML; falls back to replace/convention if unavailable - local result - local py_stderr - py_stderr=$(mktemp) - result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" python3 -c " + registry_parsed=true + fi + fi + if [ "$registry_parsed" = false ]; then + for preset in "$presets_dir"/*/; do + [ -d "$preset" ] || continue + local fallback_id + fallback_id=$(basename "$preset") + case "$fallback_id" in *[!a-z0-9-]*) continue ;; esac + sorted_presets+="${sorted_presets:+$'\n'}$fallback_id" + done + fi + + if [ -n "$sorted_presets" ]; then + while IFS= read -r preset_id; do + local strategy="replace" + local manifest_file="" + local manifest="$presets_dir/$preset_id/preset.yml" + local manifest_declared=false + if [ -f "$manifest" ]; then + if [ "${#python_cmd[@]}" -eq 0 ]; then + echo "Error: Python 3 and PyYAML are required to resolve preset template composition" >&2 + return 2 + fi + local result + local py_stderr + local parse_status + py_stderr=$(mktemp) + if result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" "${python_cmd[@]}" -c " import sys, os try: import yaml except ImportError: print('yaml_missing', file=sys.stderr) - print('replace\t') - sys.exit(0) + sys.exit(2) try: - with open(os.environ['SPECKIT_MANIFEST']) as f: + with open(os.environ['SPECKIT_MANIFEST'], encoding='utf-8') as f: data = yaml.safe_load(f) - for t in data.get('provides', {}).get('templates', []): + if not isinstance(data, dict): + raise ValueError('manifest root must be a mapping') + if 'provides' not in data: + raise ValueError('manifest missing provides section') + provides = data['provides'] + if not isinstance(provides, dict): + raise ValueError('manifest provides must be a mapping') + if 'templates' not in provides: + raise ValueError('manifest provides missing templates') + templates = provides['templates'] + if not isinstance(templates, list): + raise ValueError('manifest templates must be a list') + if not templates: + raise ValueError('manifest must provide at least one template') + valid_types = ('template', 'command', 'script') + valid_strategies = ('replace', 'prepend', 'append', 'wrap') + for t in templates: + if not isinstance(t, dict): + raise ValueError('manifest template entries must be mappings') + if 'type' not in t or 'name' not in t or 'file' not in t: + raise ValueError('manifest template entry missing type, name, or file') + for field in ('type', 'name', 'file'): + if not isinstance(t[field], str): + raise ValueError('manifest template ' + field + ' must be a string') + if t['type'] not in valid_types: + raise ValueError('invalid manifest template type') + strategy = t.get('strategy', 'replace') + if not isinstance(strategy, str): + raise ValueError('manifest template strategy must be a string') + strategy = strategy.lower() + if strategy not in valid_strategies: + raise ValueError('invalid manifest template strategy') + if t['type'] == 'script' and strategy not in ('replace', 'wrap'): + raise ValueError('invalid manifest script strategy') + for t in templates: if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template': - print(t.get('strategy', 'replace') + '\t' + t.get('file', '')) + file_value = t.get('file', '') + strategy = t.get('strategy', 'replace') + print('found\t' + strategy + '\t' + file_value) sys.exit(0) - print('replace\t') -except Exception: - print('replace\t') -" 2>"$py_stderr") - local parse_status=$? - if [ $parse_status -eq 0 ] && [ -n "$result" ]; then - IFS=$'\t' read -r strategy manifest_file <<< "$result" - strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]') - fi - if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then - echo "Warning: PyYAML not available; composition strategies may be ignored" >&2 - yaml_warned=true - fi - rm -f "$py_stderr" - fi - # Try manifest file path first, then convention path - local candidate="" - if [ -n "$manifest_file" ]; then - # Reject absolute paths and parent traversal - case "$manifest_file" in - /*|*../*|../*) manifest_file="" ;; - esac - fi - if [ -n "$manifest_file" ]; then - local mf="$presets_dir/$preset_id/$manifest_file" - [ -f "$mf" ] && candidate="$mf" - fi - if [ -z "$candidate" ]; then - local cf="$presets_dir/$preset_id/templates/${template_name}.md" - [ -f "$cf" ] && candidate="$cf" - fi - if [ -n "$candidate" ]; then - layer_paths+=("$candidate") - layer_strategies+=("$strategy") + print('absent\treplace\t') +except Exception as exc: + print(f'manifest_invalid: {exc}', file=sys.stderr) + sys.exit(3) +" 2>"$py_stderr"); then + parse_status=0 + else + parse_status=$? + fi + if [ "$parse_status" -ne 0 ]; then + if [ "$parse_status" -eq 2 ]; then + echo "Error: PyYAML is required to resolve preset template composition" >&2 + else + echo "Error: invalid preset manifest $manifest" >&2 fi - done <<< "$sorted_presets" + rm -f "$py_stderr" + return 2 + fi + if [ -n "$result" ]; then + local declaration + IFS=$'\t' read -r declaration strategy manifest_file <<< "$result" + [ "$declaration" = "found" ] && manifest_declared=true + strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]') + fi + rm -f "$py_stderr" fi - else - # python3 failed — fall back to unordered directory scan (replace only) - for preset in "$presets_dir"/*/; do - [ -d "$preset" ] || continue - local candidate="$preset/templates/${template_name}.md" - if [ -f "$candidate" ]; then - layer_paths+=("$candidate") - layer_strategies+=("replace") + + local candidate="" + if [ -n "$manifest_file" ]; then + case "$manifest_file" in + /*|*../*) manifest_file="" ;; + esac + fi + if [ -n "$manifest_file" ]; then + local mf="$presets_dir/$preset_id/$manifest_file" + [ -f "$mf" ] && candidate="$mf" + fi + if [ -z "$candidate" ] && [ "$manifest_declared" = false ]; then + local cf="$presets_dir/$preset_id/templates/${template_name}.md" + [ -f "$cf" ] && candidate="$cf" + if [ -z "$candidate" ]; then + cf="$presets_dir/$preset_id/${template_name}.md" + [ -f "$cf" ] && candidate="$cf" fi - done - fi - else - # No python3 or registry — fall back to unordered directory scan (replace only) - for preset in "$presets_dir"/*/; do - [ -d "$preset" ] || continue - local candidate="$preset/templates/${template_name}.md" - if [ -f "$candidate" ]; then + fi + if [ -n "$candidate" ]; then layer_paths+=("$candidate") - layer_strategies+=("replace") + layer_strategies+=("$strategy") + if [ "$strategy" = "replace" ]; then + effective_base_found=true + break + fi fi - done + done <<< "$sorted_presets" fi fi # Priority 3: Extension-provided templates (always "replace") local ext_dir="$repo_root/.specify/extensions" - if [ -d "$ext_dir" ]; then - for ext in "$ext_dir"/*/; do - [ -d "$ext" ] || continue - case "$(basename "$ext")" in .*) continue;; esac + if [ "$effective_base_found" = false ] && [ -d "$ext_dir" ]; then + local sorted_extensions="" + if ! sorted_extensions=$(_sorted_extension_ids "$ext_dir"); then + return 2 + fi + while IFS= read -r extension_id; do + [ -n "$extension_id" ] || continue + local ext="$ext_dir/$extension_id" local candidate="$ext/templates/${template_name}.md" + [ -f "$candidate" ] || candidate="$ext/${template_name}.md" if [ -f "$candidate" ]; then layer_paths+=("$candidate") layer_strategies+=("replace") + effective_base_found=true + break fi - done + done <<< "$sorted_extensions" fi # Priority 4: Core templates (always "replace") local core="$base/${template_name}.md" - if [ -f "$core" ]; then + if [ "$effective_base_found" = false ] && [ -f "$core" ]; then layer_paths+=("$core") layer_strategies+=("replace") fi @@ -557,12 +853,18 @@ except Exception: # If the top (highest-priority) layer is replace, it wins entirely — # lower layers are irrelevant regardless of their strategies. if [ "${layer_strategies[0]}" = "replace" ]; then - cat "${layer_paths[0]}" + if ! cat "${layer_paths[0]}"; then + echo "Error: failed to read template layer ${layer_paths[0]}" >&2 + return 2 + fi return 0 fi if [ "$has_composition" = false ]; then - cat "${layer_paths[0]}" + if ! cat "${layer_paths[0]}"; then + echo "Error: failed to read template layer ${layer_paths[0]}" >&2 + return 2 + fi return 0 fi @@ -578,12 +880,16 @@ except Exception: done if [ $base_idx -lt 0 ]; then - return 1 # no base layer found + echo "Error: template '$template_name' has composing layers but no replace base" >&2 + return 2 fi # Read the base content; compose layers above the base (higher priority) local content - content=$(cat "${layer_paths[$base_idx]}"; printf x) + if ! content=$(cat "${layer_paths[$base_idx]}"; status=$?; printf x; exit "$status"); then + echo "Error: failed to read template layer ${layer_paths[$base_idx]}" >&2 + return 2 + fi content="${content%x}" for (( i=base_idx-1; i>=0; i-- )); do @@ -591,26 +897,43 @@ except Exception: local strat="${layer_strategies[$i]}" local layer_content # Preserve trailing newlines - layer_content=$(cat "$path"; printf x) + if ! layer_content=$(cat "$path"; status=$?; printf x; exit "$status"); then + echo "Error: failed to read template layer $path" >&2 + return 2 + fi layer_content="${layer_content%x}" case "$strat" in replace) content="$layer_content" ;; - prepend) content="$(printf '%s\n\n%s' "$layer_content" "$content")" ;; - append) content="$(printf '%s\n\n%s' "$content" "$layer_content")" ;; + prepend) + content=$(printf '%s\n\n%s' "$layer_content" "$content"; printf x) + content="${content%x}" + ;; + append) + content=$(printf '%s\n\n%s' "$content" "$layer_content"; printf x) + content="${content%x}" + ;; wrap) case "$layer_content" in *'{CORE_TEMPLATE}'*) ;; - *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;; + *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 2 ;; esac - while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do - local before="${layer_content%%\{CORE_TEMPLATE\}*}" - local after="${layer_content#*\{CORE_TEMPLATE\}}" - layer_content="${before}${content}${after}" + # Consume the wrapper left to right instead of rewriting it in + # place. Rewriting re-scanned the string just modified, so base + # content holding a literal {CORE_TEMPLATE} reintroduced the + # token every pass and the loop never terminated. Advancing over + # ``rest`` bounds the work by the tokens in the original wrapper + # and leaves inserted content untouched, matching the single-pass + # semantics of .Replace()/.replace() in the PowerShell and Python + # ports. + local wrapped="" rest="$layer_content" + while [[ "$rest" == *'{CORE_TEMPLATE}'* ]]; do + wrapped="${wrapped}${rest%%\{CORE_TEMPLATE\}*}${content}" + rest="${rest#*\{CORE_TEMPLATE\}}" done - content="$layer_content" + content="${wrapped}${rest}" ;; - *) echo "Error: unknown strategy '$strat'" >&2; return 1 ;; + *) echo "Error: unknown strategy '$strat'" >&2; return 2 ;; esac done diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh index c9609764f..3ce2f9011 100755 --- a/.specify/scripts/bash/create-new-feature.sh +++ b/.specify/scripts/bash/create-new-feature.sh @@ -8,6 +8,7 @@ ALLOW_EXISTING=false SHORT_NAME="" BRANCH_NUMBER="" USE_TIMESTAMP=false +NUMBER_EXPLICIT=false ARGS=() i=1 while [ $i -le $# ]; do @@ -48,6 +49,9 @@ while [ $i -le $# ]; do exit 1 fi BRANCH_NUMBER="$next_arg" + if [ -n "$BRANCH_NUMBER" ]; then + NUMBER_EXPLICIT=true + fi ;; --timestamp) USE_TIMESTAMP=true @@ -60,7 +64,7 @@ while [ $i -le $# ]; do echo " --dry-run Compute feature name and paths without creating directories or files" echo " --allow-existing-branch Reuse an existing feature directory if it already exists" echo " --short-name <name> Provide a custom short name (2-4 words) for the feature" - echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)" echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" echo " --help, -h Show this help message" echo "" @@ -90,11 +94,25 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then exit 1 fi +MAX_FEATURE_NUMBER=9223372036854775807 +MAX_BRANCH_LENGTH=244 + +is_feature_number_in_range() { + local value="$1" + local normalized="${value#"${value%%[!0]*}"}" + [ -n "$normalized" ] || normalized=0 + [ ${#normalized} -lt ${#MAX_FEATURE_NUMBER} ] && return 0 + [ ${#normalized} -gt ${#MAX_FEATURE_NUMBER} ] && return 1 + # Equal-length digit strings must be compared without arithmetic overflow. + # shellcheck disable=SC2071 + [[ "$normalized" < "$MAX_FEATURE_NUMBER" || "$normalized" == "$MAX_FEATURE_NUMBER" ]] +} + # Function to get highest number from specs directory get_highest_from_specs() { local specs_dir="$1" local highest=0 - + if [ -d "$specs_dir" ]; then for dir in "$specs_dir"/*; do [ -d "$dir" ] || continue @@ -102,21 +120,152 @@ get_highest_from_specs() { # Match sequential prefixes (>=3 digits), but skip timestamp dirs. if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then number=$(echo "$dirname" | grep -Eo '^[0-9]+') - number=$((10#$number)) - if [ "$number" -gt "$highest" ]; then - highest=$number + if is_feature_number_in_range "$number"; then + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi fi fi done fi - + echo "$highest" } +# Return success when a spec directory owns the given numeric prefix. +spec_prefix_exists() { + local specs_dir="$1" + local feature_num="$2" + + for spec_path in "$specs_dir/${feature_num}-"*; do + [ -d "$spec_path" ] && return 0 + done + return 1 +} + # Function to clean and format a branch name +# +# Three details keep this consistent with the Python and PowerShell twins: +# * Unicode classification uses Python: POSIX [:alnum:] differs by platform. +# * `--*` instead of the GNU-only `\+`, which POSIX/BSD sed reads as a literal +# '+', leaving repeated separators uncollapsed on macOS. +# * printf instead of echo, so a name of "-n"/"-e"/"-E" is text, not options. +contains_non_ascii() { + LC_ALL=C grep -q '[^[:print:][:cntrl:]]' +} + +UNICODE_LOCALE="" +locale_candidates=(C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8 "${LC_CTYPE:-${LANG:-}}") +if [ -n "${LC_ALL:-}" ]; then + locale_candidates=("$LC_ALL") +fi +for candidate in "${locale_candidates[@]}"; do + if [ -n "$candidate" ] && [ "$(printf 'é。' | LC_ALL="$candidate" sed 's/[^[:alnum:]]/-/g' 2>/dev/null)" = 'é-' ]; then + UNICODE_LOCALE="$candidate" + break + fi +done + +if [ -z "$UNICODE_LOCALE" ]; then + UNICODE_LOCALE=C + if printf '%s' "${SHORT_NAME:-$FEATURE_DESCRIPTION}" | contains_non_ascii; then + if [ -n "${LC_ALL:-}" ]; then + echo "Error: A UTF-8 locale is required to create a Unicode feature name; LC_ALL=$LC_ALL is not usable" >&2 + else + echo "Error: A UTF-8 locale is required to create a Unicode feature name" >&2 + fi + exit 1 + fi +fi + +unicode_words() { + local name="${1//$'\n'/ }" + local separator="$2" + if printf '%s' "$name" | contains_non_ascii; then + local -a python_cmd=() + local override="${SPECKIT_PYTHON_EXECUTABLE:-${SPECKIT_PYTHON:-}}" + if [ -n "$override" ] && command -v "$override" >/dev/null 2>&1 && + "$override" -c 'import sys; raise SystemExit(sys.version_info.major != 3)' >/dev/null 2>&1; then + python_cmd=("$override") + else + local python_line + while IFS= read -r python_line; do + python_cmd+=("$python_line") + done < <(_python3_command) + fi + if [ "${#python_cmd[@]}" -eq 0 ]; then + echo "Error: Python 3 is required to create a Unicode feature name" >&2 + return 1 + fi + printf '%s' "$name" | "${python_cmd[@]}" -c ' +import sys +value = sys.stdin.buffer.read().decode("utf-8") +lower = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") +result = "".join( + char if char.isalpha() or char.isdecimal() else sys.argv[1] + for char in value.translate(lower) +) +sys.stdout.buffer.write(result.encode("utf-8")) +' "$separator" + else + printf '%s' "$name" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed "s/[^a-z0-9]/$separator/g" + fi +} + clean_branch_name() { local name="$1" - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' + local cleaned + cleaned=$(unicode_words "$name" '-') || return 1 + printf '%s\n' "$cleaned" | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//' +} + +branch_byte_count() { + printf '%s' "$1" | wc -c | tr -d '[:space:]' +} + +# Fit a feature prefix and suffix within GitHub's branch-name limit. +fit_branch_name() { + local feature_num="$1" + local branch_suffix="$2" + local branch_name="${feature_num}-${branch_suffix}" + + if [ "$(branch_byte_count "$branch_name")" -gt "$MAX_BRANCH_LENGTH" ]; then + local prefix_length=$(( ${#feature_num} + 1 )) + local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length)) + local truncated_suffix + local -x LC_ALL="$UNICODE_LOCALE" + local low=0 high=${#branch_suffix} mid + if (( high > max_suffix_length )); then + high=$max_suffix_length + fi + while (( low < high )); do + mid=$(((low + high + 1) / 2)) + if [ "$(branch_byte_count "${branch_suffix:0:$mid}")" -le "$max_suffix_length" ]; then + low=$mid + else + high=$((mid - 1)) + fi + done + truncated_suffix="${branch_suffix:0:$low}" + truncated_suffix="${truncated_suffix%-}" + branch_name="${feature_num}-${truncated_suffix}" + fi + + printf '%s' "$branch_name" +} + +# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote +# so the persistence hints match the Python variant exactly (printf %q output +# differs between bash versions and from shlex.quote for spaces/metachars). +shell_quote() { + local value="$1" LC_ALL=C + if [[ "$value" =~ ^[A-Za-z0-9_@%+=:,./-]+$ ]]; then + printf '%s' "$value" + else + local q="'\"'\"'" + printf "'%s'" "${value//\'/$q}" + fi } # Resolve repository root using common.sh functions which prioritize .specify @@ -135,35 +284,39 @@ fi # Function to generate branch name with stop word filtering and length filtering generate_branch_name() { local description="$1" - + # Common stop words to filter out local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" - - # Convert to lowercase and split into words - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') - + + # Use a UTF-8 locale for character-safe length checks and split words. + local -x LC_ALL="$UNICODE_LOCALE" + local clean_name + clean_name=$(unicode_words "$description" ' ') || return 1 + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) local meaningful_words=() for word in $clean_name; do # Skip empty words [ -z "$word" ] && continue - - # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) - if ! echo "$word" | grep -qiE "$stop_words"; then - if [ ${#word} -ge 3 ]; then + + # Retain non-ASCII words even when shorter than three characters. + if ! printf '%s\n' "$word" | LC_ALL=C grep -qE "$stop_words"; then + if [ ${#word} -ge 3 ] || printf '%s' "$word" | contains_non_ascii; then meaningful_words+=("$word") - elif echo "$description" | grep -q "\b${word^^}\b"; then - # Keep short words if they appear as uppercase in original (likely acronyms) + # Keep short words that appear as an uppercase acronym in the original. + # Uppercase via tr and match with grep -w (both portable) rather than + # bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX). + elif printf '%s' "$description" | LC_ALL=C grep -qw -- "$(printf '%s' "$word" | LC_ALL=C tr '[:lower:]' '[:upper:]')"; then meaningful_words+=("$word") fi fi done - + # If we have meaningful words, use first 3-4 of them if [ ${#meaningful_words[@]} -gt 0 ]; then local max_words=3 if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi - + local result="" local count=0 for word in "${meaningful_words[@]}"; do @@ -189,6 +342,10 @@ else BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") fi +if [ -z "$BRANCH_SUFFIX" ]; then + echo "[specify] Warning: Feature name is empty after removing unsupported characters. Use --short-name with letters or digits (for example, user-auth)." >&2 +fi + # Warn if --number and --timestamp are both specified if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then >&2 echo "[specify] Warning: --number is ignored when --timestamp is used" @@ -200,37 +357,67 @@ if [ "$USE_TIMESTAMP" = true ]; then FEATURE_NUM=$(date +%Y%m%d-%H%M%S) BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" else + if [ -n "$BRANCH_NUMBER" ] && [[ ! "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Error: --number must be an unsigned integer, got '$BRANCH_NUMBER'" >&2 + exit 1 + fi + + # Bash arithmetic is signed 64-bit; reject digit strings that would wrap. + if [ -n "$BRANCH_NUMBER" ] && ! is_feature_number_in_range "$BRANCH_NUMBER"; then + echo "Error: --number must be between 0 and $MAX_FEATURE_NUMBER, got '$BRANCH_NUMBER'" >&2 + exit 1 + fi + # Determine branch number from existing feature directories if [ -z "$BRANCH_NUMBER" ]; then HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + if [ "$HIGHEST" -eq "$MAX_FEATURE_NUMBER" ]; then + echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2 + exit 1 + fi BRANCH_NUMBER=$((HIGHEST + 1)) fi # Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") - BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + + # Treat an explicit number as a preference when its prefix is already used + # by a feature directory. Auto-detected numbers are already conflict-free. + if [ "$NUMBER_EXPLICIT" = true ]; then + SPEC_CONFLICT=false + REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX") + REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME" + if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then + spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true + fi + + if [ "$SPEC_CONFLICT" = true ]; then + REQUESTED_NUM="$FEATURE_NUM" + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$HIGHEST + while true; do + if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then + echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2 + exit 1 + fi + BRANCH_NUMBER=$((BRANCH_NUMBER + 1)) + FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") + spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break + done + >&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead" + fi + fi + fi # GitHub enforces a 244-byte limit on branch names # Validate and truncate if necessary -MAX_BRANCH_LENGTH=244 -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then - # Calculate how much we need to trim from suffix - # Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4 - PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 )) - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH)) - - # Truncate suffix at word boundary if possible - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) - # Remove trailing hyphen if truncation created one - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') - - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" - +ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX") +if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" + >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME ($(branch_byte_count "$ORIGINAL_BRANCH_NAME") bytes)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME ($(branch_byte_count "$BRANCH_NAME") bytes)" fi FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" @@ -246,24 +433,42 @@ if [ "$DRY_RUN" != true ]; then exit 1 fi + NEEDS_SPEC=false + SPEC_TEMPLATE_FOUND=false + SPEC_TEMPLATE_CONTENT="" + if [ ! -f "$SPEC_FILE" ]; then + NEEDS_SPEC=true + if SPEC_TEMPLATE_CONTENT=$(resolve_template_content "spec-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then + SPEC_TEMPLATE_CONTENT="${SPEC_TEMPLATE_CONTENT%x}" + SPEC_TEMPLATE_FOUND=true + else + resolve_status=$? + if [ "$resolve_status" -ne 1 ]; then + exit "$resolve_status" + fi + fi + fi + mkdir -p "$FEATURE_DIR" - if [ ! -f "$SPEC_FILE" ]; then - TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true - if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then - cp "$TEMPLATE" "$SPEC_FILE" + if [ "$NEEDS_SPEC" = true ]; then + if [ "$SPEC_TEMPLATE_FOUND" = true ]; then + printf '%s' "$SPEC_TEMPLATE_CONTENT" > "$SPEC_FILE" else echo "Warning: Spec template not found; created empty spec file" >&2 touch "$SPEC_FILE" fi fi - # Persist to .specify/feature.json so downstream commands can find the feature - _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR" + # Persist to .specify/feature.json so downstream commands can find the + # feature, unless the orchestrator opted out via SPECIFY_FEATURE_NO_PERSIST (#4129). + if [[ "${SPECIFY_FEATURE_NO_PERSIST:-}" != "1" && "${SPECIFY_FEATURE_NO_PERSIST:-}" != "true" ]]; then + _persist_feature_json "$REPO_ROOT" "$FEATURE_DIR" + fi # Inform the user how to set feature state in their own shell - printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 - printf '# export SPECIFY_FEATURE_DIRECTORY=%q\n' "$FEATURE_DIR" >&2 + printf '# To persist: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" >&2 + printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" >&2 fi if $JSON_MODE; then @@ -293,7 +498,7 @@ else echo "SPEC_FILE: $SPEC_FILE" echo "FEATURE_NUM: $FEATURE_NUM" if [ "$DRY_RUN" != true ]; then - printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" - printf '# export SPECIFY_FEATURE_DIRECTORY=%q\n' "$FEATURE_DIR" + printf '# To persist in your shell: export SPECIFY_FEATURE=%s\n' "$(shell_quote "$BRANCH_NAME")" + printf '# export SPECIFY_FEATURE_DIRECTORY=%s\n' "$(shell_quote "$FEATURE_DIR")" fi fi diff --git a/.specify/scripts/bash/resolve-template.sh b/.specify/scripts/bash/resolve-template.sh new file mode 100755 index 000000000..da05d2df6 --- /dev/null +++ b/.specify/scripts/bash/resolve-template.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +JSON_MODE=false +TEMPLATE_NAME="" + +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=true ;; + --help|-h) + echo "Usage: $0 <template-name> [--json]" + exit 0 + ;; + -*) + echo "ERROR: Unknown option '$arg'" >&2 + exit 1 + ;; + *) + if [[ -n "$TEMPLATE_NAME" ]]; then + echo "ERROR: Unexpected argument '$arg'" >&2 + exit 1 + fi + TEMPLATE_NAME="$arg" + ;; + esac +done + +if [[ -z "$TEMPLATE_NAME" ]]; then + echo "ERROR: Template name is required" >&2 + exit 1 +fi + +REPO_ROOT=$(get_repo_root) +if TEMPLATE_CONTENT=$(resolve_template_content "$TEMPLATE_NAME" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then + TEMPLATE_CONTENT="${TEMPLATE_CONTENT%x}" +else + echo "ERROR: Could not resolve required $TEMPLATE_NAME from the template override stack for $REPO_ROOT" >&2 + exit 1 +fi + +if $JSON_MODE; then + if has_jq; then + jq -cn \ + --arg template_name "$TEMPLATE_NAME" \ + --arg template_content "$TEMPLATE_CONTENT" \ + '{TEMPLATE_NAME:$template_name,TEMPLATE_CONTENT:$template_content}' + else + printf '{"TEMPLATE_NAME":"%s","TEMPLATE_CONTENT":"%s"}\n' \ + "$(json_escape "$TEMPLATE_NAME")" "$(json_escape "$TEMPLATE_CONTENT")" + fi +else + printf '%s' "$TEMPLATE_CONTENT" +fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh index cb679437a..9c8d16340 100755 --- a/.specify/scripts/bash/setup-plan.sh +++ b/.specify/scripts/bash/setup-plan.sh @@ -4,21 +4,21 @@ set -e # Parse command line arguments JSON_MODE=false -ARGS=() for arg in "$@"; do case "$arg" in - --json) - JSON_MODE=true + --json) + JSON_MODE=true ;; - --help|-h) + --help|-h) echo "Usage: $0 [--json]" echo " --json Output results in JSON format" echo " --help Show this help message" - exit 0 + exit 0 ;; - *) - ARGS+=("$arg") + *) + echo "ERROR: Unknown option '$arg'" >&2 + exit 1 ;; esac done @@ -43,22 +43,28 @@ if [[ -f "$IMPL_PLAN" ]]; then echo "Plan already exists at $IMPL_PLAN, skipping template copy" fi else - TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true - if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then - cp "$TEMPLATE" "$IMPL_PLAN" + template_file=$(mktemp) + trap 'rm -f "$template_file"' EXIT + if resolve_template_content "plan-template" "$REPO_ROOT" > "$template_file"; then + if [[ ! -s "$template_file" ]]; then + echo "ERROR: Plan template is empty; no new plan.md was created." >&2 + echo "Restore a valid plan template and rerun plan setup." >&2 + exit 1 + fi + cat "$template_file" > "$IMPL_PLAN" if $JSON_MODE; then echo "Copied plan template to $IMPL_PLAN" >&2 else echo "Copied plan template to $IMPL_PLAN" fi else - if $JSON_MODE; then - echo "Warning: Plan template not found" >&2 - else - echo "Warning: Plan template not found" + resolve_status=$? + if [ "$resolve_status" -ne 1 ]; then + exit "$resolve_status" fi - # Create a basic plan file if template doesn't exist - touch "$IMPL_PLAN" + echo "ERROR: Plan template not found; no new plan.md was created." >&2 + echo "Restore a valid plan template and rerun plan setup." >&2 + exit 1 fi fi @@ -68,17 +74,16 @@ if $JSON_MODE; then jq -cn \ --arg feature_spec "$FEATURE_SPEC" \ --arg impl_plan "$IMPL_PLAN" \ - --arg specs_dir "$FEATURE_DIR" \ + --arg feature_dir "$FEATURE_DIR" \ --arg branch "$CURRENT_BRANCH" \ - '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch}' + '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,FEATURE_DIR:$feature_dir,BRANCH:$branch}' else - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","FEATURE_DIR":"%s","BRANCH":"%s"}\n' \ "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" fi else echo "FEATURE_SPEC: $FEATURE_SPEC" - echo "IMPL_PLAN: $IMPL_PLAN" - echo "SPECS_DIR: $FEATURE_DIR" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "FEATURE_DIR: $FEATURE_DIR" echo "BRANCH: $CURRENT_BRANCH" fi - diff --git a/.specify/scripts/bash/setup-tasks.sh b/.specify/scripts/bash/setup-tasks.sh index ae0d7bdd5..0383f9f32 100755 --- a/.specify/scripts/bash/setup-tasks.sh +++ b/.specify/scripts/bash/setup-tasks.sh @@ -51,7 +51,9 @@ fi # Resolve tasks template through override stack TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true -if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then +if TASKS_TEMPLATE_CONTENT=$(resolve_template_content "tasks-template" "$REPO_ROOT"; status=$?; printf x; exit "$status"); then + TASKS_TEMPLATE_CONTENT="${TASKS_TEMPLATE_CONTENT%x}" +else echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2 echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2 exit 1 @@ -69,7 +71,8 @@ if $JSON_MODE; then --arg feature_dir "$FEATURE_DIR" \ --argjson docs "$json_docs" \ --arg tasks_template "${TASKS_TEMPLATE:-}" \ - '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template}' + --arg tasks_template_content "$TASKS_TEMPLATE_CONTENT" \ + '{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template,TASKS_TEMPLATE_CONTENT:$tasks_template_content}' else if [[ ${#docs[@]} -eq 0 ]]; then json_docs="[]" @@ -77,8 +80,8 @@ if $JSON_MODE; then json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done) json_docs="[${json_docs%,}]" fi - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s"}\n' \ - "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s","TASKS_TEMPLATE_CONTENT":"%s"}\n' \ + "$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")" "$(json_escape "$TASKS_TEMPLATE_CONTENT")" fi else echo "FEATURE_DIR: $FEATURE_DIR" diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md index c4aa1666e..c4c4ffb4c 100644 --- a/.specify/templates/checklist-template.md +++ b/.specify/templates/checklist-template.md @@ -4,18 +4,20 @@ **Created**: [DATE] **Feature**: [Link to spec.md or relevant documentation] -**Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements. +**Note**: This custom checklist is generated by the `/speckit-checklist` command based on feature context and requirements. +**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied. +**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete. -<!-- +<!-- ============================================================================ IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. - + The /speckit-checklist command MUST replace these with actual items based on: - User's specific checklist request - Feature requirements from spec.md - Technical context from plan.md - Implementation details from tasks.md - + DO NOT keep these sample items in the generated checklist file. ============================================================================ --> @@ -34,7 +36,10 @@ ## Notes -- Check items off as completed: `[x]` +- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied +- Leave items unchecked when they still require clarification, correction, or reviewer evaluation +- `/speckit-implement` reads checklist checkbox state as a gate and must not modify markers +- `checklists/requirements.md` has a separate built-in lifecycle maintained by `/speckit-specify` and `/speckit-clarify` - Add comments or findings inline - Link to relevant resources or documentation - Items are numbered sequentially for easy reference diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 92b96c716..be1aa8805 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -4,7 +4,7 @@ **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` -**Note**: This template is filled in by the `/speckit-plan` command. See `.specify/templates/plan-template.md` for the execution workflow. +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. ## Summary diff --git a/dev/spec-kit/presets/infrahub/preset.yml b/dev/spec-kit/presets/infrahub/preset.yml deleted file mode 100644 index 8431e30df..000000000 --- a/dev/spec-kit/presets/infrahub/preset.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -schema_version: "1.0" - -preset: - id: "infrahub" - name: "Infrahub Workflow" - version: "1.0.0" - description: "Infrahub-specific SpecKit customizations: enforces Jira/JPD ticket reference on feature branch creation" - author: "opsmill" - license: "MIT" - -requires: - speckit_version: ">=0.6.0" - -provides: - templates: - - type: "command" - name: "speckit.git.feature" - file: "commands/speckit.infrahub.feature.md" - description: "Validate Jira/JPD ticket reference and create a feature branch" - replaces: "speckit.git.feature" - strategy: "replace" - -tags: - - "infrahub" - - "jira" - - "branching" From 612213edf0d0c89c589a1e364801aa18b16861df Mon Sep 17 00:00:00 2001 From: Pol Michel <polmichel29@gmail.com> Date: Fri, 6 Nov 2026 10:10:25 +0100 Subject: [PATCH 3/5] chore(speckit): replace the infrahub branch hook with the git extension's The infrahub extension, added in #979, ran speckit.infrahub.feature before each specification to check the Jira reference in the branch name. Its command file was removed with the infrahub preset, so the hook pointed to a command that no longer exists. The jira-branch preset now does that check in speckit.git.feature, so the hook is the git extension's, as in opsmill/infrahub#11376. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .specify/extensions.yml | 10 +++----- .specify/extensions/infrahub/extension.yml | 29 ---------------------- 2 files changed, 4 insertions(+), 35 deletions(-) delete mode 100644 .specify/extensions/infrahub/extension.yml diff --git a/.specify/extensions.yml b/.specify/extensions.yml index b2f8c59c0..5100762eb 100644 --- a/.specify/extensions.yml +++ b/.specify/extensions.yml @@ -2,8 +2,6 @@ installed: - agent-context - checkpoint - critique -- id: infrahub - path: .specify/extensions/infrahub - opsmill settings: auto_execute_hooks: true @@ -17,12 +15,12 @@ hooks: description: Initialize Git repository before constitution setup condition: null before_specify: - - extension: infrahub - command: speckit.infrahub.feature + - extension: git + command: speckit.git.feature enabled: true optional: false - prompt: Execute speckit.infrahub.feature? - description: Validate Jira/JPD reference and create feature branch + prompt: Execute speckit.git.feature? + description: Create feature branch before specification condition: null before_clarify: - extension: git diff --git a/.specify/extensions/infrahub/extension.yml b/.specify/extensions/infrahub/extension.yml deleted file mode 100644 index ae5b6a6a6..000000000 --- a/.specify/extensions/infrahub/extension.yml +++ /dev/null @@ -1,29 +0,0 @@ -schema_version: "1.0" - -extension: - id: infrahub - name: "Infrahub Workflow" - version: "1.0.0" - description: "Infrahub-specific SpecKit customizations: enforces Jira/JPD ticket reference on feature branch creation" - author: opsmill - license: MIT - -requires: - speckit_version: ">=0.6.0" - -provides: - commands: - - name: speckit.infrahub.feature - file: commands/speckit.infrahub.feature.md - description: "Validate Jira/JPD ticket reference and create a feature branch" - -hooks: - before_specify: - command: speckit.infrahub.feature - optional: false - description: "Validate Jira/JPD reference and create feature branch" - -tags: - - "infrahub" - - "jira" - - "branching" From 7c1c83bccf653208e3a15bd3a388ce46a3f1925c Mon Sep 17 00:00:00 2001 From: Pol Michel <polmichel29@gmail.com> Date: Fri, 6 Nov 2026 10:19:51 +0100 Subject: [PATCH 4/5] chore(lint): exclude the spec-kit presets from rumdl The upgrade installs presets in .specify/presets, which rumdl checked, unlike .specify/extensions and .specify/templates. Spec Kit regenerates these files on each upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 868e7a0d3..fcd0f7025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -569,6 +569,7 @@ exclude = [ ".claude/skills", ".specify/templates", ".specify/extensions", + ".specify/presets", ] [tool.rumdl.MD025] From db1ddeaeaf4222a68e42d73eff7901af5527be39 Mon Sep 17 00:00:00 2001 From: Pol Michel <polmichel29@gmail.com> Date: Sun, 8 Nov 2026 19:36:12 +0100 Subject: [PATCH 5/5] chore(speckit): run the spec-kit scripts with the project's Python The plan, tasks and template scripts of Spec Kit 1.2 need PyYAML, which the system python3 often lacks. The Claude Code and Codex hooks that opsmill-speckit 1.4.1 ships point them at the project environment when it has PyYAML, and do nothing otherwise. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9ZEsTmHy1shuNRt88HiUb --- .claude/settings.json | 15 +++++++++++++++ .codex/hooks.json | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .codex/hooks.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..e1deb4bac --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.specify/extensions/opsmill/scripts/bash/python-env.sh" + } + ] + } + ] + } +} diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 000000000..95eda57ca --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "command", + "command": "sh -c '\"$(git rev-parse --show-toplevel)\"/.specify/extensions/opsmill/scripts/bash/route-codex.sh'" + } + ] + } + ] + } +}