diff --git a/.agents/skills/implementing/SKILL.md b/.agents/skills/implementing/SKILL.md new file mode 100644 index 0000000..4a22872 --- /dev/null +++ b/.agents/skills/implementing/SKILL.md @@ -0,0 +1,148 @@ +--- +name: implementing +description: Non-interactive sandbox implementation from an approved spec to a PR. Use when the user mentions "implement", "implement spec", "implement specs/.md", sandbox implementation, or autonomous implementation from a specification. +license: MIT +metadata: + author: VTEX + version: "1.2.1" +--- + +# Implementing — Spec to Code + +Implement a feature **autonomously** from an approved `specs/.md`. + +## Execution model + +This skill assumes a **sandboxed, asynchronous** run: **no human interaction** during execution (no questions, confirmations, or waiting for replies). + +| | | +|---|---| +| **Input** | `specs/.md` (must be `Approved` — see Phase 1) | +| **Success output** | Pull Request on `feat/`, spec status → `Done` | +| **Blocked output** | Agent **ends** without a completed feature PR; a **GitHub issue** documents why implementation could not finish | + +**Pipeline (end-to-end):** the **specification** skill produces the spec; this skill consumes it. There is **no intermediate planning artifact or skill** — you plan, decompose work, and sequence tasks internally (same idea as composable workflows in [superpowers](https://github.com/obra/superpowers), but without an extra handoff step). The spec is the single handoff contract, analogous to how [autoresearch](https://github.com/karpathy/autoresearch) treats `program.md` as the human-authored context and keeps a tight edit surface for the agent. + +**Operating loop:** pick a user story → implement with tests → verify against acceptance criteria → commit → repeat until every story is done. **Evidence over claims:** nothing is “done” until tests and checks prove it. + +## Constraints + +**What you CAN do:** +- Create and modify source code, tests, and configuration files required by the spec +- Install dependencies explicitly mentioned in the Technical Contract or required by the chosen approach +- Create branches, commit, and open PRs +- Open a **GitHub issue** (or equivalent) when implementation is **impossible** to complete after tie-breakers and reasonable effort — see *Non-interactive execution* + +**What you CANNOT do:** +- Implement features, endpoints, or behaviors not described in the spec +- Change the spec file itself — if the spec is wrong, contradictory, or blocking, follow *Non-interactive execution* (assumptions in PR, or issue + terminate) +- Skip writing tests for a user story that has acceptance criteria +- Merge or push to the main branch without explicit user consent (from platform policy outside this run) +- Execute DDL, DML, or other operations that change the schema or data of any database the application connects to via its configured environment — including applying migrations against that connection or running seeds, fixtures, or resets against it + +**The goal is simple: make every acceptance criterion pass.** The spec is the single source of truth. If the spec says it, implement it. If the spec doesn't say it, don't. + +## Workflow + +### Phase 1: Load and validate + +1. Read the full `specs/.md` +2. Check that the status is `Approved`. If it is **not** `Approved` (e.g. `Draft`): **end the run** immediately. Do **not** open an implementation PR. Emit a **structured report** in sandbox logs/output (reason, current status, path to the spec file) so the orchestrator can mark the job failed. +3. Extract from the spec: + - **User Stories + Acceptance Criteria** → the work units + - **Key Scenarios** → the test cases + - **Arch Decisions** → the technical approach and constraints + - **Technical Contract** → interfaces, models, and boundaries to implement exactly +4. If the spec references repositories you don't have context on, use the GitHub tool to fetch their structure, README, and dependencies + +**Internal planning:** derive order of work, file touch list, and test strategy from the spec and repo. No separate plan-approval step. + +### Phase 2: Codebase reconnaissance + +Before writing any code, understand the existing codebase: + +- Project structure, conventions, and patterns already in use +- Existing tests: framework, naming conventions, where they live +- Dependency management: what's already installed, what's available +- CI/CD: how tests are run, linting rules, build steps + +Adapt your implementation to match existing patterns. Don't introduce new conventions unless the spec explicitly calls for it. + +### Phase 3: Implementation loop + +Work through user stories one at a time. For each story: + +``` +LOOP per user story: + +1. Write failing tests derived from the acceptance criteria and key scenarios +2. Run the tests — confirm they fail for the right reason +3. Implement the minimal code to make the tests pass +4. Run the tests — confirm they pass +5. Refactor if needed (tests must still pass after) +6. Commit with a descriptive message +7. Move to the next story +``` + +**Rules:** +- Follow the architecture described in Arch Decisions — don't contradict accepted decisions +- Implement interfaces and models exactly as defined in the Technical Contract +- One commit per user story (or per logical unit if a story is large) +- Commit messages follow the repo's existing convention; if none exists, use: `feat: ` +- If a test fails after implementation and you cannot fix it after **reasonable attempts** (e.g. 3+ focused tries), treat this as **blocking** → *Non-interactive execution* (issue + terminate). Do not force a green build by gutting tests or the spec. + +**If something goes wrong:** +- Implementation breaks existing tests → fix the regression before moving on; if the fix is not achievable without violating the spec or breaks invariants, treat as **blocking** → issue + terminate +- Ambiguity in acceptance criteria → apply **tie-break order**: **Key Scenarios** → **Arch Decisions** → **Technical Contract** → **repository conventions**. If ambiguity is **resolved**, document **Assumptions** in the PR body (dedicated section). If still **unresolvable** (no coherent reading), treat as **blocking** → issue + terminate +- A dependency is missing → install it if possible and note it in the PR summary; if it cannot be installed in the sandbox, treat as **blocking** → issue + terminate +- The spec contradicts itself in a way tie-breakers cannot reconcile → **blocking** → issue + terminate; do not guess + +### Phase 4: Verification + +After all user stories are implemented: + +1. Run the full test suite — everything must pass +2. Walk through the Key Scenarios table and confirm each scenario is covered by a test +3. Check that no files were modified outside the scope of the spec +4. Review your own changes: look for leftover debug code, TODOs, or unused imports + +### Phase 5: Deliver (success path) + +1. Update the spec status from `Approved` to `Done` +2. Open a Pull Request: + - Branch: `feat/` + - Title: `feat: ` + - Body: use **sections** (not a live chat summary): + - **Summary** — what was implemented, per user story + - **Tests** — what was added or changed + - **Assumptions** — explicit assumptions from tie-breakers (omit section if none) + - **Deviations** — any deviation from the spec with justification (omit if none) + - **Follow-ups** — risks or optional next steps (omit if none) + - **Spec** — link to `specs/.md` + - Reference the spec PR if it exists + +## Non-interactive execution + +There is **no** human in the loop during the run: do not ask questions or wait for answers. + +**Success:** proof lives in the **PR description** and **passing tests** (and CI, if applicable). + +**Blocked — impossible to complete the spec:** +1. **Stop** — do not open a PR that claims the feature is done. +2. **Open a GitHub issue** with at minimum: + - Title pattern: `implementing blocked: ` + - Link to `specs/.md` + - What was attempted (stories, commits, branches if any) + - Objective reason for the block (contradiction, missing dependency, irresolvable ambiguity, tests/CI that cannot be satisfied, etc.) + - Evidence: relevant spec excerpts, error output, failing test names +3. **End the agent run.** Sandbox logs should reflect failure for the orchestrator. + +Optional: push a WIP branch only if the orchestrator requires a trace — the **issue** is the primary failure artifact. + +## Important + +- The spec is law — don't add features, don't skip requirements +- Tests are not optional — every acceptance criterion must have a corresponding test +- Match existing codebase patterns — don't impose new conventions +- Keep commits atomic — one story, one commit +- Never silently work around a broken spec — resolve with documented assumptions in the PR, or file an issue and stop diff --git a/.agents/skills/specification/SKILL.md b/.agents/skills/specification/SKILL.md new file mode 100644 index 0000000..da7d8c3 --- /dev/null +++ b/.agents/skills/specification/SKILL.md @@ -0,0 +1,150 @@ +--- +name: specification +description: Generate a Spec Driven Development (SDD) document containing Business Context, Arch Decisions, and Technical Contract. Use when the user mentions "spec", "init spec", "create specification", references a file in the specs/ folder, or wants to create a feature specification. +license: MIT +metadata: + author: VTEX + version: "1.2.0" +--- + +# Specification — Spec Driven Development + +Generate a complete SDD for a feature and write it as a structured markdown file to `specs/.md`. Uses a **hybrid approach**: for simple, well-described features the agent generates the full spec in a single pass; for complex or ambiguous features it gathers requirements interactively before writing. + +**End-to-end flow:** this skill is the **start** of the pipeline; **implementing** is the **end**. After the spec is `Approved`, the **implementing** skill takes over. There is **no required intermediate step** (no separate planning doc or skill) — capable agents plan and break work internally from the approved spec. + +## Sections + +A specification always contains all three sections: + +| # | Section | Purpose | Depends on | +|---|---------|---------|------------| +| 1 | **Business Context** | Problem, goals, requirements, acceptance criteria, key scenarios | — | +| 2 | **Arch Decisions** | Technical approach, plan & key architecture decisions | Business Context | +| 3 | **Technical Contract** | Interfaces, models, boundaries | Business Context + Arch Decisions | + +## Workflow + +### Phase 1: Identify feature + +Determine what feature the specification is for (to derive the filename). If the user's message already names the feature, move on. Otherwise, ask. + +### Phase 2: Repository context + +If the user did **not** mention which repository (or repositories) the feature relates to, **ask before proceeding**. Use the AskQuestion tool so the user can type or select the repo(s). + +Once you know the repositories, use the **GitHub** tool to fetch context and understand what already exists: + +- Repository metadata (description, language, topics) +- Languages and tech stack breakdown +- README contents (project overview) +- Dependency files (`package.json`, `requirements.txt`, `go.mod`, etc.) +- Existing specs in the `specs/` directory (to avoid duplication) + +Use the gathered context to build a **repository profile** containing: +- Primary language and tech stack +- Project purpose and domain +- Existing patterns, frameworks, and conventions +- Known dependencies and integration points +- Existing specifications (to avoid duplication) + +This profile is used in Phase 2.5 to decide whether the agent can generate the spec directly or needs to ask more questions. + +### Phase 2.5: Assess completeness + +Evaluate whether the user's initial message **plus** the repository profile already provide enough information to generate all three sections. Check each section against its core questions: + +| Section | Can generate if you know… | +|---|---| +| **Business Context** | The problem, who it affects, expected outcome, acceptance criteria per story, and key scenarios | +| **Arch Decisions** | The proposed technical approach, its trade-offs, and key architecture decisions | +| **Technical Contract** | The interfaces, models, or boundaries involved | + +**Decision rules:** + +- **All sections covered** → skip Phase 3 entirely, go to Phase 4 (single-pass) +- **Some gaps** → ask only about the gaps (targeted discovery) +- **Mostly unknown** → run full Phase 3 (interactive) + +When in doubt, prefer asking over assuming — a wrong spec is worse than a slow one. + +### Phase 3: Discovery (may be skipped) + +> **Skip this phase** if Phase 2.5 determined all sections can be generated (single-pass). + +Gather information through conversation. Use the repository profile from Phase 2 and the gap analysis from Phase 2.5 to guide which questions to ask. + +**Rules:** +- **Only ask about gaps identified in Phase 2.5** — don't re-cover what's already known +- **Skip questions that the repo context already answers** (e.g., don't ask about tech stack if `package.json` reveals it) +- **Pre-fill what you can infer** and confirm with the user instead of asking from scratch (e.g., "Based on the repo, this is a Node.js service using Express — is that correct?") +- **Only ask about what's genuinely unknown** — focus on intent, business rules, and decisions that code alone can't reveal + +**Business Context questions — Problem & Requirements** +- What problem are we solving? +- Who is affected and how? +- What happens if we don't solve it? +- What are the expected outcomes? +- What are the functional and non-functional requirements? +- Are there constraints or dependencies? +- For each user story: what are the acceptance criteria? (use given/when/then format) +- What are the key scenarios — happy path, error cases, and edge cases? What pre-conditions, steps, and expected results define each? + +**Arch Decisions questions — Technical Approach & Decisions** +- What is the proposed solution? +- What alternatives were considered and why were they rejected? +- What are the risks and how do we mitigate them? +- What key architectural or design decisions need to be made? +- For each decision: what is the context, the options, and the chosen approach? +- What is the implementation plan? + +**Technical Contract questions — Interfaces & Boundaries** +- What interfaces, data models, or system boundaries does this feature define? +- What are the inputs and outputs? +- What are the integration points with other systems or modules? + +### Phase 4: Writing + +Create the file at `specs/.md` using the template in [references/template.md](references/template.md). + +Rules: +- Use kebab-case for the filename (e.g., `specs/user-authentication.md`) +- Create the `specs/` directory at the project root if it doesn't exist +- Fill every section — no placeholders or TODOs +- Every user story must have acceptance criteria in given/when/then format +- Key Scenarios table must include at least one happy path, one error case, and one edge case +- Keep language direct and concise +- Use diagrams (mermaid) when they clarify flow or architecture + +### Phase 5: Review & Deliver + +After writing, present a summary of what was generated and ask if any section needs refinement. + +Once the user is satisfied, open a Pull Request with **only** the spec file: + +1. Create branch `spec/` from the base branch +2. Stage only `specs/.md` — no other files +3. Verify with `git status` that nothing else is staged before committing +4. Commit with message `spec: ` +5. Push and create the PR: + - Title: `spec: ` + - Body: summary of the spec contents + +## Lifecycle + +| Status | Meaning | Trigger | +|---|---|---| +| `Draft` | Written, awaiting review | Spec generated | +| `Approved` | Reviewed and accepted for implementation | User approves | +| `Done` | Fully implemented | Implementation complete | + +Update the status in the document header as the specification progresses. + +Once a spec is `Approved`, it is implemented using the **implementing** skill only — that skill loads the spec, reconnoiters the repo, runs a test-first implementation loop, verifies, and delivers (PR + status `Done`). No extra handoff artifact is required between specification and implementation. + +## Important + +- Never generate a section without having **sufficient information** for it — whether provided in the user's initial message, inferred from the repository context, or gathered through discovery questions +- Ask clarifying questions when answers are vague +- Adapt the number of Key Decisions to what the feature actually requires — don't force unnecessary decisions +- The Technical Contract section should be technology-agnostic unless the user specifies a stack diff --git a/.agents/skills/specification/references/template.md b/.agents/skills/specification/references/template.md new file mode 100644 index 0000000..22f4ad3 --- /dev/null +++ b/.agents/skills/specification/references/template.md @@ -0,0 +1,106 @@ +# {Feature Name} + +> **Status**: {Draft | Approved | Done} +> **Created**: {date} + +## 1. Business Context + +### Problem Statement + +{Why this feature exists. What problem it solves and for whom.} + +### Goals + +{Measurable outcomes this feature should achieve.} + +### User Stories + +{Key user stories in "As a [role], I want [action], so that [benefit]" format. Each story includes acceptance criteria.} + +#### US-1: {Story Title} + +- **Story**: As a {role}, I want {action}, so that {benefit}. +- **Acceptance Criteria**: + - **Given** {pre-condition}, **when** {action}, **then** {expected result}. + - **Given** {pre-condition}, **when** {action}, **then** {expected result}. + +{Repeat for each user story.} + +### Key Scenarios + +| Scenario | Pre-conditions | Steps | Expected Result | +|---|---|---|---| +| {Happy path} | {Initial state} | {Action sequence} | {Success outcome} | +| {Error case} | {Initial state} | {Action that fails} | {Error handling behavior} | +| {Edge case} | {Boundary state} | {Action sequence} | {Expected behavior} | + +### Functional Requirements + +{What the system must do.} + +### Non-Functional Requirements + +{Performance, security, scalability, accessibility constraints.} + +### Out of Scope + +{What this feature explicitly does NOT cover.} + +--- + +## 2. Arch Decisions + +### Proposed Solution + +{High-level description of the technical approach.} + +### Architecture Overview + +{System architecture, component interactions. Use mermaid diagrams when helpful.} + +### Alternatives Considered + +| Alternative | Pros | Cons | Verdict | +|---|---|---|---| +| {Option} | {Pros} | {Cons} | {Why accepted/rejected} | + +### Risks & Mitigations + +| Risk | Impact | Likelihood | Mitigation | +|---|---|---|---| +| {Risk} | {High/Med/Low} | {High/Med/Low} | {Strategy} | + +### Key Decisions + +#### Decision 1: {Decision Title} + +- **Status**: Accepted +- **Context**: {Why this decision is needed} +- **Decision**: {What was decided} +- **Consequences**: {Trade-offs and implications} + +{Repeat for each significant decision.} + +### Implementation Plan + +{Phased rollout, milestones, or sequencing of work.} + +--- + +## 3. Technical Contract + +### Data Models + +{Core entities, their attributes, and relationships.} + +### Interfaces + +{Public APIs, function signatures, event contracts, or module boundaries this feature exposes or consumes.} + +### Integration Points + +{How this feature connects with other systems, services, or modules.} + +### Invariants & Constraints + +{Rules that must always hold true in this system.} diff --git a/.specify/init-options.json b/.specify/init-options.json new file mode 100644 index 0000000..b13053d --- /dev/null +++ b/.specify/init-options.json @@ -0,0 +1,10 @@ +{ + "ai": "cursor-agent", + "ai_skills": true, + "branch_numbering": "sequential", + "context_file": ".cursor/rules/specify-rules.mdc", + "here": true, + "integration": "cursor-agent", + "script": "sh", + "speckit_version": "0.8.16.dev0" +} \ No newline at end of file diff --git a/.specify/integration.json b/.specify/integration.json new file mode 100644 index 0000000..819ca37 --- /dev/null +++ b/.specify/integration.json @@ -0,0 +1,15 @@ +{ + "version": "0.8.16.dev0", + "integration_state_schema": 1, + "installed_integrations": [ + "cursor-agent" + ], + "integration_settings": { + "cursor-agent": { + "script": "sh", + "invoke_separator": "-" + } + }, + "integration": "cursor-agent", + "default_integration": "cursor-agent" +} diff --git a/.specify/integrations/cursor-agent.manifest.json b/.specify/integrations/cursor-agent.manifest.json new file mode 100644 index 0000000..a96f6c7 --- /dev/null +++ b/.specify/integrations/cursor-agent.manifest.json @@ -0,0 +1,16 @@ +{ + "integration": "cursor-agent", + "version": "0.8.16.dev0", + "installed_at": "2026-05-27T13:44:26.425866+00:00", + "files": { + ".cursor/skills/speckit-analyze/SKILL.md": "23d10a774acad3a26e5ab01041eb65a39e627aadee6e0e366d79713ad054fdf6", + ".cursor/skills/speckit-checklist/SKILL.md": "2db3121cd5d97ca2c0902be413d39d9ec0d9323932a976330eefa208e72a483a", + ".cursor/skills/speckit-clarify/SKILL.md": "0566494550803c6c07a785f49c7ce9178edc8addb4c815e0cd84069a1150ccee", + ".cursor/skills/speckit-constitution/SKILL.md": "7310adee465ee6a439a689518e6c133ce851d808c7a6e64d66f659ad8b4bd56e", + ".cursor/skills/speckit-implement/SKILL.md": "f5b7900551975d4064bcaff71aaa2b93241da425cbd3ec5cdeaaadbdb3d97543", + ".cursor/skills/speckit-plan/SKILL.md": "310e11b00061e65db2e1772b9e5b404a0dc77ec9eec9658eb6747fc383f3bee6", + ".cursor/skills/speckit-specify/SKILL.md": "54c65816e50c5d1cd6f6da8492eff07e34232f7290b60274e5cca2ab4728939e", + ".cursor/skills/speckit-tasks/SKILL.md": "a7ffbf17012215478e4f90024874b7a8b148c64792a7ea580df21d0c486e9dbf", + ".cursor/skills/speckit-taskstoissues/SKILL.md": "32d8ee0f0482a7b2b9e6bee4dfd6947465754f2f82c404bfc7ef64f4a3b63bdd" + } +} diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json new file mode 100644 index 0000000..c794891 --- /dev/null +++ b/.specify/integrations/speckit.manifest.json @@ -0,0 +1,17 @@ +{ + "integration": "speckit", + "version": "0.8.16.dev0", + "installed_at": "2026-05-27T13:44:26.447004+00:00", + "files": { + ".specify/scripts/bash/common.sh": "dd638316259e699fd466542c77ef16af5eb198efe0447c081f86b890db414ba8", + ".specify/scripts/bash/setup-plan.sh": "b23cca3d769a217ab812a6059adb549622471f6893af234cf98ca2019ac4e1a1", + ".specify/scripts/bash/setup-tasks.sh": "e8d050c63c5afb664a8b671b0b0155513fb9cab0567b335e16b9eb035482aad2", + ".specify/scripts/bash/check-prerequisites.sh": "3fed31d68ceb4a8ccd02f650581446f20acb28f0da9cac29f258657c91300833", + ".specify/scripts/bash/create-new-feature.sh": "bcf4964ca0c6c78717bb42d9e66b8c7e5ee82779cd96afc5aa7b08b75abe5790", + ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3", + ".specify/templates/checklist-template.md": "c37695297e5d3153d64f82c21223509940b13932046c7961c42d1d669516130c", + ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842", + ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e", + ".specify/templates/plan-template.md": "cc7f7979cf8d8836ec26492785affd80791d3422a2b745062ec695be8c985ef7" + } +} diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh new file mode 100755 index 0000000..b244ea7 --- /dev/null +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash + +# Consolidated prerequisite checking script +# +# This script provides unified prerequisite checking for Spec-Driven Development workflow. +# It replaces the functionality previously spread across multiple scripts. +# +# Usage: ./check-prerequisites.sh [OPTIONS] +# +# OPTIONS: +# --json Output in JSON format +# --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) +# --help, -h Show help message +# +# OUTPUTS: +# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} +# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md +# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. + +set -e + +# Parse command line arguments +JSON_MODE=false +REQUIRE_TASKS=false +INCLUDE_TASKS=false +PATHS_ONLY=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --require-tasks) + REQUIRE_TASKS=true + ;; + --include-tasks) + INCLUDE_TASKS=true + ;; + --paths-only) + PATHS_ONLY=true + ;; + --help|-h) + cat << 'EOF' +Usage: check-prerequisites.sh [OPTIONS] + +Consolidated prerequisite checking for Spec-Driven Development workflow. + +OPTIONS: + --json Output in JSON format + --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) + --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 + exit 1 + ;; + esac +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; } +eval "$_paths_output" +unset _paths_output + +# If paths-only mode, output paths and exit (no validation) +if $PATHS_ONLY; then + if $JSON_MODE; then + # Minimal JSON paths payload (no validation performed) + if has_jq; then + jq -cn \ + --arg repo_root "$REPO_ROOT" \ + --arg branch "$CURRENT_BRANCH" \ + --arg feature_dir "$FEATURE_DIR" \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg tasks "$TASKS" \ + '{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}' + else + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")" + fi + else + echo "REPO_ROOT: $REPO_ROOT" + echo "BRANCH: $CURRENT_BRANCH" + echo "FEATURE_DIR: $FEATURE_DIR" + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "TASKS: $TASKS" + fi + exit 0 +fi + +# Validate branch name +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# Validate required directories and files +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 + exit 1 +fi + +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.plan first to create the implementation plan." >&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 + echo "Run /speckit.tasks first to create the task list." >&2 + exit 1 +fi + +# Build list of available documents +docs=() + +# Always check these optional docs +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") + +# Check contracts directory (only if it exists and has files) +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi + +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Include tasks.md if requested and it exists +if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then + docs+=("tasks.md") +fi + +# Output results +if $JSON_MODE; then + # Build JSON array of documents + if has_jq; then + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + 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}' + else + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + 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" + 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 +fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh new file mode 100755 index 0000000..03141e4 --- /dev/null +++ b/.specify/scripts/bash/common.sh @@ -0,0 +1,645 @@ +#!/usr/bin/env bash +# Common functions and variables for all scripts + +# Find repository root by searching upward for .specify directory +# This is the primary marker for spec-kit projects +find_specify_root() { + local dir="${1:-$(pwd)}" + # Normalize to absolute path to prevent infinite loop with relative paths + # Use -- to handle paths starting with - (e.g., -P, -L) + dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1 + local prev_dir="" + while true; do + if [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + # Stop if we've reached filesystem root or dirname stops changing + if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then + break + fi + prev_dir="$dir" + dir="$(dirname "$dir")" + done + return 1 +} + +# Get repository root, prioritizing .specify directory over git +# This prevents using a parent git repo when spec-kit is initialized in a subdirectory +get_repo_root() { + # First, look for .specify directory (spec-kit's own marker) + local specify_root + if specify_root=$(find_specify_root); then + echo "$specify_root" + return + fi + + # Fallback to git if no .specify found + if git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + return + fi + + # Final fallback to script location for non-git repos + local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) +} + +# Get current branch, with fallback for non-git repositories +get_current_branch() { + # First check if SPECIFY_FEATURE environment variable is set + if [[ -n "${SPECIFY_FEATURE:-}" ]]; then + echo "$SPECIFY_FEATURE" + return + fi + + # Then check git if available at the spec-kit root (not parent) + local repo_root=$(get_repo_root) + if has_git; then + git -C "$repo_root" rev-parse --abbrev-ref HEAD + return + fi + + # For non-git repos, try to find the latest feature directory + local specs_dir="$repo_root/specs" + + if [[ -d "$specs_dir" ]]; then + local latest_feature="" + local highest=0 + local latest_timestamp="" + + for dir in "$specs_dir"/*; do + if [[ -d "$dir" ]]; then + local dirname=$(basename "$dir") + if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then + # Timestamp-based branch: compare lexicographically + local ts="${BASH_REMATCH[1]}" + if [[ "$ts" > "$latest_timestamp" ]]; then + latest_timestamp="$ts" + latest_feature=$dirname + fi + elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then + local number=${BASH_REMATCH[1]} + number=$((10#$number)) + if [[ "$number" -gt "$highest" ]]; then + highest=$number + # Only update if no timestamp branch found yet + if [[ -z "$latest_timestamp" ]]; then + latest_feature=$dirname + fi + fi + fi + fi + done + + if [[ -n "$latest_feature" ]]; then + echo "$latest_feature" + return + fi + fi + + echo "main" # Final fallback +} + +# Check if we have git available at the spec-kit root level +# Returns true only if git is installed and the repo root is inside a git work tree +# Handles both regular repos (.git directory) and worktrees/submodules (.git file) +has_git() { + # First check if git command is available (before calling get_repo_root which may use git) + command -v git >/dev/null 2>&1 || return 1 + local repo_root=$(get_repo_root) + # Check if .git exists (directory or file for worktrees/submodules) + [ -e "$repo_root/.git" ] || return 1 + # Verify it's actually a valid git work tree + git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + +# Strip a single optional path segment (e.g. gitflow "feat/004-name" -> "004-name"). +# Only when the full name is exactly two slash-free segments; otherwise returns the raw name. +spec_kit_effective_branch_name() { + local raw="$1" + if [[ "$raw" =~ ^([^/]+)/([^/]+)$ ]]; then + printf '%s\n' "${BASH_REMATCH[2]}" + else + printf '%s\n' "$raw" + fi +} + +check_feature_branch() { + local raw="$1" + local has_git_repo="$2" + + # For non-git repos, we can't enforce branch naming but still provide output + if [[ "$has_git_repo" != "true" ]]; then + echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 + return 0 + fi + + local branch + branch=$(spec_kit_effective_branch_name "$raw") + + # Accept sequential prefix (3+ digits) but exclude malformed timestamps + # Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022") + local is_sequential=false + if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then + is_sequential=true + fi + if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then + echo "ERROR: Not on a feature branch. Current branch: $raw" >&2 + echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2 + return 1 + fi + + return 0 +} + +# Safely read .specify/feature.json's "feature_directory" value. +# Prints the raw value (possibly relative) to stdout, or empty string if the file +# is missing, unparseable, or does not contain the key. Always returns 0 so callers +# under `set -e` cannot be aborted by parser failure. +# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed. +read_feature_json_feature_directory() { + local repo_root="$1" + local fj="$repo_root/.specify/feature.json" + [[ -f "$fj" ]] || { printf '%s' ''; return 0; } + + 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 + # 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 + _fd='' + fi + else + # 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; } \ + | head -n 1 \ + | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' ) + fi + + printf '%s' "$_fd" + return 0 +} + +# Returns 0 when .specify/feature.json lists feature_directory that exists as a directory +# and matches the resolved active FEATURE_DIR (so /speckit.plan can skip git branch pattern checks). +# Delegates parsing to read_feature_json_feature_directory, which is safe under `set -e`. +feature_json_matches_feature_dir() { + local repo_root="$1" + local active_feature_dir="$2" + + local _fd + _fd=$(read_feature_json_feature_directory "$repo_root") + + [[ -n "$_fd" ]] || return 1 + [[ "$_fd" != /* ]] && _fd="$repo_root/$_fd" + [[ -d "$_fd" ]] || return 1 + + local norm_json norm_active + norm_json="$(cd -- "$_fd" 2>/dev/null && pwd -P)" || return 1 + norm_active="$(cd -- "$active_feature_dir" 2>/dev/null && pwd -P)" || return 1 + + [[ "$norm_json" == "$norm_active" ]] +} + +# Find feature directory by numeric prefix instead of exact branch match +# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +find_feature_dir_by_prefix() { + local repo_root="$1" + local branch_name + branch_name=$(spec_kit_effective_branch_name "$2") + local specs_dir="$repo_root/specs" + + # Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches) + local prefix="" + if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then + prefix="${BASH_REMATCH[1]}" + elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then + prefix="${BASH_REMATCH[1]}" + else + # If branch doesn't have a recognized prefix, fall back to exact match + echo "$specs_dir/$branch_name" + return + fi + + # Search for directories in specs/ that start with this prefix + local matches=() + if [[ -d "$specs_dir" ]]; then + for dir in "$specs_dir"/"$prefix"-*; do + if [[ -d "$dir" ]]; then + matches+=("$(basename "$dir")") + fi + done + fi + + # Handle results + if [[ ${#matches[@]} -eq 0 ]]; then + # No match found - return the branch name path (will fail later with clear error) + echo "$specs_dir/$branch_name" + elif [[ ${#matches[@]} -eq 1 ]]; then + # Exactly one match - perfect! + echo "$specs_dir/${matches[0]}" + else + # Multiple matches - this shouldn't happen with proper naming convention + echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 + echo "Please ensure only one spec directory exists per prefix." >&2 + return 1 + fi +} + +get_feature_paths() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + local has_git_repo="false" + + if has_git; then + has_git_repo="true" + fi + + # Resolve feature directory. Priority: + # 1. SPECIFY_FEATURE_DIRECTORY env var (explicit override) + # 2. .specify/feature.json "feature_directory" key (persisted by /speckit.specify) + # 3. Branch-name-based prefix lookup (legacy fallback) + local feature_dir + if [[ -n "${SPECIFY_FEATURE_DIRECTORY:-}" ]]; then + feature_dir="$SPECIFY_FEATURE_DIRECTORY" + # Normalize relative paths to absolute under repo root + [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir" + elif [[ -f "$repo_root/.specify/feature.json" ]]; then + # Shared, set -e-safe parser: jq -> python3 -> grep/sed. Returns empty on + # missing/unparseable/unset so we fall through to the branch-prefix lookup. + local _fd + _fd=$(read_feature_json_feature_directory "$repo_root") + if [[ -n "$_fd" ]]; then + feature_dir="$_fd" + # Normalize relative paths to absolute under repo root + [[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir" + elif ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then + echo "ERROR: Failed to resolve feature directory" >&2 + return 1 + fi + elif ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then + echo "ERROR: Failed to resolve feature directory" >&2 + return 1 + 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" + printf 'CURRENT_BRANCH=%q\n' "$current_branch" + printf 'HAS_GIT=%q\n' "$has_git_repo" + printf 'FEATURE_DIR=%q\n' "$feature_dir" + printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md" + printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md" + printf 'TASKS=%q\n' "$feature_dir/tasks.md" + printf 'RESEARCH=%q\n' "$feature_dir/research.md" + printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md" + printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md" + printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts" +} + +# Check if jq is available for safe JSON construction +has_jq() { + command -v jq >/dev/null 2>&1 +} + +# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable). +# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259). +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\n'/\\n}" + s="${s//$'\t'/\\t}" + s="${s//$'\r'/\\r}" + s="${s//$'\b'/\\b}" + s="${s//$'\f'/\\f}" + # Escape any remaining U+0001-U+001F control characters as \uXXXX. + # (U+0000/NUL cannot appear in bash strings and is excluded.) + # LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes, + # so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact. + local LC_ALL=C + local i char code + for (( i=0; i<${#s}; i++ )); do + char="${s:$i:1}" + printf -v code '%d' "'$char" 2>/dev/null || code=256 + if (( code >= 1 && code <= 31 )); then + printf '\\u%04x' "$code" + else + printf '%s' "$char" + fi + done +} + +check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } +check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } + +# Resolve a template name to a file path using the priority stack: +# 1. .specify/templates/overrides/ +# 2. .specify/presets//templates/ (sorted by priority from .registry) +# 3. .specify/extensions//templates/ +# 4. .specify/templates/ (core) +resolve_template() { + local template_name="$1" + local repo_root="$2" + local base="$repo_root/.specify/templates" + + # Priority 1: Project overrides + local override="$base/overrides/${template_name}.md" + [ -f "$override" ] && echo "$override" && return 0 + + # 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" + if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; 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 +try: + with open(os.environ['SPECKIT_REGISTRY']) 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: + print(pid) +except Exception: + sys.exit(1) +" 2>/dev/null); then + if [ -n "$sorted_presets" ]; then + # python3 succeeded and returned preset IDs — search in priority order + while IFS= read -r preset_id; do + local candidate="$presets_dir/$preset_id/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done <<< "$sorted_presets" + fi + # python3 succeeded but registry has no presets — nothing to search + else + # python3 failed (missing, or registry parse error) — fall back to unordered directory scan + for preset in "$presets_dir"/*/; do + [ -d "$preset" ] || continue + local candidate="$preset/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + else + # Fallback: alphabetical directory order (no python3 available) + for preset in "$presets_dir"/*/; do + [ -d "$preset" ] || continue + local candidate="$preset/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + fi + + # 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 candidate="$ext/templates/${template_name}.md" + [ -f "$candidate" ] && echo "$candidate" && return 0 + done + fi + + # Priority 4: Core templates + local core="$base/${template_name}.md" + [ -f "$core" ] && echo "$core" && return 0 + + # Template not found in any location. + # Return 1 so callers can distinguish "not found" from "found". + # Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true + return 1 +} + +# Resolve a template name to composed content using composition strategies. +# Reads strategy metadata from preset manifests and composes content +# from multiple layers using prepend, append, or wrap strategies. +# +# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT") +# Returns composed content string on stdout; exit code 1 if not found. +resolve_template_content() { + local template_name="$1" + local repo_root="$2" + local base="$repo_root/.specify/templates" + + # Collect all layers (highest priority first) + local -a layer_paths=() + local -a layer_strategies=() + + # Priority 1: Project overrides (always "replace") + local override="$base/overrides/${template_name}.md" + if [ -f "$override" ]; then + layer_paths+=("$override") + layer_strategies+=("replace") + fi + + # 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 +try: + with open(os.environ['SPECKIT_REGISTRY']) 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: + 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 " +import sys, os +try: + import yaml +except ImportError: + print('yaml_missing', file=sys.stderr) + print('replace\t') + sys.exit(0) +try: + with open(os.environ['SPECKIT_MANIFEST']) as f: + data = yaml.safe_load(f) + for t in data.get('provides', {}).get('templates', []): + if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template': + print(t.get('strategy', 'replace') + '\t' + t.get('file', '')) + 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") + fi + done <<< "$sorted_presets" + 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") + 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 + layer_paths+=("$candidate") + layer_strategies+=("replace") + fi + done + 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 + local candidate="$ext/templates/${template_name}.md" + if [ -f "$candidate" ]; then + layer_paths+=("$candidate") + layer_strategies+=("replace") + fi + done + fi + + # Priority 4: Core templates (always "replace") + local core="$base/${template_name}.md" + if [ -f "$core" ]; then + layer_paths+=("$core") + layer_strategies+=("replace") + fi + + local count=${#layer_paths[@]} + [ "$count" -eq 0 ] && return 1 + + # Check if any layer uses a non-replace strategy + local has_composition=false + for s in "${layer_strategies[@]}"; do + [ "$s" != "replace" ] && has_composition=true && break + done + + # 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]}" + return 0 + fi + + if [ "$has_composition" = false ]; then + cat "${layer_paths[0]}" + return 0 + fi + + # Find the effective base: scan from highest priority (index 0) downward + # to find the nearest replace layer. Only compose layers above that base. + local base_idx=-1 + local i + for (( i=0; i=0; i-- )); do + local path="${layer_paths[$i]}" + local strat="${layer_strategies[$i]}" + local layer_content + # Preserve trailing newlines + layer_content=$(cat "$path"; printf x) + 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")" ;; + wrap) + case "$layer_content" in + *'{CORE_TEMPLATE}'*) ;; + *) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;; + esac + while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do + local before="${layer_content%%\{CORE_TEMPLATE\}*}" + local after="${layer_content#*\{CORE_TEMPLATE\}}" + layer_content="${before}${content}${after}" + done + content="$layer_content" + ;; + *) echo "Error: unknown strategy '$strat'" >&2; return 1 ;; + esac + done + + printf '%s' "$content" + return 0 +} + diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh new file mode 100755 index 0000000..c353770 --- /dev/null +++ b/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,413 @@ +#!/usr/bin/env bash + +set -e + +JSON_MODE=false +DRY_RUN=false +ALLOW_EXISTING=false +SHORT_NAME="" +BRANCH_NUMBER="" +USE_TIMESTAMP=false +ARGS=() +i=1 +while [ $i -le $# ]; do + arg="${!i}" + case "$arg" in + --json) + JSON_MODE=true + ;; + --dry-run) + DRY_RUN=true + ;; + --allow-existing-branch) + ALLOW_EXISTING=true + ;; + --short-name) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + # Check if the next argument is another option (starts with --) + if [[ "$next_arg" == --* ]]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + SHORT_NAME="$next_arg" + ;; + --number) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + if [[ "$next_arg" == --* ]]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + BRANCH_NUMBER="$next_arg" + ;; + --timestamp) + USE_TIMESTAMP=true + ;; + --help|-h) + echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name ] [--number N] [--timestamp] " + echo "" + echo "Options:" + echo " --json Output in JSON format" + echo " --dry-run Compute branch name and paths without creating branches, directories, or files" + echo " --allow-existing-branch Switch to branch if it already exists instead of failing" + echo " --short-name Provide a custom short name (2-4 words) for the branch" + echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 'Add user authentication system' --short-name 'user-auth'" + echo " $0 'Implement OAuth2 integration for API' --number 5" + echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac + i=$((i + 1)) +done + +FEATURE_DESCRIPTION="${ARGS[*]}" +if [ -z "$FEATURE_DESCRIPTION" ]; then + echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name ] [--number N] [--timestamp] " >&2 + exit 1 +fi + +# Trim whitespace and validate description is not empty (e.g., user passed only whitespace) +FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') +if [ -z "$FEATURE_DESCRIPTION" ]; then + echo "Error: Feature description cannot be empty or contain only whitespace" >&2 + exit 1 +fi + +# 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 + dirname=$(basename "$dir") + # 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 + fi + fi + done + fi + + echo "$highest" +} + +# Function to get highest number from git branches +get_highest_from_branches() { + git branch -a 2>/dev/null | sed 's/^[* ]*//; s|^remotes/[^/]*/||' | _extract_highest_number +} + +# Extract the highest sequential feature number from a list of ref names (one per line). +# Shared by get_highest_from_branches and get_highest_from_remote_refs. +_extract_highest_number() { + local highest=0 + while IFS= read -r name; do + [ -z "$name" ] && continue + if echo "$name" | grep -Eq '^[0-9]{3,}-' && ! echo "$name" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then + number=$(echo "$name" | grep -Eo '^[0-9]+' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi + fi + done + echo "$highest" +} + +# Function to get highest number from remote branches without fetching (side-effect-free) +get_highest_from_remote_refs() { + local highest=0 + + for remote in $(git remote 2>/dev/null); do + local remote_highest + remote_highest=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads "$remote" 2>/dev/null | sed 's|.*refs/heads/||' | _extract_highest_number) + if [ "$remote_highest" -gt "$highest" ]; then + highest=$remote_highest + fi + done + + echo "$highest" +} + +# Function to check existing branches (local and remote) and return next available number. +# When skip_fetch is true, queries remotes via ls-remote (read-only) instead of fetching. +check_existing_branches() { + local specs_dir="$1" + local skip_fetch="${2:-false}" + + if [ "$skip_fetch" = true ]; then + # Side-effect-free: query remotes via ls-remote + local highest_remote=$(get_highest_from_remote_refs) + local highest_branch=$(get_highest_from_branches) + if [ "$highest_remote" -gt "$highest_branch" ]; then + highest_branch=$highest_remote + fi + else + # Fetch all remotes to get latest branch info (suppress errors if no remotes) + git fetch --all --prune >/dev/null 2>&1 || true + local highest_branch=$(get_highest_from_branches) + fi + + # Get highest number from ALL specs (not just matching short name) + local highest_spec=$(get_highest_from_specs "$specs_dir") + + # Take the maximum of both + local max_num=$highest_branch + if [ "$highest_spec" -gt "$max_num" ]; then + max_num=$highest_spec + fi + + # Return next number + echo $((max_num + 1)) +} + +# Function to clean and format a branch name +clean_branch_name() { + local name="$1" + echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' +} + +# Resolve repository root using common.sh functions which prioritize .specify over git +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +REPO_ROOT=$(get_repo_root) + +# Check if git is available at this repo root (not a parent) +if has_git; then + HAS_GIT=true +else + HAS_GIT=false +fi + +cd "$REPO_ROOT" + +SPECS_DIR="$REPO_ROOT/specs" +if [ "$DRY_RUN" != true ]; then + mkdir -p "$SPECS_DIR" +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') + + # 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 + meaningful_words+=("$word") + elif echo "$description" | grep -q "\b${word^^}\b"; then + # Keep short words if they appear as uppercase in original (likely acronyms) + 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 + if [ $count -ge $max_words ]; then break; fi + if [ -n "$result" ]; then result="$result-"; fi + result="$result$word" + count=$((count + 1)) + done + echo "$result" + else + # Fallback to original logic if no meaningful words found + local cleaned=$(clean_branch_name "$description") + echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' + fi +} + +# Generate branch name +if [ -n "$SHORT_NAME" ]; then + # Use provided short name, just clean it up + BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") +else + # Generate from description with smart filtering + BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") +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" + BRANCH_NUMBER="" +fi + +# Determine branch prefix +if [ "$USE_TIMESTAMP" = true ]; then + FEATURE_NUM=$(date +%Y%m%d-%H%M%S) + BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" +else + # Determine branch number + if [ -z "$BRANCH_NUMBER" ]; then + if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then + # Dry-run: query remotes via ls-remote (side-effect-free, no fetch) + BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true) + elif [ "$DRY_RUN" = true ]; then + # Dry-run without git: local spec dirs only + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$((HIGHEST + 1)) + elif [ "$HAS_GIT" = true ]; then + # Check existing branches on remotes + BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") + else + # Fall back to local directory check + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$((HIGHEST + 1)) + fi + 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}" +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}" + + >&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)" +fi + +FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" +SPEC_FILE="$FEATURE_DIR/spec.md" + +if [ "$DRY_RUN" != true ]; then + if [ "$HAS_GIT" = true ]; then + branch_create_error="" + if ! branch_create_error=$(git checkout -q -b "$BRANCH_NAME" 2>&1); then + current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" + # Check if branch already exists + if git branch --list "$BRANCH_NAME" | grep -q .; then + if [ "$ALLOW_EXISTING" = true ]; then + # If we're already on the branch, continue without another checkout. + if [ "$current_branch" = "$BRANCH_NAME" ]; then + : + # Otherwise switch to the existing branch instead of failing. + elif ! switch_branch_error=$(git checkout -q "$BRANCH_NAME" 2>&1); then + >&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again." + if [ -n "$switch_branch_error" ]; then + >&2 printf '%s\n' "$switch_branch_error" + fi + exit 1 + fi + elif [ "$USE_TIMESTAMP" = true ]; then + >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name." + exit 1 + else + >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." + exit 1 + fi + else + >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'." + if [ -n "$branch_create_error" ]; then + >&2 printf '%s\n' "$branch_create_error" + else + >&2 echo "Please check your git configuration and try again." + fi + exit 1 + fi + fi + else + >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" + 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" + else + echo "Warning: Spec template not found; created empty spec file" >&2 + touch "$SPEC_FILE" + fi + fi + + # Inform the user how to persist the feature variable in their own shell + printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2 +fi + +if $JSON_MODE; then + if command -v jq >/dev/null 2>&1; then + if [ "$DRY_RUN" = true ]; then + jq -cn \ + --arg branch_name "$BRANCH_NAME" \ + --arg spec_file "$SPEC_FILE" \ + --arg feature_num "$FEATURE_NUM" \ + '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}' + else + jq -cn \ + --arg branch_name "$BRANCH_NAME" \ + --arg spec_file "$SPEC_FILE" \ + --arg feature_num "$FEATURE_NUM" \ + '{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}' + fi + else + if [ "$DRY_RUN" = true ]; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" + else + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")" + fi + fi +else + echo "BRANCH_NAME: $BRANCH_NAME" + 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" + fi +fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh new file mode 100755 index 0000000..945385c --- /dev/null +++ b/.specify/scripts/bash/setup-plan.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false +ARGS=() + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac +done + +# Get script directory and load common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; } +eval "$_paths_output" +unset _paths_output + +# If feature.json pins an existing feature directory, branch naming is not required. +if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then + check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +fi + +# Ensure the feature directory exists +mkdir -p "$FEATURE_DIR" + +# Copy plan template if plan doesn't already exist +if [[ -f "$IMPL_PLAN" ]]; then + if $JSON_MODE; then + echo "Plan already exists at $IMPL_PLAN, skipping template copy" >&2 + else + 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" + 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" + fi + # Create a basic plan file if template doesn't exist + touch "$IMPL_PLAN" + fi +fi + +# Output results +if $JSON_MODE; then + if has_jq; then + jq -cn \ + --arg feature_spec "$FEATURE_SPEC" \ + --arg impl_plan "$IMPL_PLAN" \ + --arg specs_dir "$FEATURE_DIR" \ + --arg branch "$CURRENT_BRANCH" \ + --arg has_git "$HAS_GIT" \ + '{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}' + else + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")" + fi +else + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "SPECS_DIR: $FEATURE_DIR" + echo "BRANCH: $CURRENT_BRANCH" + echo "HAS_GIT: $HAS_GIT" +fi + diff --git a/.specify/scripts/bash/setup-tasks.sh b/.specify/scripts/bash/setup-tasks.sh new file mode 100755 index 0000000..3f6a40b --- /dev/null +++ b/.specify/scripts/bash/setup-tasks.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false + +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=true ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;; + esac +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; } +eval "$_paths_output" +unset _paths_output + +# Validate branch +# If feature.json pins an existing feature directory, branch naming is not required. +if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then + check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 +fi + +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.plan first to create the implementation plan." >&2 + exit 1 +fi + +if [[ ! -f "$FEATURE_SPEC" ]]; then + echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 + exit 1 +fi + +# Build available docs list +docs=() +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Resolve tasks template through override stack +TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true +if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then + 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 +fi + +# Output results +if $JSON_MODE; then + if has_jq; then + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .) + fi + jq -cn \ + --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}' + else + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + 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:-}")" + fi +else + echo "FEATURE_DIR: $FEATURE_DIR" + echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}" + echo "AVAILABLE_DOCS:" + check_file "$RESEARCH" "research.md" + check_file "$DATA_MODEL" "data-model.md" + check_dir "$CONTRACTS_DIR" "contracts/" + check_file "$QUICKSTART" "quickstart.md" +fi diff --git a/.specify/workflows/speckit/workflow.yml b/.specify/workflows/speckit/workflow.yml new file mode 100644 index 0000000..f69efea --- /dev/null +++ b/.specify/workflows/speckit/workflow.yml @@ -0,0 +1,77 @@ +schema_version: "1.0" +workflow: + id: "speckit" + name: "Full SDD Cycle" + version: "1.0.0" + author: "GitHub" + description: "Runs specify → plan → tasks → implement with review gates" + +requires: + # 0.8.5 is the first release with engine-side resolution of the + # ``integration: "auto"`` default. Older versions would treat "auto" + # as a literal integration key and fail at dispatch. + speckit_version: ">=0.8.5" + integrations: + # The four commands below (specify, plan, tasks, implement) are core + # spec-kit commands provided by every integration. The list here is an + # advisory, non-exhaustive compatibility hint following the documented + # ``any: [...]`` schema -- it is NOT a closed set. The workflow runs + # against any integration the project was initialized with, including + # ones not listed below, as long as that integration provides the four + # core commands referenced in ``steps``. + any: + - "claude" + - "copilot" + - "gemini" + - "opencode" + +inputs: + spec: + type: string + required: true + prompt: "Describe what you want to build" + integration: + type: string + default: "auto" + prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)" + scope: + type: string + default: "full" + enum: ["full", "backend-only", "frontend-only"] + +steps: + - id: specify + command: speckit.specify + integration: "{{ inputs.integration }}" + input: + args: "{{ inputs.spec }}" + + - id: review-spec + type: gate + message: "Review the generated spec before planning." + options: [approve, reject] + on_reject: abort + + - id: plan + command: speckit.plan + integration: "{{ inputs.integration }}" + input: + args: "{{ inputs.spec }}" + + - id: review-plan + type: gate + message: "Review the plan before generating tasks." + options: [approve, reject] + on_reject: abort + + - id: tasks + command: speckit.tasks + integration: "{{ inputs.integration }}" + input: + args: "{{ inputs.spec }}" + + - id: implement + command: speckit.implement + integration: "{{ inputs.integration }}" + input: + args: "{{ inputs.spec }}" diff --git a/.specify/workflows/workflow-registry.json b/.specify/workflows/workflow-registry.json new file mode 100644 index 0000000..c990295 --- /dev/null +++ b/.specify/workflows/workflow-registry.json @@ -0,0 +1,13 @@ +{ + "schema_version": "1.0", + "workflows": { + "speckit": { + "name": "Full SDD Cycle", + "version": "1.0.0", + "description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates", + "source": "bundled", + "installed_at": "2026-05-27T13:44:26.473982+00:00", + "updated_at": "2026-05-27T13:44:26.473988+00:00" + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b35c139 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# AGENTS.md — Checkout GraphQL + +## Project Overview + +Checkout GraphQL is a VTEX IO app that exposes a public GraphQL surface over the VTEX Checkout REST APIs (`portal.vtexcommercestable.com.br`, `portal.vtexcommercebeta.com.br`). It aggregates order-form, shipping, payment, items, coupon, profile, and SLA data into resolvers consumed by storefront and admin clients. + +This is a VTEX IO app published as `vtex/checkout-graphql@0.67.2`. Builders declared in `manifest.json`: `graphql`, `node`. Tech stack: TypeScript 3.9.7 (node builder), `yarn`, `@vtex/api` 6.46.1, `graphql` 14, `vtex-test-tools` 3, ESLint with `eslint-config-vtex` + `eslint-config-vtex-react`, Prettier with `@vtex/prettier-config`. + +## Prerequisites + +- Node.js 12.x — matches the runtime declared in `node/service.json` (`stack: nodejs`) and pinned by `.github/workflows/ci.yml` (`actions/setup-node@v4`, `node-version: 12.x`). +- VTEX CLI: `npm i -g vtex`. +- Access to a VTEX account and a development workspace (`vtex use `). +- `yarn` (Classic). Both the repository root and `node/` ship their own `yarn.lock`. + +### Build & Run + +```bash +# Authenticate +vtex login + +# Switch to a development workspace +vtex use + +# Link the app to the workspace (live-reload while you code) +vtex link + +# Publish a new version (CI-driven; local publish is acceptable for beta releases) +vtex publish + +# Deploy a published version to production +vtex deploy +``` + +The build itself runs on `builder-hub`; do **not** pre-compile locally. + +### Test Commands + +```bash +# Install (root + node builder) +yarn install --frozen-lockfile +cd node && yarn install --frozen-lockfile && cd .. + +# Lint (whole tree, root script) +yarn lint + +# Unit tests (node builder) +cd node && yarn test + +# Tests + coverage (matches CI; lcov + text summary) +cd node && yarn test:coverage + +# Local type-check (builder-hub owns the canonical one) +cd node && yarn tsc --noEmit +``` + +CI (`.github/workflows/ci.yml`) runs lint (changed-files only on PRs, full on push to `master`) and `yarn test:coverage --passWithNoTests` inside `node/` on every PR. + +### Architecture Boundaries + +| Builder | Folder | Responsibility | +|---------|--------|----------------| +| `node` | `node/` | TypeScript service backed by `@vtex/api`. Houses `clients/`, `directives/`, `resolvers/`, `utils/`, `constants/`, and the `Service` entry in `node/index.ts`. | +| `graphql` | `graphql/` | Public GraphQL schema (`schema.graphql`), directive declarations (`directives.graphql`), shared `types/`. Resolvers live under `node/resolvers/`. | + +Rules: + +- Resolvers in `node/resolvers/` reach external systems **exclusively** through clients in `node/clients/` (`checkout.ts`, `checkoutAdmin.ts`, `countryDataSettings.ts`, `graphqlServer.ts`, `session.ts`, `searchGraphQL/`). No raw HTTP from resolvers, directives, or utilities. +- Every directive in `graphql/directives.graphql` MUST have a matching implementation in `node/directives/` (and vice versa). Adding or removing one requires updating both in the same PR. +- This app has **no** `react/`, `messages/`, `store/`, `admin/`, `pages/`, or `events/` builders. Adding any of them requires a `manifest.json` change reviewed by `@vtex-apps/checkout-ui` (per `CODEOWNERS`). +- External GraphQL type imports (e.g. `vtex.checkout-graphql@0.60.0`, `vtex.graphql-server`, `vtex.messages`, `vtex.country-data-settings`) come from the URLs in `node/package.json` devDependencies and MUST stay in sync with the major versions pinned in `manifest.json.dependencies`. + +### Coding Conventions + +**Node** + +- Compose route handlers with `compose` from `@vtex/api`; one middleware per file under `node/middlewares/` if any are added. +- Add external clients in `node/clients/index.ts` extending `IOClients`; never call third-party HTTP from resolvers, directives, or utilities directly. +- Throw typed errors from `@vtex/api` (`UserInputError`, `AuthenticationError`, `ForbiddenError`, `NotFoundError`); avoid generic `Error` at the request boundary. +- Log via `ctx.vtex.logger`; every log inside a request handler includes `account` and `workspace`. +- TypeScript is `strict: true` in `node/tsconfig.json`. Suppression annotations (`@ts-ignore`, `@ts-expect-error`, `as any`) require a justifying comment on the same line or directly above. + +**GraphQL** + +- Schema lives at `graphql/schema.graphql` (and `graphql/directives.graphql`, `graphql/types/*.graphql`). +- Schema changes MUST be additive and backwards-compatible. Breaking changes require a major bump in `manifest.json` and a consumer migration note in the PR description. +- The `@cacheControl` directive MUST be reviewed when added or modified — order-form and profile responses are PII-bearing and have specific cache scoping in `node/directives/noCache.ts`, `withOrderFormId.ts`, `withOwnerId.ts`. + +### Constitution + +Because this repository is public, the non-negotiable engineering principles for this app are stored centrally in the **spec-repo**, not under `.specify/memory/`. The canonical path is `spec-repo/checkout-graphql/constitution.md`. Agents MUST read it before generating or modifying code in this repository; this `AGENTS.md` file is operational guidance only — when the two disagree, the constitution wins. + +### Safety Guardrails + +- NEVER commit `node_modules/`, build outputs (`dist/`, `build/`, `lib/`, `node/coverage/`), or any artifact compiled outside `builder-hub` — repositories ship source only. +- NEVER commit secrets, account credentials, app tokens, vendor keys, or VKS contents. +- NEVER hardcode account, workspace, region, or tenant values; resolve them from `ctx.vtex.account` / `ctx.vtex.workspace`. +- NEVER add or remove a builder from `manifest.json` without a justifying PR description; builder changes affect platform validation and runtime resolution. +- NEVER include user PII (customer ids, emails, addresses, payment data) or secret values in logs. +- NEVER bypass `builder-hub` by pre-compiling locally and committing artifacts. +- NEVER read tenant settings from `process.env`; use `ctx.vtex.settings` or the app settings client. +- NEVER ship a breaking schema change to `graphql/schema.graphql` without a corresponding major bump in `manifest.json` and a consumer migration note. +- NEVER call third-party HTTP from outside `node/clients/`. +- NEVER bypass the husky `pre-commit` hook (`--no-verify`) in normal development — it runs `lint-staged` (eslint + prettier). +- AVOID local `vtex publish` for non-beta releases; the canonical path is CI-driven publish + deploy. +- NEVER execute `vtex deploy` at all cost. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file