Skill: chanbistec/specclaw (version 0.6.1)

--- name: specclaw description: "Spec-driven development framework for OpenClaw. Propose features, generate specs, spawn coding agents, validate implementations." metadata: openclaw: emoji: "🦞" requires: tools: ["exec", "sessions_spawn", "message"] allowed-tools: ["exec", "read", "write", "edit", "sessions_spawn", "sessions_yield", "subagents", "message", "memory_search"] --- # SpecClaw — Spec-Driven Development ## Overview SpecClaw brings structured, spec-driven development to OpenClaw agents. It manages the full lifecycle: propose → plan → build → verify → archive. ## Directory Structure When initialized (`.specclaw/` exists in project root): ``` .specclaw/ ├── config.yaml # Project configuration ├── STATUS.md # Project dashboard (auto-generated) ├── patterns.md # Recurring pattern registry (cross-change) └── changes/ ├── <change-name>/ │ ├── proposal.md # Problem + solution + scope │ ├── spec.md # Requirements + acceptance criteria │ ├── design.md # Technical approach + file map │ ├── tasks.md # Ordered tasks with status markers │ ├── status.md # Progress tracking │ ├── errors.md # Build error journal (auto-generated on failures) │ ├── learnings.md # Build learnings (spec gaps, patterns, insights) │ └── verify-report.md # Verification results (auto-generated) └── archive/ # Completed changes ``` ## Commands The user triggers commands conversationally. Recognize these patterns: ### `specclaw init` **Trigger:** "specclaw init", "initialize specclaw", "set up spec-driven development" 1. Create `.specclaw/` directory structure 2. Generate `config.yaml` from template (see `templates/config.yaml`) 3. Ask user for project name/description 4. Create initial `STATUS.md` 5. Add `.specclaw/` tracking to git ### `specclaw propose "<idea>"` **Trigger:** "specclaw propose", "propose a change", "new feature proposal" 1. Create `.specclaw/changes/<slugified-name>/` 2. Generate `proposal.md` from template 3. Include: problem statement, proposed solution, scope, impact, open questions 4. Present proposal to user for review 5. Update `STATUS.md` 6. **GitHub sync** (if `github.sync` is true): Run `bash skill/scripts/gh-sync.sh create .specclaw <change>` to create a GitHub Issue for the proposal. (gh-sync.sh create requires proposal.md — validation is enforced by validate-change.sh.) ### `specclaw plan <change>` **Trigger:** "specclaw plan", "plan the feature", "generate spec for" 1. **Validate:** Run `bash skill/scripts/validate-change.sh .specclaw <change> plan`. If it fails, report missing prerequisites and stop. 2. Read the proposal 3. Analyze existing codebase (file structure, patterns, dependencies) 4. Generate: - `spec.md` — functional requirements, acceptance criteria, edge cases - `design.md` — technical approach, architecture, file changes map - `tasks.md` — ordered implementation tasks with dependencies 5. Present plan summary to user 6. Update status 7. **GitHub sync** (if enabled): Run `bash skill/scripts/gh-sync.sh update .specclaw <change>` to add the task checklist to the GitHub Issue. ### `specclaw build <change>` **Trigger:** "specclaw build", "implement the feature", "start building" **This is where OpenClaw shines.** Follow this execution flow exactly: #### Step 0 — Validate Run `bash skill/scripts/validate-change.sh .specclaw <change> build`. If it fails, report missing prerequisites and stop. #### Step 1 — Setup Run the setup script to parse config, create a git branch, and get build configuration: ```bash bash skill/scripts/build.sh setup .specclaw <change_name> ``` This returns JSON config including `parallel_tasks`, `models.coding`, `git.strategy`, and `notifications.channel`. Capture this output — you'll need `parallel_tasks` and `model` values throughout the build. **Worktree strategy:** When `git.strategy` is `"worktree-per-change"`, setup creates an isolated worktree at `.specclaw/worktrees/<change>/`. The `worktree_path` from the config JSON should be used as the `cwd` parameter when spawning coding agents via `sessions_spawn`, ensuring each change's agents work in complete isolation. **Parallel changes:** With `worktree-per-change` strategy, multiple changes can be built simultaneously since each has its own worktree. No branch switching required. Send a **build started** notification: ``` 🦞 **Build Started** **Change:** <change_name> **Branch:** specclaw/<change_name> **Tasks:** <total_count> across <wave_count> waves ``` #### Step 2 — Parse Tasks Get all actionable tasks: ```bash bash skill/scripts/parse-tasks.sh --status pending .specclaw/changes/<change>/tasks.md ``` This outputs JSON: `[{"id": "T1", "title": "...", "wave": 1, "depends": [], "files": [...], "estimate": "small"}, ...]` **For retries** (re-running build on a change with prior failures): ```bash bash skill/scripts/parse-tasks.sh --status failed .specclaw/changes/<change>/tasks.md ``` Reset failed tasks to pending before re-executing: ```bash bash skill/scripts/update-task-status.sh .specclaw/changes/<change>/tasks.md <TASK_ID> pending ``` Then re-parse with `--status pending` and continue from the appropriate wave. #### Step 3 — Wave Loop Execute tasks wave-by-wave. For each wave number (1, 2, 3...): **a. Filter tasks for this wave:** ```bash bash skill/scripts/parse-tasks.sh --wave N --status pending .specclaw/changes/<change>/tasks.md ``` If no tasks returned for this wave, the build is complete — skip to Step 4. **Skip waves with blocked tasks:** If a task's dependency failed in a prior wave, skip it and mark it failed: ```bash bash skill/scripts/update-task-status.sh .specclaw/changes/<change>/tasks.md <TASK_ID> failed ``` **b. For each task in the wave** (up to `parallel_tasks` from config): 1. **Mark in-progress:** ```bash bash skill/scripts/update-task-status.sh .specclaw/changes/<change>/tasks.md <TASK_ID> in_progress ``` 2. **Build context payload:** ```bash bash skill/scripts/build-context.sh .specclaw <change> <TASK_ID> ``` This outputs a complete context string containing: spec sections, design sections, task details, relevant source file contents, and constraints. Use this output directly as the agent's task. 3. **Spawn coding agent:** ``` sessions_spawn( task: <output from build-context.sh>, label: "specclaw-<change>-<task_id>", mode: "run", model: <models.coding from config> ) ``` **c. Yield and wait:** After spawning all tasks in the wave batch, call `sessions_yield` to wait for agent completions. Results auto-announce back to you. **d. Process completed agents:** For each agent that **succeeded**: 1. Mark complete: ```bash bash skill/scripts/update-task-status.sh .specclaw/changes/<change>/tasks.md <TASK_ID> complete ``` If this task previously failed (was `[!]` before): Run `bash skill/scripts/log-error.sh .specclaw <change> --resolve <task_id>` 2. Git commit the changes: ```bash bash skill/scripts/build.sh commit .specclaw <change> <TASK_ID> "<task_title>" <files...> ``` 3. Send a **task complete** notification: ``` ✅ **Task Complete:** <TASK_ID> — <task_title> **Change:** <change_name> | **Wave:** <N>/<total_waves> ``` **e. Process failed agents:** For each agent that **failed**: 1. Mark failed: ```bash bash skill/scripts/update-task-status.sh .specclaw/changes/<change>/tasks.md <TASK_ID> failed ``` 2. **Log error:** Run `bash skill/scripts/log-error.sh .specclaw <change> <task_id> <wave> <agent_label> "<failure summary>"` — pipe agent error output if available 3. Log the error in `status.md` with the failure reason 4. Send a **task failed** notification: ``` ❌ **Task Failed:** <TASK_ID> — <task_title> **Change:** <change_name> | **Wave:** <N>/<total_waves> **Error:** <brief failure reason> ``` 5. Mark all dependent tasks in later waves as **skipped/failed** — they cannot proceed 6. **GitHub sync** (if enabled): Run `bash skill/scripts/gh-sync.sh comment .specclaw <change> "❌ Task <task_id> failed: <summary>"` to log the error on the issue. **f. GitHub sync** (if enabled): Run `bash skill/scripts/gh-sync.sh update .specclaw <change>` to update task checkboxes. **g. Repeat** for the next wave number until no pending tasks remain. #### Step 4 — Finalize Run the finalize script to execute tests and merge the branch: ```bash bash skill/scripts/build.sh finalize .specclaw <change_name> ``` This runs the configured `test_command` (if any) and merges the branch per `git.strategy`. #### Step 5 — Post-Build Review If `automation.post_build_review` is `true` in config, run an automated review before updating the dashboard: **a. Scope deviation check:** Compare files actually changed against files declared in tasks: ```bash # Get files changed since pre-build commit (branch point) git diff --name-only main...HEAD ``` Cross-reference with files listed in each task in `tasks.md`. Flag any files changed but not declared in any task's `Files:` field. **b. Review prompt:** Evaluate the build and auto-log findings (~150 words max): ``` 🦞 Post-Build Review — <change-name> Results: X/Y tasks passed, Z failed Evaluate: 1. Were any spec requirements ambiguous or incomplete? 2. Did the design need adjustment during implementation? 3. Were any files modified outside declared task scope? 4. Did any agents struggle with context or instructions? 5. Any reusable patterns discovered? For each finding, log with: bash skill/scripts/log-learning.sh .specclaw <change> <category> <priority> "<detail>" "<action>" ``` **c. Auto-log scope deviations:** For any files changed outside declared task scope, automatically log as `design_gap`: ```bash bash skill/scripts/log-learning.sh .specclaw <change> design_gap medium "File <path> modified but not declared in any task" "Review task file declarations for completeness" ``` **d. Pattern scan:** Run `bash skill/scripts/detect-patterns.sh .specclaw scan <change>` to check for recurring patterns across changes. **e.** If any patterns have recurrence >= 3, alert the user: "⚠️ Pattern PAT-XXX has N occurrences — consider promoting its prevention rule to agent context." #### Step 6 — Update Dashboard Regenerate the project status dashboard: ```bash bash skill/scripts/update-status.sh .specclaw ``` #### Step 7 — Notify Send the **build summary** via the `message` tool to the configured notification channel: ``` 🦞 **Build Complete** **Change:** <change_name> **Status:** <succeeded|partial|failed> **Tasks:** <completed>/<total> complete, <failed> failed, <skipped> skipped **Branch:** specclaw/<change_name> → merged to <target_branch> **Duration:** <elapsed time> ``` If any tasks failed, include a remediation section: ``` ⚠️ **Failed Tasks:** - <TASK_ID>: <brief error> — re-run with `specclaw build <change>` to retry ``` #### Retry Flow When `specclaw build` is called on a change that has failed tasks: 1. Parse failed tasks: `parse-tasks.sh --status failed` 2. Reset each to pending: `update-task-status.sh ... pending` 3. Re-parse pending tasks and determine which waves need re-execution 4. Execute only the waves containing reset tasks (and their dependents) - Retried tasks automatically get previous error context via `build-context.sh` 5. Finalize and notify as normal #### Key Principles - **Fresh context always** — each agent gets ONLY what it needs via `build-context.sh`. No stale context from prior tasks. This is critical for quality. - **Parallel within waves** — tasks in the same wave with no cross-dependencies spawn simultaneously, up to `parallel_tasks` limit. - **Sequential across waves** — wave N+1 starts only after wave N completes. - **Fail-fast on dependencies** — if a task fails, all tasks depending on it are immediately marked failed. ### `specclaw learn <change> "<insight>"` **Trigger:** "specclaw learn", "log a learning", "what did we learn", "capture insight" Capture build learnings — spec gaps, design misses, and patterns discovered during implementation. **Log a learning:** ```bash bash skill/scripts/log-learning.sh .specclaw <change> <category> <priority> "<detail>" ["<action>"] ``` Categories: `spec_gap` | `design_gap` | `pattern` | `best_practice` | `agent_issue` Priorities: `low` | `medium` | `high` **List learnings for a change:** ```bash bash skill/scripts/log-learning.sh .specclaw <change> --list ``` **Promote a learning** (mark for elevation to agent prompts/SKILL.md): ```bash bash skill/scripts/log-learning.sh .specclaw <change> --promote <id> ``` **When to log:** - After a build reveals a spec gap (requirements were unclear or missing) - When a design decision needed mid-build adjustment - When agents discovered a useful pattern worth reusing - When parallel tasks created conflicts (duplicate code, shared dependencies) - When an agent struggled with the context or instructions Learnings are stored in `.specclaw/changes/<change>/learnings.md` and feed into the pattern detection system for cross-change analysis. ### `specclaw patterns` **Trigger:** "specclaw patterns", "check patterns", "recurring issues", "what keeps happening" Track recurring patterns across changes — errors and learnings that repeat become prevention rules. **Scan a change for patterns:** ```bash bash skill/scripts/detect-patterns.sh .specclaw scan <change> ``` Reads errors.md and learnings.md, matches against existing patterns, creates new or increments existing. **List all patterns:** ```bash bash skill/scripts/detect-patterns.sh .specclaw list [--min-recurrence N] ``` **Promote a pattern** (mark for elevation to agent prompts): ```bash bash skill/scripts/detect-patterns.sh .specclaw promote <pat-id> ``` **Auto-promotion:** Patterns with 3+ occurrences are flagged ⚠️ — their prevention rules should be added to agent context templates or SKILL.md build instructions. Pattern registry lives at `.specclaw/patterns.md` (global, not per-change). ### `specclaw verify <change>` **Trigger:** "specclaw verify", "validate implementation", "check against spec" Validate that the implementation satisfies the spec's acceptance criteria. #### Step 0: Validate Run `bash skill/scripts/validate-change.sh .specclaw <change> verify`. If it fails (tasks not all complete), report and stop. #### Step 1: Collect Evidence Run `bash skill/scripts/verify.sh collect .specclaw <change>` to gather: - Acceptance criteria from spec.md - Current content of all changed files - Test/lint/build command results (if configured) #### Step 2: Build Verify Context Run `bash skill/scripts/verify-context.sh .specclaw <change>` to construct the verification agent's context payload from the evidence + Verify Agent prompt template. #### Step 3: Spawn Verify Agent Spawn a verification agent: ``` sessions_spawn( task: <verify context payload>, model: <config.yaml models.review>, # default: anthropic/claude-sonnet-4-5 mode: "run", label: "specclaw-verify-<change>" ) ``` Wait for completion via `sessions_yield`. #### Step 4: Save Report Save the agent's output as `.specclaw/changes/<change>/verify-report.md`. #### Step 5: Update Status Run `bash skill/scripts/verify.sh update-status .specclaw <change> <verdict>` where verdict is PASS, FAIL, or PARTIAL (extracted from the report). Update status.md and run `bash skill/scripts/update-status.sh .specclaw` to refresh the dashboard. #### Step 6: GitHub Sync (if enabled) If `github.sync` is true, post verification summary as a comment: `bash skill/scripts/gh-sync.sh comment .specclaw <change> "<verdict summary>"` #### Step 7: Notify Send verification results via configured notification channel. #### Auto-Verify When `automation.auto_verify: true` in config.yaml, the build flow automatically triggers verification after a successful build (all tasks complete). #### Remediation If verdict is FAIL or PARTIAL: 1. List the failed acceptance criteria 2. Suggest creating remediation tasks (new tasks targeting the gaps) 3. The user can re-plan just the failed criteria or manually fix and re-verify ### `specclaw status` **Trigger:** "specclaw status", "project status", "what's the progress" For a specific change: `bash skill/scripts/validate-change.sh .specclaw <change> status` 1. Read all changes in `.specclaw/changes/` 2. Compile dashboard showing: - Active changes with progress % - Pending proposals - Recently archived - Overall project health 3. Update `STATUS.md` ### `specclaw archive <change>` **Trigger:** "specclaw archive", "mark as done", "archive the change" 1. **Validate:** Run `bash skill/scripts/validate-change.sh .specclaw <change> archive`. If it fails, report and stop. 2. Verify change is complete (all tasks done, verification passed) 3. Move to `.specclaw/changes/archive/YYYY-MM-DD-<change-name>/` 4. Update `STATUS.md` 5. **GitHub sync** (if enabled): Run `bash skill/scripts/gh-sync.sh close .specclaw <change>` to close the issue. 6. Optionally create git tag ### `specclaw auto` **Trigger:** "specclaw auto", "autonomous mode", "auto-build" 1. Check `STATUS.md` for next actionable item 2. If proposal exists without plan → generate plan 3. If plan exists without implementation → build 4. If built without verification → verify 5. Respect `config.yaml` limits (max_tasks_per_run) 6. Notify user of results ## Task Format in tasks.md ```markdown ## Tasks ### Wave 1 (no dependencies) - [ ] `T1` — Create theme context provider - Files: `src/contexts/ThemeContext.tsx` - Estimate: small - [ ] `T2` — Add CSS custom properties - Files: `src/styles/variables.css` - Estimate: small ### Wave 2 (depends on Wave 1) - [ ] `T3` — Create toggle component - Files: `src/components/ThemeToggle.tsx` - Depends: T1 - Estimate: small ### Wave 3 (depends on Wave 2) - [ ] `T4` — Integration tests - Files: `tests/theme.test.ts` - Depends: T1, T2, T3 - Estimate: medium ``` Status markers: - `[ ]` — pending - `[~]` — in progress - `[x]` — complete - `[!]` — failed (needs remediation) ## Agent Context Preparation Context construction is handled by the `build-context.sh` script: ```bash bash skill/scripts/build-context.sh .specclaw <change> <TASK_ID> ``` The script automatically assembles a complete context payload containing: 1. **Task header** — task ID, title, and estimate 2. **Spec context** — relevant sections from `spec.md` (requirements, acceptance criteria) 3. **Design context** — relevant sections from `design.md` (architecture, approach) 4. **Task details** — full task description, file list, and dependencies from `tasks.md` 5. **Source files** — current contents of files listed in the task's `Files:` field 6. **Constraints** — standard rules (follow patterns, write tests, stay in scope) The output is a single string ready to pass directly as the `task` parameter to `sessions_spawn`. Do not manually construct context — always use the script to ensure consistency and freshness. ## Configuration Reference See `templates/config.yaml` for the full config schema. Key settings: - `models.planning` — model for proposals, specs, design (default: opus) - `models.coding` — model for implementation (default: codex) - `models.review` — model for verification (default: sonnet) - `git.strategy` — "branch-per-change", "direct", or "worktree-per-change" - `notifications.channel` — where to send updates - `automation.max_tasks_per_run` — limit for auto mode ## Best Practices 1. **Keep proposals focused** — one change per proposal, small scope 2. **Review specs before building** — garbage in, garbage out 3. **Wave-based execution** — group independent tasks, respect dependencies 4. **Fresh context always** — never let agents accumulate stale context 5. **Verify early** — run verification after each wave, not just at the end ### GitHub Integration (Optional) When `github.sync: true` in config.yaml, SpecClaw creates a GitHub Issue per change and tracks progress as a task checklist. Requires `gh` CLI (authenticated) or `GITHUB_TOKEN` environment variable. Run `bash skill/scripts/gh-sync.sh setup` to verify auth and create labels.

path: references/agent-prompts.md
content: # Agent Prompt Templates

These are context payloads for spawned coding agents. The orchestrator fills in variables and passes them as the `task` parameter to `sessions_spawn`.

---

## Propose Agent

```
You are a software architect creating a change proposal for the project "{{project_name}}".

## Your Task
Create a structured proposal for: {{idea}}

## Project Context
{{project_description}}

## Existing Codebase Summary
{{codebase_summary}}

## Output Format
Write a proposal.md with these sections:
1. **Problem** — What problem are we solving? Be specific.
2. **Proposed Solution** — High-level approach (2-3 paragraphs)
3. **Scope** — In scope / out of scope lists
4. **Impact** — Estimated file count, complexity (small/medium/large), risk (low/medium/high)
5. **Open Questions** — Things to resolve before planning

Keep it concise. No fluff. Write for a developer who'll implement this.
```

---

## Planning Agent — Spec

```
You are a requirements engineer generating a specification from an approved proposal.

## Proposal
{{proposal_content}}

## Project Context
{{project_description}}

## Codebase Structure
{{file_tree}}

## Output Format
Write a spec.md with:
1. **Overview** — One paragraph summary
2. **Functional Requirements** — Numbered list (FR-1, FR-2, ...). Each must be testable.
3. **Non-Functional Requirements** — Performance, security, accessibility, etc.
4. **Acceptance Criteria** — Checklist format. Each item starts with "GIVEN ... WHEN ... THEN ..."
5. **Edge Cases** — Numbered list of edge cases to handle
6. **Dependencies** — External libraries, APIs, services needed

Every requirement must be verifiable. No vague statements like "should be fast" — specify "response time < 200ms".
```

---

## Planning Agent — Design

```
You are a software architect creating a technical design from a proposal and spec.

## Proposal
{{proposal_content}}

## Specification
{{spec_content}}

## Current File Structure
{{file_tree}}

## Existing Patterns
{{code_patterns}}

## Output Format
Write a design.md with:
1. **Technical Approach** — How we'll implement this (2-3 paragraphs)
2. **Architecture** — Component diagram or description of how pieces connect
3. **File Changes Map** — Table: File | Action (create/modify/delete) | Description
4. **Data Model Changes** — New models, schema changes, migrations
5. **API Changes** — New/modified endpoints with request/response shapes
6. **Key Decisions** — Architecture decisions with rationale (ADR-style)
7. **Risks & Mitigations** — What could go wrong and how we prevent it

Follow existing project patterns. Don't reinvent what's already there.
```

---

## Planning Agent — Tasks

```
You are a project planner breaking a design into implementable tasks.

## Specification
{{spec_content}}

## Design
{{design_content}}

## Output Format
Write a tasks.md with ordered, wave-based tasks:

### Rules:
1. Each task must be completable by a single coding agent in one session
2. Tasks in the same wave have NO dependencies on each other (can run in parallel)
3. Tasks in later waves depend on earlier waves
4. Each task specifies exact files to create/modify
5. Estimates: small (<30 min), medium (30-60 min), large (>60 min) — break large into smaller

### Format:
```
### Wave 1 — <description>
- [ ] `T1` — <title>
  - Files: <comma-separated file paths>
  - Estimate: small | medium | large
  - Notes: <brief implementation notes>

### Wave 2 — <description>
- [ ] `T3` — <title>
  - Files: <file paths>
  - Depends: T1, T2
  - Estimate: medium
```

Aim for 3-8 tasks per change. If more, the change scope is too large — flag it.
```

---

## Build Agent (per task)

```
You are a coding agent implementing a specific task in the project "{{project_name}}".

## Your Task
{{task_title}}
{{task_notes}}

## Files to Modify
{{task_files}}

## Specification Context
{{relevant_spec_sections}}

## Design Context
{{relevant_design_sections}}

## Existing Code
{{existing_file_contents}}

## Constraints
1. ONLY modify/create the files listed above
2. Follow existing code style and patterns
3. Write tests for new functionality (if test framework exists)
4. Use existing utilities/helpers — don't duplicate
5. Keep changes minimal and focused on THIS task only
6. Commit with message: "specclaw({{change_name}}): {{task_id}} — {{task_title}}"

## Definition of Done
- All listed files created/modified correctly
- Code compiles/runs without errors
- Tests pass (if applicable)
- No unrelated changes
```

---

## Verify Agent

```
You are a strict QA engineer validating an implementation against its specification.

## Change
{{change_name}}

## Specification
{{spec_content}}

## Acceptance Criteria
{{acceptance_criteria}}

## Implementation (changed files)
{{changed_files_content}}

## Test Output
{{test_output}}

## Lint Output
{{lint_output}}

## Build Output
{{build_output}}

## Your Task

For EACH acceptance criterion listed above:
1. Carefully check if the implementation satisfies it by reading the changed files
2. Mark as ✅ PASS with brief evidence of what satisfies it, or ❌ FAIL with what's missing/wrong
3. Note any edge cases the criterion implies that are not handled

After checking all criteria, review test/lint/build output for additional problems.

## Verdict Rules
- **PASS** — ALL acceptance criteria pass AND no blocking issues in test/lint/build output
- **FAIL** — ANY acceptance criterion fails OR blocking errors in test/lint/build output
- **PARTIAL** — Some criteria pass, some fail, but progress was made

## Output Format

You MUST output EXACTLY this format (fill in values, replace placeholders):

---

# Verification Report: {{change_name}}

**Verified:** <current date YYYY-MM-DD>
**Model:** <your model name>
**Verdict:** <PASS|FAIL|PARTIAL>

## Acceptance Criteria

For each criterion, output one line:
- ✅ **AC-N:** <criterion text> — <brief evidence of why it passes>
- ❌ **AC-N:** <criterion text> — <what is missing or wrong>

If a criterion has unhandled edge cases, add a sub-bullet:
  - ⚠️ Edge case: <description>

## Test Results

<Paste relevant test output, or "No tests configured" if none>
<If tests fail, highlight which ones and why>

## Issues Found

List each actionable issue:
1. **<issue title>** — <description>. **Fix:** <suggested fix>
2. ...

If no issues: "No issues found."

## Summary

**Passed:** <N>/<total> criteria
**Failed:** <N>/<total> criteria
**Verdict:** <PASS|FAIL|PARTIAL>

---

Be strict. If the spec says it, the code must do it. Do not pass criteria on intent — only on evidence in the actual code.
```

---

## Context Preparation Notes

### How to build `{{codebase_summary}}`:
```bash
# File tree (depth 3, excluding node_modules etc.)
find . -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.specclaw/*' | head -100

# Package info
cat package.json 2>/dev/null | jq '{name, description, dependencies, devDependencies}' || true
cat requirements.txt 2>/dev/null || true
cat go.mod 2>/dev/null | head -20 || true
```

### How to build `{{code_patterns}}`:
```bash
# Sample key files for pattern detection
head -50 src/index.* src/app.* src/main.* 2>/dev/null || true
```

### How to build `{{existing_file_contents}}`:
For each file in the task's file list, include its current content (or note "new file" if it doesn't exist).


path: references/build-engine.md
content: # Build Engine — Architecture & Reference

This document describes the SpecClaw build pipeline internals for developers maintaining the system. For agent instructions, see SKILL.md.

---

## Build Command Flow

When the agent runs `specclaw build <change>`, the pipeline executes:

```
1. build.sh setup .specclaw <change>              → config JSON (branch created if needed)
2. parse-tasks.sh --status pending tasks.md        → JSON task array
3. For each wave (sequential):
   a. parse-tasks.sh --wave N --status pending     → tasks in this wave
   b. For each task (parallel up to parallel_tasks limit):
      - build-context.sh .specclaw <change> <task_id>  → context payload (stdout)
      - update-task-status.sh tasks.md T1 in_progress
      - sessions_spawn with context payload
      - On completion:
        - update-task-status.sh tasks.md T1 complete|failed
        - build.sh commit .specclaw <change> T1 "title" files...
4. build.sh finalize .specclaw <change>            → summary JSON (tests, lint, merge)
5. update-status.sh .specclaw                      → regenerate STATUS.md dashboard
```

---

## Script APIs

All scripts live in `skill/scripts/` and require only bash, coreutils, and git. `jq` is optional (grep fallbacks exist).

### `build.sh` — Setup, Commit, Finalize

```
build.sh <subcommand> [args]
```

| Subcommand | Arguments | Description |
|---|---|---|
| `setup` | `<specclaw_dir> <change_name>` | Parse config.yaml, create/switch git branch (if branch-per-change), output config JSON |
| `commit` | `<specclaw_dir> <change_name> <task_id> "<title>" [files...]` | Stage listed files and commit with specclaw prefix. No-ops if nothing to commit |
| `finalize` | `<specclaw_dir> <change_name>` | Run test/lint/build commands, merge branch if branch-per-change, output summary JSON |

**`setup` reads from config.yaml:**
- `project.name`, `git.strategy`, `git.branch_prefix`, `git.auto_commit`, `git.commit_prefix`
- `build.test_command`, `build.lint_command`, `build.build_command`
- `build.parallel_tasks`, `build.timeout_seconds`, `models.coding`

**`commit` behavior:**
- Stages specified files with `git add`
- Skips commit if `git diff --cached` is clean (warns, exits 0)
- Skips commit if `auto_commit: false` (stages only)
- Commit message format: `<commit_prefix>(<change>): <task_id> — <title>`

**`finalize` behavior:**
- Runs test/lint/build commands (all optional, each reported independently)
- For `branch-per-change`: checks out main/master, runs `git merge --no-ff`
- On merge conflict: aborts merge, returns to feature branch, reports error
- Cleans up feature branch on successful merge

### `parse-tasks.sh` — Task Parser

```
parse-tasks.sh [OPTIONS] <tasks.md>
```

| Flag | Description |
|---|---|
| `--wave N` | Only output tasks from wave N |
| `--status STATUS` | Filter by status: `pending`, `in_progress`, `complete`, `failed` |
| `--validate` | Check JSON validity only (exit 0/1, no output). Requires `jq` |
| `-h, --help` | Show usage |

**Output:** JSON array to stdout. Each task object:

```json
{
  "id": "T1",
  "title": "Create parser module",
  "status": "pending",
  "wave": 1,
  "files": "src/parser.ts, src/parser.test.ts",
  "depends": "",
  "estimate": "~30 min"
}
```

**Parsing rules:**
- Waves detected by `### Wave N` headers (auto-incremented)
- Task lines must match `- [x] \`T<N>\` — Title` format
- Status markers: `[ ]` pending, `[~]` in_progress, `[x]` complete, `[!]` failed
- Sub-fields (`Files:`, `Depends:`, `Estimate:`, `Notes:`) parsed from indented lines
- Malformed tasks (no backtick ID) are skipped with a stderr warning
- Validates output with `jq` if available; errors on invalid JSON

### `build-context.sh` — Context Payload Builder

```
build-context.sh <specclaw_dir> <change_name> <task_id>
```

**Reads:**
- `.specclaw/config.yaml` — project name, commit prefix
- `.specclaw/changes/<change>/spec.md` — full specification
- `.specclaw/changes/<change>/design.md` — full design document
- `.specclaw/changes/<change>/tasks.md` — parsed via `parse-tasks.sh` internally
- Existing file contents for all files listed in the task

**Outputs to stdout:** A complete coding agent prompt containing:
1. Task title and notes
2. File list to modify
3. Full spec and design context
4. Current contents of each listed file (truncated at **500 lines** per file)
5. Constraints (file scope, code style, test requirements)
6. Commit message format

**File handling:**
- Files that don't exist yet are marked `*New file — to be created*`
- Files exceeding 500 lines show first 500 with a truncation notice
- File paths are resolved relative to the project root (parent of `.specclaw`)

**Fallback behavior:**
- Missing `jq`: uses grep-based JSON field extraction (with stderr warning)
- Missing `spec.md` or `design.md`: inserts placeholder text (with stderr warning)

### `update-task-status.sh` — Status Updater

```
update-task-status.sh [OPTIONS] <tasks.md> <task_id> <new_status>
update-task-status.sh [OPTIONS] <tasks.md> --batch T1:complete T2:failed ...
```

| Flag | Description |
|---|---|
| `--batch` | Accept multiple `TASK_ID:STATUS` pairs |
| `--quiet` | Suppress stdout (errors still go to stderr) |
| `--log` | Append transitions to a `## Status Log` section in tasks.md |
| `-h, --help` | Show usage |

**Valid statuses:** `pending`, `in_progress`, `complete`, `failed`

**Marker mapping:**
| Status | Marker |
|---|---|
| `pending` | `[ ]` |
| `in_progress` | `[~]` |
| `complete` | `[x]` |
| `failed` | `[!]` |

**Behavior:**
- Updates markers in-place via `sed -i`
- Detects previous status for transition logging
- Outputs `OK: T1: pending → in_progress` (unless `--quiet`)
- Always writes `[timestamp] T1: old → new` to stderr
- With `--log`: creates `## Status Log` section if missing, appends timestamped transitions
- Exits 1 if any task ID not found or status invalid (partial batch may have succeeded)

### `update-status.sh` — Dashboard Generator

```
update-status.sh <specclaw_dir>
```

**Reads:**
- `.specclaw/config.yaml` — project name
- `.specclaw/changes/*/tasks.md` — task counts per change
- `.specclaw/changes/archive/*/` — completed changes

**Writes:** `.specclaw/STATUS.md` with:
- Active changes with progress (`done/total tasks (pct%)`) and failure counts
- Pending proposals (directories with `proposal.md` but no `tasks.md`)
- Archived/completed changes
- Summary stats (total, active, completed)

**Status emojis:** 🔨 active, ⚠️ has failures, ✅ all complete, 📋 proposal only

---

## JSON Schemas

### `build.sh setup` Output

```json
{
  "project_name": "my-project",
  "git_strategy": "branch-per-change",
  "branch_prefix": "specclaw/",
  "branch_name": "specclaw/my-feature",
  "branch_existed": false,
  "auto_commit": true,
  "commit_prefix": "specclaw",
  "test_command": "npm test",
  "lint_command": "npm run lint",
  "build_command": "npm run build",
  "parallel_tasks": 3,
  "timeout_seconds": 300,
  "coding_model": "anthropic/claude-sonnet-4-20250514"
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `project_name` | string | — | From `config.yaml` |
| `git_strategy` | string | `"branch-per-change"` | `branch-per-change`, `direct`, or `worktree-per-change` |
| `branch_prefix` | string | `"specclaw/"` | Prefix for feature branches |
| `branch_name` | string | — | Full branch name (prefix + change) |
| `branch_existed` | bool | `false` | `true` if resuming an existing branch |
| `auto_commit` | bool | `true` | Whether `commit` subcommand actually commits |
| `commit_prefix` | string | `"specclaw"` | Used in commit message format |
| `test_command` | string | `""` | Empty = skip tests |
| `lint_command` | string | `""` | Empty = skip lint |
| `build_command` | string | `""` | Empty = skip build |
| `parallel_tasks` | int | `3` | Max concurrent agents per wave |
| `timeout_seconds` | int | `300` | Agent timeout |
| `coding_model` | string | `""` | Model for coding agents |

### `parse-tasks.sh` Output

```json
[
  {"id":"T1","title":"Create parser module","status":"pending","wave":1,"files":"src/parser.ts","depends":"","estimate":"~30 min"},
  {"id":"T2","title":"Add validation","status":"complete","wave":1,"files":"src/validate.ts","depends":"","estimate":"~20 min"},
  {"id":"T3","title":"Integration tests","status":"failed","wave":2,"files":"tests/integration.ts","depends":"T1, T2","estimate":"~45 min"}
]
```

### `build.sh finalize` Output

```json
{
  "tests_passed": true,
  "lint_passed": true,
  "build_passed": true,
  "merged": true,
  "errors": []
}
```

| Field | Type | Description |
|---|---|---|
| `tests_passed` | bool | `false` if test command exited non-zero |
| `lint_passed` | bool | `false` if lint command exited non-zero |
| `build_passed` | bool | `false` if build command exited non-zero |
| `merged` | bool | `true` if branch was merged to main (branch-per-change only) |
| `errors` | string[] | Human-readable error descriptions |

**Possible error strings:**
- `"Test command failed: <cmd>"`
- `"Lint command failed: <cmd>"`
- `"Build command failed: <cmd>"`
- `"Merge conflict merging <branch> into <main> — manual resolution required"`
- `"Failed to checkout <main> for merge"`
- `"Not on expected feature branch; skipping merge"`

---

## Parallel Execution Strategy

### Wave-Based Parallelism

Tasks are grouped into waves. Within a wave, tasks are independent and can run in parallel. Waves execute sequentially — all tasks in wave N must complete before wave N+1 starts.

```
Wave 1: [T1, T2, T3]  ← spawn all 3 simultaneously
         ↓ wait all
Wave 2: [T4, T5]       ← spawn both (they depend on wave 1)
         ↓ wait all
Wave 3: [T6]           ← final integration/testing task
```

### Concurrency Control & Batching

The `parallel_tasks` config value (default: 3) limits concurrent agents. If a wave has more tasks than the limit, they are batched:

```
Wave 1 has 5 tasks, parallel_tasks = 3:
  Batch A: [T1, T2, T3] → sessions_spawn all 3 → sessions_yield → wait
  Batch B: [T4, T5]     → sessions_spawn both  → sessions_yield → wait
  → Wave 2...
```

The agent reads `parallel_tasks` from the setup JSON and chunks tasks accordingly.

### Conflict Prevention

- Each task declares its files explicitly in tasks.md
- Tasks in the same wave MUST NOT touch the same files
- File overlap within a wave indicates a planning error (should be caught during `specclaw plan`)
- The build engine does not validate this at runtime — it's enforced at planning time

---

## Fresh Context Principle

Each coding agent gets ONLY:
1. The specific task it's working on
2. The full spec and design documents
3. Current content of files it needs to modify (capped at 500 lines each)
4. Constraints and commit format

NO accumulated context from previous tasks. This prevents:
- Context window pollution across tasks
- Conflicting instructions from earlier tasks
- Agents "remembering" wrong approaches

The `build-context.sh` script enforces this by building each payload independently from source files on disk.

---

## Error Handling

### Task Failure

When a coding agent fails or times out:

1. `update-task-status.sh` marks the task as `[!] failed`
2. The error is logged (use `--log` flag for persistent log in tasks.md)
3. Dependent tasks (in later waves) are skipped
4. Independent tasks in the same wave continue unaffected
5. After all possible tasks complete, failures are reported to the user

### Recovery & Resume

The build engine supports resumption natively:

```bash
# Re-run build — automatically picks up pending and failed tasks
specclaw build <change>
# Complete tasks ([x]) are skipped. In-progress ([~]) and failed ([!]) are retried.

# build.sh setup detects existing branch:
# "Branch 'specclaw/my-feature' already exists — resuming"
# branch_existed: true in setup JSON
```

### Batch Status Updates

For efficiency when multiple tasks complete simultaneously:

```bash
update-task-status.sh --batch --log tasks.md T1:complete T2:failed T3:complete
```

---

## Git Integration

### branch-per-change Strategy (default)

```
build.sh setup   → git checkout -b specclaw/<change>  (or switch if exists)
build.sh commit  → git add <files> && git commit -m "specclaw(<change>): T1 — title"
  ... repeat per task ...
build.sh finalize → git checkout main && git merge --no-ff specclaw/<change>
                  → git branch -d specclaw/<change>  (cleanup on success)
```

On merge conflict, `finalize` runs `git merge --abort`, switches back to the feature branch, and reports the error in the summary JSON. Manual resolution is required.

### direct Strategy

```
build.sh setup   → no branch operations
build.sh commit  → git add <files> && git commit (on current branch)
build.sh finalize → run checks only, no merge step
```

---

## Git Strategies

SpecClaw supports three git strategies, configured via `git.strategy` in config.yaml:

### branch-per-change (default)
- Creates a branch `specclaw/<change-name>` and switches to it
- Each task commits on the branch
- Finalize merges into main and deletes the branch
- **Limitation:** Only one change can build at a time (branch switching)

### direct
- Commits straight to the current branch
- No branch creation or merging
- Simplest strategy, good for small projects or CI pipelines

### worktree-per-change
- Creates an isolated git worktree at `.specclaw/worktrees/<change-name>/`
- Each change has its own working directory on its own branch
- Agents get the worktree path as their `cwd`
- **Enables parallel multi-change builds** — no branch switching, no file conflicts
- Finalize merges the worktree branch into main, removes worktree
- Best for: active projects with multiple features in flight

### When to use each:
| Strategy | Best For | Parallel Changes |
|----------|----------|-----------------|
| branch-per-change | Single active change | ❌ |
| direct | Simple projects, CI | ❌ |
| worktree-per-change | Multiple parallel changes | ✅ |

---

## Notification Payloads

### Build Started
```
🦞 **SpecClaw Build Started**
**Change:** <change-name>
**Tasks:** <count> across <waves> waves
**Model:** <coding model>
```

### Task Complete
```
✅ `<task-id>` — <task-title> (wave <n>)
```

### Task Failed
```
❌ `<task-id>` — <task-title>
Error: <error summary>
```

### Build Complete
```
🦞 **SpecClaw Build Complete**
**Change:** <change-name>
**Result:** <passed>/<total> tasks | <failed> failed
**Duration:** <time>
**Next:** Run `specclaw verify <change>` to validate
```

---

## Troubleshooting

### Build hangs

**Symptom:** Build seems stuck, no progress.

**Check:**
1. Look for `[~]` (in_progress) tasks in `tasks.md` — these are the active ones
2. Check if the spawned agent sessions are still running
3. Verify `timeout_seconds` in config.yaml (default: 300s / 5 min)

**Fix:** Kill stuck agent sessions, then re-run `specclaw build <change>`. In-progress tasks will be retried.

### JSON parse errors

**Symptom:** `parse-tasks.sh` outputs invalid JSON or the build engine can't read task data.

**Check:**
```bash
parse-tasks.sh --validate tasks.md
# Exit 0 = valid, Exit 1 = invalid
```

**Common causes:**
- Missing backtick-wrapped task IDs (e.g. `T1` not `` `T1` ``)
- Malformed wave headers (must be `### Wave N`)
- Broken indentation on `- Files:` / `- Depends:` sub-fields
- Non-UTF8 characters in task titles

### Merge conflicts

**Symptom:** `build.sh finalize` reports merge conflict error.

**What happens:** The script runs `git merge --abort` automatically and switches back to the feature branch. No data is lost.

**Fix:**
1. Manually resolve conflicts: `git checkout main && git merge specclaw/<change>`
2. Or rebase the feature branch: `git rebase main` from the feature branch

### Task context too large

**Symptom:** Coding agent gets truncated context or hits token limits.

**Cause:** `build-context.sh` includes full `spec.md` and `design.md` plus file contents.

**Mitigations:**
- File contents are capped at **500 lines** per file (configurable via `MAX_FILE_LINES` in script)
- Keep spec and design documents focused
- Split large tasks into smaller ones with fewer files

### Resuming interrupted builds

**Symptom:** Build was interrupted (crash, timeout, manual stop).

**Fix:** Simply re-run `specclaw build <change>`.

**What happens:**
- `build.sh setup` detects the existing branch and switches to it (`branch_existed: true`)
- `parse-tasks.sh --status pending` returns only incomplete tasks
- Completed tasks (`[x]`) are skipped entirely
- Failed tasks (`[!]`) and in-progress tasks (`[~]`) are picked up for retry
- Git commits from completed tasks are preserved on the branch


path: references/workflow-examples.md
content: # Workflow Examples

Real-world examples of using SpecClaw commands.

---

## Example 1: Adding Authentication to a Node.js API

### User says:
> specclaw propose "add JWT authentication with refresh tokens"

### Agent does:

```
1. Analyze codebase:
   - find . -maxdepth 3 | head -80
   - cat package.json
   - head -30 src/app.ts src/routes/*.ts

2. Create .specclaw/changes/add-jwt-auth/proposal.md

3. Present to user for review
```

### After approval, user says:
> specclaw plan add-jwt-auth

### Agent generates:

**spec.md** (excerpt):
```markdown
## Acceptance Criteria
- [ ] AC-1: GIVEN a valid username/password WHEN POST /auth/login THEN return JWT access token (15min) + refresh token (7d)
- [ ] AC-2: GIVEN an expired access token WHEN POST /auth/refresh with valid refresh token THEN return new access + refresh tokens
- [ ] AC-3: GIVEN an invalid token WHEN accessing protected route THEN return 401 with error message
```

**tasks.md** (excerpt):
```markdown
### Wave 1 — Core auth infrastructure
- [ ] `T1` — Create auth middleware
  - Files: src/middleware/auth.ts, src/types/auth.ts
  - Estimate: small
- [ ] `T2` — Add JWT utility functions
  - Files: src/utils/jwt.ts, src/config/auth.ts
  - Estimate: small

### Wave 2 — Routes and handlers
- [ ] `T3` — Create auth routes (login, refresh, logout)
  - Files: src/routes/auth.ts, src/controllers/auth.ts
  - Depends: T1, T2
  - Estimate: medium

### Wave 3 — Integration
- [ ] `T4` — Protect existing routes with auth middleware
  - Files: src/routes/users.ts, src/routes/posts.ts
  - Depends: T1
  - Estimate: small
- [ ] `T5` — Auth integration tests
  - Files: tests/auth.test.ts
  - Depends: T3
  - Estimate: medium
```

### User says:
> specclaw build add-jwt-auth

### Agent orchestrates:

```
Wave 1: Spawn 2 agents (T1 + T2 in parallel)
  → T1 agent: creates auth.ts middleware + types
  → T2 agent: creates jwt.ts utils + config
  → Both complete ✅

Wave 2: Spawn 1 agent (T3)
  → T3 agent: gets spec + design + existing middleware/utils as context
  → Creates auth routes + controllers
  → Complete ✅

Wave 3: Spawn 2 agents (T4 + T5 in parallel)
  → T4 agent: adds auth middleware to existing routes
  → T5 agent: writes integration tests
  → Both complete ✅

Post-build: npm test → all passing
```

---

## Example 2: Autonomous Mode (Cron)

### Setup:
```yaml
# .specclaw/config.yaml
automation:
  auto_mode: true
  cron: "0 2 * * *"  # 2 AM daily
  max_tasks_per_run: 5
```

### What happens at 2 AM:

```
1. Agent wakes up
2. Reads .specclaw/STATUS.md
3. Finds: "add-dark-mode" has approved plan, not yet built
4. Runs: specclaw build add-dark-mode
5. Spawns agents for wave 1...
6. Completes 5 tasks (hits max_tasks_per_run)
7. Updates status, sends Discord notification:
   "🦞 Nightly build: 5/8 tasks complete for add-dark-mode. Continuing tomorrow."
8. Next night: picks up remaining 3 tasks
9. Runs verification
10. Sends: "🦞 add-dark-mode complete and verified! Ready for review."
```

---

## Example 3: Multi-Model Routing

```yaml
models:
  planning: "anthropic/claude-opus-4-6"      # Best reasoning for specs
  coding: "openai/gpt-5.1-codex"             # Fast + cheap for implementation
  review: "anthropic/claude-sonnet-4-5"       # Good balance for verification
```

### How it flows:

```
specclaw propose → Opus drafts the proposal
specclaw plan    → Opus generates spec + design + tasks
specclaw build   → Codex implements each task (parallel, cheap)
specclaw verify  → Sonnet validates against spec
```

Cost optimization: Opus for thinking, Codex for typing, Sonnet for checking.

---

## Example 4: Discord Approval Flow

### Config:
```yaml
notifications:
  channel: "discord"
  target: "channel:1486698924512116736"  # #irp channel
  on_approval_needed: true
```

### Flow:
```
1. User: "specclaw propose add-search"
2. Agent creates proposal, sends to Discord:
   "🦞 New proposal: **add-search**
    [View Proposal] [Approve] [Reject]"
3. User clicks Approve in Discord
4. Agent auto-advances to planning
5. Sends plan summary to Discord for review
6. User approves → build starts
7. Progress updates stream to Discord channel
```


path: scripts/build-context.sh
content: #!/usr/bin/env bash
# Build context payload for a specific task — feeds the coding agent
# Usage: build-context.sh <specclaw_dir> <change_name> <task_id>
#
# Reads spec.md, design.md, task details from tasks.md, existing file contents,
# and formats everything into a complete Build Agent prompt sent to stdout.
#
# Dependencies: bash, coreutils, jq (with grep fallback)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MAX_FILE_LINES=500

# --- Help ---
usage() {
  cat <<'EOF'
Usage: build-context.sh <specclaw_dir> <change_name> <task_id>

Build a context payload for a coding agent to implement a specific task.

Arguments:
  specclaw_dir   Path to the .specclaw directory
  change_name    Name of the current change (subdirectory under changes/)
  task_id        Task ID to build context for (e.g. T1, T4)

The script reads spec.md, design.md, task details, and existing file contents,
then outputs a formatted prompt to stdout.

Examples:
  build-context.sh .specclaw my-feature T3
  build-context.sh /project/.specclaw auth-system T1
EOF
  exit 0
}

# --- Argument parsing ---
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage

if [[ $# -lt 3 ]]; then
  echo "ERROR: Expected 3 arguments: <specclaw_dir> <change_name> <task_id>" >&2
  echo "Use --help for usage." >&2
  exit 1
fi

SPECCLAW_DIR="$1"
CHANGE_NAME="$2"
TASK_ID="$3"

# Validate specclaw dir
if [[ ! -d "$SPECCLAW_DIR" ]]; then
  echo "ERROR: specclaw directory not found: $SPECCLAW_DIR" >&2
  exit 1
fi

CHANGE_DIR="$SPECCLAW_DIR/changes/$CHANGE_NAME"
if [[ ! -d "$CHANGE_DIR" ]]; then
  echo "ERROR: Change directory not found: $CHANGE_DIR" >&2
  exit 1
fi

# Project root is parent of .specclaw
PROJECT_ROOT="$(cd "$SPECCLAW_DIR/.." && pwd)"

# --- Config extraction ---
CONFIG_FILE="$SPECCLAW_DIR/config.yaml"

yaml_get() {
  # Simple YAML value extractor — handles: key: "value" or key: value
  local file="$1" key="$2"
  grep -E "^[[:space:]]*${key}:" "$file" 2>/dev/null \
    | head -1 \
    | sed 's/^[^:]*:[[:space:]]*//' \
    | sed 's/^["'\'']\(.*\)["'\'']\s*$/\1/' \
    | sed 's/[[:space:]]*$//'
}

PROJECT_NAME=""
COMMIT_PREFIX=""

if [[ -f "$CONFIG_FILE" ]]; then
  PROJECT_NAME="$(yaml_get "$CONFIG_FILE" "name")"
  COMMIT_PREFIX="$(yaml_get "$CONFIG_FILE" "commit_prefix")"
else
  echo "WARNING: config.yaml not found at $CONFIG_FILE" >&2
fi

PROJECT_NAME="${PROJECT_NAME:-unnamed-project}"
COMMIT_PREFIX="${COMMIT_PREFIX:-specclaw}"

# --- Task extraction ---
TASKS_FILE="$CHANGE_DIR/tasks.md"
if [[ ! -f "$TASKS_FILE" ]]; then
  echo "ERROR: tasks.md not found: $TASKS_FILE" >&2
  exit 1
fi

# Parse all tasks
TASKS_JSON="$("$SCRIPT_DIR/parse-tasks.sh" "$TASKS_FILE")"

# Extract the specific task by ID
extract_task_jq() {
  echo "$TASKS_JSON" | jq -e --arg id "$TASK_ID" '.[] | select(.id == $id)' 2>/dev/null
}

extract_task_grep() {
  # Fallback: grep-based extraction from JSON array
  # Find the object containing our task ID
  local in_match=false
  local result=""
  local brace_depth=0

  while IFS= read -r line; do
    if [[ "$line" == *"\"id\":\"$TASK_ID\""* ]]; then
      in_match=true
    fi
    if [[ "$in_match" == true ]]; then
      result+="$line"
      # Count braces to find object boundaries
      local opens="${line//[^\{]/}"
      local closes="${line//[^\}]/}"
      brace_depth=$(( brace_depth + ${#opens} - ${#closes} ))
      if [[ $brace_depth -le 0 && -n "$result" ]]; then
        # Clean trailing comma
        result="${result%,}"
        echo "$result"
        return 0
      fi
    fi
  done <<< "$TASKS_JSON"

  return 1
}

# Extract field from a single JSON object (no jq)
json_field_grep() {
  local json="$1" field="$2"
  echo "$json" | grep -o "\"${field}\":\"[^\"]*\"" | head -1 | sed "s/\"${field}\":\"//" | sed 's/"$//'
}

json_field_grep_num() {
  local json="$1" field="$2"
  echo "$json" | grep -o "\"${field}\":[0-9]*" | head -1 | sed "s/\"${field}\"://"
}

TASK_JSON=""
if command -v jq &>/dev/null; then
  TASK_JSON="$(extract_task_jq)" || true
else
  echo "WARNING: jq not available, using grep fallback" >&2
  TASK_JSON="$(extract_task_grep)" || true
fi

if [[ -z "$TASK_JSON" ]]; then
  echo "ERROR: Task $TASK_ID not found in $TASKS_FILE" >&2
  exit 1
fi

# Extract task fields
if command -v jq &>/dev/null; then
  TASK_TITLE="$(echo "$TASK_JSON" | jq -r '.title')"
  TASK_FILES="$(echo "$TASK_JSON" | jq -r '.files')"
  TASK_STATUS="$(echo "$TASK_JSON" | jq -r '.status')"
  TASK_DEPENDS="$(echo "$TASK_JSON" | jq -r '.depends')"
  TASK_WAVE="$(echo "$TASK_JSON" | jq -r '.wave')"
  TASK_ESTIMATE="$(echo "$TASK_JSON" | jq -r '.estimate')"
else
  TASK_TITLE="$(json_field_grep "$TASK_JSON" "title")"
  TASK_FILES="$(json_field_grep "$TASK_JSON" "files")"
  TASK_STATUS="$(json_field_grep "$TASK_JSON" "status")"
  TASK_DEPENDS="$(json_field_grep "$TASK_JSON" "depends")"
  TASK_WAVE="$(json_field_grep_num "$TASK_JSON" "wave")"
  TASK_ESTIMATE="$(json_field_grep "$TASK_JSON" "estimate")"
fi

# --- Task notes extraction ---
# Extract notes from tasks.md for this specific task
TASK_NOTES=""
if grep -q "^\`${TASK_ID}\`" "$TASKS_FILE" || grep -q " \`${TASK_ID}\`" "$TASKS_FILE"; then
  # Get the Notes: line for this task
  TASK_NOTES="$(awk -v tid="$TASK_ID" '
    $0 ~ "`"tid"`" { found=1; next }
    found && /^  - Notes:/ { sub(/^  - Notes: /, ""); print; found=0; next }
    found && /^- \[/ { found=0 }
    found && /^###/ { found=0 }
  ' "$TASKS_FILE")"
fi

# --- Spec & Design ---
SPEC_FILE="$CHANGE_DIR/spec.md"
DESIGN_FILE="$CHANGE_DIR/design.md"

SPEC_CONTENT=""
if [[ -f "$SPEC_FILE" ]]; then
  SPEC_CONTENT="$(cat "$SPEC_FILE")"
else
  echo "WARNING: spec.md not found at $SPEC_FILE — using placeholder" >&2
  SPEC_CONTENT="*No specification found. Use your best judgment based on the task description.*"
fi

DESIGN_CONTENT=""
if [[ -f "$DESIGN_FILE" ]]; then
  DESIGN_CONTENT="$(cat "$DESIGN_FILE")"
else
  echo "WARNING: design.md not found at $DESIGN_FILE — using placeholder" >&2
  DESIGN_CONTENT="*No design document found. Use your best judgment based on the task description.*"
fi

# --- Existing file contents ---
build_file_contents() {
  local file_list="$1"
  local output=""

  # Split comma-separated file list
  IFS=',' read -ra FILES <<< "$file_list"

  for raw_file in "${FILES[@]}"; do
    # Trim whitespace and backticks
    local file
    file="$(echo "$raw_file" | sed 's/^[[:space:]`]*//;s/[[:space:]`]*$//')"
    [[ -z "$file" ]] && continue

    local full_path="$PROJECT_ROOT/$file"

    output+="### \`$file\`"$'\n'

    if [[ -f "$full_path" ]]; then
      local line_count
      line_count="$(wc -l < "$full_path")"
      if [[ "$line_count" -gt "$MAX_FILE_LINES" ]]; then
        output+="*(Truncated: showing first $MAX_FILE_LINES of $line_count lines)*"$'\n'
        output+='```'$'\n'
        output+="$(head -n "$MAX_FILE_LINES" "$full_path")"$'\n'
        output+='```'$'\n'
      else
        output+='```'$'\n'
        output+="$(cat "$full_path")"$'\n'
        output+='```'$'\n'
      fi
    else
      output+="*New file — to be created*"$'\n'
    fi
    output+=$'\n'
  done

  echo "$output"
}

EXISTING_CODE=""
if [[ -n "$TASK_FILES" && "$TASK_FILES" != "null" ]]; then
  EXISTING_CODE="$(build_file_contents "$TASK_FILES")"
else
  EXISTING_CODE="*No files specified for this task.*"
fi

# --- Format file list for display ---
format_file_list() {
  local file_list="$1"
  IFS=',' read -ra FILES <<< "$file_list"
  for raw_file in "${FILES[@]}"; do
    local file
    file="$(echo "$raw_file" | sed 's/^[[:space:]`]*//;s/[[:space:]`]*$//')"
    [[ -z "$file" ]] && continue
    echo "- \`$file\`"
  done
}

FILE_LIST_FORMATTED=""
if [[ -n "$TASK_FILES" && "$TASK_FILES" != "null" ]]; then
  FILE_LIST_FORMATTED="$(format_file_list "$TASK_FILES")"
else
  FILE_LIST_FORMATTED="*No files specified*"
fi

# --- Error history for retries ---
ERRORS_FILE="$CHANGE_DIR/errors.md"
ERROR_HISTORY=""

if [[ -f "$ERRORS_FILE" ]]; then
  # Extract blocks for this task_id: from "## <TASK_ID>" line through "---"
  # Take only the last 3 entries
  TASK_ERRORS="$(awk -v tid="$TASK_ID" '
    BEGIN { block=""; capturing=0 }
    /^## / {
      if (capturing) {
        # End previous block (no trailing ---)
        blocks[++n] = block
        block = ""
        capturing = 0
      }
      if ($2 == tid) {
        capturing = 1
        block = $0 "\n"
        next
      }
    }
    /^---$/ {
      if (capturing) {
        blocks[++n] = block
        block = ""
        capturing = 0
        next
      }
    }
    capturing { block = block $0 "\n" }
    END {
      if (capturing && block != "") blocks[++n] = block
      # Output last 3 blocks
      start = (n > 3) ? n - 2 : 1
      for (i = start; i <= n; i++) {
        printf "%s---\n", blocks[i]
      }
    }
  ' "$ERRORS_FILE")"

  if [[ -n "$TASK_ERRORS" ]]; then
    ERROR_HISTORY="## Previous Errors for This Task

The following errors occurred in previous attempts. **Avoid repeating these mistakes.**

${TASK_ERRORS}"
  fi
fi

# --- Build the prompt ---
cat <<PROMPT
You are a coding agent implementing a specific task in the project "${PROJECT_NAME}".

## Your Task
${TASK_TITLE}
${TASK_NOTES}

## Files to Modify
${FILE_LIST_FORMATTED}

## Specification Context
${SPEC_CONTENT}

## Design Context
${DESIGN_CONTENT}

## Existing Code
${EXISTING_CODE}

${ERROR_HISTORY}
## Constraints
1. ONLY modify/create the files listed above
2. Follow existing code style and patterns
3. Write tests for new functionality (if test framework exists)
4. Use existing utilities/helpers — don't duplicate
5. Keep changes minimal and focused on THIS task only
6. Commit with message: "${COMMIT_PREFIX}(${CHANGE_NAME}): ${TASK_ID} — ${TASK_TITLE}"

## Definition of Done
- All listed files created/modified correctly
- Code compiles/runs without errors
- Tests pass (if applicable)
- No unrelated changes
PROMPT


path: scripts/build.sh
content: #!/usr/bin/env bash
# build.sh — Build engine for specclaw: setup, commit, finalize
# Part of the specclaw skill. Bash + git + coreutils only.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ─── Helpers ──────────────────────────────────────────────────────────────────

usage() {
  cat <<'EOF'
Usage: build.sh <subcommand> [options]

Subcommands:
  setup         <specclaw_dir> <change_name>
                Read config.yaml, create git branch/worktree if needed, output config JSON.

  commit        <specclaw_dir> <change_name> <task_id> "<title>" [files...]
                Stage listed files and commit with specclaw prefix.

  finalize      <specclaw_dir> <change_name>
                Run configured checks, merge branch if branch/worktree-per-change, output summary JSON.

  worktree-path <specclaw_dir> <change_name>
                Output the worktree path for a change (reads git.worktree_dir from config).

Git strategies (git.strategy in config.yaml):
  branch-per-change   Create a branch and switch to it (default)
  worktree-per-change Create a branch + git worktree for isolated parallel work
  direct              Work on the current branch

Options:
  -h, --help   Show this help message
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }
warn() { echo "WARN: $*" >&2; }

# Read a simple yaml value: yaml_val <file> <dotted.key>
# Handles top-level and one-level nested keys (good enough for config.yaml).
yaml_val() {
  local file="$1" key="$2"
  local section field
  if [[ "$key" == *.* ]]; then
    section="${key%%.*}"
    field="${key#*.}"
  else
    section=""
    field="$key"
  fi

  local in_section=false value=""
  while IFS= read -r line; do
    # skip comments / blank
    [[ "$line" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${line// /}" ]] && continue

    if [[ -z "$section" ]]; then
      # top-level key
      if [[ "$line" =~ ^${field}:[[:space:]]*(.*) ]]; then
        value="${BASH_REMATCH[1]}"
        break
      fi
    else
      # detect section header (no leading whitespace)
      if [[ "$line" =~ ^[a-zA-Z_] ]]; then
        if [[ "$line" =~ ^${section}: ]]; then
          in_section=true
        else
          in_section=false
        fi
        continue
      fi
      if $in_section; then
        # indented key
        if [[ "$line" =~ ^[[:space:]]+${field}:[[:space:]]*(.*) ]]; then
          value="${BASH_REMATCH[1]}"
          break
        fi
      fi
    fi
  done < "$file"

  # strip surrounding quotes
  value="${value#\"}"
  value="${value%\"}"
  value="${value#\'}"
  value="${value%\'}"
  echo "$value"
}

# JSON-escape a string value
json_str() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  printf '"%s"' "$s"
}

json_bool() {
  case "${1,,}" in
    true|yes|1) echo "true" ;;
    *) echo "false" ;;
  esac
}

# ─── setup ────────────────────────────────────────────────────────────────────

cmd_setup() {
  [[ $# -ge 2 ]] || die "setup requires: <specclaw_dir> <change_name>"
  local specclaw_dir="$1" change_name="$2"
  local config="$specclaw_dir/config.yaml"
  [[ -f "$config" ]] || die "Config not found: $config"

  # Extract values (with defaults)
  local project_name; project_name="$(yaml_val "$config" "project.name")"
  local git_strategy; git_strategy="$(yaml_val "$config" "git.strategy")"
  local branch_prefix; branch_prefix="$(yaml_val "$config" "git.branch_prefix")"
  local auto_commit; auto_commit="$(yaml_val "$config" "git.auto_commit")"
  local commit_prefix; commit_prefix="$(yaml_val "$config" "git.commit_prefix")"
  local test_command; test_command="$(yaml_val "$config" "build.test_command")"
  local lint_command; lint_command="$(yaml_val "$config" "build.lint_command")"
  local build_command; build_command="$(yaml_val "$config" "build.build_command")"
  local parallel_tasks; parallel_tasks="$(yaml_val "$config" "build.parallel_tasks")"
  local timeout_seconds; timeout_seconds="$(yaml_val "$config" "build.timeout_seconds")"
  local coding_model; coding_model="$(yaml_val "$config" "models.coding")"

  # Defaults
  git_strategy="${git_strategy:-branch-per-change}"
  branch_prefix="${branch_prefix:-specclaw/}"
  auto_commit="${auto_commit:-true}"
  commit_prefix="${commit_prefix:-specclaw}"
  parallel_tasks="${parallel_tasks:-3}"
  timeout_seconds="${timeout_seconds:-300}"

  local worktree_dir_cfg; worktree_dir_cfg="$(yaml_val "$config" "git.worktree_dir")"

  local branch_name="${branch_prefix}${change_name}"
  local branch_existed=false
  local worktree_path=""

  # Git branch handling
  if [[ "$git_strategy" == "worktree-per-change" ]]; then
    local worktree_dir="${worktree_dir_cfg:-.specclaw/worktrees}"
    worktree_path="$worktree_dir/$change_name"

    mkdir -p "$worktree_dir"

    if [ -d "$worktree_path" ]; then
      # Resume — worktree exists
      branch_existed=true
      warn "Worktree '$worktree_path' already exists — resuming"
    else
      # Create branch if needed
      if ! git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
        git branch "$branch_name"
      fi
      # Create worktree
      git worktree add "$worktree_path" "$branch_name"
    fi
  elif [[ "$git_strategy" == "branch-per-change" ]]; then
    if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then
      # Branch exists — resume case
      branch_existed=true
      warn "Branch '$branch_name' already exists — resuming"
      git checkout "$branch_name" 2>/dev/null || git switch "$branch_name"
    else
      # Create new branch
      git checkout -b "$branch_name" 2>/dev/null || git switch -c "$branch_name"
    fi
  fi

  # Output JSON
  cat <<ENDJSON
{
  "project_name": $(json_str "$project_name"),
  "git_strategy": $(json_str "$git_strategy"),
  "branch_prefix": $(json_str "$branch_prefix"),
  "branch_name": $(json_str "$branch_name"),
  "branch_existed": $branch_existed,
  "auto_commit": $(json_bool "$auto_commit"),
  "commit_prefix": $(json_str "$commit_prefix"),
  "test_command": $(json_str "$test_command"),
  "lint_command": $(json_str "$lint_command"),
  "build_command": $(json_str "$build_command"),
  "parallel_tasks": ${parallel_tasks},
  "timeout_seconds": ${timeout_seconds},
  "coding_model": $(json_str "$coding_model"),
  "worktree_path": $(json_str "$worktree_path")
}
ENDJSON
}

# ─── commit ───────────────────────────────────────────────────────────────────

cmd_commit() {
  [[ $# -ge 4 ]] || die "commit requires: <specclaw_dir> <change_name> <task_id> <title> [files...]"
  local specclaw_dir="$1" change_name="$2" task_id="$3" title="$4"
  shift 4
  local files=("$@")

  local config="$specclaw_dir/config.yaml"
  [[ -f "$config" ]] || die "Config not found: $config"

  local git_strategy; git_strategy="$(yaml_val "$config" "git.strategy")"
  local auto_commit; auto_commit="$(yaml_val "$config" "git.auto_commit")"
  local commit_prefix; commit_prefix="$(yaml_val "$config" "git.commit_prefix")"
  git_strategy="${git_strategy:-branch-per-change}"
  auto_commit="${auto_commit:-true}"
  commit_prefix="${commit_prefix:-specclaw}"

  # If worktree strategy, cd into the worktree
  if [[ "$git_strategy" == "worktree-per-change" ]]; then
    local worktree_dir="$(yaml_val "$config" "git.worktree_dir")"
    worktree_dir="${worktree_dir:-.specclaw/worktrees}"
    local worktree_path="$worktree_dir/$change_name"
    [[ -d "$worktree_path" ]] || die "Worktree not found: $worktree_path"
    cd "$worktree_path"
  fi

  # Stage files
  if [[ ${#files[@]} -gt 0 ]]; then
    git add -- "${files[@]}"
  else
    warn "No files specified — nothing to stage"
  fi

  # Check if there are staged changes
  if git diff --cached --quiet 2>/dev/null; then
    warn "No changes to commit for $task_id"
    exit 0
  fi

  # Commit (or just stage if auto_commit is off)
  local msg="${commit_prefix}(${change_name}): ${task_id} — ${title}"

  if [[ "$(json_bool "$auto_commit")" == "true" ]]; then
    git commit -m "$msg"
    echo "Committed: $msg"
  else
    warn "auto_commit is false — changes staged but not committed"
    echo "Staged: $msg"
  fi
}

# ─── finalize ─────────────────────────────────────────────────────────────────

cmd_finalize() {
  [[ $# -ge 2 ]] || die "finalize requires: <specclaw_dir> <change_name>"
  local specclaw_dir="$1" change_name="$2"
  local config="$specclaw_dir/config.yaml"
  [[ -f "$config" ]] || die "Config not found: $config"

  local git_strategy; git_strategy="$(yaml_val "$config" "git.strategy")"
  local branch_prefix; branch_prefix="$(yaml_val "$config" "git.branch_prefix")"
  local test_command; test_command="$(yaml_val "$config" "build.test_command")"
  local lint_command; lint_command="$(yaml_val "$config" "build.lint_command")"
  local build_command; build_command="$(yaml_val "$config" "build.build_command")"

  git_strategy="${git_strategy:-branch-per-change}"
  branch_prefix="${branch_prefix:-specclaw/}"

  local tests_passed=true lint_passed=true build_passed=true merged=false
  local errors=()

  # Run test command
  if [[ -n "$test_command" ]]; then
    echo "Running tests: $test_command" >&2
    if ! eval "$test_command" >&2 2>&1; then
      tests_passed=false
      errors+=("Test command failed: $test_command")
    fi
  fi

  # Run lint command
  if [[ -n "$lint_command" ]]; then
    echo "Running lint: $lint_command" >&2
    if ! eval "$lint_command" >&2 2>&1; then
      lint_passed=false
      errors+=("Lint command failed: $lint_command")
    fi
  fi

  # Run build command
  if [[ -n "$build_command" ]]; then
    echo "Running build: $build_command" >&2
    if ! eval "$build_command" >&2 2>&1; then
      build_passed=false
      errors+=("Build command failed: $build_command")
    fi
  fi

  # Merge branch if branch-per-change or worktree-per-change
  if [[ "$git_strategy" == "worktree-per-change" ]]; then
    local branch_name="${branch_prefix}${change_name}"
    local worktree_dir="$(yaml_val "$config" "git.worktree_dir")"
    worktree_dir="${worktree_dir:-.specclaw/worktrees}"
    local worktree_path="$worktree_dir/$change_name"

    # Determine the main branch name
    local main_branch="main"
    if git rev-parse --verify main >/dev/null 2>&1; then
      main_branch="main"
    elif git rev-parse --verify master >/dev/null 2>&1; then
      main_branch="master"
    fi

    # We're in the main repo — merge the worktree's branch
    if git merge --no-ff "$branch_name" -m "Merge $branch_name" 2>&1; then
      merged=true
      # Clean up worktree and branch
      git worktree remove "$worktree_path" 2>/dev/null || true
      git branch -d "$branch_name" 2>/dev/null || true
    else
      # Merge conflict — abort, keep worktree intact
      git merge --abort 2>/dev/null || true
      errors+=("Merge conflict merging $branch_name into $main_branch — worktree preserved at $worktree_path for manual resolution")
    fi
  elif [[ "$git_strategy" == "branch-per-change" ]]; then
    local branch_name="${branch_prefix}${change_name}"
    local current_branch; current_branch="$(git branch --show-current)"

    # Determine the main branch name
    local main_branch="main"
    if git rev-parse --verify main >/dev/null 2>&1; then
      main_branch="main"
    elif git rev-parse --verify master >/dev/null 2>&1; then
      main_branch="master"
    fi

    if [[ "$current_branch" == "$branch_name" ]]; then
      # Switch to main and merge
      if git checkout "$main_branch" 2>/dev/null; then
        if git merge --no-ff "$branch_name" -m "Merge $branch_name" 2>&1; then
          merged=true
          # Clean up feature branch
          git branch -d "$branch_name" 2>/dev/null || true
        else
          # Merge conflict — abort and go back
          git merge --abort 2>/dev/null || true
          git checkout "$branch_name" 2>/dev/null || true
          errors+=("Merge conflict merging $branch_name into $main_branch — manual resolution required")
        fi
      else
        errors+=("Failed to checkout $main_branch for merge")
      fi
    else
      warn "Not on feature branch ($current_branch != $branch_name) — skipping merge"
      errors+=("Not on expected feature branch; skipping merge")
    fi
  fi

  # Build errors JSON array
  local errors_json="[]"
  if [[ ${#errors[@]} -gt 0 ]]; then
    errors_json="["
    local first=true
    for err in "${errors[@]}"; do
      $first || errors_json+=", "
      first=false
      errors_json+="$(json_str "$err")"
    done
    errors_json+="]"
  fi

  # Output summary JSON
  cat <<ENDJSON
{
  "tests_passed": $tests_passed,
  "lint_passed": $lint_passed,
  "build_passed": $build_passed,
  "merged": $merged,
  "errors": $errors_json
}
ENDJSON
}

# ─── worktree-path ────────────────────────────────────────────────────────────

cmd_worktree_path() {
  [[ $# -ge 2 ]] || die "worktree-path requires: <specclaw_dir> <change_name>"
  local specclaw_dir="$1" change_name="$2"
  local config="$specclaw_dir/config.yaml"
  [[ -f "$config" ]] || die "Config not found: $config"
  local worktree_dir="$(yaml_val "$config" "git.worktree_dir")"
  worktree_dir="${worktree_dir:-.specclaw/worktrees}"
  echo "$worktree_dir/$change_name"
}

# ─── Main dispatch ────────────────────────────────────────────────────────────

[[ $# -gt 0 ]] || { usage; exit 1; }

case "$1" in
  -h|--help) usage; exit 0 ;;
  setup)         shift; cmd_setup "$@" ;;
  commit)        shift; cmd_commit "$@" ;;
  finalize)      shift; cmd_finalize "$@" ;;
  worktree-path) shift; cmd_worktree_path "$@" ;;
  *)             die "Unknown subcommand: $1 (try --help)" ;;
esac


path: scripts/detect-patterns.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE_DIR="$(cd "$SCRIPT_DIR/../templates" && pwd)"

usage() {
  cat <<'EOF'
Usage:
  detect-patterns.sh <specclaw_dir> scan <change_name>
  detect-patterns.sh <specclaw_dir> list [--min-recurrence N]
  detect-patterns.sh <specclaw_dir> promote <pat-id>

Modes:
  scan     Scan a change's errors/learnings and update the pattern registry
  list     List all recorded patterns (optionally filtered by recurrence)
  promote  Promote a pattern from active to promoted

Options:
  -h, --help   Show this help message
EOF
}

# Stop words to strip from summaries
STOP_WORDS="the a an in to and or is was for of with that this it on at by from be are were has have had not but also"

strip_stop_words() {
  local text="$1"
  text=$(echo "$text" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]/ /g' | tr -s ' ')
  local result=""
  for word in $text; do
    local is_stop=0
    for sw in $STOP_WORDS; do
      if [[ "$word" == "$sw" ]]; then
        is_stop=1
        break
      fi
    done
    if [[ $is_stop -eq 0 && ${#word} -gt 1 ]]; then
      result="$result $word"
    fi
  done
  echo "$result" | sed 's/^ //'
}

count_overlapping() {
  local kw1="$1"
  local kw2="$2"
  local count=0
  for w in $kw1; do
    for k in $kw2; do
      if [[ "$w" == "$k" ]]; then
        count=$((count + 1))
        break
      fi
    done
  done
  echo "$count"
}

get_next_pat_id() {
  local patterns_file="$1"
  local max_id=0
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[PAT-([0-9]+)\] ]]; then
      local num="${BASH_REMATCH[1]}"
      num=$((10#$num))
      if [[ $num -gt $max_id ]]; then
        max_id=$num
      fi
    fi
  done < "$patterns_file"
  printf "PAT-%03d" $((max_id + 1))
}

ensure_patterns_file() {
  local specclaw_dir="$1"
  local patterns_file="$specclaw_dir/.specclaw/patterns.md"
  if [[ ! -f "$patterns_file" ]]; then
    mkdir -p "$(dirname "$patterns_file")"
    cp "$TEMPLATE_DIR/patterns.md" "$patterns_file"
  fi
  echo "$patterns_file"
}

# Parse entries from errors.md — each entry starts with "## Error:" or "## "
# Returns lines: category|summary_keywords|summary_snippet
parse_errors() {
  local file="$1"
  [[ -f "$file" ]] || return 0
  local in_entry=0
  local summary=""
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\  ]]; then
      # Emit previous if any
      if [[ $in_entry -eq 1 && -n "$summary" ]]; then
        local kw
        kw=$(strip_stop_words "$summary")
        echo "error|$kw|$summary"
      fi
      in_entry=1
      summary=""
    elif [[ "$line" =~ ^[*]{2}Summary:[*]{2}\ *(.*) || "$line" =~ ^Summary:\ *(.*) ]]; then
      summary="${BASH_REMATCH[1]}"
    fi
  done < "$file"
  # Emit last
  if [[ $in_entry -eq 1 && -n "$summary" ]]; then
    local kw
    kw=$(strip_stop_words "$summary")
    echo "error|$kw|$summary"
  fi
}

# Parse entries from learnings.md
# Returns lines: category|summary_keywords|summary_snippet
parse_learnings() {
  local file="$1"
  [[ -f "$file" ]] || return 0
  local in_entry=0
  local summary=""
  local category=""
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\  ]]; then
      # Emit previous if any
      if [[ $in_entry -eq 1 && -n "$summary" ]]; then
        local cat="${category:-uncategorized}"
        local kw
        kw=$(strip_stop_words "$summary")
        echo "$cat|$kw|$summary"
      fi
      in_entry=1
      summary=""
      category=""
    elif [[ "$line" =~ ^[*]{2}Summary:[*]{2}\ *(.*) || "$line" =~ ^Summary:\ *(.*) ]]; then
      summary="${BASH_REMATCH[1]}"
    elif [[ "$line" =~ ^[*]{2}Category:[*]{2}\ *(.*) || "$line" =~ ^Category:\ *(.*) ]]; then
      category="${BASH_REMATCH[1]}"
      category=$(echo "$category" | tr '[:upper:]' '[:lower:]' | tr ' ' '_' | sed 's/[^a-z0-9_]//g')
    fi
  done < "$file"
  # Emit last
  if [[ $in_entry -eq 1 && -n "$summary" ]]; then
    local cat="${category:-uncategorized}"
    local kw
    kw=$(strip_stop_words "$summary")
    echo "$cat|$kw|$summary"
  fi
}

# Extract keywords from a pattern block
get_pattern_keywords() {
  local patterns_file="$1"
  local pat_id="$2"
  local in_pat=0
  local in_kw=0
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[$pat_id\] ]]; then
      in_pat=1
      continue
    fi
    if [[ $in_pat -eq 1 ]]; then
      if [[ "$line" =~ ^##\  && ! "$line" =~ ^###\  ]]; then
        break
      fi
      if [[ "$line" == "### Keywords" ]]; then
        in_kw=1
        continue
      fi
      if [[ $in_kw -eq 1 ]]; then
        if [[ "$line" =~ ^### || "$line" =~ ^## ]]; then
          break
        fi
        if [[ -n "$line" ]]; then
          echo "$line" | tr ',' ' ' | tr -s ' ' | sed 's/^ //'
          break
        fi
      fi
    fi
  done < "$patterns_file"
}

# Extract category from a pattern block header
get_pattern_category() {
  local patterns_file="$1"
  local pat_id="$2"
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[$pat_id\]\ ([a-z_]+)\ —\  ]]; then
      echo "${BASH_REMATCH[1]}"
      return
    fi
  done < "$patterns_file"
}

# Get recurrence count for a pattern
get_pattern_recurrence() {
  local patterns_file="$1"
  local pat_id="$2"
  local in_pat=0
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[$pat_id\] ]]; then
      in_pat=1
      continue
    fi
    if [[ $in_pat -eq 1 ]]; then
      if [[ "$line" =~ ^##\ \[PAT- ]]; then
        break
      fi
      if [[ "$line" =~ ^\*\*Recurrence:\*\*\ *([0-9]+) ]]; then
        echo "${BASH_REMATCH[1]}"
        return
      fi
    fi
  done < "$patterns_file"
  echo "0"
}

# Get all pattern IDs
get_all_pat_ids() {
  local patterns_file="$1"
  grep -oP '(?<=^## \[)PAT-[0-9]+(?=\])' "$patterns_file" 2>/dev/null || true
}

# Update an existing pattern: increment recurrence, add occurrence, update last seen
update_pattern() {
  local patterns_file="$1"
  local pat_id="$2"
  local change_name="$3"
  local snippet="$4"
  local today="$5"

  # Get current recurrence
  local cur_rec
  cur_rec=$(get_pattern_recurrence "$patterns_file" "$pat_id")
  local new_rec=$((cur_rec + 1))

  # Update recurrence
  sed -i "/$pat_id/,/^---$/{s/\*\*Recurrence:\*\* *[0-9]*/\*\*Recurrence:\*\* $new_rec/}" "$patterns_file"

  # Update Last Seen
  sed -i "/$pat_id/,/^---$/{s/\*\*Last Seen:\*\*.*/\*\*Last Seen:\*\* $today ($change_name)/}" "$patterns_file"

  # Add occurrence before "### Prevention Rule"
  # Find the line number of Prevention Rule within this pattern block
  local start_line end_line
  start_line=$(grep -n "^## \[$pat_id\]" "$patterns_file" | head -1 | cut -d: -f1)
  end_line=$(awk "NR>$start_line && /^---$/{print NR; exit}" "$patterns_file")
  if [[ -z "$end_line" ]]; then
    end_line=$(wc -l < "$patterns_file")
  fi

  # Find Prevention Rule line within range
  local prev_line
  prev_line=$(awk "NR>=$start_line && NR<=$end_line && /^### Prevention Rule/{print NR; exit}" "$patterns_file")

  if [[ -n "$prev_line" ]]; then
    # Insert occurrence before Prevention Rule (with blank line)
    sed -i "${prev_line}i\\- ${change_name} — ${snippet}" "$patterns_file"
  else
    # Find Occurrences section and append
    local occ_line
    occ_line=$(awk "NR>=$start_line && NR<=$end_line && /^### Occurrences/{print NR; exit}" "$patterns_file")
    if [[ -n "$occ_line" ]]; then
      # Find last occurrence line (starts with "- ") after occ_line
      local insert_line=$occ_line
      local n=$((occ_line + 1))
      while IFS= read -r line; do
        if [[ "$line" =~ ^-\  ]]; then
          insert_line=$n
        elif [[ -n "$line" && ! "$line" =~ ^[[:space:]]*$ ]]; then
          break
        fi
        n=$((n + 1))
      done < <(tail -n "+$((occ_line + 1))" "$patterns_file")
      sed -i "${insert_line}a\\- ${change_name} — ${snippet}" "$patterns_file"
    fi
  fi

  echo "$new_rec"
}

# Create a new pattern entry
create_pattern() {
  local patterns_file="$1"
  local pat_id="$2"
  local category="$3"
  local keywords="$4"
  local snippet="$5"
  local change_name="$6"
  local today="$7"

  # Format keywords as comma-separated
  local kw_formatted
  kw_formatted=$(echo "$keywords" | tr ' ' '\n' | sort -u | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')

  # Truncate snippet for the header (first ~60 chars)
  local header_snippet
  header_snippet=$(echo "$snippet" | cut -c1-80)

  cat >> "$patterns_file" <<EOF

## [$pat_id] $category — $header_snippet

**First Seen:** $today ($change_name)
**Last Seen:** $today ($change_name)
**Recurrence:** 1
**Status:** active

### Keywords
$kw_formatted

### Occurrences
- $change_name — $snippet

### Prevention Rule
> (To be defined when pattern is promoted)

---
EOF
}

# ---- SCAN MODE ----
do_scan() {
  local specclaw_dir="$1"
  local change_name="$2"
  local changes_dir="$specclaw_dir/.specclaw/changes/$change_name"
  local patterns_file
  patterns_file=$(ensure_patterns_file "$specclaw_dir")
  local today
  today=$(date +%Y-%m-%d)

  if [[ ! -d "$changes_dir" ]]; then
    echo "Error: Change directory not found: $changes_dir" >&2
    exit 1
  fi

  local errors_file="$changes_dir/errors.md"
  local learnings_file="$changes_dir/learnings.md"

  # Collect entries: category|keywords|snippet
  local entries=()
  while IFS= read -r line; do
    [[ -n "$line" ]] && entries+=("$line")
  done < <(parse_errors "$errors_file")
  while IFS= read -r line; do
    [[ -n "$line" ]] && entries+=("$line")
  done < <(parse_learnings "$learnings_file")

  if [[ ${#entries[@]} -eq 0 ]]; then
    echo "No entries found in errors.md or learnings.md for change '$change_name'."
    return
  fi

  local new_count=0
  local updated_count=0
  local promotable=()

  for entry in "${entries[@]}"; do
    local category keywords snippet
    category=$(echo "$entry" | cut -d'|' -f1)
    keywords=$(echo "$entry" | cut -d'|' -f2)
    snippet=$(echo "$entry" | cut -d'|' -f3-)

    if [[ -z "$keywords" ]]; then
      continue
    fi

    # Check existing patterns for match
    local matched=0
    local matched_id=""
    while IFS= read -r pat_id; do
      [[ -z "$pat_id" ]] && continue
      local pat_cat
      pat_cat=$(get_pattern_category "$patterns_file" "$pat_id")
      if [[ "$pat_cat" != "$category" ]]; then
        continue
      fi
      local pat_kw
      pat_kw=$(get_pattern_keywords "$patterns_file" "$pat_id")
      local overlap
      overlap=$(count_overlapping "$keywords" "$pat_kw")
      if [[ $overlap -ge 2 ]]; then
        matched=1
        matched_id="$pat_id"
        break
      fi
    done < <(get_all_pat_ids "$patterns_file")

    if [[ $matched -eq 1 ]]; then
      local new_rec
      new_rec=$(update_pattern "$patterns_file" "$matched_id" "$change_name" "$snippet" "$today")
      updated_count=$((updated_count + 1))
      echo "📊 Updated $matched_id ($category) — recurrence: $new_rec"
      if [[ $new_rec -ge 3 ]]; then
        promotable+=("$matched_id:$new_rec")
      fi
    else
      local new_id
      new_id=$(get_next_pat_id "$patterns_file")
      create_pattern "$patterns_file" "$new_id" "$category" "$keywords" "$snippet" "$change_name" "$today"
      new_count=$((new_count + 1))
      echo "🆕 Created $new_id ($category) — $snippet"
    fi
  done

  echo ""
  echo "Summary: $new_count new pattern(s), $updated_count updated."

  for p in "${promotable[@]}"; do
    local pid pcount
    pid=$(echo "$p" | cut -d: -f1)
    pcount=$(echo "$p" | cut -d: -f2)
    echo "⚠️ $pid has $pcount occurrences — eligible for promotion"
  done
}

# ---- LIST MODE ----
do_list() {
  local specclaw_dir="$1"
  shift
  local min_rec=0

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --min-recurrence)
        min_rec="$2"
        shift 2
        ;;
      *)
        echo "Unknown option: $1" >&2
        exit 1
        ;;
    esac
  done

  local patterns_file="$specclaw_dir/.specclaw/patterns.md"
  if [[ ! -f "$patterns_file" ]]; then
    echo "No patterns recorded."
    return
  fi

  local found=0
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[(PAT-[0-9]+)\]\ ([a-z_]+)\ —\ (.*) ]]; then
      local pat_id="${BASH_REMATCH[1]}"
      local category="${BASH_REMATCH[2]}"
      local description="${BASH_REMATCH[3]}"
      local rec status
      rec=$(get_pattern_recurrence "$patterns_file" "$pat_id")

      # Get status
      local in_pat=0
      status="active"
      while IFS= read -r sline; do
        if [[ "$sline" =~ ^##\ \[$pat_id\] ]]; then
          in_pat=1
          continue
        fi
        if [[ $in_pat -eq 1 ]]; then
          if [[ "$sline" =~ ^##\ \[PAT- ]]; then
            break
          fi
          if [[ "$sline" =~ ^\*\*Status:\*\*\ *(.*) ]]; then
            status="${BASH_REMATCH[1]}"
            status=$(echo "$status" | sed 's/ *|.*//' | tr -d ' ')
            break
          fi
        fi
      done < "$patterns_file"

      if [[ $rec -ge $min_rec ]]; then
        local flag=""
        if [[ "$status" == "promoted" ]]; then
          flag="⚠️ promoted"
        elif [[ $rec -ge 3 ]]; then
          flag="⚠️ eligible"
        else
          flag="   active"
        fi
        printf "%-8s  %-14s  %d  %-14s  %s\n" "$pat_id" "$category" "$rec" "$flag" "$description"
        found=1
      fi
    fi
  done < "$patterns_file"

  if [[ $found -eq 0 ]]; then
    echo "No patterns recorded."
  fi
}

# ---- PROMOTE MODE ----
do_promote() {
  local specclaw_dir="$1"
  local pat_id="$2"
  local patterns_file="$specclaw_dir/.specclaw/patterns.md"

  pat_id=$(echo "$pat_id" | tr '[:lower:]' '[:upper:]')

  if [[ ! -f "$patterns_file" ]]; then
    echo "Error: No patterns file found." >&2
    exit 1
  fi

  if ! grep -q "^## \[$pat_id\]" "$patterns_file"; then
    echo "Error: Pattern $pat_id not found." >&2
    exit 1
  fi

  # Check if already promoted
  local start_line end_line
  start_line=$(grep -n "^## \[$pat_id\]" "$patterns_file" | head -1 | cut -d: -f1)
  end_line=$(awk "NR>$start_line && /^---$/{print NR; exit}" "$patterns_file")
  if [[ -z "$end_line" ]]; then
    end_line=$(wc -l < "$patterns_file")
  fi

  if sed -n "${start_line},${end_line}p" "$patterns_file" | grep -q '^\*\*Status:\*\* *promoted'; then
    echo "Pattern $pat_id is already promoted."
  else
    sed -i "${start_line},${end_line}s/\*\*Status:\*\* *active/\*\*Status:\*\* promoted/" "$patterns_file"
    echo "✅ Pattern $pat_id promoted."
  fi

  # Output the Prevention Rule
  echo ""
  echo "Prevention Rule for $pat_id:"
  local in_rule=0
  while IFS= read -r line; do
    if [[ "$line" == "### Prevention Rule" ]]; then
      in_rule=1
      continue
    fi
    if [[ $in_rule -eq 1 ]]; then
      if [[ "$line" =~ ^### || "$line" == "---" || "$line" =~ ^##\  ]]; then
        break
      fi
      [[ -n "$line" ]] && echo "$line"
    fi
  done < <(sed -n "${start_line},${end_line}p" "$patterns_file")
}

# ---- MAIN ----
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
  usage
  exit 0
fi

SPECCLAW_DIR="$1"
shift

if [[ $# -lt 1 ]]; then
  usage
  exit 1
fi

MODE="$1"
shift

case "$MODE" in
  scan)
    if [[ $# -lt 1 ]]; then
      echo "Error: scan mode requires <change_name>" >&2
      usage
      exit 1
    fi
    do_scan "$SPECCLAW_DIR" "$1"
    ;;
  list)
    do_list "$SPECCLAW_DIR" "$@"
    ;;
  promote)
    if [[ $# -lt 1 ]]; then
      echo "Error: promote mode requires <pat-id>" >&2
      usage
      exit 1
    fi
    do_promote "$SPECCLAW_DIR" "$1"
    ;;
  -h|--help)
    usage
    ;;
  *)
    echo "Error: Unknown mode '$MODE'" >&2
    usage
    exit 1
    ;;
esac


path: scripts/gh-sync.sh
content: #!/usr/bin/env bash
# gh-sync.sh — GitHub Issues sync for specclaw changes
# Part of the specclaw skill. Bash + gh CLI or curl only.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ─── Helpers ──────────────────────────────────────────────────────────────────

usage() {
  cat <<'EOF'
Usage: gh-sync.sh <subcommand> [options]

Subcommands:
  setup    [--config <config.yaml>]
           Verify auth, create label, confirm readiness.

  create   <specclaw_dir> <change_name>
           Create a GitHub issue from proposal.md and tasks.md.

  update   <specclaw_dir> <change_name>
           Update issue body with current task statuses.

  comment  <specclaw_dir> <change_name> "<text>"
           Post a comment on the change's issue.

  close    <specclaw_dir> <change_name>
           Close the change's issue with a completion comment.

Options:
  -h, --help   Show this help message
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }
warn() { echo "WARN: $*" >&2; }

# Read a simple yaml value: yaml_val <file> <dotted.key>
# Handles top-level and one-level nested keys (same as build.sh).
yaml_val() {
  local file="$1" key="$2"
  local section field
  if [[ "$key" == *.* ]]; then
    section="${key%%.*}"
    field="${key#*.}"
  else
    section=""
    field="$key"
  fi

  local in_section=false value=""
  while IFS= read -r line; do
    [[ "$line" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${line// /}" ]] && continue

    if [[ -z "$section" ]]; then
      if [[ "$line" =~ ^${field}:[[:space:]]*(.*) ]]; then
        value="${BASH_REMATCH[1]}"
        break
      fi
    else
      if [[ "$line" =~ ^[a-zA-Z_] ]]; then
        if [[ "$line" =~ ^${section}: ]]; then
          in_section=true
        else
          in_section=false
        fi
        continue
      fi
      if $in_section; then
        if [[ "$line" =~ ^[[:space:]]+${field}:[[:space:]]*(.*) ]]; then
          value="${BASH_REMATCH[1]}"
          break
        fi
      fi
    fi
  done < "$file"

  value="${value#\"}"
  value="${value%\"}"
  value="${value#\'}"
  value="${value%\'}"
  echo "$value"
}

# JSON-escape a string (for curl payloads)
json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  # Escape newlines
  s="${s//$'\n'/\\n}"
  # Escape tabs
  s="${s//$'\t'/\\t}"
  printf '%s' "$s"
}

# ─── Auth Detection ───────────────────────────────────────────────────────────
# Sets AUTH_METHOD ("gh" or "curl") and TOKEN (for curl mode)

detect_auth() {
  local config_file="${1:-}"

  # Collect all available auth methods, then verify in order
  # This ensures fallback if a token is expired/invalid

  # Try GITHUB_TOKEN env first (most explicit)
  if [[ -n "${GITHUB_TOKEN:-}" ]]; then
    if _verify_token "$GITHUB_TOKEN"; then
      AUTH_METHOD="curl"
      [REDACTED_SECRET]"
      return 0
    else
      warn "GITHUB_TOKEN is set but failed auth check — trying fallbacks"
    fi
  fi

  # Try config.yaml github.token
  if [[ -n "$config_file" && -f "$config_file" ]]; then
    local cfg_token
    cfg_token="$(yaml_val "$config_file" "github.token")"
    if [[ -n "$cfg_token" ]]; then
      if _verify_token "$cfg_token"; then
        AUTH_METHOD="curl"
        [REDACTED_SECRET]"
        return 0
      else
        warn "config.yaml github.token failed auth check — trying fallbacks"
      fi
    fi
  fi

  # Try gh CLI (fallback)
  if command -v gh &>/dev/null && gh auth status &>/dev/null; then
    AUTH_METHOD="gh"
    return 0
  fi

  die "No working GitHub auth found. Either:
  1. Set GITHUB_TOKEN environment variable (with a valid PAT)
  2. Add github.token to config.yaml
  3. Install and auth gh CLI: gh auth login"
}

# Verify a GitHub token works by hitting /user endpoint
_verify_token() {
  local token="$1"
  local http_code
  http_code="$(curl -s -w '%{http_code}' -o /dev/null \
    "https://api.github.com/user" \
    -H "Authorization: token $token" \
    -H "Accept: application/vnd.github.v3+json" 2>/dev/null)" || true
  [[ "$http_code" -lt 400 ]]
}

# ─── Repo Detection ──────────────────────────────────────────────────────────
# Sets REPO (owner/repo format)

detect_repo() {
  local config_file="${1:-}"

  # 1. config.yaml github.repo?
  if [[ -n "$config_file" && -f "$config_file" ]]; then
    local cfg_repo
    cfg_repo="$(yaml_val "$config_file" "github.repo")"
    if [[ -n "$cfg_repo" ]]; then
      REPO="$cfg_repo"
      return 0
    fi
  fi

  # 2. Parse from git remote
  local remote_url
  remote_url="$(git remote get-url origin 2>/dev/null)" || die "No git remote 'origin' found and github.repo not in config"

  if [[ "$remote_url" =~ git@github\.com:(.+) ]]; then
    REPO="${BASH_REMATCH[1]}"
  elif [[ "$remote_url" =~ https://github\.com/(.+) ]]; then
    REPO="${BASH_REMATCH[1]}"
  else
    die "Cannot parse GitHub repo from remote URL: $remote_url"
  fi

  # Strip .git suffix
  REPO="${REPO%.git}"
}

# ─── Config ───────────────────────────────────────────────────────────────────

find_config() {
  local specclaw_dir="${1:-}"
  if [[ -n "$specclaw_dir" && -f "$specclaw_dir/config.yaml" ]]; then
    echo "$specclaw_dir/config.yaml"
  else
    echo ""
  fi
}

get_label() {
  local config_file="${1:-}"
  local label=""
  if [[ -n "$config_file" && -f "$config_file" ]]; then
    label="$(yaml_val "$config_file" "github.label")"
  fi
  echo "${label:-specclaw}"
}

# ─── Issue Number from status.md ──────────────────────────────────────────────

get_issue_number() {
  local change_dir="$1"
  local status_file="$change_dir/status.md"

  [[ -f "$status_file" ]] || die "No status.md found in $change_dir (run 'create' first)"

  local issue_ref
  issue_ref="$(grep "GitHub Issue" "$status_file" | grep -o '#[0-9]*' | head -1)" || true

  [[ -n "$issue_ref" ]] || die "No GitHub Issue reference found in $status_file"

  # Strip the # prefix
  echo "${issue_ref#\#}"
}

# ─── API Wrappers ─────────────────────────────────────────────────────────────

# curl wrapper that checks HTTP status
api_curl() {
  local method="$1" url="$2" data="${3:-}"
  local response http_code

  local tmp
  tmp="$(mktemp)"
  trap "rm -f '$tmp'" RETURN

  if [[ -n "$data" ]]; then
    http_code="$(curl -s -w '%{http_code}' -o "$tmp" -X "$method" "$url" \
      -H "Authorization: token $TOKEN" \
      -H "Accept: application/vnd.github.v3+json" \
      -H "Content-Type: application/json" \
      -d "$data")"
  else
    http_code="$(curl -s -w '%{http_code}' -o "$tmp" -X "$method" "$url" \
      -H "Authorization: token $TOKEN" \
      -H "Accept: application/vnd.github.v3+json")"
  fi

  response="$(cat "$tmp")"

  if [[ "$http_code" -ge 400 ]]; then
    local msg
    msg="$(echo "$response" | grep -o '"message":"[^"]*"' | head -1 | sed 's/"message":"//;s/"$//')" || true
    die "GitHub API error ($http_code): ${msg:-$response}"
  fi

  echo "$response"
}

# Create an issue. Returns the full JSON response.
api_create_issue() {
  local title="$1" body="$2" label="$3"
  local esc_title esc_body esc_label

  esc_title="$(json_escape "$title")"
  esc_body="$(json_escape "$body")"
  esc_label="$(json_escape "$label")"

  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh issue create --repo "$REPO" --title "$title" --body "$body" --label "$label" 2>&1
  else
    api_curl POST "https://api.github.com/repos/$REPO/issues" \
      "{\"title\":\"$esc_title\",\"body\":\"$esc_body\",\"labels\":[\"$esc_label\"]}"
  fi
}

# Update an issue body
api_update_issue() {
  local issue_num="$1" body="$2"
  local esc_body
  esc_body="$(json_escape "$body")"

  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh issue edit "$issue_num" --repo "$REPO" --body "$body"
  else
    api_curl PATCH "https://api.github.com/repos/$REPO/issues/$issue_num" \
      "{\"body\":\"$esc_body\"}"
  fi
}

# Comment on an issue
api_comment_issue() {
  local issue_num="$1" comment="$2"
  local esc_comment
  esc_comment="$(json_escape "$comment")"

  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh issue comment "$issue_num" --repo "$REPO" --body "$comment"
  else
    api_curl POST "https://api.github.com/repos/$REPO/issues/$issue_num/comments" \
      "{\"body\":\"$esc_comment\"}"
  fi
}

# Close an issue with a comment
api_close_issue() {
  local issue_num="$1" comment="$2"
  local esc_comment
  esc_comment="$(json_escape "$comment")"

  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh issue close "$issue_num" --repo "$REPO" --comment "$comment"
  else
    # Post comment first
    api_curl POST "https://api.github.com/repos/$REPO/issues/$issue_num/comments" \
      "{\"body\":\"$esc_comment\"}"
    # Then close
    api_curl PATCH "https://api.github.com/repos/$REPO/issues/$issue_num" \
      '{"state":"closed"}'
  fi
}

# Create label (idempotent)
api_create_label() {
  local label="$1"

  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh label create "$label" --repo "$REPO" --color "FF6B35" --description "SpecClaw managed" --force 2>&1 || true
  else
    local esc_label esc_desc
    esc_label="$(json_escape "$label")"
    esc_desc="$(json_escape "SpecClaw managed")"
    # Try create; ignore 422 (already exists)
    local response http_code tmp
    tmp="$(mktemp)"
    http_code="$(curl -s -w '%{http_code}' -o "$tmp" -X POST \
      "https://api.github.com/repos/$REPO/labels" \
      -H "Authorization: token $TOKEN" \
      -H "Accept: application/vnd.github.v3+json" \
      -H "Content-Type: application/json" \
      -d "{\"name\":\"$esc_label\",\"color\":\"FF6B35\",\"description\":\"$esc_desc\"}")"
    rm -f "$tmp"
    if [[ "$http_code" -ge 400 && "$http_code" -ne 422 ]]; then
      warn "Could not create label '$label' (HTTP $http_code), continuing anyway"
    fi
  fi
}

# Verify auth works
api_verify_auth() {
  if [[ "$AUTH_METHOD" == "gh" ]]; then
    gh auth status &>/dev/null || die "gh auth verification failed"
  else
    local http_code
    http_code="$(curl -s -w '%{http_code}' -o /dev/null \
      "https://api.github.com/repos/$REPO" \
      -H "Authorization: token $TOKEN" \
      -H "Accept: application/vnd.github.v3+json")"
    [[ "$http_code" -lt 400 ]] || die "GitHub API auth verification failed (HTTP $http_code)"
  fi
}

# ─── Task Checklist Builder ───────────────────────────────────────────────────

# Build a markdown checklist from tasks.md
# Args: <tasks.md path> [include_status]
# If include_status is "true", marks complete tasks as [x] and failed as ⚠️
build_task_checklist() {
  local tasks_file="$1"
  local include_status="${2:-false}"
  local checklist=""
  local current_wave=""

  [[ -f "$tasks_file" ]] || { echo "Planning pending"; return; }

  while IFS= read -r line; do
    # Detect wave headers: ### Wave N ...
    if [[ "$line" =~ ^###?[[:space:]]+Wave[[:space:]]+([0-9]+) ]]; then
      current_wave="${BASH_REMATCH[1]}"
      continue
    fi

    # Detect task lines in SpecClaw format: - [x] `T1` — Title
    # Also support: - **T1** — Title
    local task_id="" task_title="" task_status=""

    if [[ "$line" =~ ^-[[:space:]]+\[(.)\][[:space:]]+\`([Tt][0-9]+)\`[[:space:]]*[—–-][[:space:]]*(.*) ]]; then
      local marker="${BASH_REMATCH[1]}"
      task_id="${BASH_REMATCH[2]}"
      task_title="${BASH_REMATCH[3]}"
      case "$marker" in
        x) task_status="complete" ;;
        '~') task_status="in_progress" ;;
        '!') task_status="failed" ;;
        *) task_status="pending" ;;
      esac
    elif [[ "$line" =~ ^-[[:space:]]+\*\*([Tt][0-9]+)\*\*[[:space:]]*[—:–-][[:space:]]*(.*) ]]; then
      task_id="${BASH_REMATCH[1]}"
      task_title="${BASH_REMATCH[2]}"
      task_status="pending"
    fi

    if [[ -n "$task_id" ]]; then
      local wave_suffix=""
      [[ -n "$current_wave" ]] && wave_suffix=" (Wave $current_wave)"

      local checkbox="[ ]"
      local suffix=""

      if [[ "$include_status" == "true" ]]; then
        if [[ "$task_status" == "complete" ]]; then
          checkbox="[x]"
        elif [[ "$task_status" == "failed" ]]; then
          suffix=" ⚠️"
        fi
      fi

      checklist+="- $checkbox $task_id — ${task_title%%$'\n'*}${wave_suffix}${suffix}"$'\n'
    fi
  done < "$tasks_file"

  if [[ -z "$checklist" ]]; then
    echo "Planning pending"
  else
    printf '%s' "$checklist"
  fi
}

# Count task statuses from tasks.md for the update command
# Reads status from status.md or tasks.md task entries
count_task_statuses() {
  local change_dir="$1"
  local tasks_file="$change_dir/tasks.md"
  local status_file="$change_dir/status.md"
  local total=0 complete=0

  [[ -f "$tasks_file" ]] || { echo "0 0"; return; }

  # Parse tasks from tasks.md — support both `- [x] \`T1\`` and `- **T1**` formats
  while IFS= read -r line; do
    if [[ "$line" =~ ^-[[:space:]]+\[.\][[:space:]]+\`[Tt][0-9]+\` ]]; then
      ((total++)) || true
      if [[ "$line" =~ ^-[[:space:]]+\[x\] ]]; then
        ((complete++)) || true
      fi
    elif [[ "$line" =~ ^-[[:space:]]+\*\*[Tt][0-9]+\*\* ]]; then
      ((total++)) || true
    fi
  done < "$tasks_file"

  echo "$total $complete"
}

# Build updated checklist from tasks.md + status.md for update command
build_updated_checklist() {
  local change_dir="$1"
  local tasks_file="$change_dir/tasks.md"
  local status_file="$change_dir/status.md"
  local checklist=""
  local current_wave=""
  local total=0 complete=0

  [[ -f "$tasks_file" ]] || { echo "Planning pending"; echo "0 0" >&2; return; }

  # Build a map of task statuses from status.md
  declare -A task_statuses
  if [[ -f "$status_file" ]]; then
    while IFS= read -r line; do
      # Match lines like: - **T1** — Title... status/emoji indicators
      if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+\*\*([Tt][0-9]+)\*\* ]]; then
        local sid="${BASH_REMATCH[1]^^}"  # uppercase
        if [[ "$line" =~ ✅ ]] || [[ "$line" =~ complete ]]; then
          task_statuses["$sid"]="complete"
        elif [[ "$line" =~ ❌ ]] || [[ "$line" =~ failed ]]; then
          task_statuses["$sid"]="failed"
        elif [[ "$line" =~ 🔄 ]] || [[ "$line" =~ in.progress ]]; then
          task_statuses["$sid"]="in_progress"
        fi
      fi
    done < "$status_file"
  fi

  while IFS= read -r line; do
    if [[ "$line" =~ ^###?[[:space:]]+Wave[[:space:]]+([0-9]+) ]]; then
      current_wave="${BASH_REMATCH[1]}"
      continue
    fi

    local task_id="" task_title="" task_status=""

    # SpecClaw format: - [x] `T1` — Title
    if [[ "$line" =~ ^-[[:space:]]+\[(.)\][[:space:]]+\`([Tt][0-9]+)\`[[:space:]]*[—–-][[:space:]]*(.*) ]]; then
      local marker="${BASH_REMATCH[1]}"
      task_id="${BASH_REMATCH[2]^^}"
      task_title="${BASH_REMATCH[3]}"
      case "$marker" in
        x) task_status="complete" ;;
        '~') task_status="in_progress" ;;
        '!') task_status="failed" ;;
        *) task_status="pending" ;;
      esac
    # Alt format: - **T1** — Title
    elif [[ "$line" =~ ^-[[:space:]]+\*\*([Tt][0-9]+)\*\*[[:space:]]*[—:–-][[:space:]]*(.*) ]]; then
      task_id="${BASH_REMATCH[1]^^}"
      task_title="${BASH_REMATCH[2]}"
      task_status="pending"
    fi

    if [[ -n "$task_id" ]]; then
      local wave_suffix=""
      [[ -n "$current_wave" ]] && wave_suffix=" (Wave $current_wave)"

      ((total++)) || true

      local checkbox="[ ]"
      local suffix=""

      # Check status.md map first, then fall back to tasks.md marker
      local status="${task_statuses[$task_id]:-$task_status}"

      case "$status" in
        complete)
          checkbox="[x]"
          ((complete++)) || true
          ;;
        failed)
          suffix=" ⚠️"
          ;;
      esac

      checklist+="- $checkbox $task_id — ${task_title%%$'\n'*}${wave_suffix}${suffix}"$'\n'
    fi
  done < "$tasks_file"

  printf '%s' "$checklist"
  # Output counts on stderr for capture
  echo "$complete $total" >&2
}

# ─── Issue Body Builder ──────────────────────────────────────────────────────

build_issue_body() {
  local change_name="$1" proposal_content="$2" task_checklist="$3"

  # Trim proposal to 2000 chars if very long
  if [[ ${#proposal_content} -gt 2000 ]]; then
    proposal_content="${proposal_content:0:2000}..."
  fi

  cat <<EOF
## 🦞 SpecClaw: $change_name

**Change:** \`$change_name\`

### Proposal
$proposal_content

### Tasks
$task_checklist

---
*Managed by [SpecClaw](https://github.com/chanbistec/specclaw)*
EOF
}

# ─── Subcommands ──────────────────────────────────────────────────────────────

cmd_setup() {
  local config_file=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --config) config_file="$2"; shift 2 ;;
      *) die "Unknown option: $1" ;;
    esac
  done

  detect_auth "$config_file"
  detect_repo "$config_file"

  local label
  label="$(get_label "$config_file")"

  api_verify_auth
  api_create_label "$label"

  echo "✅ GitHub sync ready for $REPO"
}

cmd_create() {
  local specclaw_dir="$1"
  local change_name="$2"
  local config_file
  config_file="$(find_config "$specclaw_dir")"

  detect_auth "$config_file"
  detect_repo "$config_file"

  local label
  label="$(get_label "$config_file")"

  local change_dir="$specclaw_dir/changes/$change_name"
  [[ -d "$change_dir" ]] || die "Change directory not found: $change_dir"

  # Read proposal
  local proposal_file="$change_dir/proposal.md"
  [[ -f "$proposal_file" ]] || die "proposal.md not found in $change_dir"
  local proposal_content
  proposal_content="$(cat "$proposal_file")"

  # Build task checklist
  local task_checklist
  local tasks_file="$change_dir/tasks.md"
  task_checklist="$(build_task_checklist "$tasks_file" false)"

  # Build issue body
  local body
  body="$(build_issue_body "$change_name" "$proposal_content" "$task_checklist")"

  local title="🦞 $change_name"

  # Create issue
  local response
  response="$(api_create_issue "$title" "$body" "$label")"

  # Extract issue number
  local issue_num=""
  if [[ "$AUTH_METHOD" == "gh" ]]; then
    # gh outputs the issue URL like https://github.com/owner/repo/issues/42
    issue_num="$(echo "$response" | grep -o '/issues/[0-9]*' | grep -o '[0-9]*' | head -1)" || true
  else
    # JSON response: "number": 42
    issue_num="$(echo "$response" | grep -o '"number":[[:space:]]*[0-9]*' | grep -o '[0-9]*' | head -1)" || true
  fi

  [[ -n "$issue_num" ]] || die "Failed to extract issue number from API response"

  # Update status.md
  local status_file="$change_dir/status.md"
  if [[ ! -f "$status_file" ]]; then
    cat > "$status_file" <<STATUSEOF
# Status: $change_name

**Status:** planning
**GitHub Issue:** #$issue_num
STATUSEOF
  else
    echo "**GitHub Issue:** #$issue_num" >> "$status_file"
  fi

  echo "Created issue #$issue_num for $change_name"
}

cmd_update() {
  local specclaw_dir="$1"
  local change_name="$2"
  local config_file
  config_file="$(find_config "$specclaw_dir")"

  detect_auth "$config_file"
  detect_repo "$config_file"

  local change_dir="$specclaw_dir/changes/$change_name"
  [[ -d "$change_dir" ]] || die "Change directory not found: $change_dir"

  local issue_num
  issue_num="$(get_issue_number "$change_dir")"

  # Build updated checklist (counts come on stderr)
  local task_checklist counts complete total
  counts_file="$(mktemp)"
  task_checklist="$(build_updated_checklist "$change_dir" 2>"$counts_file")"
  counts="$(cat "$counts_file")"
  rm -f "$counts_file"
  complete="${counts%% *}"
  total="${counts##* }"

  # Read proposal for body rebuild
  local proposal_content=""
  local proposal_file="$change_dir/proposal.md"
  if [[ -f "$proposal_file" ]]; then
    proposal_content="$(cat "$proposal_file")"
  fi

  local body
  body="$(build_issue_body "$change_name" "$proposal_content" "$task_checklist")"

  api_update_issue "$issue_num" "$body" >/dev/null

  echo "Updated issue #$issue_num ($complete/$total tasks complete)"
}

cmd_comment() {
  local specclaw_dir="$1"
  local change_name="$2"
  local comment_text="$3"
  local config_file
  config_file="$(find_config "$specclaw_dir")"

  detect_auth "$config_file"
  detect_repo "$config_file"

  local change_dir="$specclaw_dir/changes/$change_name"
  [[ -d "$change_dir" ]] || die "Change directory not found: $change_dir"

  local issue_num
  issue_num="$(get_issue_number "$change_dir")"

  api_comment_issue "$issue_num" "$comment_text" >/dev/null

  echo "Commented on issue #$issue_num"
}

cmd_close() {
  local specclaw_dir="$1"
  local change_name="$2"
  local config_file
  config_file="$(find_config "$specclaw_dir")"

  detect_auth "$config_file"
  detect_repo "$config_file"

  local change_dir="$specclaw_dir/changes/$change_name"
  [[ -d "$change_dir" ]] || die "Change directory not found: $change_dir"

  local issue_num
  issue_num="$(get_issue_number "$change_dir")"

  local comment="✅ Change completed and archived by SpecClaw"
  api_close_issue "$issue_num" "$comment" >/dev/null

  echo "Closed issue #$issue_num"
}

# ─── Main ─────────────────────────────────────────────────────────────────────

[[ $# -gt 0 ]] || { usage; exit 1; }

case "$1" in
  -h|--help)
    usage
    exit 0
    ;;
  setup)
    shift
    cmd_setup "$@"
    ;;
  create)
    shift
    [[ $# -ge 2 ]] || die "Usage: gh-sync.sh create <specclaw_dir> <change_name>"
    cmd_create "$1" "$2"
    ;;
  update)
    shift
    [[ $# -ge 2 ]] || die "Usage: gh-sync.sh update <specclaw_dir> <change_name>"
    cmd_update "$1" "$2"
    ;;
  comment)
    shift
    [[ $# -ge 3 ]] || die "Usage: gh-sync.sh comment <specclaw_dir> <change_name> \"<text>\""
    cmd_comment "$1" "$2" "$3"
    ;;
  close)
    shift
    [[ $# -ge 2 ]] || die "Usage: gh-sync.sh close <specclaw_dir> <change_name>"
    cmd_close "$1" "$2"
    ;;
  *)
    die "Unknown subcommand: $1. Use --help for usage."
    ;;
esac


path: scripts/init.sh
content: #!/usr/bin/env bash
# specclaw init — Initialize SpecClaw in a project directory
# Usage: init.sh <project_dir> [project_name] [project_description]

set -euo pipefail

PROJECT_DIR="${1:-.}"
PROJECT_NAME="${2:-$(basename "$(cd "$PROJECT_DIR" && pwd)")}"
PROJECT_DESC="${3:-}"

SPECCLAW_DIR="$PROJECT_DIR/.specclaw"
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"

if [ -d "$SPECCLAW_DIR" ]; then
  echo "ERROR: .specclaw/ already exists in $PROJECT_DIR"
  exit 1
fi

# Create directory structure
mkdir -p "$SPECCLAW_DIR/changes/archive"

# Generate config from template
sed \
  -e "s|^  name: \"\"|  name: \"$PROJECT_NAME\"|" \
  -e "s|^  description: \"\"|  description: \"$PROJECT_DESC\"|" \
  "$SKILL_DIR/templates/config.yaml" > "$SPECCLAW_DIR/config.yaml"

# Create initial STATUS.md
cat > "$SPECCLAW_DIR/STATUS.md" << EOF
# 🦞 SpecClaw Dashboard

**Project:** $PROJECT_NAME
**Last Updated:** $(date -u +"%Y-%m-%d %H:%M UTC")

## Active Changes

_No active changes yet. Run \`specclaw propose "<idea>"\` to start._

## Pending Proposals

_None._

## Recently Completed

_None._

## Stats

- **Total changes:** 0
- **Active:** 0
- **Completed:** 0
EOF

echo "OK: Initialized SpecClaw in $SPECCLAW_DIR"
echo "  config: $SPECCLAW_DIR/config.yaml"
echo "  status: $SPECCLAW_DIR/STATUS.md"
echo "  changes: $SPECCLAW_DIR/changes/"


path: scripts/log-error.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
Usage:
  log-error.sh <specclaw_dir> <change_name> <task_id> <wave> <agent_label> "<summary>" [--error-file <path>]
  log-error.sh <specclaw_dir> <change_name> --resolve <task_id>

Log mode:
  Appends a structured error entry to <specclaw_dir>/changes/<change_name>/errors.md.
  Error output is read from --error-file, stdin (if piped), or left empty.
  Output is capped at 50 lines.

Resolve mode:
  Marks all pending entries for <task_id> as resolved_on_retry.

Options:
  -h, --help    Show this help message
EOF
}

# --- Help flag ---
for arg in "$@"; do
  case "$arg" in
    -h|--help) usage; exit 0 ;;
  esac
done

if [[ $# -lt 3 ]]; then
  usage >&2
  exit 1
fi

SPECCLAW_DIR="$1"
CHANGE_NAME="$2"
ERRORS_MD="${SPECCLAW_DIR}/changes/${CHANGE_NAME}/errors.md"
TEMPLATE_HEADER="# Error Journal: ${CHANGE_NAME}

Build errors and their resolutions.

---
"

# --- Resolve mode ---
if [[ "$3" == "--resolve" ]]; then
  TASK_ID="${4:?Missing task_id for --resolve}"
  if [[ ! -f "$ERRORS_MD" ]]; then
    exit 0
  fi
  # Replace pending → resolved_on_retry only in sections matching this task_id
  # We use a simple sed: find lines with [TASK_ID] header, then flip status in following lines
  # Simpler approach: since Status lines are unique enough, scope by task_id block
  tmp="$(mktemp)"
  awk -v tid="$TASK_ID" '
    /^## \[/ { in_block = (index($0, "[" tid "]") > 0) }
    in_block && $0 == "**Status:** pending" { $0 = "**Status:** resolved_on_retry" }
    { print }
  ' "$ERRORS_MD" > "$tmp"
  mv "$tmp" "$ERRORS_MD"
  exit 0
fi

# --- Log mode ---
if [[ $# -lt 6 ]]; then
  echo "Error: log mode requires at least 6 arguments" >&2
  usage >&2
  exit 1
fi

TASK_ID="$3"
WAVE="$4"
AGENT_LABEL="$5"
SUMMARY="$6"
shift 6

ERROR_FILE=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --error-file)
      ERROR_FILE="${2:?--error-file requires a path}"
      shift 2
      ;;
    *)
      echo "Unknown option: $1" >&2
      exit 1
      ;;
  esac
done

# Read error output
ERROR_TEXT=""
if [[ -n "$ERROR_FILE" ]]; then
  ERROR_TEXT="$(cat "$ERROR_FILE" 2>/dev/null || true)"
elif [[ ! -t 0 ]]; then
  ERROR_TEXT="$(cat)"
fi

# Cap at 50 lines
LINE_COUNT="$(echo "$ERROR_TEXT" | wc -l)"
if [[ "$LINE_COUNT" -gt 50 ]]; then
  ERROR_TEXT="$(echo "$ERROR_TEXT" | head -n 50)"
  ERROR_TEXT="${ERROR_TEXT}
[truncated — original output was ${LINE_COUNT} lines]"
fi

# Ensure errors.md exists
if [[ ! -f "$ERRORS_MD" ]]; then
  mkdir -p "$(dirname "$ERRORS_MD")"
  printf '%s' "$TEMPLATE_HEADER" > "$ERRORS_MD"
fi

# Compute attempt number (count existing entries for this task_id)
EXISTING="$(grep -c "^## \\[${TASK_ID}\\] Attempt " "$ERRORS_MD" 2>/dev/null || true)"
ATTEMPT=$(( EXISTING + 1 ))

# Timestamp
WHEN="$(date -u '+%Y-%m-%d %H:%M UTC')"

# Append entry
cat >> "$ERRORS_MD" <<EOF

## [${TASK_ID}] Attempt ${ATTEMPT} — Wave ${WAVE}

**When:** ${WHEN}
**Agent:** ${AGENT_LABEL}
**Status:** pending

### Summary
${SUMMARY}

### Error Output
\`\`\`
${ERROR_TEXT}
\`\`\`

---
EOF

exit 0


path: scripts/log-learning.sh
content: #!/usr/bin/env bash
set -euo pipefail

VALID_CATEGORIES="spec_gap design_gap pattern best_practice agent_issue"
VALID_PRIORITIES="low medium high"

usage() {
  cat <<'EOF'
Usage:
  log-learning.sh <specclaw_dir> <change_name> <category> <priority> "<detail>" ["<action>"]
  log-learning.sh <specclaw_dir> <change_name> --list
  log-learning.sh <specclaw_dir> <change_name> --promote <learning_id>

Modes:
  log (default)   Append a new learning entry
  --list          Show summary table of all learnings
  --promote <id>  Promote a learning (e.g. L1) from pending to promoted

Categories: spec_gap, design_gap, pattern, best_practice, agent_issue
Priorities: low, medium, high
EOF
  exit 0
}

# Help check
for arg in "$@"; do
  [[ "$arg" == "--help" || "$arg" == "-h" ]] && usage
done

if [[ $# -lt 3 ]]; then
  echo "Error: Not enough arguments. Use --help for usage." >&2
  exit 1
fi

SPECCLAW_DIR="$1"
CHANGE_NAME="$2"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATE_DIR="$(cd "$SCRIPT_DIR/../templates" && pwd)"
LEARNINGS_FILE="$SPECCLAW_DIR/learnings.md"

# Ensure learnings.md exists, create from template if missing
ensure_learnings() {
  if [[ ! -f "$LEARNINGS_FILE" ]]; then
    if [[ ! -f "$TEMPLATE_DIR/learnings.md" ]]; then
      echo "Error: Template not found at $TEMPLATE_DIR/learnings.md" >&2
      exit 1
    fi
    sed "s/{{change_name}}/$CHANGE_NAME/g" "$TEMPLATE_DIR/learnings.md" > "$LEARNINGS_FILE"
  fi
}

# Count existing entries to compute next ID
next_id() {
  local count
  count=$(grep -c '^## \[L' "$LEARNINGS_FILE" 2>/dev/null || true)
  echo "L$(( count + 1 ))"
}

# --- List mode ---
if [[ "$3" == "--list" ]]; then
  if [[ ! -f "$LEARNINGS_FILE" ]] || ! grep -q '^## \[L' "$LEARNINGS_FILE" 2>/dev/null; then
    echo "No learnings recorded."
    exit 0
  fi

  # Parse entries: ID, category, priority, status, title
  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[(L[0-9]+)\]\ ([a-z_]+)\ —\ (.+)$ ]]; then
      id="${BASH_REMATCH[1]}"
      cat="${BASH_REMATCH[2]}"
      title="${BASH_REMATCH[3]}"
      priority=""
      status=""
    elif [[ "$line" =~ ^\*\*Priority:\*\*\ (.+)$ ]]; then
      priority="${BASH_REMATCH[1]}"
    elif [[ "$line" =~ ^\*\*Status:\*\*\ (.+)$ ]]; then
      status="${BASH_REMATCH[1]}"
      printf "%-4s %-15s %-8s %-10s %s\n" "$id" "$cat" "$priority" "$status" "$title"
    fi
  done < "$LEARNINGS_FILE"
  exit 0
fi

# --- Promote mode ---
if [[ "$3" == "--promote" ]]; then
  if [[ $# -lt 4 ]]; then
    echo "Error: --promote requires a learning ID (e.g. L1)" >&2
    exit 1
  fi
  TARGET_ID="$4"

  if [[ ! -f "$LEARNINGS_FILE" ]]; then
    echo "Error: No learnings file found at $LEARNINGS_FILE" >&2
    exit 1
  fi

  if ! grep -q "^## \[${TARGET_ID}\]" "$LEARNINGS_FILE"; then
    echo "Error: Learning $TARGET_ID not found" >&2
    exit 1
  fi

  # Replace status for the target entry
  # We need to find the entry and change its status
  in_target=false
  promoted=false
  tmp_file="${LEARNINGS_FILE}.tmp"
  entry_lines=""

  while IFS= read -r line; do
    if [[ "$line" =~ ^##\ \[${TARGET_ID}\] ]]; then
      in_target=true
      entry_lines="$line"$'\n'
      echo "$line" >> "$tmp_file"
    elif $in_target && [[ "$line" =~ ^##\ \[L ]]; then
      # Next entry starts, stop capturing
      in_target=false
      echo "$line" >> "$tmp_file"
    elif $in_target && [[ "$line" == "**Status:** pending" ]]; then
      echo "**Status:** promoted" >> "$tmp_file"
      entry_lines+="**Status:** promoted"$'\n'
      promoted=true
    elif $in_target; then
      echo "$line" >> "$tmp_file"
      entry_lines+="$line"$'\n'
    else
      echo "$line" >> "$tmp_file"
    fi
  done < "$LEARNINGS_FILE"

  mv "$tmp_file" "$LEARNINGS_FILE"

  if $promoted; then
    # Print the full entry
    in_target=false
    while IFS= read -r line; do
      if [[ "$line" =~ ^##\ \[${TARGET_ID}\] ]]; then
        in_target=true
        echo "$line"
      elif $in_target && [[ "$line" =~ ^##\ \[L ]]; then
        break
      elif $in_target; then
        echo "$line"
      fi
    done < "$LEARNINGS_FILE"
  else
    echo "Warning: $TARGET_ID status was not 'pending', no change made." >&2
  fi
  exit 0
fi

# --- Log mode ---
if [[ $# -lt 5 ]]; then
  echo "Error: Log mode requires: <specclaw_dir> <change_name> <category> <priority> \"<detail>\" [\"<action>\"]" >&2
  exit 1
fi

CATEGORY="$3"
PRIORITY="$4"
DETAIL="$5"
ACTION="${6:-}"

# Validate category
if ! echo "$VALID_CATEGORIES" | grep -qw "$CATEGORY"; then
  echo "Error: Invalid category '$CATEGORY'. Must be one of: $VALID_CATEGORIES" >&2
  exit 1
fi

# Validate priority
if ! echo "$VALID_PRIORITIES" | grep -qw "$PRIORITY"; then
  echo "Error: Invalid priority '$PRIORITY'. Must be one of: $VALID_PRIORITIES" >&2
  exit 1
fi

ensure_learnings

ID=$(next_id)
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC")

# Build short title from first ~60 chars of detail
SHORT_TITLE="$DETAIL"
if [[ ${#SHORT_TITLE} -gt 60 ]]; then
  SHORT_TITLE="${SHORT_TITLE:0:57}..."
fi

ACTION_TEXT="${ACTION:-No action specified}"

cat >> "$LEARNINGS_FILE" <<EOF

## [$ID] $CATEGORY — $SHORT_TITLE

**When:** $TIMESTAMP
**Category:** $CATEGORY
**Priority:** $PRIORITY
**Status:** pending

### Detail
$DETAIL

### Action
$ACTION_TEXT

---
EOF

echo "Logged $ID: $CATEGORY ($PRIORITY) — $SHORT_TITLE"


path: scripts/parse-tasks.sh
content: #!/usr/bin/env bash
# Parse tasks.md and output JSON for the build engine
# Usage: parse-tasks.sh [OPTIONS] <tasks.md>
# Output: JSON array of tasks with id, title, status, files, depends, wave
#
# Options:
#   --wave N         Only output tasks from wave N
#   --status STATUS  Only output tasks matching status (pending|in_progress|complete|failed)
#   --validate       Only check if output is valid JSON (exit 0/1, no output)
#   -h, --help       Show this help message

set -euo pipefail

# --- Argument parsing ---
FILTER_WAVE=""
FILTER_STATUS=""
VALIDATE_ONLY=false
TASKS_FILE=""

usage() {
  cat <<'EOF'
Usage: parse-tasks.sh [OPTIONS] <tasks.md>

Parse tasks.md into a JSON array of task objects.

Options:
  --wave N         Only output tasks from wave N
  --status STATUS  Only output tasks matching STATUS
                   (pending, in_progress, complete, failed)
  --validate       Check if output is valid JSON (exit 0/1)
  -h, --help       Show this help message

Examples:
  parse-tasks.sh tasks.md
  parse-tasks.sh --wave 2 tasks.md
  parse-tasks.sh --status pending tasks.md
  parse-tasks.sh --wave 1 --status complete tasks.md
  parse-tasks.sh --validate tasks.md
EOF
  exit 0
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help)
      usage
      ;;
    --wave)
      [[ $# -lt 2 ]] && { echo "ERROR: --wave requires a number" >&2; exit 1; }
      FILTER_WAVE="$2"
      if ! [[ "$FILTER_WAVE" =~ ^[0-9]+$ ]]; then
        echo "ERROR: --wave value must be a positive integer, got '$FILTER_WAVE'" >&2
        exit 1
      fi
      shift 2
      ;;
    --status)
      [[ $# -lt 2 ]] && { echo "ERROR: --status requires a value" >&2; exit 1; }
      FILTER_STATUS="$2"
      case "$FILTER_STATUS" in
        pending|in_progress|complete|failed) ;;
        *) echo "ERROR: --status must be one of: pending, in_progress, complete, failed" >&2; exit 1 ;;
      esac
      shift 2
      ;;
    --validate)
      VALIDATE_ONLY=true
      shift
      ;;
    -*)
      echo "ERROR: Unknown option '$1'. Use --help for usage." >&2
      exit 1
      ;;
    *)
      if [[ -z "$TASKS_FILE" ]]; then
        TASKS_FILE="$1"
      else
        echo "ERROR: Unexpected argument '$1'. Only one file allowed." >&2
        exit 1
      fi
      shift
      ;;
  esac
done

if [[ -z "$TASKS_FILE" ]]; then
  echo "ERROR: No tasks file specified. Use --help for usage." >&2
  exit 1
fi

if [[ ! -f "$TASKS_FILE" ]]; then
  echo "ERROR: $TASKS_FILE not found" >&2
  exit 1
fi

# --- Parse tasks into JSON using awk ---
JSON_OUTPUT=$(awk \
  -v filter_wave="$FILTER_WAVE" \
  -v filter_status="$FILTER_STATUS" \
'
# Escape a string for safe JSON embedding
function json_escape(s,   out, i, c) {
  out = ""
  for (i = 1; i <= length(s); i++) {
    c = substr(s, i, 1)
    if      (c == "\\") out = out "\\\\"
    else if (c == "\"") out = out "\\\""
    else if (c == "\t") out = out "\\t"
    else if (c == "\n") out = out "\\n"
    else if (c == "\r") out = out "\\r"
    else                out = out c
  }
  return out
}

function emit_task() {
  if (task_id == "") return
  # Apply filters
  if (filter_wave != "" && wave != int(filter_wave)) { task_id = ""; return }
  if (filter_status != "" && status != filter_status) { task_id = ""; return }

  if (!first) printf ","
  first = 0
  printf "\n  {\"id\":\"%s\",\"title\":\"%s\",\"status\":\"%s\",\"wave\":%d,\"files\":\"%s\",\"depends\":\"%s\",\"estimate\":\"%s\"}",
    json_escape(task_id),
    json_escape(title),
    json_escape(status),
    wave,
    json_escape(files),
    json_escape(depends),
    json_escape(estimate)
  task_id = ""
  next_is_detail = 0
}

BEGIN {
  printf "["
  first = 1
  wave = 0
  task_id = ""
  next_is_detail = 0
}

/^### Wave/ {
  # Emit any pending task before switching waves
  emit_task()
  wave++
  gsub(/^### Wave [0-9]+ — /, "")
  gsub(/^### Wave [0-9]+/, "")
  wave_desc = $0
  next
}

/^- \[/ {
  # Emit any previous pending task
  emit_task()

  # Extract status
  status = "pending"
  if ($0 ~ /\[x\]/) status = "complete"
  else if ($0 ~ /\[~\]/) status = "in_progress"
  else if ($0 ~ /\[!\]/) status = "failed"

  # Extract task ID — require backtick-wrapped ID
  match($0, /`T[0-9]+`/)
  if (RSTART > 0) {
    task_id = substr($0, RSTART+1, RLENGTH-2)
  } else {
    # Malformed: no backtick ID — skip with warning
    printf "WARNING: Skipping malformed task at line %d (no task ID): %s\n", NR, $0 > "/dev/stderr"
    task_id = ""
    next_is_detail = 0
    next
  }

  # Extract title (after the task ID and em-dash)
  title = $0
  sub(/^- \[.\] `T[0-9]+` — /, "", title)
  sub(/^- \[.\] `T[0-9]+` /, "", title)
  sub(/^- \[.\] /, "", title)

  # Reset per-task fields
  files = ""
  depends = ""
  estimate = ""
  notes = ""
  next_is_detail = 1
  next
}

next_is_detail && /^  - Files:/ {
  files = $0
  sub(/^  - Files: /, "", files)
  next
}

next_is_detail && /^  - Depends:/ {
  depends = $0
  sub(/^  - Depends: /, "", depends)
  next
}

next_is_detail && /^  - Estimate:/ {
  estimate = $0
  sub(/^  - Estimate: /, "", estimate)
  next
}

next_is_detail && /^  - Notes:/ {
  notes = $0
  sub(/^  - Notes: /, "", notes)
  next
}

# When we hit a non-detail line after a task, emit the task
next_is_detail && !/^  -/ && !/^$/ {
  emit_task()
}

/^$/ {
  if (task_id != "" && next_is_detail) {
    emit_task()
  }
}

END {
  emit_task()
  print "\n]"
}
' "$TASKS_FILE")

# --- Validate JSON if jq is available ---
if command -v jq &>/dev/null; then
  if ! echo "$JSON_OUTPUT" | jq --exit-status . >/dev/null 2>&1; then
    echo "ERROR: Generated JSON is invalid" >&2
    echo "$JSON_OUTPUT" >&2
    exit 1
  fi

  if [[ "$VALIDATE_ONLY" == true ]]; then
    exit 0
  fi
else
  if [[ "$VALIDATE_ONLY" == true ]]; then
    echo "WARNING: jq not available, cannot validate JSON" >&2
    exit 1
  fi
fi

# --- Output ---
echo "$JSON_OUTPUT"


path: scripts/update-status.sh
content: #!/usr/bin/env bash
# Regenerate STATUS.md dashboard from current state
# Usage: update-status.sh <specclaw_dir>

set -euo pipefail

SPECCLAW_DIR="$1"
CHANGES_DIR="$SPECCLAW_DIR/changes"
ARCHIVE_DIR="$CHANGES_DIR/archive"
STATUS_FILE="$SPECCLAW_DIR/STATUS.md"

# Read project name from config
PROJECT_NAME=$(grep 'name:' "$SPECCLAW_DIR/config.yaml" | head -1 | sed 's/.*name: *"\(.*\)"/\1/')

ACTIVE_COUNT=0
COMPLETED_COUNT=0
ACTIVE_LINES=""
PENDING_LINES=""
COMPLETED_LINES=""

# Scan active changes
for change_dir in "$CHANGES_DIR"/*/; do
  [ -d "$change_dir" ] || continue
  [ "$(basename "$change_dir")" = "archive" ] && continue

  change_name=$(basename "$change_dir")

  if [ -f "$change_dir/tasks.md" ]; then
    total=$(grep -c '^\- \[' "$change_dir/tasks.md" 2>/dev/null || true)
    done=$(grep -c '^\- \[x\]' "$change_dir/tasks.md" 2>/dev/null || true)
    failed=$(grep -c '^\- \[!\]' "$change_dir/tasks.md" 2>/dev/null || true)
    total=${total:-0}
    done=${done:-0}
    failed=${failed:-0}

    if [ "$total" -gt 0 ]; then
      pct=$((done * 100 / total))
    else
      pct=0
    fi

    status_emoji="🔨"
    [ "$failed" -gt 0 ] && status_emoji="⚠️"
    [ "$done" -eq "$total" ] && [ "$total" -gt 0 ] && status_emoji="✅"

    ACTIVE_LINES="${ACTIVE_LINES}\n- $status_emoji **$change_name** — $done/$total tasks ($pct%) ${failed:+| $failed failed}"
    ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
  elif [ -f "$change_dir/proposal.md" ]; then
    PENDING_LINES="${PENDING_LINES}\n- 📋 **$change_name** — proposal ready, awaiting planning"
  fi
done

# Scan archived changes
if [ -d "$ARCHIVE_DIR" ]; then
  for arch_dir in "$ARCHIVE_DIR"/*/; do
    [ -d "$arch_dir" ] || continue
    arch_name=$(basename "$arch_dir")
    COMPLETED_LINES="${COMPLETED_LINES}\n- ✅ **$arch_name**"
    COMPLETED_COUNT=$((COMPLETED_COUNT + 1))
  done
fi

TOTAL=$((ACTIVE_COUNT + COMPLETED_COUNT))

cat > "$STATUS_FILE" << EOF
# 🦞 SpecClaw Dashboard

**Project:** $PROJECT_NAME
**Last Updated:** $(date -u +"%Y-%m-%d %H:%M UTC")

## Active Changes

$([ -n "$ACTIVE_LINES" ] && echo -e "$ACTIVE_LINES" || echo "_No active changes._")

## Pending Proposals

$([ -n "$PENDING_LINES" ] && echo -e "$PENDING_LINES" || echo "_None._")

## Recently Completed

$([ -n "$COMPLETED_LINES" ] && echo -e "$COMPLETED_LINES" || echo "_None._")

## Stats

- **Total changes:** $TOTAL
- **Active:** $ACTIVE_COUNT
- **Completed:** $COMPLETED_COUNT
EOF

echo "OK: Updated $STATUS_FILE"


path: scripts/update-task-status.sh
content: #!/usr/bin/env bash
# Update task status(es) in tasks.md
#
# Usage:
#   update-task-status.sh [OPTIONS] <tasks.md> <task_id> <new_status>
#   update-task-status.sh [OPTIONS] <tasks.md> --batch T1:complete T2:failed ...
#
# Statuses: pending | in_progress | complete | failed
# Markers:  [ ] pending  [~] in_progress  [x] complete  [!] failed
#
# Options:
#   --batch        Accept multiple task_id:status pairs
#   --quiet        Suppress stdout (errors still go to stderr)
#   --log          Append transitions to a ## Status Log section in tasks.md
#   -h, --help     Show this help

set -euo pipefail

# ── Helpers ──────────────────────────────────────────────────────────────────

show_help() {
  sed -n '2,/^$/{ s/^# \?//; p }' "$0"
  exit 0
}

status_to_marker() {
  case "$1" in
    pending)     echo "[ ]" ;;
    in_progress) echo "[~]" ;;
    complete)    echo "[x]" ;;
    failed)      echo "[!]" ;;
    *) return 1 ;;
  esac
}

marker_to_status() {
  case "$1" in
    "[ ]") echo "pending" ;;
    "[~]") echo "in_progress" ;;
    "[x]") echo "complete" ;;
    "[!]") echo "failed" ;;
    *)     echo "unknown" ;;
  esac
}

# Read the current marker for a task from the file
read_current_status() {
  local file="$1" task_id="$2"
  local marker
  marker=$(grep -oP "^- \[\K.\](?= \`${task_id}\`)" "$file" 2>/dev/null | head -1) || true
  if [[ -n "$marker" ]]; then
    marker_to_status "[$marker"
  else
    echo ""
  fi
}

# ── Parse flags ──────────────────────────────────────────────────────────────

BATCH=false
QUIET=false
LOG=false
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help)  show_help ;;
    --batch)    BATCH=true; shift ;;
    --quiet)    QUIET=true; shift ;;
    --log)      LOG=true;   shift ;;
    --)         shift; POSITIONAL+=("$@"); break ;;
    -*)         echo "ERROR: Unknown option '$1'. Use --help for usage." >&2; exit 1 ;;
    *)          POSITIONAL+=("$1"); shift ;;
  esac
done
set -- "${POSITIONAL[@]}"

if [[ $# -lt 1 ]]; then
  echo "ERROR: Missing tasks file argument. Use --help for usage." >&2
  exit 1
fi

TASKS_FILE="$1"; shift

if [[ ! -f "$TASKS_FILE" ]]; then
  echo "ERROR: File not found: $TASKS_FILE" >&2
  exit 1
fi

# ── Build pairs list ─────────────────────────────────────────────────────────

declare -a PAIRS=()

if $BATCH; then
  if [[ $# -lt 1 ]]; then
    echo "ERROR: --batch requires at least one TASK_ID:STATUS pair." >&2
    exit 1
  fi
  for arg in "$@"; do
    if [[ "$arg" != *:* ]]; then
      echo "ERROR: Invalid batch pair '$arg'. Expected TASK_ID:STATUS." >&2
      exit 1
    fi
    PAIRS+=("$arg")
  done
else
  # Single-task (backward-compatible) mode
  if [[ $# -lt 2 ]]; then
    echo "ERROR: Missing task_id and/or status. Use --help for usage." >&2
    exit 1
  fi
  PAIRS+=("$1:$2")
fi

# ── Process each pair ────────────────────────────────────────────────────────

HAD_ERROR=false

for pair in "${PAIRS[@]}"; do
  TASK_ID="${pair%%:*}"
  NEW_STATUS="${pair#*:}"
  TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M UTC')

  # Validate status
  if ! NEW_MARKER=$(status_to_marker "$NEW_STATUS"); then
    echo "ERROR: Invalid status '$NEW_STATUS' for $TASK_ID. Use: pending|in_progress|complete|failed" >&2
    HAD_ERROR=true
    continue
  fi

  # Check task exists
  if ! grep -q "\`$TASK_ID\`" "$TASKS_FILE"; then
    echo "ERROR: Task $TASK_ID not found in $TASKS_FILE" >&2
    HAD_ERROR=true
    continue
  fi

  # Detect previous status
  PREV_STATUS=$(read_current_status "$TASKS_FILE" "$TASK_ID")
  PREV_STATUS="${PREV_STATUS:-unknown}"

  # Update marker in file
  sed -i "s/^- \[.\] \`$TASK_ID\`/- $NEW_MARKER \`$TASK_ID\`/" "$TASKS_FILE"

  # Build transition string
  TRANSITION="$TASK_ID: $PREV_STATUS → $NEW_STATUS"

  # Timestamp to stderr (always, for logging)
  echo "[$TIMESTAMP] $TRANSITION" >&2

  # Stdout (unless --quiet)
  if ! $QUIET; then
    echo "OK: $TRANSITION"
  fi

  # Append to ## Status Log section if --log
  if $LOG; then
    if ! grep -q '^## Status Log' "$TASKS_FILE"; then
      printf '\n## Status Log\n' >> "$TASKS_FILE"
    fi
    echo "- $TIMESTAMP — $TRANSITION" >> "$TASKS_FILE"
  fi
done

if $HAD_ERROR; then
  exit 1
fi


path: scripts/validate-change.sh
content: #!/usr/bin/env bash
# Validate phase prerequisites and show change status
# Usage: validate-change.sh <specclaw_dir> <change_name> <phase|status>
#
# Checks that required artifacts exist before entering a phase.
# Reads workflow.strict from config.yaml (default: true).
# In strict mode, failures exit 1. In non-strict mode, warnings only.

set -euo pipefail

# --- Help ---
usage() {
  cat <<'EOF'
Usage: validate-change.sh <specclaw_dir> <change_name> <phase|status>

Validate that prerequisites are met before entering a workflow phase,
or show the current status of a change's artifacts.

Arguments:
  specclaw_dir   Path to the .specclaw directory
  change_name    Name of the change (subdirectory under changes/)
  phase          One of: propose, plan, build, verify, archive, github-create
  status         Show artifact status overview

Behavior:
  - Reads workflow.strict from config.yaml (default: true)
  - Runs ALL checks for the phase (does not stop at first failure)
  - strict=true  → failures go to stderr, exit 1
  - strict=false → warnings go to stderr, exit 0
  - All pass     → "✅ Ready for <phase>", exit 0

Examples:
  validate-change.sh .specclaw my-feature build
  validate-change.sh .specclaw my-feature status
EOF
  exit 0
}

[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage

# --- Args ---
if [[ $# -lt 3 ]]; then
  echo "ERROR: Expected 3 arguments: <specclaw_dir> <change_name> <phase|status>" >&2
  echo "Use --help for usage." >&2
  exit 1
fi

SPECCLAW_DIR="$1"
CHANGE_NAME="$2"
PHASE="$3"

if [[ ! -d "$SPECCLAW_DIR" ]]; then
  echo "ERROR: specclaw directory not found: $SPECCLAW_DIR" >&2
  exit 1
fi

CHANGE_DIR="$SPECCLAW_DIR/changes/$CHANGE_NAME"
CONFIG_FILE="$SPECCLAW_DIR/config.yaml"

# --- Config reading ---
# Read a simple yaml value: yaml_val <file> <dotted.key>
# Handles top-level and one-level nested keys.
yaml_val() {
  local file="$1" key="$2"
  local section field
  if [[ "$key" == *.* ]]; then
    section="${key%%.*}"
    field="${key#*.}"
  else
    section=""
    field="$key"
  fi

  local in_section=false value=""
  while IFS= read -r line; do
    [[ "$line" =~ ^[[:space:]]*# ]] && continue
    [[ -z "${line// /}" ]] && continue

    if [[ -z "$section" ]]; then
      if [[ "$line" =~ ^${field}:[[:space:]]*(.*) ]]; then
        value="${BASH_REMATCH[1]}"
        break
      fi
    else
      if [[ "$line" =~ ^[a-zA-Z_] ]]; then
        if [[ "$line" =~ ^${section}: ]]; then
          in_section=true
        else
          in_section=false
        fi
        continue
      fi
      if $in_section; then
        if [[ "$line" =~ ^[[:space:]]+${field}:[[:space:]]*(.*) ]]; then
          value="${BASH_REMATCH[1]}"
          break
        fi
      fi
    fi
  done < "$file"

  # Strip surrounding quotes
  value="${value#\"}"
  value="${value%\"}"
  value="${value#\'}"
  value="${value%\'}"
  echo "$value"
}

# Read strict mode (default: true)
STRICT="true"
if [[ -f "$CONFIG_FILE" ]]; then
  local_strict="$(yaml_val "$CONFIG_FILE" "workflow.strict")"
  if [[ -n "$local_strict" ]]; then
    STRICT="$local_strict"
  fi
fi

# --- Helpers ---
FAILURES=()

fail() {
  FAILURES+=("$1")
}

# Count tasks by marker in tasks.md
count_tasks() {
  local file="$1" marker="$2"
  local n
  n=$(grep -c "^- \[${marker}\]" "$file" 2>/dev/null) || true
  echo "${n:-0}"
}

count_incomplete() {
  local file="$1"
  local n
  n=$(grep -cE '^\- \[( |~|!)\]' "$file" 2>/dev/null) || true
  echo "${n:-0}"
}

count_total() {
  local file="$1"
  local n
  n=$(grep -cE '^\- \[.\]' "$file" 2>/dev/null) || true
  echo "${n:-0}"
}

# --- Status subcommand ---
if [[ "$PHASE" == "status" ]]; then
  if [[ ! -d "$CHANGE_DIR" ]]; then
    echo "Change directory not found: $CHANGE_DIR" >&2
    exit 1
  fi

  # Determine phase
  phase_label="unknown"
  if [[ -f "$CHANGE_DIR/verify-report.md" ]]; then
    phase_label="verified"
  elif [[ -f "$CHANGE_DIR/tasks.md" ]]; then
    total=$(count_total "$CHANGE_DIR/tasks.md")
    complete=$(count_tasks "$CHANGE_DIR/tasks.md" "x")
    incomplete=$(count_incomplete "$CHANGE_DIR/tasks.md")
    if [[ "$incomplete" -eq 0 && "$total" -gt 0 ]]; then
      phase_label="build-complete"
    else
      phase_label="building"
    fi
  elif [[ -f "$CHANGE_DIR/spec.md" ]]; then
    phase_label="planned"
  elif [[ -f "$CHANGE_DIR/proposal.md" ]]; then
    phase_label="proposed"
  else
    phase_label="empty"
  fi

  echo "Change: $CHANGE_NAME"
  echo "Phase: $phase_label"
  echo "Artifacts:"

  # proposal.md
  if [[ -f "$CHANGE_DIR/proposal.md" ]]; then
    echo "  ✅ proposal.md"
  else
    echo "  ❌ proposal.md"
  fi

  # spec.md
  if [[ -f "$CHANGE_DIR/spec.md" ]]; then
    echo "  ✅ spec.md"
  else
    echo "  ❌ spec.md"
  fi

  # design.md
  if [[ -f "$CHANGE_DIR/design.md" ]]; then
    echo "  ✅ design.md"
  else
    echo "  ❌ design.md"
  fi

  # tasks.md (with completion count)
  if [[ -f "$CHANGE_DIR/tasks.md" ]]; then
    total=$(count_total "$CHANGE_DIR/tasks.md")
    complete=$(count_tasks "$CHANGE_DIR/tasks.md" "x")
    echo "  ✅ tasks.md ($complete/$total complete)"
  else
    echo "  ❌ tasks.md"
  fi

  # verify-report.md
  if [[ -f "$CHANGE_DIR/verify-report.md" ]]; then
    echo "  ✅ verify-report.md"
  else
    echo "  ❌ verify-report.md"
  fi

  # errors.md
  if [[ -f "$CHANGE_DIR/errors.md" ]]; then
    echo "  ✅ errors.md"
  else
    echo "  ❌ errors.md"
  fi

  # learnings.md
  if [[ -f "$CHANGE_DIR/learnings.md" ]]; then
    echo "  ✅ learnings.md"
  else
    echo "  ❌ learnings.md"
  fi

  # GitHub Issue (if sync configured)
  if [[ -f "$CONFIG_FILE" ]]; then
    gh_sync_val="$(yaml_val "$CONFIG_FILE" "github.sync")"
    if [[ "$gh_sync_val" == "true" ]]; then
      if [[ -f "$CHANGE_DIR/status.md" ]] && grep -q "GitHub Issue" "$CHANGE_DIR/status.md" 2>/dev/null; then
        issue_num="$(grep "GitHub Issue" "$CHANGE_DIR/status.md" | grep -o '#[0-9]*' | head -1)"
        echo "  ✅ GitHub Issue ($issue_num)"
      else
        echo "  ❌ GitHub Issue (sync enabled, not created)"
      fi
    fi
  fi

  exit 0
fi

# --- GitHub issue check helper ---
_check_github_issue() {
  if [[ -f "$CONFIG_FILE" ]]; then
    local gh_sync
    gh_sync="$(yaml_val "$CONFIG_FILE" "github.sync")"
    if [[ "$gh_sync" == "true" ]]; then
      if [[ -f "$CHANGE_DIR/status.md" ]] && grep -q "GitHub Issue" "$CHANGE_DIR/status.md" 2>/dev/null; then
        : # issue exists
      else
        fail "GitHub sync is enabled but no issue created — run gh-sync.sh create first"
      fi
    fi
  fi
}

# --- Phase validation ---
case "$PHASE" in
  propose)
    if [[ -d "$CHANGE_DIR" ]]; then
      fail "Change directory already exists"
    fi
    ;;

  plan)
    if [[ ! -f "$CHANGE_DIR/proposal.md" ]]; then
      fail "Missing proposal.md — run specclaw propose first"
    fi
    # Enforce GitHub issue if sync is configured
    _check_github_issue
    ;;

  build)
    if [[ ! -f "$CHANGE_DIR/spec.md" ]]; then
      fail "Missing spec.md — run specclaw plan first"
    fi
    if [[ ! -f "$CHANGE_DIR/design.md" ]]; then
      fail "Missing design.md — run specclaw plan first"
    fi
    if [[ ! -f "$CHANGE_DIR/tasks.md" ]]; then
      fail "Missing tasks.md — run specclaw plan first"
    fi
    # Enforce GitHub issue if sync is configured
    _check_github_issue
    ;;

  verify)
    if [[ ! -f "$CHANGE_DIR/tasks.md" ]]; then
      fail "Missing tasks.md"
    else
      incomplete=$(count_incomplete "$CHANGE_DIR/tasks.md")
      if [[ "$incomplete" -gt 0 ]]; then
        fail "$incomplete tasks not complete — finish build first"
      fi
    fi
    ;;

  archive)
    if [[ ! -f "$CHANGE_DIR/verify-report.md" ]]; then
      fail "Missing verify-report.md — run specclaw verify first"
    fi
    ;;

  github-create)
    if [[ ! -f "$CHANGE_DIR/proposal.md" ]]; then
      fail "Missing proposal.md — needed for GitHub issue creation"
    fi
    ;;

  *)
    echo "ERROR: Unknown phase '$PHASE'. Expected: propose, plan, build, verify, archive, github-create, status" >&2
    exit 1
    ;;
esac

# --- Report results ---
if [[ ${#FAILURES[@]} -gt 0 ]]; then
  for msg in "${FAILURES[@]}"; do
    if [[ "$STRICT" == "true" ]]; then
      echo "ERROR: $msg" >&2
    else
      echo "WARNING: $msg" >&2
    fi
  done

  if [[ "$STRICT" == "true" ]]; then
    exit 1
  else
    exit 0
  fi
fi

echo "✅ Ready for $PHASE"
exit 0


path: scripts/verify-context.sh
content: #!/usr/bin/env bash
# verify-context.sh — Build the full context payload for the Verify Agent
# Part of the specclaw skill. Bash + coreutils only (jq optional).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ─── Helpers ──────────────────────────────────────────────────────────────────

usage() {
  cat <<'EOF'
Usage: verify-context.sh <specclaw_dir> <change_name>

Build the Verify Agent prompt context by:
  1. Reading config.yaml for project.name and models.review
  2. Collecting evidence via verify.sh collect
  3. Reading spec.md in full
  4. Extracting the Verify Agent prompt template from agent-prompts.md
  5. Filling in template variables
  6. Outputting the complete context payload to stdout

Arguments:
  specclaw_dir   Path to the .specclaw directory
  change_name    Name of the change to verify

Options:
  -h, --help     Show this help message
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }
warn() { echo "WARN: $*" >&2; }

# Read a simple YAML scalar value
# Usage: yaml_val <file> <field_name>
yaml_val() {
  local file="$1" field="$2"
  local val
  val=$(grep -E "^[[:space:]]*${field}:" "$file" 2>/dev/null \
    | head -1 \
    | sed 's/^[^:]*:[[:space:]]*//' \
    | sed 's/^"//' | sed 's/"$//' \
    | sed "s/^'//" | sed "s/'$//")
  printf '%s' "$val"
}

# ─── JSON field extraction ────────────────────────────────────────────────────

# Extract a JSON string field value (top-level) using jq or grep fallback.
# Usage: json_field <json_string> <field_name>
json_field() {
  local json="$1" field="$2"
  if command -v jq &>/dev/null; then
    printf '%s' "$json" | jq -r ".${field} // empty" 2>/dev/null || true
  else
    # Grep fallback — extract value after "field": "..."
    # Handles simple single-line string values
    printf '%s' "$json" | grep -oP "\"${field}\"[[:space:]]*:[[:space:]]*\"\\K[^\"]*" 2>/dev/null | head -1 || true
  fi
}

# Extract a JSON array of strings using jq or grep fallback.
# Usage: json_array <json_string> <field_name>  → outputs one element per line
json_array() {
  local json="$1" field="$2"
  if command -v jq &>/dev/null; then
    printf '%s' "$json" | jq -r ".${field}[]? // empty" 2>/dev/null || true
  else
    # Grep fallback — look for lines that are bare quoted strings inside the array
    # This is fragile but handles the verify.sh output format
    local in_array=false
    while IFS= read -r line; do
      if $in_array; then
        if printf '%s' "$line" | grep -qE '^\s*\]'; then
          break
        fi
        # Extract quoted string value
        local val
        val=$(printf '%s' "$line" | sed 's/^[[:space:]]*//' | sed 's/^"//' | sed 's/",\?$//' | sed 's/"$//')
        [ -n "$val" ] && printf '%s\n' "$val"
      elif printf '%s' "$line" | grep -qE "\"${field}\"[[:space:]]*:"; then
        # Check if array starts on same line or next
        if printf '%s' "$line" | grep -qE '\['; then
          in_array=true
        fi
      fi
    done <<< "$json"
  fi
}

# Extract changed_files array (array of objects) and format as markdown.
# Usage: json_changed_files <json_string>  → outputs formatted markdown
json_changed_files() {
  local json="$1"
  if command -v jq &>/dev/null; then
    printf '%s' "$json" | jq -r '
      .changed_files[]? |
      if .exists then
        "### " + .path + "\n```\n" + .content + "\n```"
      else
        "### " + .path + "\n*File does not exist*"
      end
    ' 2>/dev/null || true
  else
    # Grep-based fallback: extract path and content pairs
    local in_files=false depth=0
    local current_path="" current_content="" in_content=false exists=true
    while IFS= read -r line; do
      if printf '%s' "$line" | grep -qE '"changed_files"[[:space:]]*:'; then
        in_files=true
        continue
      fi
      if ! $in_files; then continue; fi

      # Detect path
      if printf '%s' "$line" | grep -qE '"path"[[:space:]]*:'; then
        current_path=$(printf '%s' "$line" | sed 's/.*"path"[[:space:]]*:[[:space:]]*//' | sed 's/^"//' | sed 's/",\?$//' | sed 's/"$//')
      fi

      # Detect exists
      if printf '%s' "$line" | grep -qE '"exists"[[:space:]]*:[[:space:]]*false'; then
        exists=false
      elif printf '%s' "$line" | grep -qE '"exists"[[:space:]]*:[[:space:]]*true'; then
        exists=true
      fi

      # Detect content field
      if printf '%s' "$line" | grep -qE '"content"[[:space:]]*:'; then
        local val
        val=$(printf '%s' "$line" | sed 's/.*"content"[[:space:]]*:[[:space:]]*//')
        if printf '%s' "$val" | grep -qE '^null'; then
          current_content=""
        else
          current_content=$(printf '%s' "$val" | sed 's/^"//' | sed 's/",\?$//' | sed 's/"$//')
        fi
      fi

      # End of an object in the array — emit
      if printf '%s' "$line" | grep -qE '^\s*\}'; then
        if [ -n "$current_path" ]; then
          if $exists && [ -n "$current_content" ]; then
            # Unescape JSON newlines for display
            local decoded
            decoded=$(printf '%s' "$current_content" | sed 's/\\n/\n/g' | sed 's/\\t/\t/g' | sed 's/\\"/"/g' | sed 's/\\\\/\\/g')
            printf '### %s\n```\n%s\n```\n' "$current_path" "$decoded"
          else
            printf '### %s\n*File does not exist*\n' "$current_path"
          fi
        fi
        current_path=""
        current_content=""
        exists=true
      fi

      # Detect end of changed_files array
      if printf '%s' "$line" | grep -qE '^\s*\]' && $in_files; then
        # Could be end of changed_files — heuristic: break after first top-level ]
        break
      fi
    done <<< "$json"
  fi
}

# ─── Main ─────────────────────────────────────────────────────────────────────

case "${1:-}" in
  -h|--help)
    usage
    exit 0
    ;;
esac

[ $# -ge 2 ] || { usage >&2; exit 1; }

SPECCLAW_DIR="$1"
CHANGE_NAME="$2"
CHANGE_DIR="${SPECCLAW_DIR}/changes/${CHANGE_NAME}"
CONFIG_FILE="${SPECCLAW_DIR}/config.yaml"
SPEC_FILE="${CHANGE_DIR}/spec.md"

# Validate
[ -d "$SPECCLAW_DIR" ] || die "specclaw directory not found: ${SPECCLAW_DIR}"
[ -d "$CHANGE_DIR" ] || die "Change directory not found: ${CHANGE_DIR}"
[ -f "$CONFIG_FILE" ] || die "config.yaml not found: ${CONFIG_FILE}"
[ -f "$SPEC_FILE" ] || die "spec.md not found: ${SPEC_FILE}"

# ─── Step 1: Read config ─────────────────────────────────────────────────────

PROJECT_NAME=$(yaml_val "$CONFIG_FILE" "name")
MODEL_REVIEW=$(yaml_val "$CONFIG_FILE" "review")

[ -n "$PROJECT_NAME" ] || warn "Could not read project.name from config.yaml"
[ -n "$MODEL_REVIEW" ] || warn "Could not read models.review from config.yaml"

# ─── Step 2: Collect evidence ────────────────────────────────────────────────

EVIDENCE_JSON=$("$SCRIPT_DIR/verify.sh" collect "$SPECCLAW_DIR" "$CHANGE_NAME")
[ -n "$EVIDENCE_JSON" ] || die "verify.sh collect returned empty output"

# ─── Step 3: Read spec.md ────────────────────────────────────────────────────

SPEC_CONTENT=$(cat "$SPEC_FILE")

# ─── Step 4: Extract Verify Agent template from agent-prompts.md ─────────────

PROMPTS_FILE="$SCRIPT_DIR/../references/agent-prompts.md"
[ -f "$PROMPTS_FILE" ] || die "agent-prompts.md not found: ${PROMPTS_FILE}"

# Extract from "## Verify Agent" to the next "## " header or EOF
# Must track code fences so that ## headers inside ``` blocks don't end extraction early.
TEMPLATE_FILE=$(mktemp)
awk '
  /^## Verify Agent/ { found=1; next }
  found && /^```/ { in_fence = !in_fence }
  found && !in_fence && /^## / { exit }
  found { print }
' "$PROMPTS_FILE" > "$TEMPLATE_FILE"

# Strip leading/trailing blank lines
sed -i '/./,$!d' "$TEMPLATE_FILE"
# Remove trailing blank lines
sed -i -e :a -e '/^\s*$/{ $d; N; ba; }' "$TEMPLATE_FILE"

# If the template is wrapped in a code fence, strip first and last fence lines
if head -1 "$TEMPLATE_FILE" | grep -qE '^\s*```'; then
  sed -i '1d' "$TEMPLATE_FILE"
  sed -i '${ /^```/d }' "$TEMPLATE_FILE"
fi

[ -s "$TEMPLATE_FILE" ] || die "Could not extract Verify Agent template from agent-prompts.md"

# ─── Step 5: Extract evidence fields to temp files ───────────────────────────

TMPDIR_CTX=$(mktemp -d)
trap 'rm -rf "$TMPDIR_CTX" "$TEMPLATE_FILE"' EXIT

# Acceptance criteria — one per line
json_array "$EVIDENCE_JSON" "acceptance_criteria" > "$TMPDIR_CTX/acceptance_criteria"
[ -s "$TMPDIR_CTX/acceptance_criteria" ] || echo "No acceptance criteria found" > "$TMPDIR_CTX/acceptance_criteria"

# Changed files — formatted as markdown
json_changed_files "$EVIDENCE_JSON" > "$TMPDIR_CTX/changed_files_content"
[ -s "$TMPDIR_CTX/changed_files_content" ] || echo "No changed files found" > "$TMPDIR_CTX/changed_files_content"

# Test, lint, build output
json_field "$EVIDENCE_JSON" "test_output" > "$TMPDIR_CTX/test_output"
[ -s "$TMPDIR_CTX/test_output" ] || echo "No tests configured" > "$TMPDIR_CTX/test_output"

json_field "$EVIDENCE_JSON" "lint_output" > "$TMPDIR_CTX/lint_output"
[ -s "$TMPDIR_CTX/lint_output" ] || echo "No linter configured" > "$TMPDIR_CTX/lint_output"

json_field "$EVIDENCE_JSON" "build_output" > "$TMPDIR_CTX/build_output"
[ -s "$TMPDIR_CTX/build_output" ] || echo "No build command configured" > "$TMPDIR_CTX/build_output"

# Spec and change name
cat "$SPEC_FILE" > "$TMPDIR_CTX/spec_content"
printf '%s' "$CHANGE_NAME" > "$TMPDIR_CTX/change_name"

# ─── Step 6: Perform template substitution and output ────────────────────────

# replace_var: replace {{varname}} in TEMPLATE_FILE with contents of a file
# Uses awk with file reading to handle multi-line content safely.
replace_var() {
  local varname="$1"
  local valfile="$2"
  local outfile
  outfile=$(mktemp)

  awk -v placeholder="{{${varname}}}" -v valfile="$valfile" '
    BEGIN {
      # Read replacement content from file
      replacement = ""
      while ((getline line < valfile) > 0) {
        if (replacement != "") replacement = replacement "\n"
        replacement = replacement line
      }
      close(valfile)
    }
    {
      idx = index($0, placeholder)
      if (idx > 0) {
        before = substr($0, 1, idx - 1)
        after = substr($0, idx + length(placeholder))
        print before replacement after
      } else {
        print
      }
    }
  ' "$TEMPLATE_FILE" > "$outfile"

  mv "$outfile" "$TEMPLATE_FILE"
}

replace_var "change_name" "$TMPDIR_CTX/change_name"
replace_var "spec_content" "$TMPDIR_CTX/spec_content"
replace_var "acceptance_criteria" "$TMPDIR_CTX/acceptance_criteria"
replace_var "changed_files_content" "$TMPDIR_CTX/changed_files_content"
replace_var "test_output" "$TMPDIR_CTX/test_output"
replace_var "lint_output" "$TMPDIR_CTX/lint_output"
replace_var "build_output" "$TMPDIR_CTX/build_output"

cat "$TEMPLATE_FILE"


path: scripts/verify.sh
content: #!/usr/bin/env bash
# verify.sh — Evidence collection and test execution for specclaw verification
# Part of the specclaw skill. Bash + coreutils only (jq optional for validation).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# ─── Helpers ──────────────────────────────────────────────────────────────────

usage() {
  cat <<'EOF'
Usage: verify.sh <subcommand> [options]

Subcommands:
  collect       <specclaw_dir> <change_name>
                Collect evidence: acceptance criteria, file contents, test results.
                Outputs JSON to stdout.

  report        <specclaw_dir> <change_name>
                Read verify-report.md and output verdict summary.

  update-status <specclaw_dir> <change_name> <verdict>
                Update the Verify row in status.md.
                verdict: PASS | FAIL | PARTIAL

Options:
  -h, --help    Show this help message
EOF
}

die() { echo "ERROR: $*" >&2; exit 1; }
warn() { echo "WARN: $*" >&2; }

# JSON-escape a string: backslashes, quotes, newlines, tabs, carriage returns
json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf '%s' "$s"
}

# Read a YAML value (simple single-line scalar) from config.yaml
# Usage: yaml_val <file> <dotted.key>
# e.g. yaml_val config.yaml build.test_command
yaml_val() {
  local file="$1" key="$2"
  local field="${key##*.}"
  local val
  val=$(grep -E "^[[:space:]]*${field}:" "$file" 2>/dev/null | head -1 | sed 's/^[^:]*:[[:space:]]*//' | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//")
  printf '%s' "$val"
}

# Run a command, capture output (capped at 100 lines), return exit code
# Usage: run_capped <varname_output> <command_string>
# Sets: <varname_output> to captured output, returns command exit code
run_capped() {
  local -n _out_ref="$1"
  local cmd="$2"
  local tmpfile
  tmpfile=$(mktemp)
  local rc=0
  eval "$cmd" >"$tmpfile" 2>&1 || rc=$?
  _out_ref=$(head -100 "$tmpfile")
  local total
  total=$(wc -l < "$tmpfile")
  if [ "$total" -gt 100 ]; then
    _out_ref="${_out_ref}
... (truncated, ${total} total lines)"
  fi
  rm -f "$tmpfile"
  return "$rc"
}

# ─── Subcommand: collect ──────────────────────────────────────────────────────

cmd_collect() {
  local specclaw_dir="$1"
  local change_name="$2"
  local change_dir="${specclaw_dir}/changes/${change_name}"

  # Validate required files
  local spec_file="${change_dir}/spec.md"
  local tasks_file="${change_dir}/tasks.md"
  local config_file="${specclaw_dir}/config.yaml"

  [ -f "$spec_file" ] || die "spec.md not found at ${spec_file}"
  [ -f "$tasks_file" ] || die "tasks.md not found at ${tasks_file}"

  # 1. Extract acceptance criteria
  local ac_lines=()
  while IFS= read -r line; do
    # Strip the checkbox prefix and bold markers to get clean AC text
    local clean
    clean=$(printf '%s' "$line" | sed 's/^[[:space:]]*- \[ \] \*\*//' | sed 's/^[[:space:]]*- \[ \] //' | sed 's/\*\*$//' | sed 's/\*\*//')
    ac_lines+=("$clean")
  done < <(grep -E '^\s*- \[ \] (\*\*)?AC-' "$spec_file" || true)

  # 2. Collect file paths from tasks.md Files: fields
  local file_paths=()
  while IFS= read -r line; do
    # Files: field contains comma-separated paths like `path1`, `path2`
    local paths_str
    paths_str=$(printf '%s' "$line" | sed 's/^[^:]*:[[:space:]]*//')
    # Extract backtick-delimited paths
    while [[ "$paths_str" =~ \`([^\`]+)\` ]]; do
      file_paths+=("${BASH_REMATCH[1]}")
      paths_str="${paths_str#*\`${BASH_REMATCH[1]}\`}"
    done
    # If no backticks, try comma-separated plain text
    if [ ${#file_paths[@]} -eq 0 ] 2>/dev/null; then
      IFS=',' read -ra parts <<< "$paths_str"
      for p in "${parts[@]}"; do
        p=$(printf '%s' "$p" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
        [ -n "$p" ] && file_paths+=("$p")
      done
    fi
  done < <(grep -iE '^\s*\*?\*?Files\*?\*?\s*:' "$tasks_file" || true)

  # Deduplicate file paths
  local unique_paths=()
  local -A seen_paths=()
  for fp in "${file_paths[@]}"; do
    if [ -z "${seen_paths[$fp]+x}" ]; then
      seen_paths[$fp]=1
      unique_paths+=("$fp")
    fi
  done

  # 3. Read each file's content
  local files_json=""
  for fp in "${unique_paths[@]}"; do
    local full_path
    # Resolve relative to project root (specclaw_dir parent or repo root)
    if [[ "$fp" = /* ]]; then
      full_path="$fp"
    else
      # Try relative to specclaw_dir first, then cwd
      if [ -f "${specclaw_dir}/${fp}" ]; then
        full_path="${specclaw_dir}/${fp}"
      elif [ -f "$fp" ]; then
        full_path="$fp"
      else
        full_path="${specclaw_dir}/${fp}"
      fi
    fi

    [ -n "$files_json" ] && files_json="${files_json},"

    if [ -f "$full_path" ]; then
      local content
      local total_lines
      total_lines=$(wc -l < "$full_path")
      content=$(head -200 "$full_path")
      if [ "$total_lines" -gt 200 ]; then
        content="${content}
... (truncated, ${total_lines} total lines)"
      fi
      files_json="${files_json}
    {\"path\": \"$(json_escape "$fp")\", \"exists\": true, \"content\": \"$(json_escape "$content")\"}"
    else
      files_json="${files_json}
    {\"path\": \"$(json_escape "$fp")\", \"exists\": false, \"content\": null}"
    fi
  done

  # 4. Read config for commands
  local test_cmd="" lint_cmd="" build_cmd=""
  if [ -f "$config_file" ]; then
    test_cmd=$(yaml_val "$config_file" "build.test_command")
    lint_cmd=$(yaml_val "$config_file" "build.lint_command")
    build_cmd=$(yaml_val "$config_file" "build.build_command")
  else
    warn "config.yaml not found at ${config_file}, skipping commands"
  fi

  # 5. Run commands
  local test_output="" lint_output="" build_output=""
  local tests_passed=true lint_passed=true build_passed=true

  if [ -n "$test_cmd" ]; then
    if ! run_capped test_output "$test_cmd"; then
      tests_passed=false
    fi
  fi

  if [ -n "$lint_cmd" ]; then
    if ! run_capped lint_output "$lint_cmd"; then
      lint_passed=false
    fi
  fi

  if [ -n "$build_cmd" ]; then
    if ! run_capped build_output "$build_cmd"; then
      build_passed=false
    fi
  fi

  # 6. Build AC JSON array
  local ac_json=""
  for ac in "${ac_lines[@]}"; do
    [ -n "$ac_json" ] && ac_json="${ac_json},"
    ac_json="${ac_json}
    \"$(json_escape "$ac")\""
  done

  # 7. Output JSON
  local output
  output=$(cat <<ENDJSON
{
  "change": "$(json_escape "$change_name")",
  "acceptance_criteria": [${ac_json}
  ],
  "changed_files": [${files_json}
  ],
  "test_output": "$(json_escape "$test_output")",
  "lint_output": "$(json_escape "$lint_output")",
  "build_output": "$(json_escape "$build_output")",
  "tests_passed": ${tests_passed},
  "lint_passed": ${lint_passed},
  "build_passed": ${build_passed}
}
ENDJSON
)

  # 8. Validate with jq if available
  if command -v jq &>/dev/null; then
    if printf '%s' "$output" | jq . >/dev/null 2>&1; then
      printf '%s\n' "$output"
    else
      warn "JSON validation failed, outputting best-effort"
      printf '%s\n' "$output"
    fi
  else
    printf '%s\n' "$output"
  fi
}

# ─── Subcommand: report ──────────────────────────────────────────────────────

cmd_report() {
  local specclaw_dir="$1"
  local change_name="$2"
  local change_dir="${specclaw_dir}/changes/${change_name}"
  local report_file="${change_dir}/verify-report.md"

  [ -f "$report_file" ] || die "verify-report.md not found at ${report_file}"

  # Extract verdict line (look for Verdict: or **Verdict:**)
  local verdict
  verdict=$(grep -iE '(verdict|overall)[[:space:]]*:' "$report_file" | head -1 | sed 's/^.*:[[:space:]]*//' | sed 's/\*//g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
  [ -z "$verdict" ] && verdict="UNKNOWN"

  # Normalize verdict
  local norm_verdict="UNKNOWN"
  case "$(printf '%s' "$verdict" | tr '[:upper:]' '[:lower:]')" in
    *pass*) norm_verdict="PASS" ;;
    *fail*) norm_verdict="FAIL" ;;
    *partial*) norm_verdict="PARTIAL" ;;
    *) norm_verdict="$verdict" ;;
  esac

  # Count pass/fail from checklist items
  local total=0 passed=0
  while IFS= read -r line; do
    total=$((total + 1))
    if printf '%s' "$line" | grep -qE '^\s*- \[x\]'; then
      passed=$((passed + 1))
    fi
  done < <(grep -E '^\s*- \[(x| )\]' "$report_file" || true)

  local issues=$((total - passed))

  echo "Verdict: ${norm_verdict}"
  echo "Passed: ${passed}/${total}"
  echo "Issues: ${issues}"
}

# ─── Subcommand: update-status ───────────────────────────────────────────────

cmd_update_status() {
  local specclaw_dir="$1"
  local change_name="$2"
  local verdict="$3"
  local change_dir="${specclaw_dir}/changes/${change_name}"
  local status_file="${change_dir}/status.md"

  [ -f "$status_file" ] || die "status.md not found at ${status_file}"

  # Determine status emoji
  local status_text
  case "$(printf '%s' "$verdict" | tr '[:upper:]' '[:lower:]')" in
    pass)    status_text="✅ Passed" ;;
    fail)    status_text="❌ Failed" ;;
    partial) status_text="⚠️ Partial" ;;
    *)       die "Invalid verdict: ${verdict}. Must be PASS, FAIL, or PARTIAL" ;;
  esac

  # Check if Verify row exists in the table
  if grep -qE '^\|[[:space:]]*Verify[[:space:]]*\|' "$status_file"; then
    # Update existing Verify row — replace the status column
    local tmpfile
    tmpfile=$(mktemp)
    while IFS= read -r line; do
      if printf '%s' "$line" | grep -qE '^\|[[:space:]]*Verify[[:space:]]*\|'; then
        # Rebuild the row preserving the Notes column
        local notes
        notes=$(printf '%s' "$line" | awk -F'|' '{print $4}' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
        printf '| Verify | %s | %s |\n' "$status_text" "$notes"
      else
        printf '%s\n' "$line"
      fi
    done < "$status_file" > "$tmpfile"
    mv "$tmpfile" "$status_file"
  else
    # Append a Verify row — find the table and add after the last row
    # Or just append if we can't find the table
    local tmpfile
    tmpfile=$(mktemp)
    local in_table=false table_ended=false appended=false
    while IFS= read -r line; do
      printf '%s\n' "$line"
      # Detect table rows (lines starting with |)
      if printf '%s' "$line" | grep -qE '^\|.*(Phase|Build|Tasks|Design|Spec|Proposal).*\|'; then
        in_table=true
      elif $in_table && ! printf '%s' "$line" | grep -qE '^\|'; then
        # Table ended, insert Verify row before this line
        if ! $appended; then
          printf '| Verify | %s |  |\n' "$status_text"
          appended=true
        fi
        in_table=false
      fi
    done < "$status_file" > "$tmpfile"
    # If we never left the table (file ends with table), append
    if ! $appended; then
      printf '| Verify | %s |  |\n' "$status_text" >> "$tmpfile"
    fi
    mv "$tmpfile" "$status_file"
  fi

  echo "Updated status.md: Verify → ${status_text}"
}

# ─── Main ─────────────────────────────────────────────────────────────────────

case "${1:-}" in
  -h|--help)
    usage
    exit 0
    ;;
  collect)
    [ $# -ge 3 ] || die "Usage: verify.sh collect <specclaw_dir> <change_name>"
    cmd_collect "$2" "$3"
    ;;
  report)
    [ $# -ge 3 ] || die "Usage: verify.sh report <specclaw_dir> <change_name>"
    cmd_report "$2" "$3"
    ;;
  update-status)
    [ $# -ge 4 ] || die "Usage: verify.sh update-status <specclaw_dir> <change_name> <verdict>"
    cmd_update_status "$2" "$3" "$4"
    ;;
  "")
    usage
    exit 1
    ;;
  *)
    die "Unknown subcommand: $1. Run verify.sh --help for usage."
    ;;
esac


path: templates/STATUS-dashboard.md
content: # 🦞 SpecClaw Dashboard

**Project:** {{project_name}}
**Last Updated:** {{updated}}

## Active Changes

{{active_changes}}

## Pending Proposals

{{pending_proposals}}

## Recently Completed

{{completed_changes}}

## Stats

- **Total changes:** {{total}}
- **Active:** {{active_count}}
- **Completed:** {{completed_count}}
- **Tasks completed today:** {{tasks_today}}


path: templates/config.yaml
content: # SpecClaw Configuration
# Generated by: specclaw init
version: 1

project:
  name: ""
  description: ""
  repo: ""

# Model routing — use the best model for each phase
models:
  planning: "anthropic/claude-opus-4-6"      # Proposals, specs, design
  coding: "openai/gpt-5.1-codex"             # Implementation
  review: "anthropic/claude-sonnet-4-5"       # Verification & code review

# Git strategy
#
# Git strategies:
#   branch-per-change    — one branch per change, switches working directory (default)
#   direct               — commits straight to current branch
#   worktree-per-change  — isolated worktree per change, enables parallel multi-change builds
git:
  strategy: "branch-per-change"   # "branch-per-change" | "direct" | "worktree-per-change"
  auto_commit: true
  commit_prefix: "specclaw"       # Prefix for auto-commits
  branch_prefix: "specclaw/"      # Prefix for feature branches
  worktree_dir: ".specclaw/worktrees"  # Base directory for worktrees (worktree-per-change only)

# Build settings
build:
  test_command: ""                 # e.g., "npm test", "pytest"
  lint_command: ""                 # e.g., "npm run lint"
  build_command: ""                # e.g., "npm run build"
  parallel_tasks: 3               # Max concurrent agent spawns
  timeout_seconds: 300             # Per-task timeout

# Notifications
notifications:
  enabled: true
  channel: "discord"              # "discord" | "telegram" | "slack"
  target: ""                      # Channel/user ID for updates
  on_complete: true
  on_failure: true
  on_approval_needed: true

# GitHub Issues integration (optional)
# When enabled, syncs proposals as GitHub Issues with task checklists
github:
  sync: false                    # Enable GitHub Issues sync
  repo: ""                       # owner/repo (auto-detected from git remote if empty)
  token: ""                      # GitHub PAT for curl fallback (not needed if gh CLI is authed)
  label: "specclaw"              # Label applied to SpecClaw issues

# Workflow enforcement
workflow:
  strict: true                     # Enforce phase prerequisites (disable for rapid prototyping)

# Automation
automation:
  auto_mode: false
  cron: ""                         # Cron expression for scheduled builds
  max_tasks_per_run: 5
  auto_verify: true                # Run verification after build
  auto_archive: false              # Archive on successful verification
  post_build_review: true          # Run automated review after build completes


path: templates/design.md
content: # Design: {{title}}

**Change:** {{change_name}}
**Created:** {{date}}

## Technical Approach

{{approach}}

## Architecture

{{architecture}}

## File Changes Map

| File | Action | Description |
|------|--------|-------------|
{{file_changes}}

## Data Model Changes

{{data_model}}

## API Changes

{{api_changes}}

## Key Decisions

{{decisions}}

## Risks & Mitigations

{{risks}}


path: templates/errors.md
content: # Error Journal: {{change_name}}

Build errors and their resolutions.

---


path: templates/learnings.md
content: # Learnings: {{change_name}}

Build learnings, spec gaps, and patterns discovered.

**Categories:** spec_gap | design_gap | pattern | best_practice | agent_issue

---


path: templates/patterns.md
content: # Pattern Registry

Cross-change patterns detected from build errors and learnings.
Patterns with recurrence >= 3 are eligible for promotion to agent prompts.

---


path: templates/proposal.md
content: # Proposal: {{title}}

**Created:** {{date}}
**Status:** 🟡 Draft

## Problem

_What problem are we solving? Why does it matter?_

{{problem}}

## Proposed Solution

_What are we building? High-level approach._

{{solution}}

## Scope

### In Scope
{{in_scope}}

### Out of Scope
{{out_scope}}

## Impact

- **Files affected:** {{file_count}} (estimated)
- **Complexity:** {{complexity}} (small / medium / large)
- **Risk:** {{risk}} (low / medium / high)

## Open Questions

{{questions}}

---

**To proceed:** Review this proposal and approve to begin planning.


path: templates/spec.md
content: # Spec: {{title}}

**Change:** {{change_name}}
**Created:** {{date}}
**Status:** 🟡 Draft

## Overview

{{overview}}

## Requirements

### Functional Requirements

{{functional_requirements}}

### Non-Functional Requirements

{{non_functional_requirements}}

## Acceptance Criteria

Each criterion must pass for the change to be considered complete.

{{acceptance_criteria}}

## Edge Cases

{{edge_cases}}

## Dependencies

{{dependencies}}

## Notes

{{notes}}


path: templates/status.md
content: # Status: {{title}}

**Change:** {{change_name}}
**Started:** {{date}}
**Last Updated:** {{updated}}

## Progress

| Phase | Status | Notes |
|-------|--------|-------|
| Proposal | {{proposal_status}} | {{proposal_notes}} |
| Spec | {{spec_status}} | {{spec_notes}} |
| Design | {{design_status}} | {{design_notes}} |
| Tasks | {{tasks_status}} | {{tasks_notes}} |
| Build | {{build_status}} | {{build_notes}} |
| Verify | {{verify_status}} | {{verify_notes}} |

## Task Progress

**Completed:** {{completed}} / {{total}}
**Failed:** {{failed}}

{{task_details}}

## Agent Runs

| Task | Agent | Model | Status | Duration |
|------|-------|-------|--------|----------|
{{agent_runs}}

## Issues

{{issues}}


path: templates/tasks.md
content: # Tasks: {{title}}

**Change:** {{change_name}}
**Created:** {{date}}
**Total Tasks:** {{task_count}}

## Summary

{{summary}}

## Tasks

### Wave 1 — {{wave_1_description}}

{{wave_1_tasks}}

### Wave 2 — {{wave_2_description}}

{{wave_2_tasks}}

---

## Legend

- `[ ]` Pending
- `[~]` In Progress
- `[x]` Complete
- `[!]` Failed

**Task format:**
```
- [ ] `T<n>` — <title>
  - Files: <files to create/modify>
  - Estimate: small | medium | large
  - Depends: <task ids> (if any)
  - Notes: <additional context>
```


path: templates/verify-report.md
content: # Verification Report: {{change_name}}

**Verified:** {{date}}
**Model:** {{model}}
**Verdict:** {{verdict}}

## Acceptance Criteria

{{criteria_results}}

## Test Results

{{test_output}}

## Issues Found

{{issues}}

## Summary

**Passed:** {{passed_count}}/{{total_count}} criteria
**Failed:** {{failed_count}}/{{total_count}} criteria
**Verdict:** {{verdict}}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: stale | SkillSpector: suspicious (CRITICAL)