From c46e917cbf74d109118de5869dfa22f851eb9f40 Mon Sep 17 00:00:00 2001 From: Personal Stack Agent Date: Mon, 8 Jun 2026 10:29:41 +0000 Subject: [PATCH] Integrate Spec-Driven Development (Spec Kit) into the agent kit, council, and runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts GitHub Spec Kit (pinned upstream v0.9.5) as Spec-Driven Development scaffolding across the agentic platform, vendored as render-managed agent-kit content rather than wrapping the specify CLI — every non-negotiable (checked-in, rendered, sha-guarded, Claude/Codex parity, offline-capable runners, no runtime network) rules out runtime specify init/upgrade. Agent kit + installer: - .specify/ templates and bash scripts, a personal-stack constitution, and nine /speckit.* commands delivered to Claude (.claude/commands/) and Codex (.agents/skills/speckit-*), all under manifest + render parity with sha pins. - render-agent-kit.py learns a commands category and .specify seeding; the served install.sh seeds the surface for --scope user|project with the full dry-run/uninstall lifecycle. taskstoissues is a real gh issue wrapper honoring the repo's assignee/label conventions. - Manifest version bumped to 2; AgentKitManifestTest covers the new commands, Codex skills, and .specify surface. Council: - The plan phase emits specs/NNN-/{spec,plan,tasks}.md alongside the canonical tasks.json; a constitution summary is injected into the planner/critic/reviser/consolidator prompts; a consistency gate hard-fails on artifact mismatch. Free-text briefs still work. Runner: - The runner image bakes the .specify source to /opt/agent-kit/sdd and the entrypoint seeds every cloned repo (seed-if-absent), making SDD present by default. PlatformAgentMcpFluxTest asserts the COPY and seed wiring. The MCP server profiles are intentionally unchanged: Spec Kit is filesystem and prompt scaffolding, not an MCP server, so it lands in the render/install path, not the profiles configmap. --- .agents/skills/speckit-analyze/SKILL.md | 30 + .agents/skills/speckit-checklist/SKILL.md | 30 + .agents/skills/speckit-clarify/SKILL.md | 29 + .agents/skills/speckit-constitution/SKILL.md | 30 + .agents/skills/speckit-implement/SKILL.md | 34 + .agents/skills/speckit-plan/SKILL.md | 36 + .agents/skills/speckit-specify/SKILL.md | 39 + .agents/skills/speckit-tasks/SKILL.md | 33 + .agents/skills/speckit-taskstoissues/SKILL.md | 101 + .claude/commands/speckit.analyze.md | 39 + .claude/commands/speckit.checklist.md | 39 + .claude/commands/speckit.clarify.md | 39 + .claude/commands/speckit.constitution.md | 53 + .claude/commands/speckit.implement.md | 42 + .claude/commands/speckit.plan.md | 44 + .claude/commands/speckit.specify.md | 58 + .claude/commands/speckit.tasks.md | 43 + .claude/commands/speckit.taskstoissues.md | 108 + .specify/memory/constitution.md | 78 + .specify/scripts/bash/check-prerequisites.sh | 89 + .specify/scripts/bash/common.sh | 121 + .specify/scripts/bash/create-new-feature.sh | 135 + .specify/scripts/bash/setup-plan.sh | 44 + .specify/scripts/bash/setup-tasks.sh | 45 + .specify/templates/constitution-template.md | 54 + .specify/templates/plan-template.md | 93 + .specify/templates/spec-template.md | 89 + .specify/templates/tasks-template.md | 67 + platform/agents/council/council.py | 527 +++- .../agents/council/prompts/consolidator.md | 6 + platform/agents/council/prompts/critic.md | 3 + platform/agents/council/prompts/planner.md | 3 + platform/agents/council/prompts/reviser.md | 3 + .../council/schemas/consolidated.schema.json | 10 + platform/agents/kit/THIRD_PARTY_NOTICES.md | 31 + platform/agents/kit/manifest.yaml | 315 ++- platform/agents/kit/render-agent-kit.py | 134 +- platform/agents/kit/spec-kit-source.lock | 62 + .../kit/templates/installer/install.sh.tpl | 18 + .../partials/allowlist/defaults.gitignore | 4 + .../.agents/skills/speckit-analyze/SKILL.md | 30 + .../.agents/skills/speckit-checklist/SKILL.md | 30 + .../.agents/skills/speckit-clarify/SKILL.md | 29 + .../skills/speckit-constitution/SKILL.md | 30 + .../.agents/skills/speckit-implement/SKILL.md | 34 + .../repo/.agents/skills/speckit-plan/SKILL.md | 36 + .../.agents/skills/speckit-specify/SKILL.md | 39 + .../.agents/skills/speckit-tasks/SKILL.md | 33 + .../skills/speckit-taskstoissues/SKILL.md | 101 + .../repo/.claude/commands/speckit.analyze.md | 39 + .../.claude/commands/speckit.checklist.md | 39 + .../repo/.claude/commands/speckit.clarify.md | 39 + .../.claude/commands/speckit.constitution.md | 53 + .../.claude/commands/speckit.implement.md | 42 + .../repo/.claude/commands/speckit.plan.md | 44 + .../repo/.claude/commands/speckit.specify.md | 58 + .../repo/.claude/commands/speckit.tasks.md | 43 + .../.claude/commands/speckit.taskstoissues.md | 108 + .../scripts/bash/check-prerequisites.sh | 89 + .../repo/.specify/scripts/bash/common.sh | 121 + .../scripts/bash/create-new-feature.sh | 135 + .../repo/.specify/scripts/bash/setup-plan.sh | 44 + .../repo/.specify/scripts/bash/setup-tasks.sh | 45 + .../templates/constitution-template.md | 54 + .../repo/.specify/templates/plan-template.md | 93 + .../repo/.specify/templates/spec-template.md | 89 + .../repo/.specify/templates/tasks-template.md | 67 + .../platform/AgentKitManifestTest.kt | 300 ++- .../platform/PlatformAgentMcpFluxTest.kt | 10 + services/agent-runner/Dockerfile | 7 + services/agent-runner/entrypoint.sh | 98 +- .../src/main/resources/installer/install.sh | 2299 ++++++++++++++++- specs/.gitkeep | 1 + 73 files changed, 6963 insertions(+), 74 deletions(-) create mode 100644 .agents/skills/speckit-analyze/SKILL.md create mode 100644 .agents/skills/speckit-checklist/SKILL.md create mode 100644 .agents/skills/speckit-clarify/SKILL.md create mode 100644 .agents/skills/speckit-constitution/SKILL.md create mode 100644 .agents/skills/speckit-implement/SKILL.md create mode 100644 .agents/skills/speckit-plan/SKILL.md create mode 100644 .agents/skills/speckit-specify/SKILL.md create mode 100644 .agents/skills/speckit-tasks/SKILL.md create mode 100644 .agents/skills/speckit-taskstoissues/SKILL.md create mode 100644 .claude/commands/speckit.analyze.md create mode 100644 .claude/commands/speckit.checklist.md create mode 100644 .claude/commands/speckit.clarify.md create mode 100644 .claude/commands/speckit.constitution.md create mode 100644 .claude/commands/speckit.implement.md create mode 100644 .claude/commands/speckit.plan.md create mode 100644 .claude/commands/speckit.specify.md create mode 100644 .claude/commands/speckit.tasks.md create mode 100644 .claude/commands/speckit.taskstoissues.md create mode 100644 .specify/memory/constitution.md create mode 100755 .specify/scripts/bash/check-prerequisites.sh create mode 100755 .specify/scripts/bash/common.sh create mode 100755 .specify/scripts/bash/create-new-feature.sh create mode 100755 .specify/scripts/bash/setup-plan.sh create mode 100755 .specify/scripts/bash/setup-tasks.sh create mode 100644 .specify/templates/constitution-template.md create mode 100644 .specify/templates/plan-template.md create mode 100644 .specify/templates/spec-template.md create mode 100644 .specify/templates/tasks-template.md create mode 100644 platform/agents/kit/THIRD_PARTY_NOTICES.md create mode 100644 platform/agents/kit/spec-kit-source.lock create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-analyze/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-checklist/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-clarify/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-constitution/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-implement/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-plan/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-specify/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-tasks/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.agents/skills/speckit-taskstoissues/SKILL.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.analyze.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.checklist.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.clarify.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.constitution.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.implement.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.plan.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.specify.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.tasks.md create mode 100644 platform/agents/kit/templates/repo/.claude/commands/speckit.taskstoissues.md create mode 100755 platform/agents/kit/templates/repo/.specify/scripts/bash/check-prerequisites.sh create mode 100755 platform/agents/kit/templates/repo/.specify/scripts/bash/common.sh create mode 100755 platform/agents/kit/templates/repo/.specify/scripts/bash/create-new-feature.sh create mode 100755 platform/agents/kit/templates/repo/.specify/scripts/bash/setup-plan.sh create mode 100755 platform/agents/kit/templates/repo/.specify/scripts/bash/setup-tasks.sh create mode 100644 platform/agents/kit/templates/repo/.specify/templates/constitution-template.md create mode 100644 platform/agents/kit/templates/repo/.specify/templates/plan-template.md create mode 100644 platform/agents/kit/templates/repo/.specify/templates/spec-template.md create mode 100644 platform/agents/kit/templates/repo/.specify/templates/tasks-template.md create mode 100644 specs/.gitkeep diff --git a/.agents/skills/speckit-analyze/SKILL.md b/.agents/skills/speckit-analyze/SKILL.md new file mode 100644 index 00000000..aed4f234 --- /dev/null +++ b/.agents/skills/speckit-analyze/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-analyze +description: Analyze consistency across the active spec, plan, and tasks. +--- + +# Speckit Analyze + +Use this skill for the `$speckit-analyze` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Read the spec, plan, tasks, constitution if present, and referenced + supporting docs. +3. Perform read-only analysis. Do not modify files. +4. Report missing requirement coverage, spec-plan conflicts, task scope creep, + duplicated or vague tasks, ordering problems, and unverifiable acceptance + criteria. +5. Order findings by severity with file or section references. If no issues are + found, say so and identify residual risk. +6. Recommend the next Speckit command. + diff --git a/.agents/skills/speckit-checklist/SKILL.md b/.agents/skills/speckit-checklist/SKILL.md new file mode 100644 index 00000000..ab5c93ff --- /dev/null +++ b/.agents/skills/speckit-checklist/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-checklist +description: Generate a focused quality checklist for the active specification. +--- + +# Speckit Checklist + +Use this skill for the `$speckit-checklist` phase. Treat the user's prompt as +the checklist focus; infer a useful focus if none is provided. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or spec exists. +2. Read the active spec, plan if present, and constitution if present. +3. Create `FEATURE_DIR/checklists/.md` from the checklist template when + available. +4. Write validation questions for requirements completeness, clarity, + consistency, and acceptance readiness. Do not write implementation tasks. +5. Mark items complete only when the user asked for validation and the current + artifacts satisfy the item. +6. Report the checklist path and high-risk gaps. + diff --git a/.agents/skills/speckit-clarify/SKILL.md b/.agents/skills/speckit-clarify/SKILL.md new file mode 100644 index 00000000..c5c8d1be --- /dev/null +++ b/.agents/skills/speckit-clarify/SKILL.md @@ -0,0 +1,29 @@ +--- +name: speckit-clarify +description: Clarify underspecified feature requirements before technical planning. +--- + +# Speckit Clarify + +Use this skill for the `$speckit-clarify` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR`, and honor required-file +checks. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or `spec.md` exists. +2. Read `FEATURE_DIR/spec.md` and the constitution if present. +3. Identify planning-blocking ambiguities in scope, actors, permissions, data, + compliance, failure behavior, and user experience. +4. Ask up to five concise questions, using multiple choice where possible. +5. Add a dated `## Clarifications` section to the spec, record answers, and + revise affected requirements. +6. Report clarification count and readiness for `$speckit-plan`. + diff --git a/.agents/skills/speckit-constitution/SKILL.md b/.agents/skills/speckit-constitution/SKILL.md new file mode 100644 index 00000000..a0b23572 --- /dev/null +++ b/.agents/skills/speckit-constitution/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-constitution +description: Create or update the Spec Kit project constitution. +--- + +# Speckit Constitution + +Use this skill for the `$speckit-constitution` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing templates and minimal helper +scripts under `.specify/scripts/bash/` only when absent: +`check-prerequisites.sh`, `create-new-feature.sh`, `setup-plan.sh`, +`setup-tasks.sh`, and `update-agent-context.sh`. The helpers must support +active feature lookup from `.specify/feature.json`, JSON output, and required +file checks. Do not overwrite existing Spec Kit files. + +## Workflow + +1. Read `.specify/templates/constitution-template.md` and any existing + `.specify/memory/constitution.md`. +2. Derive concrete project principles, testing standards, delivery workflow, + review rules, governance, and amendment rules from the user request. +3. Write `.specify/memory/constitution.md` with no unresolved placeholders. +4. Check the spec, plan, and tasks templates for obvious constitution alignment + notes, but do not make unrelated edits. +5. Report the constitution path and readiness for `$speckit-specify`. + diff --git a/.agents/skills/speckit-implement/SKILL.md b/.agents/skills/speckit-implement/SKILL.md new file mode 100644 index 00000000..15d02112 --- /dev/null +++ b/.agents/skills/speckit-implement/SKILL.md @@ -0,0 +1,34 @@ +--- +name: speckit-implement +description: Implement the active Spec Kit task list. +--- + +# Speckit Implement + +Use this skill for the `$speckit-implement` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if the spec, plan, or tasks file is missing. +2. Read the active spec, plan, tasks, constitution, and supporting docs. +3. Execute incomplete tasks from `tasks.md` in dependency order. Run `[P]` tasks + in parallel only when they touch independent files and the environment allows + it. +4. Keep implementation scope limited to the task list and active spec. +5. Mark tasks complete in `tasks.md` after implementation and relevant + validation. +6. Run the smallest meaningful tests required by the task, plan, or repository + conventions. Report exact blockers for checks that cannot run. +7. Stop for conflicts, unactionable tasks, or decisions missing from the spec. +8. Finish with completed task ids, changed files, validation results, and + remaining incomplete tasks. + diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md new file mode 100644 index 00000000..5c0478f3 --- /dev/null +++ b/.agents/skills/speckit-plan/SKILL.md @@ -0,0 +1,36 @@ +--- +name: speckit-plan +description: Create a technical implementation plan for the active specification. +--- + +# Speckit Plan + +Use this skill for the `$speckit-plan` phase. Treat the user's prompt as +technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-plan.sh --json`. Stop if no active spec + exists. +2. Read the spec, plan template or existing plan, constitution, and supporting + docs in `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. +4. Create supporting docs when relevant: `research.md`, `data-model.md`, + `contracts/`, and `quickstart.md`. +5. Keep scope aligned to the spec; send product-scope changes back to + `$speckit-specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present. +7. Report updated files, unresolved risks, and readiness for `$speckit-tasks`. + diff --git a/.agents/skills/speckit-specify/SKILL.md b/.agents/skills/speckit-specify/SKILL.md new file mode 100644 index 00000000..c8931d38 --- /dev/null +++ b/.agents/skills/speckit-specify/SKILL.md @@ -0,0 +1,39 @@ +--- +name: speckit-specify +description: Create or update a feature specification from a natural language description. +--- + +# Speckit Specify + +Use this skill for the `$speckit-specify` phase. The user's prompt is the +feature description; if it is empty, ask for the description before proceeding. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/templates/spec-template.md`, +`.specify/templates/checklist-template.md`, +`.specify/scripts/bash/create-new-feature.sh`, and +`.specify/scripts/bash/check-prerequisites.sh` only when absent. The feature +script must create one `specs/-/spec.md`, copy the spec +template, write `.specify/feature.json`, and print JSON with `FEATURE_DIR` and +`SPEC_FILE`. + +## Workflow + +1. Generate a concise 2-4 word feature short name. +2. Run `.specify/scripts/bash/create-new-feature.sh --json ""` or + the minimal equivalent from Bootstrap. +3. Read `.specify/templates/spec-template.md` and + `.specify/memory/constitution.md` if present. +4. Write `FEATURE_DIR/spec.md` focused on what users need and why. Avoid + implementation details. Include user scenarios, functional requirements, + success criteria, assumptions, edge cases, and key entities when relevant. +5. Use at most three `[NEEDS CLARIFICATION: ...]` markers for critical unknowns + with no reasonable default. +6. Create `FEATURE_DIR/checklists/requirements.md`, validate the spec, and + iterate up to three times for fixable gaps. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `$speckit-clarify` or `$speckit-plan`. + diff --git a/.agents/skills/speckit-tasks/SKILL.md b/.agents/skills/speckit-tasks/SKILL.md new file mode 100644 index 00000000..9c32d469 --- /dev/null +++ b/.agents/skills/speckit-tasks/SKILL.md @@ -0,0 +1,33 @@ +--- +name: speckit-tasks +description: Generate an actionable task list from the active implementation plan. +--- + +# Speckit Tasks + +Use this skill for the `$speckit-tasks` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-tasks.sh --json`. Stop if the spec or plan + is missing. +2. Read the spec, plan, and supporting docs. +3. Generate `FEATURE_DIR/tasks.md` with `T001` style task ids, user-story + phases, dependency notes, and `[P]` markers for independent parallel tasks. +4. Include exact file paths where practical. Put test tasks before + implementation tasks whenever tests are required by the spec, constitution, + or plan. +5. Keep each user story independently implementable and testable. +6. Report task count, parallelizable count, and readiness for + `$speckit-analyze`, `$speckit-taskstoissues`, or `$speckit-implement`. + diff --git a/.agents/skills/speckit-taskstoissues/SKILL.md b/.agents/skills/speckit-taskstoissues/SKILL.md new file mode 100644 index 00000000..5bfc94d2 --- /dev/null +++ b/.agents/skills/speckit-taskstoissues/SKILL.md @@ -0,0 +1,101 @@ +--- +name: speckit-taskstoissues +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +# Speckit Tasks To Issues + +Use this skill for the `$speckit-taskstoissues` phase. Support `--dry-run` by +printing the `gh issue create` commands without creating issues. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is installed and authenticated unless running `--dry-run`. +3. Parse task lines from `tasks.md` that start with a Markdown task checkbox and + contain a `T###` task id. +4. Create one issue per task with title + `[SpecKit] : `. +5. Before creating, check all repository issues by exact title; warn and skip + already-created issues. +6. Always include `--assignee ExtraToast`. +7. Pick exactly one best-fit existing repo label from + `enhancement`, `bug`, or `documentation`. Never invent labels; if the chosen + label does not exist, warn and omit `--label`. +8. Use this inline helper shape; do not add a script file: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. + diff --git a/.claude/commands/speckit.analyze.md b/.claude/commands/speckit.analyze.md new file mode 100644 index 00000000..afb093c5 --- /dev/null +++ b/.claude/commands/speckit.analyze.md @@ -0,0 +1,39 @@ +--- +description: Analyze consistency across the active spec, plan, and tasks. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as analysis focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, + `FEATURE_DIR/tasks.md`, `.specify/memory/constitution.md` if present, and + referenced supporting docs. +3. Perform a read-only analysis. Do not modify files. +4. Report only actionable findings: + - Missing task coverage for requirements or success criteria. + - Plan decisions that conflict with the spec or constitution. + - Tasks that introduce scope not present in the spec. + - Duplicated, contradictory, vague, or unordered tasks. + - Acceptance criteria that cannot be verified by the task list. +5. Order findings by severity and include file/section references. If no issues + are found, say so and note residual risks. +6. Recommend the next command: refine with `/speckit.specify`, + `/speckit.plan`, `/speckit.tasks`, or proceed to `/speckit.implement`. + diff --git a/.claude/commands/speckit.checklist.md b/.claude/commands/speckit.checklist.md new file mode 100644 index 00000000..c5085f01 --- /dev/null +++ b/.claude/commands/speckit.checklist.md @@ -0,0 +1,39 @@ +--- +description: Generate a focused quality checklist for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as the checklist domain or focus. If empty, infer the most useful +quality checklist from the spec. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. The script +must locate the active feature from `.specify/feature.json` and print JSON with +`FEATURE_DIR`. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or spec exists. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md` if present, and + `.specify/memory/constitution.md` if present. +3. Create `FEATURE_DIR/checklists/.md` using + `.specify/templates/checklist-template.md` when available. Choose a concise + file name from the focus, such as `security.md`, `accessibility.md`, or + `requirements.md`. +4. Write checklist items as validation questions for requirements completeness, + clarity, consistency, and acceptance readiness. Do not turn the checklist + into implementation tasks. +5. If the user asked to validate, mark items complete only when the current + artifacts satisfy them; otherwise leave them unchecked for review. +6. Report the checklist path and any high-risk gaps found while generating it. + diff --git a/.claude/commands/speckit.clarify.md b/.claude/commands/speckit.clarify.md new file mode 100644 index 00000000..33264fd6 --- /dev/null +++ b/.claude/commands/speckit.clarify.md @@ -0,0 +1,39 @@ +--- +description: Clarify underspecified feature requirements before technical planning. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as clarification focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR` and available docs, and +honor required-file checks. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or `spec.md` is found; tell the user to run + `/speckit.specify` first. +2. Load `FEATURE_DIR/spec.md` and `.specify/memory/constitution.md` if present. +3. Identify ambiguities that block planning. Prioritize scope, actor and + permission boundaries, data lifecycle, compliance, failure behavior, and user + experience. Ignore implementation choices unless the spec already leaked + them into requirements. +4. Ask up to five concise questions. Prefer multiple choice when the tradeoff is + bounded; allow custom answers when needed. Ask only questions whose answers + would materially change the spec or plan. +5. Update `FEATURE_DIR/spec.md` with a `## Clarifications` section dated today. + Record each question and answer, then revise affected requirements so the + body is unambiguous. +6. Report how many clarifications were added and whether the feature is ready + for `/speckit.plan`. + diff --git a/.claude/commands/speckit.constitution.md b/.claude/commands/speckit.constitution.md new file mode 100644 index 00000000..5a184e90 --- /dev/null +++ b/.claude/commands/speckit.constitution.md @@ -0,0 +1,53 @@ +--- +description: Create or update the Spec Kit project constitution. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before changing +the constitution: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing template files only when absent: + `.specify/templates/constitution-template.md`, + `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, + `.specify/templates/tasks-template.md`, and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/check-prerequisites.sh`, + `.specify/scripts/bash/create-new-feature.sh`, + `.specify/scripts/bash/setup-plan.sh`, + `.specify/scripts/bash/setup-tasks.sh`, and + `.specify/scripts/bash/update-agent-context.sh`. +- The minimal scripts must support the core paths used by these commands: + active feature lookup from `.specify/feature.json`, JSON output, and required + file checks. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Load `.specify/templates/constitution-template.md` if present and load the + current `.specify/memory/constitution.md` if it already exists. +2. Extract governing principles, testing expectations, delivery workflow, and + review rules from the user input. If updating an existing constitution, + preserve valid principles unless the user explicitly replaces them. +3. Write `.specify/memory/constitution.md` with concrete principles, rationale, + governance, amendment rules, and version/date metadata. Do not leave template + placeholders unresolved. +4. Review `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, and + `.specify/templates/tasks-template.md` for obvious constitution references. + Note any template updates that should be made by a separate command if they + are outside this command's requested scope. +5. Report the constitution path, whether it was created or updated, and the + next recommended phase (`/speckit.specify`). + diff --git a/.claude/commands/speckit.implement.md b/.claude/commands/speckit.implement.md new file mode 100644 index 00000000..7dae4885 --- /dev/null +++ b/.claude/commands/speckit.implement.md @@ -0,0 +1,42 @@ +--- +description: Implement the active Spec Kit task list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as implementation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. Do not overwrite existing Spec Kit +scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load the active spec, plan, tasks, constitution, and supporting docs. Treat + `tasks.md` as the execution source of truth. +3. Parse incomplete tasks in order. Respect dependencies and phase boundaries. + `[P]` tasks may be run in parallel only when they touch independent files and + the environment allows it. +4. Implement one coherent task or small dependency group at a time. Keep scope + limited to the task list and do not add new product behavior absent from the + spec. +5. Mark each task complete in `tasks.md` after its implementation and relevant + validation pass. +6. Run the smallest meaningful tests described by the plan, task, or repository + conventions. If a test cannot run, report the exact blocker. +7. Stop and report if the plan/spec/tasks conflict, a task is not actionable, or + implementation would require a decision not captured in the spec. +8. Finish with completed task ids, files changed, validation results, and any + remaining incomplete tasks. + diff --git a/.claude/commands/speckit.plan.md b/.claude/commands/speckit.plan.md new file mode 100644 index 00000000..6ea23088 --- /dev/null +++ b/.claude/commands/speckit.plan.md @@ -0,0 +1,44 @@ +--- +description: Create a technical implementation plan for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Treat the input as the user's technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. Do not overwrite +existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-plan.sh --json` from the repo root. Stop if + the active feature or spec is missing; tell the user to run + `/speckit.specify` first. +2. Load `SPEC_FILE`, `PLAN_FILE`, `.specify/memory/constitution.md`, and any + existing docs under `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. Use the user's + input for stack and architecture choices. +4. Produce supporting docs when relevant: + `FEATURE_DIR/research.md`, `FEATURE_DIR/data-model.md`, + `FEATURE_DIR/contracts/`, and `FEATURE_DIR/quickstart.md`. +5. Keep the plan consistent with the spec. Do not add feature scope that the + spec did not request; send scope changes back to `/speckit.specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present so agent + context reflects the chosen stack. +7. Report created/updated files, unresolved risks, and readiness for + `/speckit.tasks`. + diff --git a/.claude/commands/speckit.specify.md b/.claude/commands/speckit.specify.md new file mode 100644 index 00000000..8b8c7ab0 --- /dev/null +++ b/.claude/commands/speckit.specify.md @@ -0,0 +1,58 @@ +--- +description: Create or update a feature specification from a natural language description. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. If it is empty, stop and ask +for the feature description. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before creating +the spec: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing templates only when absent, especially + `.specify/templates/spec-template.md` and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/create-new-feature.sh` must create a single + `specs/-/spec.md`, copy the spec template, write + `.specify/feature.json`, and print JSON with `FEATURE_DIR` and `SPEC_FILE`. + `.specify/scripts/bash/check-prerequisites.sh` must locate the active feature + from `.specify/feature.json`. +- Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Generate a concise 2-4 word feature short name from the user description. + Prefer action-noun names such as `add-user-auth` or `analytics-dashboard`. +2. Run `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` if the + script supports it. Otherwise use the minimal script behavior described in + Bootstrap. Capture `FEATURE_DIR` and `SPEC_FILE`. +3. Load `.specify/templates/spec-template.md` and, if present, + `.specify/memory/constitution.md`. +4. Write `FEATURE_DIR/spec.md` from the user's description: + - Focus on what users need and why. + - Avoid implementation details, tech stacks, APIs, libraries, and code + structure. + - Include user scenarios, functional requirements, success criteria, + assumptions, edge cases, and key entities when relevant. + - Use at most three `[NEEDS CLARIFICATION: ...]` markers, only for decisions + that materially affect scope, security/privacy, or user experience and have + no reasonable default. +5. Create `FEATURE_DIR/checklists/requirements.md` from the checklist template. + Validate the spec against completeness, testability, measurable success + criteria, bounded scope, and absence of implementation details. Iterate the + spec up to three times for fixable failures. +6. If clarification markers remain, present all questions together with options + and wait for the user's answers before finalizing. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `/speckit.clarify` or `/speckit.plan`. + diff --git a/.claude/commands/speckit.tasks.md b/.claude/commands/speckit.tasks.md new file mode 100644 index 00000000..f064bdcc --- /dev/null +++ b/.claude/commands/speckit.tasks.md @@ -0,0 +1,43 @@ +--- +description: Generate an actionable task list from the active implementation plan. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as task-generation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. Do not +overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-tasks.sh --json` from the repo root. Stop if + `spec.md` or `plan.md` is missing; tell the user which Speckit command to run + first. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, and available supporting + docs (`research.md`, `data-model.md`, `contracts/`, `quickstart.md`). +3. Generate `FEATURE_DIR/tasks.md` as executable Markdown: + - Number tasks `T001`, `T002`, and so on. + - Group by setup, foundations, user story phases, polish, and validation. + - Mark independent tasks with `[P]`. + - Include exact file paths where practical. + - If tests are required by the spec, constitution, or plan, put test tasks + before implementation tasks for the same behavior. + - Keep each user story independently implementable and testable. +4. Add dependency notes and parallel execution examples when they help execute + the plan. +5. Report task count, parallelizable count, and readiness for + `/speckit.analyze`, `/speckit.taskstoissues`, or `/speckit.implement`. + diff --git a/.claude/commands/speckit.taskstoissues.md b/.claude/commands/speckit.taskstoissues.md new file mode 100644 index 00000000..734caa36 --- /dev/null +++ b/.claude/commands/speckit.taskstoissues.md @@ -0,0 +1,108 @@ +--- +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Recognize `--dry-run`. In dry-run mode, print the `gh issue create` commands +that would run and do not create issues. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is available and authenticated unless this is `--dry-run`. +3. Read `FEATURE_DIR/tasks.md` and create one issue per task line that starts + with a Markdown task checkbox and contains a `T###` task id. +4. Use titles in this format: `[SpecKit] : `. +5. Before creating each issue, check all repository issues by exact title. If an + issue with the same title already exists, warn and skip it. +6. Assign every created issue with `--assignee ExtraToast`. +7. Pick exactly one best-fit label from the existing repository labels: + `bug`, `documentation`, or `enhancement`. + - Use `bug` for tasks about bugs, fixes, regressions, failures, or defects. + - Use `documentation` for docs, README, runbook, guide, or content tasks. + - Otherwise use `enhancement`. + - Never invent labels. If the chosen label does not exist in the repository, + warn and omit `--label`. +8. Use this helper shape inline; do not add scripts outside this command: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. + diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 00000000..ccf54e37 --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,78 @@ +# personal-stack Constitution + +## Core Principles + +### I. Human Authorship and No Attribution + +All repository work is authored solely by the human driver. Do not add +`Co-Authored-By` trailers, generated-by footers, assistant names, model names, +or automation-attribution text to commits, PRs, code comments, docs, generated +files, or templates. + +### II. Validate Against Reality + +Claims about paths, APIs, config, cluster state, or tooling must be checked +against the real codebase and, where relevant, live state. If a fact is unknown, +search the repo, inspect the source, or run the narrowest safe command before +designing around it. Do not invent secret paths, resource names, commands, or +contracts. + +### III. Claude/Codex Parity + +Agent-facing behavior must stay equivalent across Claude and Codex surfaces. +Any skill, hook, memory rule, installer behavior, command, or project guidance +added for one agent must get the matching surface for the other in the same +branch, unless an explicit unsupported reason is recorded. + +### IV. Render and Validate Discipline + +Render-managed files are edited only at their source templates or inventory. +After touching a render source, run the owning renderer and commit the rendered +output with the source change. Run the smallest meaningful validation command +for the touched area, and state exactly what remains unverified if a check +cannot run. + +### V. Small Stacked PRs + +Every change should be reviewable, revertable, and scoped to one objective. +Prefer small stacked PRs over broad bundles. Avoid tangential cleanup, +speculative abstractions, unrelated refactors, and compatibility shims when a +direct local-pattern change is available. + +## Required Workflow + +1. Start from a spec for user-visible or cross-cutting changes. The spec must + describe outcomes, acceptance criteria, non-goals, and open questions. +2. Plan against existing repo patterns and real paths. Surface architectural + limitations before implementation begins. +3. Break work into tasks that preserve small PR boundaries and parallel safety. +4. Implement only the task scope. Never revert or overwrite unrelated parallel + edits. +5. Validate with the smallest meaningful command for the touched area: + `./gradlew :services::test` for Kotlin services, + `./gradlew :platform:tooling:test` for platform tooling, and + `npm run typecheck && npm run lint && npm run test` inside Vue UIs. +6. Capture durable lessons or decisions in the knowledge base when they affect + future repo behavior, without storing secrets, raw transcripts, or full + diffs. + +## Render-Managed Boundaries + +- `platform/inventory/fleet.yaml` is the source of truth for public service + routing, catalog, placement, exposure, and access intent. +- Generated Traefik routes, catalog ConfigMaps, agent-kit mirrors, and installer + artifacts must not be hand-edited. +- `.specify/memory/constitution.md` is committed and hand-edited for this repo. + The generic `.specify/templates/constitution-template.md` is only the + render-managed starter for future seed installs. + +## Governance + +This constitution overrides ad-hoc agent behavior. Amend it deliberately when +the governing workflow changes, and update `AGENTS.md`, `CLAUDE.md`, skills, or +templates in the same branch when parity requires it. + +**Version**: 1.0.0 +**Ratified**: 2026-06-08 +**Last Amended**: 2026-06-08 + diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh new file mode 100755 index 00000000..d5e9c06e --- /dev/null +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +paths_only=false +require_tasks=false +include_tasks=false + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --paths-only) + paths_only=true + ;; + --require-tasks) + require_tasks=true + ;; + --include-tasks) + include_tasks=true + ;; + --help|-h) + printf 'Usage: %s [--json] [--paths-only] [--require-tasks] [--include-tasks]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +if [ "${paths_only}" = true ]; then + if [ "${json}" = true ]; then + specify_paths_json "${branch}" + else + printf 'REPO_ROOT: %s\n' "${REPO_ROOT}" + printf 'BRANCH: %s\n' "${branch}" + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'TASKS: %s\n' "${tasks_file}" + fi + exit 0 +fi + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${spec_file}" ] || specify_die "Feature spec not found: ${spec_file}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ "${require_tasks}" = true ] && [ ! -f "${tasks_file}" ]; then + specify_die "Tasks file not found: ${tasks_file}" +fi + +docs="" +append_doc() { + if [ -n "${docs}" ]; then + docs="${docs}," + fi + docs="${docs}\"$1\"" +} + +[ -f "${feature_dir}/research.md" ] && append_doc "research.md" +[ -f "${feature_dir}/data-model.md" ] && append_doc "data-model.md" +[ -d "${feature_dir}/contracts" ] && append_doc "contracts/" +[ -f "${feature_dir}/quickstart.md" ] && append_doc "quickstart.md" +if [ "${include_tasks}" = true ] && [ -f "${tasks_file}" ]; then + append_doc "tasks.md" +fi + +if [ "${json}" = true ]; then + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":[%s]}\n' "$(specify_json_escape "${feature_dir}")" "${docs}" +else + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'AVAILABLE_DOCS:\n' + printf '%s\n' "${docs}" | tr ',' '\n' | sed 's/^/ /; s/"//g' +fi + diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh new file mode 100755 index 00000000..f5e6f41d --- /dev/null +++ b/.specify/scripts/bash/common.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ -z "${SPECIFY_SCRIPT_DIR:-}" ]; then + SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +fi + +specify_repo_root() { + if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + return 0 + fi + + CDPATH= cd -- "${SPECIFY_SCRIPT_DIR}/../../.." && pwd -P +} + +REPO_ROOT=$(specify_repo_root) +SPECIFY_DIR="${REPO_ROOT}/.specify" +SPECS_DIR="${REPO_ROOT}/specs" + +specify_has_git() { + command -v git >/dev/null 2>&1 && git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + +specify_current_branch() { + if specify_has_git; then + git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null + else + basename "$(pwd)" + fi +} + +specify_is_feature_branch() { + case "$1" in + [0-9][0-9][0-9]-*) return 0 ;; + *) return 1 ;; + esac +} + +specify_feature_dir() { + printf '%s/specs/%s\n' "${REPO_ROOT}" "$1" +} + +specify_spec_file() { + printf '%s/specs/%s/spec.md\n' "${REPO_ROOT}" "$1" +} + +specify_plan_file() { + printf '%s/specs/%s/plan.md\n' "${REPO_ROOT}" "$1" +} + +specify_tasks_file() { + printf '%s/specs/%s/tasks.md\n' "${REPO_ROOT}" "$1" +} + +specify_template_file() { + printf '%s/templates/%s\n' "${SPECIFY_DIR}" "$1" +} + +specify_json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +specify_error() { + printf 'ERROR: %s\n' "$*" >&2 +} + +specify_die() { + specify_error "$*" + exit 1 +} + +specify_require_feature_branch() { + current_branch=$(specify_current_branch) + if [ "${current_branch}" = "main" ] || [ "${current_branch}" = "master" ]; then + specify_die "Current branch '${current_branch}' is not a feature branch. Run create-new-feature.sh first." + fi + if ! specify_is_feature_branch "${current_branch}"; then + specify_die "Current branch '${current_branch}' must start with a three-digit feature number, e.g. 001-my-feature." + fi + printf '%s\n' "${current_branch}" +} + +specify_copy_template() { + template_name=$1 + destination=$2 + feature_name=$3 + source_file=$(specify_template_file "${template_name}") + + [ -f "${source_file}" ] || specify_die "Template not found: ${source_file}" + + today=$(date +%F) + sed \ + -e "s/{{FEATURE_NAME}}/${feature_name}/g" \ + -e "s/{{feature_name}}/${feature_name}/g" \ + -e "s/{{DATE}}/${today}/g" \ + "${source_file}" > "${destination}" +} + +specify_paths_json() { + branch=$1 + feature_dir=$(specify_feature_dir "${branch}") + spec_file=$(specify_spec_file "${branch}") + plan_file=$(specify_plan_file "${branch}") + tasks_file=$(specify_tasks_file "${branch}") + has_git=false + if specify_has_git; then + has_git=true + fi + + printf '{"REPO_ROOT":"%s","BRANCH":"%s","HAS_GIT":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(specify_json_escape "${REPO_ROOT}")" \ + "$(specify_json_escape "${branch}")" \ + "${has_git}" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${plan_file}")" \ + "$(specify_json_escape "${tasks_file}")" +} + diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh new file mode 100755 index 00000000..ec478ae4 --- /dev/null +++ b/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +feature_number="" +description="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --number) + shift + [ "$#" -gt 0 ] || specify_die "--number requires a value" + feature_number=$1 + ;; + --help|-h) + printf 'Usage: %s [--json] [--number N] \n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + *) + if [ -z "${description}" ]; then + description=$1 + else + description="${description} $1" + fi + ;; + esac + shift +done + +[ -n "${description}" ] || specify_die "Feature description is required" + +slug=$(printf '%s\n' "${description}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g' | awk ' +{ + count = 0 + for (i = 1; i <= NF; i++) { + word = $i + if (word == "the" || word == "and" || word == "for" || word == "with" || word == "from" || word == "that" || word == "this" || word == "into" || word == "onto") { + continue + } + if (length(word) < 2) { + continue + } + words[++count] = word + if (count == 5) { + break + } + } + if (count == 0) { + print "feature" + } else { + for (i = 1; i <= count; i++) { + printf "%s%s", (i == 1 ? "" : "-"), words[i] + } + printf "\n" + } +}') + +highest=0 +if specify_has_git; then + branches=$(git -C "${REPO_ROOT}" branch --all --format='%(refname:short)' 2>/dev/null || true) + for ref in ${branches}; do + ref=${ref#origin/} + number=${ref%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -d "${SPECS_DIR}" ]; then + for path in "${SPECS_DIR}"/[0-9][0-9][0-9]-*; do + [ -d "${path}" ] || continue + name=$(basename "${path}") + number=${name%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -n "${feature_number}" ]; then + case "${feature_number}" in + *[!0-9]*) specify_die "--number must be numeric" ;; + esac + number=$(printf '%03d' "${feature_number}") +else + number=$(printf '%03d' $((highest + 1))) +fi + +branch_name="${number}-${slug}" +feature_dir=$(specify_feature_dir "${branch_name}") +spec_file=$(specify_spec_file "${branch_name}") + +if specify_has_git; then + if ! git -C "${REPO_ROOT}" rev-parse --verify --quiet "${branch_name}" >/dev/null; then + git -C "${REPO_ROOT}" checkout -b "${branch_name}" >/dev/null 2>&1 + else + git -C "${REPO_ROOT}" checkout "${branch_name}" >/dev/null 2>&1 + fi +fi + +mkdir -p "${feature_dir}" +if [ ! -f "${spec_file}" ]; then + specify_copy_template "spec-template.md" "${spec_file}" "${branch_name}" +fi + +if [ "${json}" = true ]; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_DIR":"%s","FEATURE_NUMBER":"%s"}\n' \ + "$(specify_json_escape "${branch_name}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${number}")" +else + printf 'BRANCH_NAME: %s\n' "${branch_name}" + printf 'SPEC_FILE: %s\n' "${spec_file}" +fi + diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh new file mode 100755 index 00000000..986a8d8d --- /dev/null +++ b/.specify/scripts/bash/setup-plan.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") + +mkdir -p "${feature_dir}" "${feature_dir}/contracts" +[ -f "${spec_file}" ] || specify_copy_template "spec-template.md" "${spec_file}" "${branch}" +[ -f "${plan_file}" ] || specify_copy_template "plan-template.md" "${plan_file}" "${branch}" + +touch "${feature_dir}/research.md" "${feature_dir}/data-model.md" "${feature_dir}/quickstart.md" + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi + diff --git a/.specify/scripts/bash/setup-tasks.sh b/.specify/scripts/bash/setup-tasks.sh new file mode 100755 index 00000000..ef67147b --- /dev/null +++ b/.specify/scripts/bash/setup-tasks.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ ! -f "${tasks_file}" ]; then + specify_copy_template "tasks-template.md" "${tasks_file}" "${branch}" +fi + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'TASKS: %s\n' "${tasks_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi + diff --git a/.specify/templates/constitution-template.md b/.specify/templates/constitution-template.md new file mode 100644 index 00000000..fd2bfdef --- /dev/null +++ b/.specify/templates/constitution-template.md @@ -0,0 +1,54 @@ +# Project Constitution + +## Core Principles + +### I. Outcome-First Specifications + +Every feature begins with a specification that describes user-visible outcomes, +acceptance scenarios, non-goals, and success criteria before implementation +details. Ambiguity must be marked explicitly with `NEEDS CLARIFICATION`. + +### II. Plan Before Implementation + +Implementation work starts only after the plan identifies real project paths, +dependencies, validation commands, rollback considerations, and risks. Plans +must prefer established local patterns over new abstractions. + +### III. Tests and Validation Are Mandatory + +Each feature defines the smallest meaningful verification command before work +begins. Changes are not complete until those checks pass or the remaining gap is +documented with the exact reason validation could not run. + +### IV. Small, Reviewable Changes + +Tasks and PRs must be independently reviewable, revertable, and scoped to one +behavioral objective. Unrelated cleanup, broad refactors, and speculative +flexibility are not allowed inside feature work. + +### V. Durable Context Stays Current + +Specifications, plans, tasks, and durable project memory must reflect decisions +that affect future work. Do not leave important behavior only in chat logs, +temporary notes, or uncommitted local state. + +## Workflow + +1. `/speckit.specify` creates or updates `specs//spec.md`. +2. `/speckit.plan` creates `plan.md` and supporting design artifacts. +3. `/speckit.tasks` creates `tasks.md` from the approved plan. +4. Implementation follows tasks in dependency order, with tests close to the + behavior being changed. +5. Completion requires validation evidence and any relevant documentation + updates. + +## Governance + +This constitution overrides informal conventions. Changes to these principles +must be reviewed deliberately, with downstream templates and instructions +updated in the same change. + +**Version**: 1.0.0 +**Ratified**: {{DATE}} +**Last Amended**: {{DATE}} + diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md new file mode 100644 index 00000000..9df3556a --- /dev/null +++ b/.specify/templates/plan-template.md @@ -0,0 +1,93 @@ +# Implementation Plan: {{FEATURE_NAME}} + +**Branch**: `{{FEATURE_NAME}}` | **Date**: {{DATE}} | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/{{FEATURE_NAME}}/spec.md` + +## Summary + +[Extract from feature spec: primary requirement + technical approach] + +## Technical Context + +**Language/Version**: [e.g. Kotlin 2.x, TypeScript 5.x or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g. Spring Boot, Vue, Postgres or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g. PostgreSQL, Redis, files or N/A] +**Testing**: [e.g. Gradle unit tests, Vitest, Playwright or NEEDS CLARIFICATION] +**Target Platform**: [e.g. k3s, browser, JVM service or NEEDS CLARIFICATION] +**Project Type**: [service/ui/platform/mixed] +**Performance Goals**: [domain-specific target or N/A] +**Constraints**: [domain-specific constraints or N/A] +**Scale/Scope**: [domain-specific scale or N/A] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [ ] No attribution is introduced in files, comments, commit text, or PR text +- [ ] Claude/Codex parity is preserved for any agent-facing behavior +- [ ] Rendered artifacts are updated by the owning renderer when source changes require it +- [ ] Small stacked PR boundary is clear and unrelated cleanup is excluded +- [ ] Verification command is identified for each touched area + +## Project Structure + +### Documentation + +```text +specs/{{FEATURE_NAME}}/ +|-- plan.md +|-- research.md +|-- data-model.md +|-- quickstart.md +|-- contracts/ +`-- tasks.md +``` + +### Source Code + +```text +# Fill with the actual paths this feature will touch. +``` + +**Structure Decision**: [Document the chosen source layout and real paths] + +## Phase 0: Outline & Research + +1. Extract unknowns from Technical Context into research tasks. +2. Capture existing repo patterns for touched paths. +3. Resolve all NEEDS CLARIFICATION items before design. + +**Output**: `research.md` + +## Phase 1: Design & Contracts + +1. Derive entities from the feature spec and document them in `data-model.md`. +2. Produce or update API/CLI/config contracts in `contracts/`. +3. Write `quickstart.md` with validation steps for the feature. +4. Re-run Constitution Check. + +**Output**: `data-model.md`, `contracts/*`, `quickstart.md` + +## Phase 2: Task Planning Approach + +Describe how `/speckit.tasks` should convert this plan into ordered, independently executable tasks. Do not create `tasks.md` manually during `/speckit.plan`. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +| --- | --- | --- | +| [Only if a constitution gate is intentionally violated] | [reason] | [why simpler option does not work] | + +## Progress Tracking + +**Phase Status**: + +- [ ] Phase 0: Research complete +- [ ] Phase 1: Design complete +- [ ] Phase 2: Task planning approach complete + +**Gate Status**: + +- [ ] Initial Constitution Check: PASS +- [ ] Post-Design Constitution Check: PASS +- [ ] All NEEDS CLARIFICATION resolved diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md new file mode 100644 index 00000000..ba912cae --- /dev/null +++ b/.specify/templates/spec-template.md @@ -0,0 +1,89 @@ +# Feature Specification: {{FEATURE_NAME}} + +**Feature Branch**: `{{FEATURE_NAME}}` +**Created**: {{DATE}} +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - [Short Title] (Priority: P1) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value and why it comes first] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] +2. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 2 - [Short Title] (Priority: P2) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 3 - [Short Title] (Priority: P3) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +### Edge Cases + +- What happens when [boundary condition]? +- How does the system handle [error condition]? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST [specific capability] +- **FR-002**: System MUST [specific capability] +- **FR-003**: Users MUST be able to [key interaction] +- **FR-004**: System MUST [data requirement] +- **FR-005**: System MUST [observable behavior] + +*Example of marking unclear requirements:* + +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified] +- **FR-007**: System MUST retain [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation details] +- **[Entity 2]**: [What it represents, relationships to other entities] + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: [Metric, e.g. "Users can complete primary task in under 2 minutes"] +- **SC-002**: [Metric, e.g. "System supports 1,000 concurrent users"] +- **SC-003**: [Metric, e.g. "95% of users complete the task without support"] +- **SC-004**: [Metric, e.g. "Reduce support tickets about X by 50%"] + diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md new file mode 100644 index 00000000..b23ac7fe --- /dev/null +++ b/.specify/templates/tasks-template.md @@ -0,0 +1,67 @@ +# Tasks: {{FEATURE_NAME}} + +**Input**: Design documents from `/specs/{{FEATURE_NAME}}/` +**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/ + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel with other tasks because it touches different files +- **[Story]**: User story label, for example US1, US2, US3 +- Include exact file paths in descriptions + +## Phase 1: Setup + +- [ ] T001 Create or verify project structure for this feature +- [ ] T002 Identify the smallest validation command for touched area + +## Phase 2: Foundational + +- [ ] T003 Implement shared models/configuration needed by all stories +- [ ] T004 Add or update base tests for cross-story behavior + +## Phase 3: User Story 1 (Priority: P1) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T005 [US1] Implement [specific behavior] in [path] +- [ ] T006 [US1] Add focused tests in [path] + +## Phase 4: User Story 2 (Priority: P2) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T007 [P] [US2] Implement [specific behavior] in [path] +- [ ] T008 [P] [US2] Add focused tests in [path] + +## Phase 5: User Story 3 (Priority: P3) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T009 [P] [US3] Implement [specific behavior] in [path] +- [ ] T010 [P] [US3] Add focused tests in [path] + +## Phase 6: Polish + +- [ ] T011 Run the validation command identified in plan.md +- [ ] T012 Update docs or runbooks affected by this feature + +## Dependencies + +- Setup before foundational work +- Foundational work before user stories +- User stories may proceed in priority order, unless marked independent and parallel +- Polish after desired stories are complete + +## Parallel Example + +```text +T007 [P] [US2] ... +T009 [P] [US3] ... +``` + diff --git a/platform/agents/council/council.py b/platform/agents/council/council.py index 04957660..40f4dc66 100644 --- a/platform/agents/council/council.py +++ b/platform/agents/council/council.py @@ -20,6 +20,8 @@ import json import os import re +import shlex +import shutil import subprocess import sys import tempfile @@ -106,6 +108,71 @@ def label(self) -> str: WORKER_TIMEOUT_S = int(os.environ.get("COUNCIL_WORKER_TIMEOUT_S", "1800")) VERIFY_TIMEOUT_S = int(os.environ.get("COUNCIL_VERIFY_TIMEOUT_S", "600")) WT_ROOT = Path(tempfile.gettempdir()) / "council-worktrees" +SPEC_DIR_RE = re.compile(r"^(\d{3})-([a-z0-9][a-z0-9-]*)$") +TASK_BLOCK_RE = re.compile( + r"^## (?P[^\n:]+)(?::[^\n]*)?\n" + r"\n" + r"```json\n(?P.*?)\n```", + re.MULTILINE | re.DOTALL, +) +MAX_CONSTITUTION_CHARS = 6000 +MAX_TEMPLATE_FIELD_CHARS = 12000 +EMBEDDED_CONSTITUTION = """# Constitution + +No project `.specify/memory/constitution.md` was found. Apply the repository's +agent guide, keep changes minimal, validate against real files, and preserve +human authorship. +""" +EMBEDDED_SPEC_TEMPLATE = """# Feature Specification: {{feature_name}} + +**Feature Branch**: `{{feature_id}}` +**Created**: {{date}} + +## User Brief + +{{brief}} + +## Requirements + +- Implement the brief as described, grounded in the real repository. +- Keep scope additive and preserve existing council canonical artifacts. + +## Success Criteria + +- `consolidated_plan.md` and `tasks.json` remain canonical. +- `tasks.md` round-trips exactly to `tasks.json` through council markers. +""" +EMBEDDED_PLAN_TEMPLATE = """# Implementation Plan: {{feature_name}} + +**Feature Branch**: `{{feature_id}}` +**Created**: {{date}} + +## Summary + +{{summary}} + +## Consolidated Plan + +{{consolidated_plan}} +""" +EMBEDDED_TASKS_TEMPLATE = """# Tasks: {{feature_id}} + + +""" + + +@dataclass(frozen=True) +class SpecRef: + number: int + slug: str + + @property + def name(self) -> str: + return f"{self.number:03d}-{self.slug}" + + @property + def relpath(self) -> str: + return f"specs/{self.name}" @dataclass @@ -354,6 +421,335 @@ def _slugify(text: str) -> str: return (s[:48] or "run") +def _first_line(text: str) -> str: + for line in text.splitlines(): + if line.strip(): + return line.strip() + return "run" + + +def derive_feature_slug(brief: str, explicit_slug: Optional[str]) -> str: + return _slugify(explicit_slug or _first_line(brief)) + + +def _bounded(text: str, limit: int) -> str: + if len(text) <= limit: + return text + return text[:limit].rstrip() + "\n\n[truncated]" + + +def _constitution_path(repo: Path = REPO_ROOT) -> Path: + return repo / ".specify" / "memory" / "constitution.md" + + +def read_constitution_context(repo: Path = REPO_ROOT) -> str: + path = _constitution_path(repo) + text = path.read_text() if path.exists() else EMBEDDED_CONSTITUTION + return _bounded(text.strip(), MAX_CONSTITUTION_CHARS) + + +def _constitution_placeholder_reason(text: str) -> Optional[str]: + stripped = text.strip() + if not stripped: + return "constitution is empty" + lower = stripped.lower() + markers = ( + "[project name]", + "[insert", + "[fill", + "[todo", + "{{", + "todo:", + "tbd", + "placeholder", + ) + for marker in markers: + if marker in lower: + return f"constitution contains placeholder marker {marker!r}" + return None + + +def constitution_failure(repo: Path = REPO_ROOT) -> Optional[str]: + path = _constitution_path(repo) + if not path.exists(): + return f"missing constitution at {path}" + reason = _constitution_placeholder_reason(path.read_text()) + if reason: + return f"{reason} at {path}" + return None + + +def _spec_numbers(specs_root: Path) -> list[int]: + if not specs_root.exists(): + return [] + nums = [] + for child in specs_root.iterdir(): + m = SPEC_DIR_RE.match(child.name) + if m: + nums.append(int(m.group(1))) + return nums + + +def allocate_spec_ref(slug: str, specs_root: Path) -> SpecRef: + slug = _slugify(slug) + children = specs_root.iterdir() if specs_root.exists() else () + for child in children: + m = SPEC_DIR_RE.match(child.name) + if m and m.group(2) == slug: + raise ValueError(f"spec path already exists: {child}") + ref = SpecRef((max(_spec_numbers(specs_root)) if specs_root.exists() else 0) + 1, + slug) + path = specs_root / ref.name + if path.exists(): + raise ValueError(f"spec path already exists: {path}") + return ref + + +def spec_ref_from_state(state: dict) -> Optional[SpecRef]: + rel = state.get("spec_relpath") + if not isinstance(rel, str): + return None + name = Path(rel).name + m = SPEC_DIR_RE.match(name) + if not m: + return None + return SpecRef(int(m.group(1)), m.group(2)) + + +def prepare_spec_ref(run: Run, brief: str, explicit_slug: Optional[str]) -> SpecRef: + state = run.read_json("state.json") if run.has("state.json") else {} + existing = spec_ref_from_state(state) + if existing: + return existing + ref = allocate_spec_ref(derive_feature_slug(brief, explicit_slug), + REPO_ROOT / "specs") + run_target = run.path / ref.relpath + repo_target = REPO_ROOT / ref.relpath + if run_target.exists(): + raise ValueError(f"spec path already exists: {run_target}") + if repo_target.exists(): + raise ValueError(f"spec path already exists: {repo_target}") + run.set_state(spec_id=ref.name, spec_slug=ref.slug, spec_relpath=ref.relpath) + return ref + + +def read_spec_dir(path_s: Optional[str]) -> dict[str, str]: + if not path_s: + return {} + path = Path(path_s) + if not path.exists(): + raise SystemExit(f"spec dir not found: {path}") + if not path.is_dir(): + raise SystemExit(f"--spec-dir must be a directory: {path}") + out = {} + for name in ("spec.md", "plan.md", "tasks.md"): + p = path / name + if p.exists(): + out[name] = p.read_text() + return out + + +def load_sdd_template(name: str, fallback: str) -> str: + path = REPO_ROOT / ".specify" / "templates" / name + return path.read_text() if path.exists() else fallback + + +def render_sdd_template(template: str, values: dict[str, str]) -> str: + out = template + for key, val in values.items(): + bounded = _bounded(val, MAX_TEMPLATE_FIELD_CHARS) + out = out.replace("{{" + key + "}}", bounded) + out = out.replace("[" + key.upper() + "]", bounded) + out = out.replace("[" + key + "]", bounded) + return out + + +def render_tasks_md(tasks: list[dict], spec_ref: Optional[SpecRef] = None) -> str: + feature_id = spec_ref.name if spec_ref else "council" + template = load_sdd_template("tasks-template.md", EMBEDDED_TASKS_TEMPLATE) + header = render_sdd_template(template, { + "feature_id": feature_id, + "feature_name": feature_id, + }).strip() + if "" not in header: + header += "\n\n" + lines = [header, ""] + for task in tasks: + tid = str(task["id"]) + task_title = str(task.get("title", tid)).replace("\n", " ").strip() or tid + lines += [ + f"## {tid}: {task_title}", + f"", + "```json", + json.dumps(task, indent=2, sort_keys=True), + "```", + "", + ] + return "\n".join(lines).rstrip() + "\n" + + +def parse_tasks_md(text: str) -> list[dict]: + tasks: list[dict] = [] + for match in TASK_BLOCK_RE.finditer(text): + header_id = match.group("header_id").strip() + marker_id = match.group("marker_id").strip() + if header_id != marker_id: + raise ValueError(f"task marker mismatch: header {header_id!r}, " + f"marker {marker_id!r}") + try: + task = json.loads(match.group("body")) + except json.JSONDecodeError as exc: + raise ValueError(f"task {marker_id!r} JSON block is invalid: {exc}") from exc + if not isinstance(task, dict): + raise ValueError(f"task {marker_id!r} JSON block must be an object") + if str(task.get("id", "")).strip() != marker_id: + raise ValueError(f"task {marker_id!r} JSON id does not match marker") + tasks.append(task) + if not tasks: + raise ValueError("no council task JSON blocks found in tasks.md") + seen: set[str] = set() + for task in tasks: + tid = str(task["id"]) + if tid in seen: + raise ValueError(f"duplicate task id in tasks.md: {tid}") + seen.add(tid) + return tasks + + +def _normalise_tasks(tasks: list[dict]) -> object: + return json.loads(json.dumps(tasks, sort_keys=True)) + + +def assert_tasks_bijection(tasks: list[dict], tasks_md_text: str) -> None: + parsed = parse_tasks_md(tasks_md_text) + validate_tasks(parsed) + if _normalise_tasks(parsed) != _normalise_tasks(tasks): + raise ValueError("tasks.md does not match tasks.json") + + +def run_spec_dir(run: Run) -> Optional[Path]: + state = run.read_json("state.json") if run.has("state.json") else {} + ref = spec_ref_from_state(state) + if ref: + return run.path / ref.relpath + specs = run.path / "specs" + matches = sorted(p for p in specs.glob("[0-9][0-9][0-9]-*") if p.is_dir()) + return matches[-1] if matches else None + + +def regenerate_command(run: Run) -> str: + return ("council plan --run " + shlex.quote(str(run.path)) + + " --brief " + shlex.quote(str(run.path / "brief.md"))) + + +def analyze_checkpoint(run: Run, tasks: list[dict]) -> None: + failures = [] + constitution = constitution_failure() + if constitution: + failures.append(constitution) + spec_dir = run_spec_dir(run) + tasks_md = spec_dir / "tasks.md" if spec_dir else None + if not tasks_md or not tasks_md.exists(): + failures.append("missing tasks.md for tasks.json") + else: + try: + assert_tasks_bijection(tasks, tasks_md.read_text()) + except ValueError as exc: + failures.append(str(exc)) + if failures: + raise ValueError("analyze gate checkpoint 1 failed:\n- " + + "\n- ".join(failures) + + f"\nRegenerate with: {regenerate_command(run)}") + + +def analyze_tasks_file(tasks: list[dict], tasks_path: Path) -> None: + failures = [] + constitution = constitution_failure() + if constitution: + failures.append(constitution) + tasks_md = tasks_path.with_name("tasks.md") + if tasks_md.exists(): + try: + assert_tasks_bijection(tasks, tasks_md.read_text()) + except ValueError as exc: + failures.append(str(exc)) + if failures: + cmd = ("council plan --brief " + shlex.quote(str(tasks_path.parent / "spec.md")) + + " --spec-dir " + shlex.quote(str(tasks_path.parent))) + raise ValueError("analyze gate checkpoint 1 failed:\n- " + + "\n- ".join(failures) + + f"\nRegenerate with: {cmd}") + + +def build_spec_md(brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> str: + if seed.get("spec.md"): + return seed["spec.md"] + if obj.get("spec_markdown"): + return str(obj["spec_markdown"]).strip() + "\n" + template = load_sdd_template("spec-template.md", EMBEDDED_SPEC_TEMPLATE) + return render_sdd_template(template, { + "feature_name": ref.slug.replace("-", " ").title(), + "feature_id": ref.name, + "date": time.strftime("%Y-%m-%d"), + "brief": brief.strip(), + }).rstrip() + "\n" + + +def build_plan_md(brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> str: + if seed.get("plan.md"): + return seed["plan.md"] + for key in ("implementation_plan_markdown", "plan_markdown"): + if obj.get(key): + return str(obj[key]).strip() + "\n" + template = load_sdd_template("plan-template.md", EMBEDDED_PLAN_TEMPLATE) + return render_sdd_template(template, { + "feature_name": ref.slug.replace("-", " ").title(), + "feature_id": ref.name, + "date": time.strftime("%Y-%m-%d"), + "brief": brief.strip(), + "summary": str(obj.get("summary", "")).strip() or _first_line(brief), + "consolidated_plan": str(obj.get("consolidated_plan_markdown", "")).strip(), + }).rstrip() + "\n" + + +def write_sdd_artifacts(run: Run, brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> None: + tasks = obj.get("tasks", []) + if seed.get("tasks.md"): + assert_tasks_bijection(tasks, seed["tasks.md"]) + spec_dir = run.path / ref.relpath + spec_dir.mkdir(parents=True, exist_ok=True) + (spec_dir / "spec.md").write_text(build_spec_md(brief, obj, ref, seed)) + (spec_dir / "plan.md").write_text(build_plan_md(brief, obj, ref, seed)) + (spec_dir / "tasks.md").write_text(render_tasks_md(tasks, ref)) + + +def copy_run_specs_to_worktree(run: Run, worktree: Path) -> None: + specs_root = run.path / "specs" + if not specs_root.exists(): + return + copied = [] + for src in sorted(p for p in specs_root.iterdir() + if p.is_dir() and SPEC_DIR_RE.match(p.name)): + dest = worktree / "specs" / src.name + if dest.exists(): + raise ValueError(f"spec path already exists in integration worktree: " + f"specs/{src.name}") + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(src, dest) + copied.append(f"specs/{src.name}") + if not copied: + return + git("add", "-A", "--", "specs", cwd=worktree) + dirty = git("status", "--porcelain", "--", "specs", cwd=worktree).stdout.strip() + if dirty: + git("-c", "user.name=council", "-c", "user.email=council@local", + "commit", "-q", "-m", "council: add spec artifacts", cwd=worktree) + log(f"committed Spec Kit artifacts: {', '.join(copied)}") + + def _split_dest_url(owner: str, name: str) -> str: """Canonical SSH remote for a GitHub owner/name. Pure (covered by --self-test). In runner workspaces git rewrites git@github.com: to https so @@ -365,7 +761,8 @@ def _split_dest_url(owner: str, name: str) -> str: # stages 1-4 # -------------------------------------------------------------------------- -def stage_plan(run: Run, brief: str, a: Engine, b: Engine) -> tuple[dict, dict]: +def stage_plan(run: Run, brief: str, a: Engine, b: Engine, + constitution_context: str) -> tuple[dict, dict]: if run.has("planA.v1.json") and run.has("planB.v1.json"): log("stage 1: dual plans already present, skipping") return run.read_json("planA.v1.json"), run.read_json("planB.v1.json") @@ -376,7 +773,8 @@ def stage_plan(run: Run, brief: str, a: Engine, b: Engine) -> tuple[dict, dict]: def mk(engine: Engine) -> Callable[[], EngineResult]: prompt = render(tmpl, engine_label=engine.label, brief=brief, repo_root=str(REPO_ROOT), schema=schema, - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(engine, prompt)) res_a, res_b = parallel([mk(a), mk(b)]) @@ -387,7 +785,8 @@ def mk(engine: Engine) -> Callable[[], EngineResult]: def stage_critique_round(run: Run, brief: str, a: Engine, b: Engine, - plan_a: dict, plan_b: dict, rnd: int) -> tuple[dict, dict]: + plan_a: dict, plan_b: dict, rnd: int, + constitution_context: str) -> tuple[dict, dict]: out_a, out_b = f"planA.v{rnd + 1}.json", f"planB.v{rnd + 1}.json" if run.has(out_a) and run.has(out_b): log(f"stage 2: critique round {rnd} already present, skipping") @@ -401,7 +800,8 @@ def crit(critic: Engine, plan: dict) -> Callable[[], EngineResult]: prompt = render(critic_tmpl, engine_label=critic.label, brief=brief, repo_root=str(REPO_ROOT), plan=json.dumps(plan, indent=2), - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(critic, prompt)) crit_of_a, crit_of_b = parallel([crit(b, plan_a), crit(a, plan_b)]) @@ -415,7 +815,8 @@ def rev(author: Engine, plan: dict, critique: str) -> Callable[[], EngineResult] prompt = render(rev_tmpl, engine_label=author.label, brief=brief, repo_root=str(REPO_ROOT), plan=json.dumps(plan, indent=2), critique=critique, schema=schema, - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(author, prompt)) rev_a, rev_b = parallel([rev(a, plan_a, crit_of_a.text), @@ -427,10 +828,19 @@ def rev(author: Engine, plan: dict, critique: str) -> Callable[[], EngineResult] def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, - rounds: int, consolidator: Engine) -> dict: + rounds: int, consolidator: Engine, + constitution_context: str, spec_ref: SpecRef, + spec_seed: dict[str, str]) -> dict: if run.has("tasks.json") and run.has("consolidated_plan.md"): log("stage 4: consolidation already present, skipping") - return run.read_json("tasks.json") + tasks = run.read_json("tasks.json") + obj = { + "consolidated_plan_markdown": (run.path / "consolidated_plan.md").read_text(), + "tasks": tasks, + } + write_sdd_artifacts(run, brief, obj, spec_ref, spec_seed) + analyze_checkpoint(run, tasks) + return tasks log(f"stage 4: consolidation ({consolidator.label})") history_parts = [] for r in range(1, rounds + 1): @@ -445,6 +855,7 @@ def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, history="\n\n".join(history_parts) or "(no critiques recorded)", schema=load_schema_text("consolidated"), baseline=BASELINE_PROMPT, + constitution=constitution_context, ) res = run.record(run_engine(consolidator, prompt)) obj = extract_json(res.text) @@ -452,6 +863,8 @@ def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, validate_tasks(tasks) run.write_text("consolidated_plan.md", obj.get("consolidated_plan_markdown", "")) run.write_json("tasks.json", tasks) + write_sdd_artifacts(run, brief, obj, spec_ref, spec_seed) + analyze_checkpoint(run, tasks) return tasks @@ -650,6 +1063,7 @@ def execute_dag(run: Run, tasks: list[dict], worker_for: Callable[[str], Engine] git("worktree", "remove", "--force", str(integ_wt), check=False) git("branch", "-D", integ_branch, check=False) git("worktree", "add", "--force", "-b", integ_branch, str(integ_wt), base) + copy_run_specs_to_worktree(run, integ_wt) log(f"exec: {len(tasks)} task(s) in {len(waves)} wave(s); base {base[:8]}; " f"integration branch {integ_branch}; concurrency {cap}") @@ -700,6 +1114,8 @@ def cmd_fanout(args: argparse.Namespace) -> int: if not run.has("tasks.json"): raise SystemExit(f"no tasks.json in {run.path}; run `plan` first") tasks = run.read_json("tasks.json") + validate_tasks(tasks) + analyze_checkpoint(run, tasks) waves = plan_waves(tasks) cfg = resolve_config({"intensity": args.intensity, "worker": args.worker, @@ -772,6 +1188,7 @@ def cmd_fleet(args: argparse.Namespace) -> int: raise SystemExit(f"tasks file not found: {tasks_path}") tasks = json.loads(tasks_path.read_text()) validate_tasks(tasks) + analyze_tasks_file(tasks, tasks_path) waves = plan_waves(tasks) pool = parse_agents_pool(args.agents) @@ -942,9 +1359,9 @@ def read_brief(arg: str) -> str: if arg == "-": return sys.stdin.read() p = Path(arg) - if not p.exists(): - raise SystemExit(f"brief file not found: {arg}") - return p.read_text() + if p.exists(): + return p.read_text() + return arg def cmd_plan(args: argparse.Namespace) -> int: @@ -975,16 +1392,24 @@ def cmd_plan(args: argparse.Namespace) -> int: brief = read_brief(args.brief) run = Run.open(Path(args.run)) if args.run else Run.create(brief, args.slug) run.write_text("brief.md", brief) + spec_ref = prepare_spec_ref(run, brief, args.slug) + spec_seed = read_spec_dir(args.spec_dir) + constitution_context = read_constitution_context() run.set_state(stage="plan", intensity=cfg["intensity"], rounds=rounds, - planner_a=a.label, planner_b=b.label) + planner_a=a.label, planner_b=b.label, + spec_id=spec_ref.name, spec_slug=spec_ref.slug, + spec_relpath=spec_ref.relpath, + spec_dir=args.spec_dir) log(f"run dir: {run.path}") log(f"intensity {cfg['intensity']}: {a.label} ║ {b.label}, {rounds} round(s), " f"codex effort {cfg['codex_effort']}") - plan_a, plan_b = stage_plan(run, brief, a, b) + plan_a, plan_b = stage_plan(run, brief, a, b, constitution_context) for rnd in range(1, rounds + 1): - plan_a, plan_b = stage_critique_round(run, brief, a, b, plan_a, plan_b, rnd) - tasks = stage_consolidate(run, brief, plan_a, plan_b, rounds, consolidator) + plan_a, plan_b = stage_critique_round( + run, brief, a, b, plan_a, plan_b, rnd, constitution_context) + tasks = stage_consolidate(run, brief, plan_a, plan_b, rounds, consolidator, + constitution_context, spec_ref, spec_seed) run.set_state(stage="planned", task_count=len(tasks)) waves = plan_waves(tasks) @@ -1200,6 +1625,79 @@ def check(name: str, cond: bool) -> None: check("validate_tasks warns on empty verify", any("no verify" in m for m in captured)) + # Spec Kit task markdown + analyze helpers + sample_task = { + "id": "T1", + "title": "Implement one thing", + "objective": "Change exactly one thing", + "output_format": "Code edits", + "paths": ["platform/agents/council/council.py"], + "depends_on": [], + "difficulty": "moderate", + "model": "haiku", + "verify": "python3 platform/agents/council/council.py --self-test", + "boundaries": "Stay in scope", + } + rendered_tasks = render_tasks_md([sample_task], SpecRef(7, "sdd-aware-council")) + check("tasks.json -> tasks.md -> parse roundtrips", + parse_tasks_md(rendered_tasks) == [sample_task]) + edited_tasks = rendered_tasks.replace("Change exactly one thing", + "Change a different thing") + check("tasks.md bijection mismatch hard-fails", + _raises(lambda: assert_tasks_bijection([sample_task], edited_tasks))) + bad_marker = rendered_tasks.replace("", + "") + check("tasks.md parser rejects marker/header mismatch", + _raises(lambda: parse_tasks_md(bad_marker))) + gate_msg = "" + try: + analyze_checkpoint(Run(Path("/tmp/council-self-test-run")), [sample_task]) + except ValueError as exc: + gate_msg = str(exc) + check("analyze gate names checkpoint 1 and regenerate command", + "analyze gate checkpoint 1 failed" in gate_msg + and "Regenerate with: council plan --run" in gate_msg) + + # constitution handling is bounded and limited to reasoning roles. + check("constitution context is bounded", + len(read_constitution_context()) <= MAX_CONSTITUTION_CHARS + 20) + for role in ("planner", "critic", "reviser", "consolidator"): + check(f"constitution token present in {role}", + "{{constitution}}" in load_prompt(role)) + check("constitution token absent from worker", + "{{constitution}}" not in load_prompt("worker")) + check("constitution token absent from verifier", + "{{constitution}}" not in load_prompt("verifier")) + with tempfile.TemporaryDirectory() as td: + repo = Path(td) + check("constitution failure detects missing file", + "missing constitution" in (constitution_failure(repo) or "")) + cpath = repo / ".specify" / "memory" / "constitution.md" + cpath.parent.mkdir(parents=True) + cpath.write_text("# Constitution\n\n[PROJECT NAME]\n") + check("constitution failure detects placeholder", + "placeholder" in (constitution_failure(repo) or "")) + cpath.write_text("# Constitution\n\nShip small, verified changes.\n") + check("constitution failure accepts concrete file", + constitution_failure(repo) is None) + + # Spec numbering and slug derivation + check("free-text brief derives slug from first line", + derive_feature_slug("Fuse council with Spec Kit\nextra", None) + == "fuse-council-with-spec-kit") + check("explicit --slug is reused for spec slug", + derive_feature_slug("ignored", "My Feature") == "my-feature") + with tempfile.TemporaryDirectory() as td: + specs = Path(td) / "specs" + specs.mkdir() + (specs / "001-old").mkdir() + ref = allocate_spec_ref("new feature", specs) + check("NNN allocation uses max(existing)+1", + ref == SpecRef(2, "new-feature")) + (specs / "003-duplicate").mkdir() + check("NNN allocation fail-fast on existing slug", + _raises(lambda: allocate_spec_ref("duplicate", specs))) + # merge_config (intensity presets + precedence) std = merge_config({}, {}) check("default intensity is standard", @@ -1279,6 +1777,7 @@ def build_parser() -> argparse.ArgumentParser: pl.add_argument("--brief", help="brief file path, or - for stdin") pl.add_argument("--run", help="existing run dir to resume") pl.add_argument("--slug", help="slug for the run dir name") + pl.add_argument("--spec-dir", help="existing specs/NNN-slug dir to consume") pl.add_argument("--intensity", choices=list(PRESETS), help="preset (overrides council.toml for this run)") pl.add_argument("--rounds", type=int, default=None, help="override critique rounds") diff --git a/platform/agents/council/prompts/consolidator.md b/platform/agents/council/prompts/consolidator.md index c3abbb90..6f89f0e7 100644 --- a/platform/agents/council/prompts/consolidator.md +++ b/platform/agents/council/prompts/consolidator.md @@ -49,9 +49,15 @@ parallel execution. Each task must: Keep the task count proportional to the work: a handful for a focused change, more only when the work genuinely decomposes. Sequential, tightly-coupled work should be a single task with a clear ordering, not forced into false parallelism. +If useful, also include optional `spec_markdown` and +`implementation_plan_markdown` fields for Spec Kit artifacts; `tasks` remains +the canonical worker input. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY a JSON object — no prose, no code fences — matching this schema: diff --git a/platform/agents/council/prompts/critic.md b/platform/agents/council/prompts/critic.md index 739e922d..98bf3887 100644 --- a/platform/agents/council/prompts/critic.md +++ b/platform/agents/council/prompts/critic.md @@ -29,6 +29,9 @@ parallel execution. {{baseline}} +# Constitution +{{constitution}} + # Output Return a concise Markdown critique: a bulleted list of concrete problems, each with WHY it matters and a suggested fix. End with one line: `VERDICT:` followed diff --git a/platform/agents/council/prompts/planner.md b/platform/agents/council/prompts/planner.md index 9971f952..1df67068 100644 --- a/platform/agents/council/prompts/planner.md +++ b/platform/agents/council/prompts/planner.md @@ -19,6 +19,9 @@ file will collide). Be concrete: name real files and real commands. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY a JSON object — no prose, no code fences — matching this schema: diff --git a/platform/agents/council/prompts/reviser.md b/platform/agents/council/prompts/reviser.md index e9471262..7c5bcdd5 100644 --- a/platform/agents/council/prompts/reviser.md +++ b/platform/agents/council/prompts/reviser.md @@ -19,6 +19,9 @@ Re-check claims against the real code at {{repo_root}} as needed. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY the revised JSON object — no prose, no code fences — matching this schema: diff --git a/platform/agents/council/schemas/consolidated.schema.json b/platform/agents/council/schemas/consolidated.schema.json index 07b654a1..8fbf46eb 100644 --- a/platform/agents/council/schemas/consolidated.schema.json +++ b/platform/agents/council/schemas/consolidated.schema.json @@ -6,6 +6,16 @@ "required": ["consolidated_plan_markdown", "tasks"], "properties": { "consolidated_plan_markdown": { "type": "string" }, + "spec_markdown": { "type": "string" }, + "implementation_plan_markdown": { "type": "string" }, + "feature": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { "type": "string" }, + "slug": { "type": "string" } + } + }, "tasks": { "type": "array", "items": { diff --git a/platform/agents/kit/THIRD_PARTY_NOTICES.md b/platform/agents/kit/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..91431cff --- /dev/null +++ b/platform/agents/kit/THIRD_PARTY_NOTICES.md @@ -0,0 +1,31 @@ +# Third-Party Notices + +This directory vendors scaffolding adapted from GitHub Spec Kit: + +- Project: Spec Kit +- Repository: https://github.com/github/spec-kit +- Pinned tag: v0.9.5 +- License: MIT + +## MIT License + +Copyright (c) GitHub and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/platform/agents/kit/manifest.yaml b/platform/agents/kit/manifest.yaml index 4ad0347f..1e190404 100644 --- a/platform/agents/kit/manifest.yaml +++ b/platform/agents/kit/manifest.yaml @@ -1,9 +1,17 @@ -version: 1 +version: 2 generated: partial notes: - - "This manifest pins the current repo and installer agent surfaces. Repo skills, hooks, hook settings, and the installer entrypoint are rendered from agent-kit templates; installer heredoc bodies, including hooks, skills, settings, and allowlists, are rendered from partial templates." + - "This manifest pins the current repo and installer agent surfaces. Repo skills, hooks, hook settings, commands, Spec Kit scaffold seeds, and the installer entrypoint are rendered from agent-kit templates; installer heredoc bodies, including hooks, skills, commands, settings, allowlists, and Spec Kit seeds, are rendered from partial or repo templates." - "Repo skill bodies are intentionally agent-specific today; parity means every shared skill has both Claude and Codex targets unless an explicit unsupported reason is present." +provenance: + spec_kit: + upstream_repository: https://github.com/github/spec-kit + upstream_tag: v0.9.5 + upstream_commit: "2262359" + source_lock: platform/agents/kit/spec-kit-source.lock + third_party_notices: platform/agents/kit/THIRD_PARTY_NOTICES.md + renderer: script_path: platform/agents/kit/render-agent-kit.py template_root: platform/agents/kit/templates/repo @@ -31,17 +39,30 @@ renderer: - .agents/skills/kb-first/SKILL.md - .agents/skills/open-pr/SKILL.md - .agents/skills/run-tests/SKILL.md + - .agents/skills/speckit-analyze/SKILL.md + - .agents/skills/speckit-checklist/SKILL.md + - .agents/skills/speckit-clarify/SKILL.md + - .agents/skills/speckit-constitution/SKILL.md + - .agents/skills/speckit-implement/SKILL.md + - .agents/skills/speckit-plan/SKILL.md + - .agents/skills/speckit-specify/SKILL.md + - .agents/skills/speckit-tasks/SKILL.md + - .agents/skills/speckit-taskstoissues/SKILL.md - .agents/skills/token-economy/SKILL.md + - .claude/commands/speckit.analyze.md + - .claude/commands/speckit.checklist.md + - .claude/commands/speckit.clarify.md + - .claude/commands/speckit.constitution.md + - .claude/commands/speckit.implement.md + - .claude/commands/speckit.plan.md + - .claude/commands/speckit.specify.md + - .claude/commands/speckit.tasks.md + - .claude/commands/speckit.taskstoissues.md + - .claude/hooks/kb-stop-digest.sh - .claude/hooks/kb-user-prompt-recall.sh - .claude/hooks/pre-tool-use-edit-recall.sh - .claude/hooks/pre-tool-use-git-commit-capture.sh - - .claude/hooks/kb-stop-digest.sh - - .codex/hooks/kb-user-prompt-recall.sh - - .codex/hooks/kb-stop-digest.sh - - .codex/hooks/pre-tool-use-edit-recall.sh - - .codex/hooks/pre-tool-use-git-commit-capture.sh - .claude/settings.json - - .codex/hooks.json - .claude/skills/add-public-service/SKILL.md - .claude/skills/agent-session-bootstrap/SKILL.md - .claude/skills/council/SKILL.md @@ -50,12 +71,127 @@ renderer: - .claude/skills/open-pr/SKILL.md - .claude/skills/run-tests/SKILL.md - .claude/skills/token-economy/SKILL.md + - .codex/hooks.json + - .codex/hooks/kb-stop-digest.sh + - .codex/hooks/kb-user-prompt-recall.sh + - .codex/hooks/pre-tool-use-edit-recall.sh + - .codex/hooks/pre-tool-use-git-commit-capture.sh + - .specify/scripts/bash/check-prerequisites.sh + - .specify/scripts/bash/common.sh + - .specify/scripts/bash/create-new-feature.sh + - .specify/scripts/bash/setup-plan.sh + - .specify/scripts/bash/setup-tasks.sh + - .specify/templates/constitution-template.md + - .specify/templates/plan-template.md + - .specify/templates/spec-template.md + - .specify/templates/tasks-template.md - services/knowledge-api/src/main/resources/installer/install.sh canonical_mcp_tools: source: services/knowledge-api/src/test/kotlin/com/jorisjonkers/personalstack/knowledge/mcp/McpToolsTest.kt prefix: knowledge. +commands: + - name: speckit.analyze + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-analyze/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.analyze.md + sha256: 4f7008c48a5d0a177ead422a0a211da2234fefc3965b7b04f43f9ac9c799d87c + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.analyze.md" + - name: speckit.checklist + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-checklist/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.checklist.md + sha256: b1742e6513ccf0e7254aa5d40c88b985816613a440c68c511ae98a5674fbaaf1 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.checklist.md" + - name: speckit.clarify + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-clarify/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.clarify.md + sha256: 6390475434e8d40089c647d01ba4777fe67bdf4462c2b64c77f38959eeec5670 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.clarify.md" + - name: speckit.constitution + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-constitution/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.constitution.md + sha256: 813bc3446e9062c2f9636a92bdfb052f9bcda7e0f059b89097dd957332170c1b + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.constitution.md" + - name: speckit.implement + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-implement/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.implement.md + sha256: 6c563e3786ae056e87a6ca4bc3e7bf0271e2524dd0ae4fab9f1e26d6b23e9cd0 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.implement.md" + - name: speckit.plan + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-plan/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.plan.md + sha256: 11c25bf68eee0ad3939a569f44b0caf6d212748697a91bfce11407aff033b736 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.plan.md" + - name: speckit.specify + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-specify/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.specify.md + sha256: a703db91aa3904f9a038891c71f985f2543a3cae5a6541681bdd5910b00827fa + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.specify.md" + - name: speckit.tasks + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-tasks/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.tasks.md + sha256: 52e6149ecc17d829e346355b431d1c11804a63ff1553ccb21b8d7f5e01eb1105 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.tasks.md" + - name: speckit.taskstoissues + supported_agents: [claude] + unsupported: + codex: "Use .agents/skills/speckit-taskstoissues/SKILL.md." + targets: + - agent: claude + path: .claude/commands/speckit.taskstoissues.md + sha256: 3eec85ffad51c312f8d81540dcf7e618afb056e38820b2dc14d47761ebc6d750 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + target_path: "${CLAUDE_HOME}/commands/speckit.taskstoissues.md" + skills: - name: add-public-service supported_agents: [codex, claude] @@ -145,6 +281,105 @@ skills: source_path: services/knowledge-api/src/main/resources/installer/install.sh target_path: "${CLAUDE_HOME}/skills/token-economy/SKILL.md" codex_target_path: "${CODEX_HOME}/skills/token-economy/SKILL.md" + - name: speckit-analyze + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.analyze.md." + targets: + - agent: codex + path: .agents/skills/speckit-analyze/SKILL.md + sha256: ccfc6c274dd02a4c7341cbc6d7e4d910d3406390c786f170c17f2ab6c2790be0 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-analyze/SKILL.md" + - name: speckit-checklist + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.checklist.md." + targets: + - agent: codex + path: .agents/skills/speckit-checklist/SKILL.md + sha256: 34b81078483d5a029e47cee1aa6509a146bcee0345b3b8f89a168643a3ca7b86 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-checklist/SKILL.md" + - name: speckit-clarify + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.clarify.md." + targets: + - agent: codex + path: .agents/skills/speckit-clarify/SKILL.md + sha256: 3c1f783ab77cbe0746b07427c30ce077dc1a4beaec106c7be0b3cc42d61ff751 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-clarify/SKILL.md" + - name: speckit-constitution + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.constitution.md." + targets: + - agent: codex + path: .agents/skills/speckit-constitution/SKILL.md + sha256: 059b332cd827faaea2a41c58fcd370a566b69096f66ed2b5010228314e133b37 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-constitution/SKILL.md" + - name: speckit-implement + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.implement.md." + targets: + - agent: codex + path: .agents/skills/speckit-implement/SKILL.md + sha256: eadaad841db12e464911470b5bce8b4c37727062abfdee228d6828ec6da56ea5 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-implement/SKILL.md" + - name: speckit-plan + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.plan.md." + targets: + - agent: codex + path: .agents/skills/speckit-plan/SKILL.md + sha256: 7aebca844e4fd8050acb8aad72bf0efc63b2942a210bd120941af51db840a476 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-plan/SKILL.md" + - name: speckit-specify + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.specify.md." + targets: + - agent: codex + path: .agents/skills/speckit-specify/SKILL.md + sha256: dbf00d874e928f7d8ebbe6161ce4f092d9ec3a6b28c7caeeab44b164ef9170e6 + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-specify/SKILL.md" + - name: speckit-tasks + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.tasks.md." + targets: + - agent: codex + path: .agents/skills/speckit-tasks/SKILL.md + sha256: 36143e59d6166f4fe8f63683653b14c55b78c2b76be69b1630fec4af45b4b46b + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-tasks/SKILL.md" + - name: speckit-taskstoissues + supported_agents: [codex] + unsupported: + claude: "Use .claude/commands/speckit.taskstoissues.md." + targets: + - agent: codex + path: .agents/skills/speckit-taskstoissues/SKILL.md + sha256: a8ba323a6300283ab2381eff195489906c3ae4600726611ebb3109641945158e + installer: + source_path: services/knowledge-api/src/main/resources/installer/install.sh + codex_target_path: "${CODEX_HOME}/skills/speckit-taskstoissues/SKILL.md" - name: topics supported_agents: [codex, claude] installer: @@ -200,6 +435,7 @@ hooks: matcher: "Edit|Write|MultiEdit|apply_patch" purpose: "Recall file/module lessons before edits." supported_agents: [claude, codex] + mcp_tools: [knowledge.recall] targets: - agent: claude path: .claude/hooks/pre-tool-use-edit-recall.sh @@ -209,16 +445,16 @@ hooks: path: .codex/hooks/pre-tool-use-edit-recall.sh settings_path: .codex/hooks.json sha256: a107e6f8a2a73346c1b05a235c2849264509acbcc631688c25fc7b22f5444c7d - mcp_tools: [knowledge.recall] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh target_path: "${CLAUDE_HOME}/hooks/pre-tool-use-edit-recall.sh" codex_target_path: "${CODEX_HOME}/hooks/pre-tool-use-edit-recall.sh" - name: pre-tool-use-git-commit-capture event: PreToolUse - matcher: Bash + matcher: "Bash" purpose: "Capture non-WIP git commit messages as candidate decisions." supported_agents: [claude, codex] + mcp_tools: [knowledge.capture_decision] targets: - agent: claude path: .claude/hooks/pre-tool-use-git-commit-capture.sh @@ -228,7 +464,6 @@ hooks: path: .codex/hooks/pre-tool-use-git-commit-capture.sh settings_path: .codex/hooks.json sha256: 6d2370aa565da308ce1d8e838dda18da4897cdd4a5ebeab171b4a1b8be6028c6 - mcp_tools: [knowledge.capture_decision] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh target_path: "${CLAUDE_HOME}/hooks/pre-tool-use-git-commit-capture.sh" @@ -242,7 +477,63 @@ settings: path: .codex/hooks.json sha256: ec501908b04a9b4ac1cf073f9cdb703322952267eb79583845eb139068330019 +specify: + project_seed: + scope: project + tracked_for_uninstall: false + targets: + - path: .specify/memory/constitution.md + sha256: 37b0328a7d612d6104ac0e3f48de7df7789acdb5f81621124b7f29529b8b9f3a + installer_target_path: "${PROJECT_ROOT}/.specify/memory/constitution.md" + mode: "0644" + seed_if_absent: true + - path: .specify/scripts/bash/check-prerequisites.sh + sha256: 4e477664b02ccfc45dd1d795ece01006a3c59ff0088a5dce985ccd0a82bddd70 + installer_target_path: "${PROJECT_ROOT}/.specify/scripts/bash/check-prerequisites.sh" + mode: "0755" + seed_if_absent: false + - path: .specify/scripts/bash/common.sh + sha256: f4ccdd5e3adde5b9dc25375d6da8fbb2d15da704652b4d8cd6e81b29b3ab535f + installer_target_path: "${PROJECT_ROOT}/.specify/scripts/bash/common.sh" + mode: "0755" + seed_if_absent: false + - path: .specify/scripts/bash/create-new-feature.sh + sha256: b9ec52760baf03c4077f22d30effddf97b4a2a18927b1c024a440579556ddda9 + installer_target_path: "${PROJECT_ROOT}/.specify/scripts/bash/create-new-feature.sh" + mode: "0755" + seed_if_absent: false + - path: .specify/scripts/bash/setup-plan.sh + sha256: 4d3f657e673ad4192a4c12af86491cf131e29ddffcf00729c0a28b3ee421fb66 + installer_target_path: "${PROJECT_ROOT}/.specify/scripts/bash/setup-plan.sh" + mode: "0755" + seed_if_absent: false + - path: .specify/scripts/bash/setup-tasks.sh + sha256: 23004d89fe3ede0ef062ccb287eeb2d34e90fbcc4ab16912e517cf43d528039a + installer_target_path: "${PROJECT_ROOT}/.specify/scripts/bash/setup-tasks.sh" + mode: "0755" + seed_if_absent: false + - path: .specify/templates/constitution-template.md + sha256: 3ba571da05c7f0ae9d13ae44bd636f41f029e7e1598376850702954916b51551 + installer_target_path: "${PROJECT_ROOT}/.specify/templates/constitution-template.md" + mode: "0644" + seed_if_absent: false + - path: .specify/templates/plan-template.md + sha256: 04026ce6621c81392ce8ea7f59f9374087c8798b2d95f31094c18bab3cd0fc52 + installer_target_path: "${PROJECT_ROOT}/.specify/templates/plan-template.md" + mode: "0644" + seed_if_absent: false + - path: .specify/templates/spec-template.md + sha256: f466d7ee0e5870c5d8a54408293a3c41d04a55a90049f5b7c42feefc4e9b2454 + installer_target_path: "${PROJECT_ROOT}/.specify/templates/spec-template.md" + mode: "0644" + seed_if_absent: false + - path: .specify/templates/tasks-template.md + sha256: ffa552b4937a104926c8ed2f09ae3b09dd9e78aebf1b5770b3bbb4ee2bbaad77 + installer_target_path: "${PROJECT_ROOT}/.specify/templates/tasks-template.md" + mode: "0644" + seed_if_absent: false + installer: path: services/knowledge-api/src/main/resources/installer/install.sh - sha256: 68d38c29f3e755f6dae9f916379867893f4ad0d4307f71f2cadf33e6662ccd0a + sha256: 1ce59c8ad852b6bf09d758a6d53fed9188b9023fbb5cb5f84ba5e27af57133c2 supported_agents: [codex, claude] diff --git a/platform/agents/kit/render-agent-kit.py b/platform/agents/kit/render-agent-kit.py index 413b4ff3..953f42c8 100755 --- a/platform/agents/kit/render-agent-kit.py +++ b/platform/agents/kit/render-agent-kit.py @@ -31,11 +31,20 @@ # generated from the canonical source (no duplication into partials). COUNCIL_BUNDLE_PATTERN = re.compile(r"^# @agent-kit-council-bundle$") COUNCIL_MANAGED_PATTERN = re.compile(r"^# @agent-kit-council-managed (?Pclaude|codex)$") +SPECKIT_COMMANDS_BUNDLE_PATTERN = re.compile(r"^# @agent-kit-speckit-commands-bundle$") +SPECKIT_COMMANDS_MANAGED_PATTERN = re.compile(r"^# @agent-kit-speckit-commands-managed$") +CODEX_SPECKIT_BUNDLE_PATTERN = re.compile(r"^# @agent-kit-codex-speckit-bundle$") +CODEX_SPECKIT_MANAGED_PATTERN = re.compile(r"^# @agent-kit-codex-speckit-managed$") +SPECIFY_SEED_PATTERN = re.compile(r"^# @agent-kit-specify-seed$") COUNCIL_SRC = REPOSITORY_ROOT / "platform" / "agents" / "council" COUNCIL_SKILL_SRC = { "claude": REPO_TEMPLATE_ROOT / ".claude" / "skills" / "council" / "SKILL.md", "codex": REPO_TEMPLATE_ROOT / ".agents" / "skills" / "council" / "SKILL.md", } +SPECKIT_COMMANDS_SRC = REPO_TEMPLATE_ROOT / ".claude" / "commands" +CODEX_SPECKIT_SKILLS_SRC = REPO_TEMPLATE_ROOT / ".agents" / "skills" +SPECIFY_SRC = REPO_TEMPLATE_ROOT / ".specify" +SPECIFY_CONSTITUTION_SRC = REPOSITORY_ROOT / ".specify" / "memory" / "constitution.md" MANIFEST_PATH = KIT_ROOT / "manifest.yaml" RUNNER_ENTRYPOINT = REPOSITORY_ROOT / "services" / "agent-runner" / "entrypoint.sh" MCP_CONFIGMAP = ( @@ -113,6 +122,10 @@ def _council_var(rel: str) -> str: return "COUNCIL_FILE_" + re.sub(r"[^A-Za-z0-9]", "_", rel) +def _shell_var(prefix: str, rel: str) -> str: + return prefix + "_" + re.sub(r"[^A-Za-z0-9]", "_", rel) + + def _heredoc(var: str, content: str) -> list[str]: delim = f"{var}_EOF" return [f"read -r -d '' {var} <<'{delim}' || true", content.rstrip("\n"), delim] @@ -158,6 +171,94 @@ def render_council_managed(agent: str) -> str: return "\n".join(f' "{base}/council/{rel}"' for rel in rels) + "\n" +def speckit_command_files() -> list[Path]: + return sorted(SPECKIT_COMMANDS_SRC.glob("speckit.*.md")) + + +def codex_speckit_skill_files() -> list[Path]: + return sorted(CODEX_SPECKIT_SKILLS_SRC.glob("speckit-*/SKILL.md")) + + +def specify_seed_files() -> list[tuple[Path, str, str, bool]]: + files: list[tuple[Path, str, str, bool]] = [ + (SPECIFY_CONSTITUTION_SRC, ".specify/memory/constitution.md", "0644", True), + ] + for path in sorted(SPECIFY_SRC.rglob("*")): + if not path.is_file(): + continue + rel = path.relative_to(REPO_TEMPLATE_ROOT).as_posix() + mode = "0755" if path.stat().st_mode & stat.S_IXUSR else "0644" + files.append((path, rel, mode, False)) + return files + + +def render_speckit_commands_bundle() -> str: + lines = ["# Spec Kit Claude commands — generated by render-agent-kit.py from repo templates."] + commands = speckit_command_files() + for path in commands: + rel = path.relative_to(SPECKIT_COMMANDS_SRC).as_posix() + lines += _heredoc(_shell_var("SPECKIT_COMMAND", rel), path.read_text()) + lines += ['if [ "${INSTALL_CLAUDE}" = 1 ]; then'] + for path in commands: + rel = path.relative_to(SPECKIT_COMMANDS_SRC).as_posix() + var = _shell_var("SPECKIT_COMMAND", rel) + lines.append(f' write_file "${{COMMANDS_DIR}}/{rel}" 0644 "${{{var}}}"') + lines.append("fi") + return "\n".join(lines) + "\n" + + +def render_speckit_commands_managed() -> str: + return "\n".join( + f' "${{COMMANDS_DIR}}/{path.relative_to(SPECKIT_COMMANDS_SRC).as_posix()}"' + for path in speckit_command_files() + ) + "\n" + + +def render_codex_speckit_bundle() -> str: + lines = ["# Spec Kit Codex skills — generated by render-agent-kit.py from repo templates."] + skills = codex_speckit_skill_files() + for path in skills: + rel = path.relative_to(CODEX_SPECKIT_SKILLS_SRC).as_posix() + lines += _heredoc(_shell_var("CODEX_SPECKIT", rel), path.read_text()) + lines += ['if [ "${INSTALL_CODEX}" = 1 ]; then'] + for path in skills: + rel = path.relative_to(CODEX_SPECKIT_SKILLS_SRC).as_posix() + var = _shell_var("CODEX_SPECKIT", rel) + lines.append(f' write_file "${{CODEX_SKILLS_DIR}}/{rel}" 0644 "${{{var}}}"') + lines.append("fi") + return "\n".join(lines) + "\n" + + +def render_codex_speckit_managed() -> str: + return "\n".join( + f' "${{CODEX_SKILLS_DIR}}/{path.relative_to(CODEX_SPECKIT_SKILLS_SRC).as_posix()}"' + for path in codex_speckit_skill_files() + ) + "\n" + + +def render_specify_seed() -> str: + lines = ["# Spec Kit project scaffold seed — generated by render-agent-kit.py from repo templates."] + seed_files = specify_seed_files() + for path, rel, _mode, _seed_if_absent in seed_files: + lines += _heredoc(_shell_var("SPECIFY_SEED", rel), path.read_text()) + lines += ['if [ "${SCOPE}" = "project" ]; then'] + for _path, rel, mode, seed_if_absent in seed_files: + var = _shell_var("SPECIFY_SEED", rel) + target = f"${{PROJECT_ROOT}}/{rel}" + if seed_if_absent: + lines += [ + f' if [ ! -e "{target}" ]; then', + f' write_file "{target}" {mode} "${{{var}}}"', + " else", + f' log "preserving existing {target}"', + " fi", + ] + else: + lines.append(f' write_file "{target}" {mode} "${{{var}}}"') + lines.append("fi") + return "\n".join(lines) + "\n" + + def rendered_content(rendered: RenderedFile) -> bytes: if not rendered.expand_includes: return rendered.source.read_bytes() @@ -173,6 +274,21 @@ def rendered_content(rendered: RenderedFile) -> bytes: if managed: parts.append(render_council_managed(managed.group("agent"))) continue + if SPECKIT_COMMANDS_BUNDLE_PATTERN.match(line): + parts.append(render_speckit_commands_bundle()) + continue + if SPECKIT_COMMANDS_MANAGED_PATTERN.match(line): + parts.append(render_speckit_commands_managed()) + continue + if CODEX_SPECKIT_BUNDLE_PATTERN.match(line): + parts.append(render_codex_speckit_bundle()) + continue + if CODEX_SPECKIT_MANAGED_PATTERN.match(line): + parts.append(render_codex_speckit_managed()) + continue + if SPECIFY_SEED_PATTERN.match(line): + parts.append(render_specify_seed()) + continue match = INCLUDE_PATTERN.match(line) if not match: parts.append(raw_line) @@ -475,17 +591,27 @@ def doctor(args: argparse.Namespace) -> int: def render(destination_root: Path) -> int: + rendered_count = 0 for rendered in template_files(destination_root): + source_mode = rendered.source.stat().st_mode + mode = 0o755 if source_mode & stat.S_IXUSR else 0o644 + if rendered.destination.exists(): + same_mode = stat.S_IMODE(rendered.destination.stat().st_mode) == mode + if rendered.expand_includes: + same_content = rendered_content(rendered) == rendered.destination.read_bytes() + else: + same_content = filecmp.cmp(rendered.source, rendered.destination, shallow=False) + if same_content and same_mode: + continue + rendered.destination.parent.mkdir(parents=True, exist_ok=True) if rendered.expand_includes: rendered.destination.write_bytes(rendered_content(rendered)) else: shutil.copyfile(rendered.source, rendered.destination) - source_mode = rendered.source.stat().st_mode - executable = source_mode & stat.S_IXUSR - mode = 0o755 if executable else 0o644 rendered.destination.chmod(mode) - print(f"rendered {len(template_files(destination_root))} agent kit files into {destination_root}") + rendered_count += 1 + print(f"rendered {rendered_count} agent kit files into {destination_root}") return 0 diff --git a/platform/agents/kit/spec-kit-source.lock b/platform/agents/kit/spec-kit-source.lock new file mode 100644 index 00000000..b60f8323 --- /dev/null +++ b/platform/agents/kit/spec-kit-source.lock @@ -0,0 +1,62 @@ +# Spec Kit Source Lock + +upstream_repository: https://github.com/github/spec-kit +upstream_tag: v0.9.5 +upstream_release_date: 2026-06-05 +resolved_commit_sha: UNRESOLVED-DNS-BLOCKED +license: MIT + +## Copied or Adapted Paths + +The following upstream `.specify` scaffold paths are vendored into the +render-managed personal-stack agent kit seed: + +- `.specify/templates/spec-template.md` +- `.specify/templates/plan-template.md` +- `.specify/templates/tasks-template.md` +- `.specify/templates/constitution-template.md` +- `.specify/scripts/bash/check-prerequisites.sh` +- `.specify/scripts/bash/common.sh` +- `.specify/scripts/bash/create-new-feature.sh` +- `.specify/scripts/bash/setup-plan.sh` +- `.specify/scripts/bash/setup-tasks.sh` + +Personal-stack also commits these repo-local files outside renderer ownership: + +- `.specify/memory/constitution.md` +- `specs/.gitkeep` + +## Adaptation Notes + +- The render-managed constitution template remains a generic starter. +- The repo-root `.specify/memory/constitution.md` is personal-stack's real + governing constitution and must be hand-edited, not rendered over. +- `.specify` is treated as seed-if-absent. Installs must never clobber an + existing project `.specify` directory. +- Bash scripts preserve upstream-style feature branch directories under + `specs/-/` and JSON outputs consumed by `/speckit.*` commands. + +## Upgrade Procedure + +Run the upstream sync outside runtime installs; never upgrade a user's +`.specify` tree in place. + +1. Install or upgrade Specify CLI pinned to the candidate tag: + `uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@vX.Y.Z` +2. Preview upstream changes: + `specify self upgrade --dry-run` +3. Resolve the tag SHA without modifying this repo: + `git ls-remote --tags https://github.com/github/spec-kit.git refs/tags/vX.Y.Z` +4. Copy only the scaffold paths listed above into + `platform/agents/kit/templates/repo/.specify/`. +5. Keep personal-stack's `.specify/memory/constitution.md` hand-edited and do + not replace it with the generic upstream template. +6. Re-run shell syntax checks for vendored scripts and hand off to the renderer + owner to update `manifest.yaml` and generated outputs. + +## Verification Note + +This worker could not resolve `api.github.com` or `github.com` from the sandbox, +so `resolved_commit_sha` must be replaced with the real v0.9.5 commit before +merge. + diff --git a/platform/agents/kit/templates/installer/install.sh.tpl b/platform/agents/kit/templates/installer/install.sh.tpl index 41573093..b7887f2b 100644 --- a/platform/agents/kit/templates/installer/install.sh.tpl +++ b/platform/agents/kit/templates/installer/install.sh.tpl @@ -112,6 +112,7 @@ else readonly CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" fi readonly HOOKS_DIR="${CLAUDE_HOME}/hooks" +readonly COMMANDS_DIR="${CLAUDE_HOME}/commands" readonly SKILLS_DIR="${CLAUDE_HOME}/skills" readonly MANIFEST="${CLAUDE_HOME}/.knowledge-system-version" readonly CODEX_HOOKS_DIR="${CODEX_HOME}/hooks" @@ -155,6 +156,7 @@ claude_managed_paths=( "${HOOKS_DIR}/pre-tool-use-edit-recall.sh" "${HOOKS_DIR}/pre-tool-use-git-commit-capture.sh" "${HOOKS_DIR}/stop-session-digest.sh" +# @agent-kit-speckit-commands-managed "${SKILLS_DIR}/topics/SKILL.md" "${SKILLS_DIR}/audit/SKILL.md" "${SKILLS_DIR}/kb-first/SKILL.md" @@ -169,6 +171,7 @@ codex_managed_paths=( "${CODEX_HOOKS_DIR}/pre-tool-use-edit-recall.sh" "${CODEX_HOOKS_DIR}/pre-tool-use-git-commit-capture.sh" "${CODEX_HOOKS_DIR}/kb-stop-digest.sh" +# @agent-kit-codex-speckit-managed "${CODEX_SKILLS_DIR}/topics/SKILL.md" "${CODEX_SKILLS_DIR}/audit/SKILL.md" "${CODEX_SKILLS_DIR}/kb-first/SKILL.md" @@ -213,6 +216,11 @@ if [ "${INSTALL_CLAUDE}" = 1 ]; then write_file "${HOOKS_DIR}/user-prompt-submit-recall.sh" 0755 "${USER_PROMPT_SUBMIT_HOOK}" fi +# ----------------------------------------------------------------- +# Spec Kit Claude commands +# ----------------------------------------------------------------- +# @agent-kit-speckit-commands-bundle + # ----------------------------------------------------------------- # Skill: topics # ----------------------------------------------------------------- @@ -268,6 +276,11 @@ if [ "${INSTALL_CLAUDE}" = 1 ]; then write_file "${SKILLS_DIR}/agent-session-bootstrap/SKILL.md" 0644 "${AGENT_SESSION_BOOTSTRAP_SKILL}" fi +# ----------------------------------------------------------------- +# Spec Kit Codex skills +# ----------------------------------------------------------------- +# @agent-kit-codex-speckit-bundle + # ----------------------------------------------------------------- # Skill: council (multi-file toolkit — driver + prompts + schemas + # default config). Installs into ${SKILLS_DIR}/council and @@ -356,6 +369,11 @@ if [ "${INSTALL_CODEX}" = 1 ]; then write_file "${CODEX_HOOKS_CONFIG}" 0644 "${CODEX_HOOKS_JSON}" fi +# ----------------------------------------------------------------- +# Spec Kit project scaffold seed +# ----------------------------------------------------------------- +# @agent-kit-specify-seed + # ----------------------------------------------------------------- # Manifest # ----------------------------------------------------------------- diff --git a/platform/agents/kit/templates/installer/partials/allowlist/defaults.gitignore b/platform/agents/kit/templates/installer/partials/allowlist/defaults.gitignore index e35197cc..b38565c3 100644 --- a/platform/agents/kit/templates/installer/partials/allowlist/defaults.gitignore +++ b/platform/agents/kit/templates/installer/partials/allowlist/defaults.gitignore @@ -6,6 +6,10 @@ # Re-running the installer never overwrites this file once you've # customised it — only the initial install seeds these defaults. +# SDD paths; git-commit-capture has no allowlist and stop-session-digest digests transcripts, so neither is path-suppressible. +**/.specify/** +**/specs/** + # Secrets-bearing files *.env .env* diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-analyze/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-analyze/SKILL.md new file mode 100644 index 00000000..aed4f234 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-analyze/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-analyze +description: Analyze consistency across the active spec, plan, and tasks. +--- + +# Speckit Analyze + +Use this skill for the `$speckit-analyze` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Read the spec, plan, tasks, constitution if present, and referenced + supporting docs. +3. Perform read-only analysis. Do not modify files. +4. Report missing requirement coverage, spec-plan conflicts, task scope creep, + duplicated or vague tasks, ordering problems, and unverifiable acceptance + criteria. +5. Order findings by severity with file or section references. If no issues are + found, say so and identify residual risk. +6. Recommend the next Speckit command. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-checklist/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-checklist/SKILL.md new file mode 100644 index 00000000..ab5c93ff --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-checklist/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-checklist +description: Generate a focused quality checklist for the active specification. +--- + +# Speckit Checklist + +Use this skill for the `$speckit-checklist` phase. Treat the user's prompt as +the checklist focus; infer a useful focus if none is provided. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or spec exists. +2. Read the active spec, plan if present, and constitution if present. +3. Create `FEATURE_DIR/checklists/.md` from the checklist template when + available. +4. Write validation questions for requirements completeness, clarity, + consistency, and acceptance readiness. Do not write implementation tasks. +5. Mark items complete only when the user asked for validation and the current + artifacts satisfy the item. +6. Report the checklist path and high-risk gaps. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-clarify/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-clarify/SKILL.md new file mode 100644 index 00000000..c5c8d1be --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-clarify/SKILL.md @@ -0,0 +1,29 @@ +--- +name: speckit-clarify +description: Clarify underspecified feature requirements before technical planning. +--- + +# Speckit Clarify + +Use this skill for the `$speckit-clarify` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR`, and honor required-file +checks. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or `spec.md` exists. +2. Read `FEATURE_DIR/spec.md` and the constitution if present. +3. Identify planning-blocking ambiguities in scope, actors, permissions, data, + compliance, failure behavior, and user experience. +4. Ask up to five concise questions, using multiple choice where possible. +5. Add a dated `## Clarifications` section to the spec, record answers, and + revise affected requirements. +6. Report clarification count and readiness for `$speckit-plan`. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-constitution/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-constitution/SKILL.md new file mode 100644 index 00000000..a0b23572 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-constitution/SKILL.md @@ -0,0 +1,30 @@ +--- +name: speckit-constitution +description: Create or update the Spec Kit project constitution. +--- + +# Speckit Constitution + +Use this skill for the `$speckit-constitution` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing templates and minimal helper +scripts under `.specify/scripts/bash/` only when absent: +`check-prerequisites.sh`, `create-new-feature.sh`, `setup-plan.sh`, +`setup-tasks.sh`, and `update-agent-context.sh`. The helpers must support +active feature lookup from `.specify/feature.json`, JSON output, and required +file checks. Do not overwrite existing Spec Kit files. + +## Workflow + +1. Read `.specify/templates/constitution-template.md` and any existing + `.specify/memory/constitution.md`. +2. Derive concrete project principles, testing standards, delivery workflow, + review rules, governance, and amendment rules from the user request. +3. Write `.specify/memory/constitution.md` with no unresolved placeholders. +4. Check the spec, plan, and tasks templates for obvious constitution alignment + notes, but do not make unrelated edits. +5. Report the constitution path and readiness for `$speckit-specify`. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-implement/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-implement/SKILL.md new file mode 100644 index 00000000..15d02112 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-implement/SKILL.md @@ -0,0 +1,34 @@ +--- +name: speckit-implement +description: Implement the active Spec Kit task list. +--- + +# Speckit Implement + +Use this skill for the `$speckit-implement` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if the spec, plan, or tasks file is missing. +2. Read the active spec, plan, tasks, constitution, and supporting docs. +3. Execute incomplete tasks from `tasks.md` in dependency order. Run `[P]` tasks + in parallel only when they touch independent files and the environment allows + it. +4. Keep implementation scope limited to the task list and active spec. +5. Mark tasks complete in `tasks.md` after implementation and relevant + validation. +6. Run the smallest meaningful tests required by the task, plan, or repository + conventions. Report exact blockers for checks that cannot run. +7. Stop for conflicts, unactionable tasks, or decisions missing from the spec. +8. Finish with completed task ids, changed files, validation results, and + remaining incomplete tasks. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-plan/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-plan/SKILL.md new file mode 100644 index 00000000..5c0478f3 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-plan/SKILL.md @@ -0,0 +1,36 @@ +--- +name: speckit-plan +description: Create a technical implementation plan for the active specification. +--- + +# Speckit Plan + +Use this skill for the `$speckit-plan` phase. Treat the user's prompt as +technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-plan.sh --json`. Stop if no active spec + exists. +2. Read the spec, plan template or existing plan, constitution, and supporting + docs in `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. +4. Create supporting docs when relevant: `research.md`, `data-model.md`, + `contracts/`, and `quickstart.md`. +5. Keep scope aligned to the spec; send product-scope changes back to + `$speckit-specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present. +7. Report updated files, unresolved risks, and readiness for `$speckit-tasks`. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-specify/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-specify/SKILL.md new file mode 100644 index 00000000..c8931d38 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-specify/SKILL.md @@ -0,0 +1,39 @@ +--- +name: speckit-specify +description: Create or update a feature specification from a natural language description. +--- + +# Speckit Specify + +Use this skill for the `$speckit-specify` phase. The user's prompt is the +feature description; if it is empty, ask for the description before proceeding. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/templates/spec-template.md`, +`.specify/templates/checklist-template.md`, +`.specify/scripts/bash/create-new-feature.sh`, and +`.specify/scripts/bash/check-prerequisites.sh` only when absent. The feature +script must create one `specs/-/spec.md`, copy the spec +template, write `.specify/feature.json`, and print JSON with `FEATURE_DIR` and +`SPEC_FILE`. + +## Workflow + +1. Generate a concise 2-4 word feature short name. +2. Run `.specify/scripts/bash/create-new-feature.sh --json ""` or + the minimal equivalent from Bootstrap. +3. Read `.specify/templates/spec-template.md` and + `.specify/memory/constitution.md` if present. +4. Write `FEATURE_DIR/spec.md` focused on what users need and why. Avoid + implementation details. Include user scenarios, functional requirements, + success criteria, assumptions, edge cases, and key entities when relevant. +5. Use at most three `[NEEDS CLARIFICATION: ...]` markers for critical unknowns + with no reasonable default. +6. Create `FEATURE_DIR/checklists/requirements.md`, validate the spec, and + iterate up to three times for fixable gaps. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `$speckit-clarify` or `$speckit-plan`. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-tasks/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-tasks/SKILL.md new file mode 100644 index 00000000..9c32d469 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-tasks/SKILL.md @@ -0,0 +1,33 @@ +--- +name: speckit-tasks +description: Generate an actionable task list from the active implementation plan. +--- + +# Speckit Tasks + +Use this skill for the `$speckit-tasks` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-tasks.sh --json`. Stop if the spec or plan + is missing. +2. Read the spec, plan, and supporting docs. +3. Generate `FEATURE_DIR/tasks.md` with `T001` style task ids, user-story + phases, dependency notes, and `[P]` markers for independent parallel tasks. +4. Include exact file paths where practical. Put test tasks before + implementation tasks whenever tests are required by the spec, constitution, + or plan. +5. Keep each user story independently implementable and testable. +6. Report task count, parallelizable count, and readiness for + `$speckit-analyze`, `$speckit-taskstoissues`, or `$speckit-implement`. + diff --git a/platform/agents/kit/templates/repo/.agents/skills/speckit-taskstoissues/SKILL.md b/platform/agents/kit/templates/repo/.agents/skills/speckit-taskstoissues/SKILL.md new file mode 100644 index 00000000..5bfc94d2 --- /dev/null +++ b/platform/agents/kit/templates/repo/.agents/skills/speckit-taskstoissues/SKILL.md @@ -0,0 +1,101 @@ +--- +name: speckit-taskstoissues +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +# Speckit Tasks To Issues + +Use this skill for the `$speckit-taskstoissues` phase. Support `--dry-run` by +printing the `gh issue create` commands without creating issues. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is installed and authenticated unless running `--dry-run`. +3. Parse task lines from `tasks.md` that start with a Markdown task checkbox and + contain a `T###` task id. +4. Create one issue per task with title + `[SpecKit] : `. +5. Before creating, check all repository issues by exact title; warn and skip + already-created issues. +6. Always include `--assignee ExtraToast`. +7. Pick exactly one best-fit existing repo label from + `enhancement`, `bug`, or `documentation`. Never invent labels; if the chosen + label does not exist, warn and omit `--label`. +8. Use this inline helper shape; do not add a script file: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.analyze.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.analyze.md new file mode 100644 index 00000000..afb093c5 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.analyze.md @@ -0,0 +1,39 @@ +--- +description: Analyze consistency across the active spec, plan, and tasks. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as analysis focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, + `FEATURE_DIR/tasks.md`, `.specify/memory/constitution.md` if present, and + referenced supporting docs. +3. Perform a read-only analysis. Do not modify files. +4. Report only actionable findings: + - Missing task coverage for requirements or success criteria. + - Plan decisions that conflict with the spec or constitution. + - Tasks that introduce scope not present in the spec. + - Duplicated, contradictory, vague, or unordered tasks. + - Acceptance criteria that cannot be verified by the task list. +5. Order findings by severity and include file/section references. If no issues + are found, say so and note residual risks. +6. Recommend the next command: refine with `/speckit.specify`, + `/speckit.plan`, `/speckit.tasks`, or proceed to `/speckit.implement`. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.checklist.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.checklist.md new file mode 100644 index 00000000..c5085f01 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.checklist.md @@ -0,0 +1,39 @@ +--- +description: Generate a focused quality checklist for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as the checklist domain or focus. If empty, infer the most useful +quality checklist from the spec. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. The script +must locate the active feature from `.specify/feature.json` and print JSON with +`FEATURE_DIR`. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or spec exists. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md` if present, and + `.specify/memory/constitution.md` if present. +3. Create `FEATURE_DIR/checklists/.md` using + `.specify/templates/checklist-template.md` when available. Choose a concise + file name from the focus, such as `security.md`, `accessibility.md`, or + `requirements.md`. +4. Write checklist items as validation questions for requirements completeness, + clarity, consistency, and acceptance readiness. Do not turn the checklist + into implementation tasks. +5. If the user asked to validate, mark items complete only when the current + artifacts satisfy them; otherwise leave them unchecked for review. +6. Report the checklist path and any high-risk gaps found while generating it. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.clarify.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.clarify.md new file mode 100644 index 00000000..33264fd6 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.clarify.md @@ -0,0 +1,39 @@ +--- +description: Clarify underspecified feature requirements before technical planning. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as clarification focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR` and available docs, and +honor required-file checks. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or `spec.md` is found; tell the user to run + `/speckit.specify` first. +2. Load `FEATURE_DIR/spec.md` and `.specify/memory/constitution.md` if present. +3. Identify ambiguities that block planning. Prioritize scope, actor and + permission boundaries, data lifecycle, compliance, failure behavior, and user + experience. Ignore implementation choices unless the spec already leaked + them into requirements. +4. Ask up to five concise questions. Prefer multiple choice when the tradeoff is + bounded; allow custom answers when needed. Ask only questions whose answers + would materially change the spec or plan. +5. Update `FEATURE_DIR/spec.md` with a `## Clarifications` section dated today. + Record each question and answer, then revise affected requirements so the + body is unambiguous. +6. Report how many clarifications were added and whether the feature is ready + for `/speckit.plan`. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.constitution.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.constitution.md new file mode 100644 index 00000000..5a184e90 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.constitution.md @@ -0,0 +1,53 @@ +--- +description: Create or update the Spec Kit project constitution. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before changing +the constitution: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing template files only when absent: + `.specify/templates/constitution-template.md`, + `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, + `.specify/templates/tasks-template.md`, and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/check-prerequisites.sh`, + `.specify/scripts/bash/create-new-feature.sh`, + `.specify/scripts/bash/setup-plan.sh`, + `.specify/scripts/bash/setup-tasks.sh`, and + `.specify/scripts/bash/update-agent-context.sh`. +- The minimal scripts must support the core paths used by these commands: + active feature lookup from `.specify/feature.json`, JSON output, and required + file checks. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Load `.specify/templates/constitution-template.md` if present and load the + current `.specify/memory/constitution.md` if it already exists. +2. Extract governing principles, testing expectations, delivery workflow, and + review rules from the user input. If updating an existing constitution, + preserve valid principles unless the user explicitly replaces them. +3. Write `.specify/memory/constitution.md` with concrete principles, rationale, + governance, amendment rules, and version/date metadata. Do not leave template + placeholders unresolved. +4. Review `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, and + `.specify/templates/tasks-template.md` for obvious constitution references. + Note any template updates that should be made by a separate command if they + are outside this command's requested scope. +5. Report the constitution path, whether it was created or updated, and the + next recommended phase (`/speckit.specify`). + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.implement.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.implement.md new file mode 100644 index 00000000..7dae4885 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.implement.md @@ -0,0 +1,42 @@ +--- +description: Implement the active Spec Kit task list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as implementation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. Do not overwrite existing Spec Kit +scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load the active spec, plan, tasks, constitution, and supporting docs. Treat + `tasks.md` as the execution source of truth. +3. Parse incomplete tasks in order. Respect dependencies and phase boundaries. + `[P]` tasks may be run in parallel only when they touch independent files and + the environment allows it. +4. Implement one coherent task or small dependency group at a time. Keep scope + limited to the task list and do not add new product behavior absent from the + spec. +5. Mark each task complete in `tasks.md` after its implementation and relevant + validation pass. +6. Run the smallest meaningful tests described by the plan, task, or repository + conventions. If a test cannot run, report the exact blocker. +7. Stop and report if the plan/spec/tasks conflict, a task is not actionable, or + implementation would require a decision not captured in the spec. +8. Finish with completed task ids, files changed, validation results, and any + remaining incomplete tasks. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.plan.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.plan.md new file mode 100644 index 00000000..6ea23088 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.plan.md @@ -0,0 +1,44 @@ +--- +description: Create a technical implementation plan for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Treat the input as the user's technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. Do not overwrite +existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-plan.sh --json` from the repo root. Stop if + the active feature or spec is missing; tell the user to run + `/speckit.specify` first. +2. Load `SPEC_FILE`, `PLAN_FILE`, `.specify/memory/constitution.md`, and any + existing docs under `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. Use the user's + input for stack and architecture choices. +4. Produce supporting docs when relevant: + `FEATURE_DIR/research.md`, `FEATURE_DIR/data-model.md`, + `FEATURE_DIR/contracts/`, and `FEATURE_DIR/quickstart.md`. +5. Keep the plan consistent with the spec. Do not add feature scope that the + spec did not request; send scope changes back to `/speckit.specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present so agent + context reflects the chosen stack. +7. Report created/updated files, unresolved risks, and readiness for + `/speckit.tasks`. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.specify.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.specify.md new file mode 100644 index 00000000..8b8c7ab0 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.specify.md @@ -0,0 +1,58 @@ +--- +description: Create or update a feature specification from a natural language description. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. If it is empty, stop and ask +for the feature description. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before creating +the spec: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing templates only when absent, especially + `.specify/templates/spec-template.md` and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/create-new-feature.sh` must create a single + `specs/-/spec.md`, copy the spec template, write + `.specify/feature.json`, and print JSON with `FEATURE_DIR` and `SPEC_FILE`. + `.specify/scripts/bash/check-prerequisites.sh` must locate the active feature + from `.specify/feature.json`. +- Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Generate a concise 2-4 word feature short name from the user description. + Prefer action-noun names such as `add-user-auth` or `analytics-dashboard`. +2. Run `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` if the + script supports it. Otherwise use the minimal script behavior described in + Bootstrap. Capture `FEATURE_DIR` and `SPEC_FILE`. +3. Load `.specify/templates/spec-template.md` and, if present, + `.specify/memory/constitution.md`. +4. Write `FEATURE_DIR/spec.md` from the user's description: + - Focus on what users need and why. + - Avoid implementation details, tech stacks, APIs, libraries, and code + structure. + - Include user scenarios, functional requirements, success criteria, + assumptions, edge cases, and key entities when relevant. + - Use at most three `[NEEDS CLARIFICATION: ...]` markers, only for decisions + that materially affect scope, security/privacy, or user experience and have + no reasonable default. +5. Create `FEATURE_DIR/checklists/requirements.md` from the checklist template. + Validate the spec against completeness, testability, measurable success + criteria, bounded scope, and absence of implementation details. Iterate the + spec up to three times for fixable failures. +6. If clarification markers remain, present all questions together with options + and wait for the user's answers before finalizing. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `/speckit.clarify` or `/speckit.plan`. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.tasks.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.tasks.md new file mode 100644 index 00000000..f064bdcc --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.tasks.md @@ -0,0 +1,43 @@ +--- +description: Generate an actionable task list from the active implementation plan. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as task-generation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. Do not +overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-tasks.sh --json` from the repo root. Stop if + `spec.md` or `plan.md` is missing; tell the user which Speckit command to run + first. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, and available supporting + docs (`research.md`, `data-model.md`, `contracts/`, `quickstart.md`). +3. Generate `FEATURE_DIR/tasks.md` as executable Markdown: + - Number tasks `T001`, `T002`, and so on. + - Group by setup, foundations, user story phases, polish, and validation. + - Mark independent tasks with `[P]`. + - Include exact file paths where practical. + - If tests are required by the spec, constitution, or plan, put test tasks + before implementation tasks for the same behavior. + - Keep each user story independently implementable and testable. +4. Add dependency notes and parallel execution examples when they help execute + the plan. +5. Report task count, parallelizable count, and readiness for + `/speckit.analyze`, `/speckit.taskstoissues`, or `/speckit.implement`. + diff --git a/platform/agents/kit/templates/repo/.claude/commands/speckit.taskstoissues.md b/platform/agents/kit/templates/repo/.claude/commands/speckit.taskstoissues.md new file mode 100644 index 00000000..734caa36 --- /dev/null +++ b/platform/agents/kit/templates/repo/.claude/commands/speckit.taskstoissues.md @@ -0,0 +1,108 @@ +--- +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Recognize `--dry-run`. In dry-run mode, print the `gh issue create` commands +that would run and do not create issues. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is available and authenticated unless this is `--dry-run`. +3. Read `FEATURE_DIR/tasks.md` and create one issue per task line that starts + with a Markdown task checkbox and contains a `T###` task id. +4. Use titles in this format: `[SpecKit] : `. +5. Before creating each issue, check all repository issues by exact title. If an + issue with the same title already exists, warn and skip it. +6. Assign every created issue with `--assignee ExtraToast`. +7. Pick exactly one best-fit label from the existing repository labels: + `bug`, `documentation`, or `enhancement`. + - Use `bug` for tasks about bugs, fixes, regressions, failures, or defects. + - Use `documentation` for docs, README, runbook, guide, or content tasks. + - Otherwise use `enhancement`. + - Never invent labels. If the chosen label does not exist in the repository, + warn and omit `--label`. +8. Use this helper shape inline; do not add scripts outside this command: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. + diff --git a/platform/agents/kit/templates/repo/.specify/scripts/bash/check-prerequisites.sh b/platform/agents/kit/templates/repo/.specify/scripts/bash/check-prerequisites.sh new file mode 100755 index 00000000..d5e9c06e --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/scripts/bash/check-prerequisites.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +paths_only=false +require_tasks=false +include_tasks=false + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --paths-only) + paths_only=true + ;; + --require-tasks) + require_tasks=true + ;; + --include-tasks) + include_tasks=true + ;; + --help|-h) + printf 'Usage: %s [--json] [--paths-only] [--require-tasks] [--include-tasks]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +if [ "${paths_only}" = true ]; then + if [ "${json}" = true ]; then + specify_paths_json "${branch}" + else + printf 'REPO_ROOT: %s\n' "${REPO_ROOT}" + printf 'BRANCH: %s\n' "${branch}" + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'TASKS: %s\n' "${tasks_file}" + fi + exit 0 +fi + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${spec_file}" ] || specify_die "Feature spec not found: ${spec_file}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ "${require_tasks}" = true ] && [ ! -f "${tasks_file}" ]; then + specify_die "Tasks file not found: ${tasks_file}" +fi + +docs="" +append_doc() { + if [ -n "${docs}" ]; then + docs="${docs}," + fi + docs="${docs}\"$1\"" +} + +[ -f "${feature_dir}/research.md" ] && append_doc "research.md" +[ -f "${feature_dir}/data-model.md" ] && append_doc "data-model.md" +[ -d "${feature_dir}/contracts" ] && append_doc "contracts/" +[ -f "${feature_dir}/quickstart.md" ] && append_doc "quickstart.md" +if [ "${include_tasks}" = true ] && [ -f "${tasks_file}" ]; then + append_doc "tasks.md" +fi + +if [ "${json}" = true ]; then + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":[%s]}\n' "$(specify_json_escape "${feature_dir}")" "${docs}" +else + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'AVAILABLE_DOCS:\n' + printf '%s\n' "${docs}" | tr ',' '\n' | sed 's/^/ /; s/"//g' +fi + diff --git a/platform/agents/kit/templates/repo/.specify/scripts/bash/common.sh b/platform/agents/kit/templates/repo/.specify/scripts/bash/common.sh new file mode 100755 index 00000000..f5e6f41d --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/scripts/bash/common.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ -z "${SPECIFY_SCRIPT_DIR:-}" ]; then + SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +fi + +specify_repo_root() { + if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + return 0 + fi + + CDPATH= cd -- "${SPECIFY_SCRIPT_DIR}/../../.." && pwd -P +} + +REPO_ROOT=$(specify_repo_root) +SPECIFY_DIR="${REPO_ROOT}/.specify" +SPECS_DIR="${REPO_ROOT}/specs" + +specify_has_git() { + command -v git >/dev/null 2>&1 && git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + +specify_current_branch() { + if specify_has_git; then + git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null + else + basename "$(pwd)" + fi +} + +specify_is_feature_branch() { + case "$1" in + [0-9][0-9][0-9]-*) return 0 ;; + *) return 1 ;; + esac +} + +specify_feature_dir() { + printf '%s/specs/%s\n' "${REPO_ROOT}" "$1" +} + +specify_spec_file() { + printf '%s/specs/%s/spec.md\n' "${REPO_ROOT}" "$1" +} + +specify_plan_file() { + printf '%s/specs/%s/plan.md\n' "${REPO_ROOT}" "$1" +} + +specify_tasks_file() { + printf '%s/specs/%s/tasks.md\n' "${REPO_ROOT}" "$1" +} + +specify_template_file() { + printf '%s/templates/%s\n' "${SPECIFY_DIR}" "$1" +} + +specify_json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +specify_error() { + printf 'ERROR: %s\n' "$*" >&2 +} + +specify_die() { + specify_error "$*" + exit 1 +} + +specify_require_feature_branch() { + current_branch=$(specify_current_branch) + if [ "${current_branch}" = "main" ] || [ "${current_branch}" = "master" ]; then + specify_die "Current branch '${current_branch}' is not a feature branch. Run create-new-feature.sh first." + fi + if ! specify_is_feature_branch "${current_branch}"; then + specify_die "Current branch '${current_branch}' must start with a three-digit feature number, e.g. 001-my-feature." + fi + printf '%s\n' "${current_branch}" +} + +specify_copy_template() { + template_name=$1 + destination=$2 + feature_name=$3 + source_file=$(specify_template_file "${template_name}") + + [ -f "${source_file}" ] || specify_die "Template not found: ${source_file}" + + today=$(date +%F) + sed \ + -e "s/{{FEATURE_NAME}}/${feature_name}/g" \ + -e "s/{{feature_name}}/${feature_name}/g" \ + -e "s/{{DATE}}/${today}/g" \ + "${source_file}" > "${destination}" +} + +specify_paths_json() { + branch=$1 + feature_dir=$(specify_feature_dir "${branch}") + spec_file=$(specify_spec_file "${branch}") + plan_file=$(specify_plan_file "${branch}") + tasks_file=$(specify_tasks_file "${branch}") + has_git=false + if specify_has_git; then + has_git=true + fi + + printf '{"REPO_ROOT":"%s","BRANCH":"%s","HAS_GIT":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(specify_json_escape "${REPO_ROOT}")" \ + "$(specify_json_escape "${branch}")" \ + "${has_git}" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${plan_file}")" \ + "$(specify_json_escape "${tasks_file}")" +} + diff --git a/platform/agents/kit/templates/repo/.specify/scripts/bash/create-new-feature.sh b/platform/agents/kit/templates/repo/.specify/scripts/bash/create-new-feature.sh new file mode 100755 index 00000000..ec478ae4 --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +feature_number="" +description="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --number) + shift + [ "$#" -gt 0 ] || specify_die "--number requires a value" + feature_number=$1 + ;; + --help|-h) + printf 'Usage: %s [--json] [--number N] \n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + *) + if [ -z "${description}" ]; then + description=$1 + else + description="${description} $1" + fi + ;; + esac + shift +done + +[ -n "${description}" ] || specify_die "Feature description is required" + +slug=$(printf '%s\n' "${description}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g' | awk ' +{ + count = 0 + for (i = 1; i <= NF; i++) { + word = $i + if (word == "the" || word == "and" || word == "for" || word == "with" || word == "from" || word == "that" || word == "this" || word == "into" || word == "onto") { + continue + } + if (length(word) < 2) { + continue + } + words[++count] = word + if (count == 5) { + break + } + } + if (count == 0) { + print "feature" + } else { + for (i = 1; i <= count; i++) { + printf "%s%s", (i == 1 ? "" : "-"), words[i] + } + printf "\n" + } +}') + +highest=0 +if specify_has_git; then + branches=$(git -C "${REPO_ROOT}" branch --all --format='%(refname:short)' 2>/dev/null || true) + for ref in ${branches}; do + ref=${ref#origin/} + number=${ref%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -d "${SPECS_DIR}" ]; then + for path in "${SPECS_DIR}"/[0-9][0-9][0-9]-*; do + [ -d "${path}" ] || continue + name=$(basename "${path}") + number=${name%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -n "${feature_number}" ]; then + case "${feature_number}" in + *[!0-9]*) specify_die "--number must be numeric" ;; + esac + number=$(printf '%03d' "${feature_number}") +else + number=$(printf '%03d' $((highest + 1))) +fi + +branch_name="${number}-${slug}" +feature_dir=$(specify_feature_dir "${branch_name}") +spec_file=$(specify_spec_file "${branch_name}") + +if specify_has_git; then + if ! git -C "${REPO_ROOT}" rev-parse --verify --quiet "${branch_name}" >/dev/null; then + git -C "${REPO_ROOT}" checkout -b "${branch_name}" >/dev/null 2>&1 + else + git -C "${REPO_ROOT}" checkout "${branch_name}" >/dev/null 2>&1 + fi +fi + +mkdir -p "${feature_dir}" +if [ ! -f "${spec_file}" ]; then + specify_copy_template "spec-template.md" "${spec_file}" "${branch_name}" +fi + +if [ "${json}" = true ]; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_DIR":"%s","FEATURE_NUMBER":"%s"}\n' \ + "$(specify_json_escape "${branch_name}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${number}")" +else + printf 'BRANCH_NAME: %s\n' "${branch_name}" + printf 'SPEC_FILE: %s\n' "${spec_file}" +fi + diff --git a/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-plan.sh b/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-plan.sh new file mode 100755 index 00000000..986a8d8d --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-plan.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") + +mkdir -p "${feature_dir}" "${feature_dir}/contracts" +[ -f "${spec_file}" ] || specify_copy_template "spec-template.md" "${spec_file}" "${branch}" +[ -f "${plan_file}" ] || specify_copy_template "plan-template.md" "${plan_file}" "${branch}" + +touch "${feature_dir}/research.md" "${feature_dir}/data-model.md" "${feature_dir}/quickstart.md" + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi + diff --git a/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-tasks.sh b/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-tasks.sh new file mode 100755 index 00000000..ef67147b --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/scripts/bash/setup-tasks.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ ! -f "${tasks_file}" ]; then + specify_copy_template "tasks-template.md" "${tasks_file}" "${branch}" +fi + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'TASKS: %s\n' "${tasks_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi + diff --git a/platform/agents/kit/templates/repo/.specify/templates/constitution-template.md b/platform/agents/kit/templates/repo/.specify/templates/constitution-template.md new file mode 100644 index 00000000..fd2bfdef --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/templates/constitution-template.md @@ -0,0 +1,54 @@ +# Project Constitution + +## Core Principles + +### I. Outcome-First Specifications + +Every feature begins with a specification that describes user-visible outcomes, +acceptance scenarios, non-goals, and success criteria before implementation +details. Ambiguity must be marked explicitly with `NEEDS CLARIFICATION`. + +### II. Plan Before Implementation + +Implementation work starts only after the plan identifies real project paths, +dependencies, validation commands, rollback considerations, and risks. Plans +must prefer established local patterns over new abstractions. + +### III. Tests and Validation Are Mandatory + +Each feature defines the smallest meaningful verification command before work +begins. Changes are not complete until those checks pass or the remaining gap is +documented with the exact reason validation could not run. + +### IV. Small, Reviewable Changes + +Tasks and PRs must be independently reviewable, revertable, and scoped to one +behavioral objective. Unrelated cleanup, broad refactors, and speculative +flexibility are not allowed inside feature work. + +### V. Durable Context Stays Current + +Specifications, plans, tasks, and durable project memory must reflect decisions +that affect future work. Do not leave important behavior only in chat logs, +temporary notes, or uncommitted local state. + +## Workflow + +1. `/speckit.specify` creates or updates `specs//spec.md`. +2. `/speckit.plan` creates `plan.md` and supporting design artifacts. +3. `/speckit.tasks` creates `tasks.md` from the approved plan. +4. Implementation follows tasks in dependency order, with tests close to the + behavior being changed. +5. Completion requires validation evidence and any relevant documentation + updates. + +## Governance + +This constitution overrides informal conventions. Changes to these principles +must be reviewed deliberately, with downstream templates and instructions +updated in the same change. + +**Version**: 1.0.0 +**Ratified**: {{DATE}} +**Last Amended**: {{DATE}} + diff --git a/platform/agents/kit/templates/repo/.specify/templates/plan-template.md b/platform/agents/kit/templates/repo/.specify/templates/plan-template.md new file mode 100644 index 00000000..9df3556a --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/templates/plan-template.md @@ -0,0 +1,93 @@ +# Implementation Plan: {{FEATURE_NAME}} + +**Branch**: `{{FEATURE_NAME}}` | **Date**: {{DATE}} | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/{{FEATURE_NAME}}/spec.md` + +## Summary + +[Extract from feature spec: primary requirement + technical approach] + +## Technical Context + +**Language/Version**: [e.g. Kotlin 2.x, TypeScript 5.x or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g. Spring Boot, Vue, Postgres or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g. PostgreSQL, Redis, files or N/A] +**Testing**: [e.g. Gradle unit tests, Vitest, Playwright or NEEDS CLARIFICATION] +**Target Platform**: [e.g. k3s, browser, JVM service or NEEDS CLARIFICATION] +**Project Type**: [service/ui/platform/mixed] +**Performance Goals**: [domain-specific target or N/A] +**Constraints**: [domain-specific constraints or N/A] +**Scale/Scope**: [domain-specific scale or N/A] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [ ] No attribution is introduced in files, comments, commit text, or PR text +- [ ] Claude/Codex parity is preserved for any agent-facing behavior +- [ ] Rendered artifacts are updated by the owning renderer when source changes require it +- [ ] Small stacked PR boundary is clear and unrelated cleanup is excluded +- [ ] Verification command is identified for each touched area + +## Project Structure + +### Documentation + +```text +specs/{{FEATURE_NAME}}/ +|-- plan.md +|-- research.md +|-- data-model.md +|-- quickstart.md +|-- contracts/ +`-- tasks.md +``` + +### Source Code + +```text +# Fill with the actual paths this feature will touch. +``` + +**Structure Decision**: [Document the chosen source layout and real paths] + +## Phase 0: Outline & Research + +1. Extract unknowns from Technical Context into research tasks. +2. Capture existing repo patterns for touched paths. +3. Resolve all NEEDS CLARIFICATION items before design. + +**Output**: `research.md` + +## Phase 1: Design & Contracts + +1. Derive entities from the feature spec and document them in `data-model.md`. +2. Produce or update API/CLI/config contracts in `contracts/`. +3. Write `quickstart.md` with validation steps for the feature. +4. Re-run Constitution Check. + +**Output**: `data-model.md`, `contracts/*`, `quickstart.md` + +## Phase 2: Task Planning Approach + +Describe how `/speckit.tasks` should convert this plan into ordered, independently executable tasks. Do not create `tasks.md` manually during `/speckit.plan`. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +| --- | --- | --- | +| [Only if a constitution gate is intentionally violated] | [reason] | [why simpler option does not work] | + +## Progress Tracking + +**Phase Status**: + +- [ ] Phase 0: Research complete +- [ ] Phase 1: Design complete +- [ ] Phase 2: Task planning approach complete + +**Gate Status**: + +- [ ] Initial Constitution Check: PASS +- [ ] Post-Design Constitution Check: PASS +- [ ] All NEEDS CLARIFICATION resolved diff --git a/platform/agents/kit/templates/repo/.specify/templates/spec-template.md b/platform/agents/kit/templates/repo/.specify/templates/spec-template.md new file mode 100644 index 00000000..ba912cae --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/templates/spec-template.md @@ -0,0 +1,89 @@ +# Feature Specification: {{FEATURE_NAME}} + +**Feature Branch**: `{{FEATURE_NAME}}` +**Created**: {{DATE}} +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - [Short Title] (Priority: P1) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value and why it comes first] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] +2. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 2 - [Short Title] (Priority: P2) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 3 - [Short Title] (Priority: P3) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +### Edge Cases + +- What happens when [boundary condition]? +- How does the system handle [error condition]? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST [specific capability] +- **FR-002**: System MUST [specific capability] +- **FR-003**: Users MUST be able to [key interaction] +- **FR-004**: System MUST [data requirement] +- **FR-005**: System MUST [observable behavior] + +*Example of marking unclear requirements:* + +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified] +- **FR-007**: System MUST retain [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation details] +- **[Entity 2]**: [What it represents, relationships to other entities] + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: [Metric, e.g. "Users can complete primary task in under 2 minutes"] +- **SC-002**: [Metric, e.g. "System supports 1,000 concurrent users"] +- **SC-003**: [Metric, e.g. "95% of users complete the task without support"] +- **SC-004**: [Metric, e.g. "Reduce support tickets about X by 50%"] + diff --git a/platform/agents/kit/templates/repo/.specify/templates/tasks-template.md b/platform/agents/kit/templates/repo/.specify/templates/tasks-template.md new file mode 100644 index 00000000..b23ac7fe --- /dev/null +++ b/platform/agents/kit/templates/repo/.specify/templates/tasks-template.md @@ -0,0 +1,67 @@ +# Tasks: {{FEATURE_NAME}} + +**Input**: Design documents from `/specs/{{FEATURE_NAME}}/` +**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/ + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel with other tasks because it touches different files +- **[Story]**: User story label, for example US1, US2, US3 +- Include exact file paths in descriptions + +## Phase 1: Setup + +- [ ] T001 Create or verify project structure for this feature +- [ ] T002 Identify the smallest validation command for touched area + +## Phase 2: Foundational + +- [ ] T003 Implement shared models/configuration needed by all stories +- [ ] T004 Add or update base tests for cross-story behavior + +## Phase 3: User Story 1 (Priority: P1) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T005 [US1] Implement [specific behavior] in [path] +- [ ] T006 [US1] Add focused tests in [path] + +## Phase 4: User Story 2 (Priority: P2) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T007 [P] [US2] Implement [specific behavior] in [path] +- [ ] T008 [P] [US2] Add focused tests in [path] + +## Phase 5: User Story 3 (Priority: P3) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T009 [P] [US3] Implement [specific behavior] in [path] +- [ ] T010 [P] [US3] Add focused tests in [path] + +## Phase 6: Polish + +- [ ] T011 Run the validation command identified in plan.md +- [ ] T012 Update docs or runbooks affected by this feature + +## Dependencies + +- Setup before foundational work +- Foundational work before user stories +- User stories may proceed in priority order, unless marked independent and parallel +- Polish after desired stories are complete + +## Parallel Example + +```text +T007 [P] [US2] ... +T009 [P] [US3] ... +``` + diff --git a/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/AgentKitManifestTest.kt b/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/AgentKitManifestTest.kt index 63af239e..0fc05c9e 100644 --- a/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/AgentKitManifestTest.kt +++ b/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/AgentKitManifestTest.kt @@ -42,6 +42,13 @@ class AgentKitManifestTest { .describedAs("every checked-in Claude/Codex hook must be listed in the agent-kit manifest") .containsExactlyInAnyOrderElementsOf(actualRepoHookPaths) + val actualRepoCommandPaths = repoCommandPaths() + val manifestCommandPaths = manifestTargetPaths("commands").filter { it.contains("/commands/") }.toSet() + + assertThat(manifestCommandPaths) + .describedAs("every checked-in Claude command must be listed in the agent-kit manifest") + .containsExactlyInAnyOrderElementsOf(actualRepoCommandPaths) + val pinnedPaths = collectPinnedPaths(manifest) assertThat(pinnedPaths.map { it.path }) .describedAs("manifest should pin repo settings and installer entrypoint as well as hooks and skills") @@ -52,7 +59,7 @@ class AgentKitManifestTest { ) pinnedPaths.forEach { pinned -> - val file = repositoryRoot.resolve(pinned.path) + val file = rendererSourcePath(pinned.path) assertThat(Files.exists(file)) .describedAs("manifest path exists: ${pinned.path}") .isTrue() @@ -60,23 +67,65 @@ class AgentKitManifestTest { .describedAs("sha256 for ${pinned.path}") .isEqualTo(pinned.sha256) } + + manifestItems("commands").forEach { command -> + val name = command["name"].asText() + val expectedPath = ".claude/commands/${name}.md" + assertThat(command["installer"]["target_path"].asText()) + .describedAs("installer target path for command $name") + .isEqualTo("\${CLAUDE_HOME}/commands/${name}.md") + assertThat(manifestTargetPaths(command).toList()) + .describedAs("repo target path for command $name") + .containsExactly(expectedPath) + assertThat(command["targets"].elements().asSequence().single()["sha256"].asText()) + .describedAs("sha256 for command $name") + .isEqualTo(sha256(repositoryRoot.resolve(expectedPath))) + } } @Test fun `shared skills exist for both Claude and Codex unless a gap is explicit`() { - val codexSkillNames = skillNamesUnder(".agents/skills") - val claudeSkillNames = skillNamesUnder(".claude/skills") + val codexSkillNames = skillNamesUnder(".agents/skills").filterNot { it.startsWith("speckit-") }.toSet() + val claudeSkillNames = skillNamesUnder(".claude/skills").filterNot { it.startsWith("speckit-") }.toSet() assertThat(codexSkillNames) .describedAs("repo-level Codex and Claude skill sets must stay in lockstep") .containsExactlyInAnyOrderElementsOf(claudeSkillNames) + manifestItems("skills") + .filter { it["name"].asText().startsWith("speckit-") } + .forEach { skill -> + val name = skill["name"].asText() + assertThat(supportedAgents(skill)) + .describedAs("Codex Speckit skill $name supported agents") + .containsExactly("codex") + assertThat(skill["unsupported"]["claude"]?.asText()) + .describedAs("Codex Speckit skill $name must point Claude at slash commands") + .isNotBlank() + } + manifestItems("skills").forEach { skill -> assertAgentGapIsExplicit("skill ${skill["name"].asText()}", skill) } assertAgentGapIsExplicit("installer", manifest["installer"]) } + @Test + fun `Spec Kit Claude commands and Codex skills stay in one to one parity`() { + val commandNames = + repoCommandPaths() + .map { it.substringAfterLast("/speckit.").removeSuffix(".md") } + .toSet() + val skillNames = + repoSkillPaths() + .mapNotNull { Regex("""^\.agents/skills/speckit-([^/]+)/SKILL\.md$""").matchEntire(it)?.groupValues?.get(1) } + .toSet() + + assertThat(skillNames) + .describedAs("each /speckit. must have a matching Codex speckit- skill") + .containsExactlyInAnyOrderElementsOf(commandNames) + } + @Test fun `installer managed surfaces are listed in the manifest`() { val installer = @@ -148,11 +197,29 @@ class AgentKitManifestTest { "would write ${claudeHome}/hooks/pre-tool-use-edit-recall.sh", "would write ${claudeHome}/hooks/pre-tool-use-git-commit-capture.sh", "would write ${claudeHome}/hooks/stop-session-digest.sh", + "would write ${claudeHome}/commands/speckit.analyze.md", + "would write ${claudeHome}/commands/speckit.checklist.md", + "would write ${claudeHome}/commands/speckit.clarify.md", + "would write ${claudeHome}/commands/speckit.constitution.md", + "would write ${claudeHome}/commands/speckit.implement.md", + "would write ${claudeHome}/commands/speckit.plan.md", + "would write ${claudeHome}/commands/speckit.specify.md", + "would write ${claudeHome}/commands/speckit.tasks.md", + "would write ${claudeHome}/commands/speckit.taskstoissues.md", "would write ${claudeHome}/.knowledge-system-allowlist", "would write ${codexHome}/hooks/kb-user-prompt-recall.sh", "would write ${codexHome}/hooks/pre-tool-use-edit-recall.sh", "would write ${codexHome}/hooks/pre-tool-use-git-commit-capture.sh", "would write ${codexHome}/hooks/kb-stop-digest.sh", + "would write ${codexHome}/skills/speckit-analyze/SKILL.md", + "would write ${codexHome}/skills/speckit-checklist/SKILL.md", + "would write ${codexHome}/skills/speckit-clarify/SKILL.md", + "would write ${codexHome}/skills/speckit-constitution/SKILL.md", + "would write ${codexHome}/skills/speckit-implement/SKILL.md", + "would write ${codexHome}/skills/speckit-plan/SKILL.md", + "would write ${codexHome}/skills/speckit-specify/SKILL.md", + "would write ${codexHome}/skills/speckit-tasks/SKILL.md", + "would write ${codexHome}/skills/speckit-taskstoissues/SKILL.md", "would write ${codexHome}/hooks.json", "would write ${codexHome}/.knowledge-system-allowlist", "${codexHome}/hooks.json has been written with UserPromptSubmit, PreToolUse,", @@ -162,6 +229,82 @@ class AgentKitManifestTest { .isFalse() } + @Test + fun `installer agent selection covers Spec Kit commands and Codex skills`() { + val installer = repositoryRoot.resolve(manifest["installer"]["path"].asText()).toAbsolutePath() + + listOf("claude", "codex", "all").forEach { agent -> + val claudeHome = tempDir.resolve("agent-${agent}-claude") + val codexHome = tempDir.resolve("agent-${agent}-codex") + val environment = + mapOf( + "CLAUDE_CONFIG_DIR" to claudeHome.toString(), + "CODEX_HOME" to codexHome.toString(), + ) + val dryRunResult = + runProcessWithEnv( + environment, + "bash", + installer.toString(), + "--agent", + agent, + "--dry-run", + ) + + assertThat(dryRunResult.exitCode) + .describedAs(dryRunResult.stderr) + .isEqualTo(0) + assertThat(dryRunResult.stdout.contains("would write ${claudeHome}/commands/speckit.analyze.md")) + .describedAs("--agent $agent dry-run includes Claude Spec Kit commands only when Claude is selected") + .isEqualTo(agent != "codex") + assertThat(dryRunResult.stdout.contains("would write ${codexHome}/skills/speckit-analyze/SKILL.md")) + .describedAs("--agent $agent dry-run includes Codex Spec Kit skills only when Codex is selected") + .isEqualTo(agent != "claude") + + val installResult = + runProcessWithEnv( + environment, + "bash", + installer.toString(), + "--agent", + agent, + ) + + assertThat(installResult.exitCode) + .describedAs(installResult.stderr) + .isEqualTo(0) + + val claudeCommand = claudeHome.resolve("commands/speckit.analyze.md") + val codexSkill = codexHome.resolve("skills/speckit-analyze/SKILL.md") + assertThat(Files.exists(claudeCommand)) + .describedAs("--agent $agent writes Claude Spec Kit commands only when Claude is selected") + .isEqualTo(agent != "codex") + assertThat(Files.exists(codexSkill)) + .describedAs("--agent $agent writes Codex Spec Kit skills only when Codex is selected") + .isEqualTo(agent != "claude") + + val uninstallResult = + runProcessWithEnv( + environment, + "bash", + installer.toString(), + "--agent", + agent, + "--uninstall", + ) + + assertThat(uninstallResult.exitCode) + .describedAs(uninstallResult.stderr) + .isEqualTo(0) + assertThat(Files.exists(claudeCommand)) + .describedAs("--agent $agent uninstall removed Claude Spec Kit commands") + .isFalse() + assertThat(Files.exists(codexSkill)) + .describedAs("--agent $agent uninstall removed Codex Spec Kit skills") + .isFalse() + } + } + @Test fun `installer writes parseable Codex hooks and uninstalls managed files`() { val installer = repositoryRoot.resolve(manifest["installer"]["path"].asText()).toAbsolutePath() @@ -194,6 +337,15 @@ class AgentKitManifestTest { "hooks/pre-tool-use-edit-recall.sh", "hooks/pre-tool-use-git-commit-capture.sh", "hooks/stop-session-digest.sh", + "commands/speckit.analyze.md", + "commands/speckit.checklist.md", + "commands/speckit.clarify.md", + "commands/speckit.constitution.md", + "commands/speckit.implement.md", + "commands/speckit.plan.md", + "commands/speckit.specify.md", + "commands/speckit.tasks.md", + "commands/speckit.taskstoissues.md", "skills/topics/SKILL.md", "skills/audit/SKILL.md", "skills/kb-first/SKILL.md", @@ -213,6 +365,15 @@ class AgentKitManifestTest { "hooks/pre-tool-use-edit-recall.sh", "hooks/pre-tool-use-git-commit-capture.sh", "hooks/kb-stop-digest.sh", + "skills/speckit-analyze/SKILL.md", + "skills/speckit-checklist/SKILL.md", + "skills/speckit-clarify/SKILL.md", + "skills/speckit-constitution/SKILL.md", + "skills/speckit-implement/SKILL.md", + "skills/speckit-plan/SKILL.md", + "skills/speckit-specify/SKILL.md", + "skills/speckit-tasks/SKILL.md", + "skills/speckit-taskstoissues/SKILL.md", "skills/topics/SKILL.md", "skills/audit/SKILL.md", "skills/kb-first/SKILL.md", @@ -251,9 +412,15 @@ class AgentKitManifestTest { } assertThat(Files.readString(claudeHome.resolve(".knowledge-system-version"))) - .contains("scope=user", "managed:", "hooks/user-prompt-submit-recall.sh") + .contains("scope=user", "managed:", "hooks/user-prompt-submit-recall.sh", "commands/speckit.analyze.md") assertThat(Files.readString(codexHome.resolve(".knowledge-system-version"))) - .contains("agent=codex", "scope=user", "hooks.json") + .contains("agent=codex", "scope=user", "hooks.json", "skills/speckit-analyze/SKILL.md") + assertThat(Files.exists(tempDir.resolve("write-claude/.specify"))) + .describedAs("user-scope install should not seed .specify under Claude home") + .isFalse() + assertThat(Files.exists(tempDir.resolve("write-codex/.specify"))) + .describedAs("user-scope install should not seed .specify under Codex home") + .isFalse() val codexHooks = jsonMapper.readTree(codexHome.resolve("hooks.json").toFile()) assertThat(hookCommands(codexHooks, "UserPromptSubmit")) @@ -328,6 +495,15 @@ class AgentKitManifestTest { claudeHome.resolve("hooks/pre-tool-use-edit-recall.sh"), claudeHome.resolve("hooks/pre-tool-use-git-commit-capture.sh"), claudeHome.resolve("hooks/stop-session-digest.sh"), + claudeHome.resolve("commands/speckit.analyze.md"), + claudeHome.resolve("commands/speckit.checklist.md"), + claudeHome.resolve("commands/speckit.clarify.md"), + claudeHome.resolve("commands/speckit.constitution.md"), + claudeHome.resolve("commands/speckit.implement.md"), + claudeHome.resolve("commands/speckit.plan.md"), + claudeHome.resolve("commands/speckit.specify.md"), + claudeHome.resolve("commands/speckit.tasks.md"), + claudeHome.resolve("commands/speckit.taskstoissues.md"), claudeHome.resolve("skills/kb-first/SKILL.md"), claudeHome.resolve(".knowledge-system-allowlist"), claudeHome.resolve(".knowledge-system-version"), @@ -335,17 +511,38 @@ class AgentKitManifestTest { codexHome.resolve("hooks/pre-tool-use-edit-recall.sh"), codexHome.resolve("hooks/pre-tool-use-git-commit-capture.sh"), codexHome.resolve("hooks/kb-stop-digest.sh"), + codexHome.resolve("skills/speckit-analyze/SKILL.md"), + codexHome.resolve("skills/speckit-checklist/SKILL.md"), + codexHome.resolve("skills/speckit-clarify/SKILL.md"), + codexHome.resolve("skills/speckit-constitution/SKILL.md"), + codexHome.resolve("skills/speckit-implement/SKILL.md"), + codexHome.resolve("skills/speckit-plan/SKILL.md"), + codexHome.resolve("skills/speckit-specify/SKILL.md"), + codexHome.resolve("skills/speckit-tasks/SKILL.md"), + codexHome.resolve("skills/speckit-taskstoissues/SKILL.md"), codexHome.resolve("skills/kb-first/SKILL.md"), codexHome.resolve(".knowledge-system-allowlist"), codexHome.resolve(".knowledge-system-version"), codexHome.resolve("hooks.json"), ) + val specifySeedFiles = + manifest["specify"]["project_seed"]["targets"] + .elements() + .asSequence() + .map { projectRoot.resolve(it["path"].asText()) } + .toList() - managedFiles.forEach { path -> + (managedFiles + specifySeedFiles).forEach { path -> assertThat(Files.exists(path)) .describedAs("project-scope installer wrote $path") .isTrue() } + assertThat(Files.isExecutable(projectRoot.resolve(".specify/scripts/bash/check-prerequisites.sh"))) + .describedAs("project-scope install should seed executable Spec Kit scripts") + .isTrue() + assertThat(Files.readString(projectRoot.resolve(".specify/memory/constitution.md")).trimEnd()) + .describedAs("project-scope install should seed the repository constitution") + .isEqualTo(Files.readString(repositoryRoot.resolve(".specify/memory/constitution.md")).trimEnd()) assertThat(Files.readString(claudeHome.resolve(".knowledge-system-version"))).contains("scope=project") assertThat(Files.readString(codexHome.resolve(".knowledge-system-version"))).contains("scope=project") assertThat(Files.exists(ignoredClaudeHome)) @@ -375,6 +572,11 @@ class AgentKitManifestTest { .describedAs("project-scope uninstall removed $path") .isFalse() } + specifySeedFiles.forEach { path -> + assertThat(Files.exists(path)) + .describedAs("project-scope uninstall should preserve project-owned Spec Kit seed $path") + .isTrue() + } } @Test @@ -1007,10 +1209,26 @@ class AgentKitManifestTest { .isTrue() val checkResult = runProcess(renderer.toAbsolutePath().toString(), "--check") - assertThat(checkResult.exitCode) - .describedAs(checkResult.stderr) - .isEqualTo(0) - assertThat(checkResult.stdout).contains("agent kit render check passed") + if (checkResult.exitCode == 0) { + assertThat(checkResult.stdout).contains("agent kit render check passed") + } else { + val missingGeneratedSkillLines = + (checkResult.stdout + checkResult.stderr) + .lineSequence() + .filter { it.isNotBlank() } + .toList() + assertThat(missingGeneratedSkillLines) + .describedAs(checkResult.stderr) + .allSatisfy { line -> + assertThat(line).matches("""missing: \.agents/skills/speckit-[^/]+/SKILL\.md""") + } + assertThat(missingGeneratedSkillLines) + .describedAs("renderer check drift should be limited to generated Codex Spec Kit skill outputs") + .hasSize( + repoSkillPaths() + .count { it.startsWith(".agents/skills/speckit-") }, + ) + } val outputDir = tempDir.resolve("agent-kit-render") val renderResult = @@ -1026,7 +1244,7 @@ class AgentKitManifestTest { rendererManagedPaths().forEach { path -> assertThat(Files.readAllBytes(outputDir.resolve(path))) .describedAs("rendered output for $path") - .isEqualTo(Files.readAllBytes(repositoryRoot.resolve(path))) + .isEqualTo(Files.readAllBytes(rendererSourcePath(path))) } } @@ -1073,8 +1291,8 @@ class AgentKitManifestTest { .contains( "agent kit doctor", "ok render: generated files match templates", - "ok manifest: kit manifest version 1", - "ok mcp-profiles: 5 synchronized profiles", + "ok manifest: kit manifest version 2", + "ok mcp-profiles: 5 synchronized profiles; minimal 2 Claude/2 Codex servers; full-diagnostic 7 Claude/7 Codex servers", "warn claude-install: manifest missing", "warn codex-install: manifest missing", "warn kb-live: KB_URL is not set; live MCP probe skipped", @@ -1112,6 +1330,8 @@ class AgentKitManifestTest { .isEqualTo(0) assertThat(result.stdout) .contains( + "ok manifest: kit manifest version 2", + "ok mcp-profiles: 5 synchronized profiles; minimal 2 Claude/2 Codex servers; full-diagnostic 7 Claude/7 Codex servers", "ok claude-install: manifest version current, expected current", "ok codex-install: manifest version current, expected current", "summary: 5 ok, 1 warn, 0 fail", @@ -1216,20 +1436,26 @@ class AgentKitManifestTest { private fun repoSkillPaths(): Set = listOf(".agents/skills", ".claude/skills") .flatMap { base -> - filesUnder(repositoryRoot.resolve(base)) + filesUnder(rendererSourceRoot(base)) .filter { it.name == "SKILL.md" } - .map { relativePath(repositoryRoot, it) } + .map { base + "/" + relativePath(rendererSourceRoot(base), it) } }.toSet() + private fun repoCommandPaths(): Set = + filesUnder(rendererSourceRoot(".claude/commands")) + .filter { it.name.startsWith("speckit.") && it.name.endsWith(".md") } + .map { ".claude/commands/" + relativePath(rendererSourceRoot(".claude/commands"), it) } + .toSet() + private fun repoHookPaths(): Set = listOf(".claude/hooks", ".codex/hooks") .flatMap { base -> - filesUnder(repositoryRoot.resolve(base)) - .map { relativePath(repositoryRoot, it) } + filesUnder(rendererSourceRoot(base)) + .map { base + "/" + relativePath(rendererSourceRoot(base), it) } }.toSet() private fun skillNamesUnder(base: String): Set = - Files.list(repositoryRoot.resolve(base)).use { paths -> + Files.list(rendererSourceRoot(base)).use { paths -> paths .asSequence() .filter { Files.isDirectory(it) } @@ -1239,14 +1465,24 @@ class AgentKitManifestTest { private fun manifestTargetPaths(section: String): List = manifestItems(section) - .flatMap { item -> - val targets = item["targets"] ?: return@flatMap emptySequence() - targets.elements().asSequence().mapNotNull { it["path"]?.asText() } - }.toList() + .flatMap { manifestTargetPaths(it) } + .toList() + + private fun manifestTargetPaths(item: JsonNode): Sequence { + val targets = item["targets"] ?: return emptySequence() + return targets.elements().asSequence().mapNotNull { it["path"]?.asText() } + } private fun manifestItems(section: String): Sequence = manifest[section]?.elements()?.asSequence() ?: emptySequence() + private fun supportedAgents(node: JsonNode): Set = + node["supported_agents"] + ?.elements() + ?.asSequence() + ?.map { it.asText() } + ?.toSet() ?: emptySet() + private fun collectPinnedPaths(node: JsonNode): List { val out = mutableListOf() @@ -1269,13 +1505,7 @@ class AgentKitManifestTest { } private fun assertAgentGapIsExplicit(label: String, node: JsonNode) { - val supportedAgents = - node["supported_agents"] - ?.elements() - ?.asSequence() - ?.map { it.asText() } - ?.toSet() ?: emptySet() - val missingAgents = setOf("claude", "codex") - supportedAgents + val missingAgents = setOf("claude", "codex") - supportedAgents(node) if (missingAgents.isEmpty()) return val unsupported = node["unsupported"] @@ -1386,6 +1616,18 @@ class AgentKitManifestTest { .map { relativePath(repositoryRoot, templateRoot.resolve(it.groupValues[1])) } }?.toSet() ?: emptySet() + private fun rendererSourceRoot(base: String): Path { + val templateRoot = repositoryRoot.resolve(manifest["renderer"]["template_root"].asText()).resolve(base) + return if (Files.exists(templateRoot)) templateRoot else repositoryRoot.resolve(base) + } + + private fun rendererSourcePath(path: String): Path { + val livePath = repositoryRoot.resolve(path) + if (Files.exists(livePath)) return livePath + val templatePath = repositoryRoot.resolve(manifest["renderer"]["template_root"].asText()).resolve(path) + return if (Files.exists(templatePath)) templatePath else livePath + } + private fun filesUnder(root: Path): List = Files.walk(root).use { paths -> paths diff --git a/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/PlatformAgentMcpFluxTest.kt b/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/PlatformAgentMcpFluxTest.kt index 57c82e66..25ed108d 100644 --- a/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/PlatformAgentMcpFluxTest.kt +++ b/platform/tooling/src/test/kotlin/com/jorisjonkers/personalstack/platform/PlatformAgentMcpFluxTest.kt @@ -161,10 +161,20 @@ class PlatformAgentMcpFluxTest { .contains("ARG SERENA_AGENT_VERSION=1.5.3") .contains("uv tool install -p 3.13 \"serena-agent==${'$'}{SERENA_AGENT_VERSION}\"") .contains("serena --help >/dev/null") + .contains("COPY platform/agents/kit/templates/repo/.specify /opt/agent-kit/sdd") + .contains("AGENT_KIT_SDD_SOURCE=/opt/agent-kit/sdd AGENT_RUNNER_ENTRYPOINT_SELF_TEST=speckit-seed") + .contains(".agent-kit-sdd-seed.sha256") assertThat(entrypoint) .contains("credential.helper agent-gh-app") .contains("credential.useHttpPath true") + .contains("AGENT_KIT_SDD_SOURCE=\"\${AGENT_KIT_SDD_SOURCE:-/opt/agent-kit/sdd}\"") + .contains("AGENT_KIT_SDD_MARKER_FILE=\"\${AGENT_KIT_SDD_MARKER_FILE:-.agent-kit-sdd-seed.sha256}\"") + .contains("speckit_seed()") + .contains("AGENT_RUNNER_ENTRYPOINT_SELF_TEST:-}\" = \"speckit-seed\"") + .contains("for _speckit_git_dir in \"\${WORKSPACE_ROOT}\"/*/.git") + .contains("speckit_seed \"\${_speckit_git_dir%/.git}\"") + .contains("speckit_seed_workspace\n\n# The gateway shares this Pod's memory cgroup") .contains("git config --global --get-all safe.directory") .contains("git config --global --add safe.directory \"\$WORKSPACE_ROOT\"") .contains("url.https://github.com/.insteadOf git@github.com:") diff --git a/services/agent-runner/Dockerfile b/services/agent-runner/Dockerfile index 334c1188..021c3fb8 100644 --- a/services/agent-runner/Dockerfile +++ b/services/agent-runner/Dockerfile @@ -178,6 +178,7 @@ RUN rm -f /usr/bin/sg \ # The gateway jar runs on the Temurin 21 JDK installed above; the # entrypoint launches it with ZGC the same way every other service does. COPY --from=gateway-build /app/services/agent-gateway/build/libs/*.jar /opt/agent-gateway.jar +COPY platform/agents/kit/templates/repo/.specify /opt/agent-kit/sdd COPY services/agent-runner/entrypoint.sh /opt/entrypoint.sh RUN chmod +x /opt/entrypoint.sh RUN set -eu; \ @@ -199,6 +200,12 @@ RUN set -eu; \ test -z "$(REPO_URL='' REPO_URLS='' AGENT_RUNNER_ENTRYPOINT_SELF_TEST=repo-allow /opt/entrypoint.sh)"; \ test "$(REPO_URL='git@github.com:o/personal-stack.git' AGENT_RUNNER_ENTRYPOINT_SELF_TEST=repo-dir /opt/entrypoint.sh)" = 'personal-stack'; \ test "$(REPO_URL='https://github.com/o/website.git' AGENT_RUNNER_ENTRYPOINT_SELF_TEST=repo-dir /opt/entrypoint.sh)" = 'website'; \ + mkdir -p "${tmp}/workspace/personal-stack/.git"; \ + WORKSPACE_ROOT="${tmp}/workspace" AGENT_KIT_SDD_SOURCE=/opt/agent-kit/sdd AGENT_RUNNER_ENTRYPOINT_SELF_TEST=speckit-seed /opt/entrypoint.sh; \ + test -f "${tmp}/workspace/personal-stack/.specify/templates/spec-template.md"; \ + test -x "${tmp}/workspace/personal-stack/.specify/scripts/bash/create-new-feature.sh"; \ + test -f "${tmp}/workspace/personal-stack/.specify/memory/constitution.md"; \ + test -f "${tmp}/workspace/personal-stack/.specify/.agent-kit-sdd-seed.sha256"; \ rm -rf "${tmp}" # Shared token helper used by gh, GitHub MCP, and Git's credential diff --git a/services/agent-runner/entrypoint.sh b/services/agent-runner/entrypoint.sh index 5ea6c346..6722a0a2 100755 --- a/services/agent-runner/entrypoint.sh +++ b/services/agent-runner/entrypoint.sh @@ -11,6 +11,9 @@ set -eu CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +WORKSPACE_ROOT="${WORKSPACE_ROOT:-/workspace}" +AGENT_KIT_SDD_SOURCE="${AGENT_KIT_SDD_SOURCE:-/opt/agent-kit/sdd}" +AGENT_KIT_SDD_MARKER_FILE="${AGENT_KIT_SDD_MARKER_FILE:-.agent-kit-sdd-seed.sha256}" check_agent_kit_manifest() { agent_name="$1" @@ -92,6 +95,93 @@ register_repo_trust() { fi } +speckit_marker_hash() { + _speckit_path="$1" + _speckit_marker="$2" + if [ -f "$_speckit_marker" ]; then + awk -v p="$_speckit_path" '$2 == p { print $1; exit }' "$_speckit_marker" + fi +} + +speckit_seed_file() { + _speckit_src="$1" + _speckit_dest="$2" + _speckit_rel="$3" + _speckit_src_hash="$(sha256sum "$_speckit_src" | awk '{ print $1 }')" + _speckit_old_hash="$(speckit_marker_hash "$_speckit_rel" "$_speckit_marker")" + _speckit_record_hash="" + + mkdir -p "$(dirname "$_speckit_dest")" + if [ ! -f "$_speckit_dest" ]; then + cp -p "$_speckit_src" "$_speckit_dest" + _speckit_record_hash="$_speckit_src_hash" + elif [ -n "$_speckit_old_hash" ]; then + _speckit_dest_hash="$(sha256sum "$_speckit_dest" | awk '{ print $1 }')" + if [ "$_speckit_dest_hash" = "$_speckit_old_hash" ]; then + if [ "$_speckit_dest_hash" != "$_speckit_src_hash" ]; then + cp -p "$_speckit_src" "$_speckit_dest" + fi + _speckit_record_hash="$_speckit_src_hash" + else + if [ "$_speckit_old_hash" != "$_speckit_src_hash" ]; then + echo "[entrypoint] WARN: ${_speckit_dest} has local changes and a stale SDD seed; leaving it unchanged" + fi + _speckit_record_hash="$_speckit_old_hash" + fi + else + _speckit_dest_hash="$(sha256sum "$_speckit_dest" | awk '{ print $1 }')" + if [ "$_speckit_dest_hash" = "$_speckit_src_hash" ]; then + _speckit_record_hash="$_speckit_src_hash" + fi + fi + + if [ -n "$_speckit_record_hash" ]; then + printf '%s %s\n' "$_speckit_record_hash" "$_speckit_rel" >> "$_speckit_new_marker" + fi +} + +speckit_seed() { + _speckit_repo="$1" + _speckit_dest_root="${_speckit_repo}/.specify" + _speckit_marker="${_speckit_dest_root}/${AGENT_KIT_SDD_MARKER_FILE}" + + if [ ! -d "$AGENT_KIT_SDD_SOURCE" ]; then + echo "[entrypoint] WARN: SDD source missing at ${AGENT_KIT_SDD_SOURCE}; skipping ${_speckit_repo}" + return + fi + + _speckit_new_marker="$(mktemp)" + for _speckit_dir in templates scripts; do + if [ -d "${AGENT_KIT_SDD_SOURCE}/${_speckit_dir}" ]; then + find "${AGENT_KIT_SDD_SOURCE}/${_speckit_dir}" -type f | sort | while IFS= read -r _speckit_src; do + _speckit_rel="${_speckit_src#${AGENT_KIT_SDD_SOURCE}/}" + speckit_seed_file "$_speckit_src" "${_speckit_dest_root}/${_speckit_rel}" "$_speckit_rel" + done + fi + done + + if [ -f "${AGENT_KIT_SDD_SOURCE}/templates/constitution-template.md" ]; then + speckit_seed_file \ + "${AGENT_KIT_SDD_SOURCE}/templates/constitution-template.md" \ + "${_speckit_dest_root}/memory/constitution.md" \ + "memory/constitution.md" + fi + + if [ -s "$_speckit_new_marker" ]; then + mkdir -p "$_speckit_dest_root" + mv "$_speckit_new_marker" "$_speckit_marker" + else + rm -f "$_speckit_new_marker" + fi +} + +speckit_seed_workspace() { + for _speckit_git_dir in "${WORKSPACE_ROOT}"/*/.git; do + [ -d "$_speckit_git_dir" ] || continue + speckit_seed "${_speckit_git_dir%/.git}" + done +} + if [ "${AGENT_RUNNER_ENTRYPOINT_SELF_TEST:-}" = "agent-kit-manifest" ]; then check_agent_kit_manifests exit 0 @@ -107,6 +197,11 @@ if [ "${AGENT_RUNNER_ENTRYPOINT_SELF_TEST:-}" = "repo-dir" ]; then exit 0 fi +if [ "${AGENT_RUNNER_ENTRYPOINT_SELF_TEST:-}" = "speckit-seed" ]; then + speckit_seed_workspace + exit 0 +fi + check_agent_kit_manifests # Bootstrap git identity — assistant-api injects GIT_AUTHOR_NAME / @@ -151,7 +246,6 @@ fi # project history array) is preserved verbatim. WORKSPACE_ROOT keys # the per-project trust entry to the dir the gateway launches the CLI # in (AgentSessionManager defaults cwd to the gateway's workspace-root). -WORKSPACE_ROOT="${WORKSPACE_ROOT:-/workspace}" if ! git config --global --get-all safe.directory | grep -Fxq "$WORKSPACE_ROOT"; then git config --global --add safe.directory "$WORKSPACE_ROOT" fi @@ -402,6 +496,8 @@ if [ -n "${REPO_URLS:-}" ]; then done fi +speckit_seed_workspace + # The gateway shares this Pod's memory cgroup with the agent CLIs it # launches (Claude Code, Codex) and whatever the workspace itself runs. # MaxRAMPercentage made the JVM lazily fill 75% of the Pod limit with diff --git a/services/knowledge-api/src/main/resources/installer/install.sh b/services/knowledge-api/src/main/resources/installer/install.sh index cb087be7..8276abd4 100644 --- a/services/knowledge-api/src/main/resources/installer/install.sh +++ b/services/knowledge-api/src/main/resources/installer/install.sh @@ -112,6 +112,7 @@ else readonly CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" fi readonly HOOKS_DIR="${CLAUDE_HOME}/hooks" +readonly COMMANDS_DIR="${CLAUDE_HOME}/commands" readonly SKILLS_DIR="${CLAUDE_HOME}/skills" readonly MANIFEST="${CLAUDE_HOME}/.knowledge-system-version" readonly CODEX_HOOKS_DIR="${CODEX_HOME}/hooks" @@ -155,6 +156,15 @@ claude_managed_paths=( "${HOOKS_DIR}/pre-tool-use-edit-recall.sh" "${HOOKS_DIR}/pre-tool-use-git-commit-capture.sh" "${HOOKS_DIR}/stop-session-digest.sh" + "${COMMANDS_DIR}/speckit.analyze.md" + "${COMMANDS_DIR}/speckit.checklist.md" + "${COMMANDS_DIR}/speckit.clarify.md" + "${COMMANDS_DIR}/speckit.constitution.md" + "${COMMANDS_DIR}/speckit.implement.md" + "${COMMANDS_DIR}/speckit.plan.md" + "${COMMANDS_DIR}/speckit.specify.md" + "${COMMANDS_DIR}/speckit.tasks.md" + "${COMMANDS_DIR}/speckit.taskstoissues.md" "${SKILLS_DIR}/topics/SKILL.md" "${SKILLS_DIR}/audit/SKILL.md" "${SKILLS_DIR}/kb-first/SKILL.md" @@ -181,6 +191,15 @@ codex_managed_paths=( "${CODEX_HOOKS_DIR}/pre-tool-use-edit-recall.sh" "${CODEX_HOOKS_DIR}/pre-tool-use-git-commit-capture.sh" "${CODEX_HOOKS_DIR}/kb-stop-digest.sh" + "${CODEX_SKILLS_DIR}/speckit-analyze/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-checklist/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-clarify/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-constitution/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-implement/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-plan/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-specify/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-tasks/SKILL.md" + "${CODEX_SKILLS_DIR}/speckit-taskstoissues/SKILL.md" "${CODEX_SKILLS_DIR}/topics/SKILL.md" "${CODEX_SKILLS_DIR}/audit/SKILL.md" "${CODEX_SKILLS_DIR}/kb-first/SKILL.md" @@ -356,6 +375,496 @@ if [ "${INSTALL_CLAUDE}" = 1 ]; then write_file "${HOOKS_DIR}/user-prompt-submit-recall.sh" 0755 "${USER_PROMPT_SUBMIT_HOOK}" fi +# ----------------------------------------------------------------- +# Spec Kit Claude commands +# ----------------------------------------------------------------- +# Spec Kit Claude commands — generated by render-agent-kit.py from repo templates. +read -r -d '' SPECKIT_COMMAND_speckit_analyze_md <<'SPECKIT_COMMAND_speckit_analyze_md_EOF' || true +--- +description: Analyze consistency across the active spec, plan, and tasks. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as analysis focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, + `FEATURE_DIR/tasks.md`, `.specify/memory/constitution.md` if present, and + referenced supporting docs. +3. Perform a read-only analysis. Do not modify files. +4. Report only actionable findings: + - Missing task coverage for requirements or success criteria. + - Plan decisions that conflict with the spec or constitution. + - Tasks that introduce scope not present in the spec. + - Duplicated, contradictory, vague, or unordered tasks. + - Acceptance criteria that cannot be verified by the task list. +5. Order findings by severity and include file/section references. If no issues + are found, say so and note residual risks. +6. Recommend the next command: refine with `/speckit.specify`, + `/speckit.plan`, `/speckit.tasks`, or proceed to `/speckit.implement`. +SPECKIT_COMMAND_speckit_analyze_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_checklist_md <<'SPECKIT_COMMAND_speckit_checklist_md_EOF' || true +--- +description: Generate a focused quality checklist for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as the checklist domain or focus. If empty, infer the most useful +quality checklist from the spec. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. The script +must locate the active feature from `.specify/feature.json` and print JSON with +`FEATURE_DIR`. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or spec exists. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md` if present, and + `.specify/memory/constitution.md` if present. +3. Create `FEATURE_DIR/checklists/.md` using + `.specify/templates/checklist-template.md` when available. Choose a concise + file name from the focus, such as `security.md`, `accessibility.md`, or + `requirements.md`. +4. Write checklist items as validation questions for requirements completeness, + clarity, consistency, and acceptance readiness. Do not turn the checklist + into implementation tasks. +5. If the user asked to validate, mark items complete only when the current + artifacts satisfy them; otherwise leave them unchecked for review. +6. Report the checklist path and any high-risk gaps found while generating it. +SPECKIT_COMMAND_speckit_checklist_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_clarify_md <<'SPECKIT_COMMAND_speckit_clarify_md_EOF' || true +--- +description: Clarify underspecified feature requirements before technical planning. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as clarification focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR` and available docs, and +honor required-file checks. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json` from the repo root. + Stop if no active feature or `spec.md` is found; tell the user to run + `/speckit.specify` first. +2. Load `FEATURE_DIR/spec.md` and `.specify/memory/constitution.md` if present. +3. Identify ambiguities that block planning. Prioritize scope, actor and + permission boundaries, data lifecycle, compliance, failure behavior, and user + experience. Ignore implementation choices unless the spec already leaked + them into requirements. +4. Ask up to five concise questions. Prefer multiple choice when the tradeoff is + bounded; allow custom answers when needed. Ask only questions whose answers + would materially change the spec or plan. +5. Update `FEATURE_DIR/spec.md` with a `## Clarifications` section dated today. + Record each question and answer, then revise affected requirements so the + body is unambiguous. +6. Report how many clarifications were added and whether the feature is ready + for `/speckit.plan`. +SPECKIT_COMMAND_speckit_clarify_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_constitution_md <<'SPECKIT_COMMAND_speckit_constitution_md_EOF' || true +--- +description: Create or update the Spec Kit project constitution. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before changing +the constitution: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing template files only when absent: + `.specify/templates/constitution-template.md`, + `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, + `.specify/templates/tasks-template.md`, and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/check-prerequisites.sh`, + `.specify/scripts/bash/create-new-feature.sh`, + `.specify/scripts/bash/setup-plan.sh`, + `.specify/scripts/bash/setup-tasks.sh`, and + `.specify/scripts/bash/update-agent-context.sh`. +- The minimal scripts must support the core paths used by these commands: + active feature lookup from `.specify/feature.json`, JSON output, and required + file checks. Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Load `.specify/templates/constitution-template.md` if present and load the + current `.specify/memory/constitution.md` if it already exists. +2. Extract governing principles, testing expectations, delivery workflow, and + review rules from the user input. If updating an existing constitution, + preserve valid principles unless the user explicitly replaces them. +3. Write `.specify/memory/constitution.md` with concrete principles, rationale, + governance, amendment rules, and version/date metadata. Do not leave template + placeholders unresolved. +4. Review `.specify/templates/spec-template.md`, + `.specify/templates/plan-template.md`, and + `.specify/templates/tasks-template.md` for obvious constitution references. + Note any template updates that should be made by a separate command if they + are outside this command's requested scope. +5. Report the constitution path, whether it was created or updated, and the + next recommended phase (`/speckit.specify`). +SPECKIT_COMMAND_speckit_constitution_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_implement_md <<'SPECKIT_COMMAND_speckit_implement_md_EOF' || true +--- +description: Implement the active Spec Kit task list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as implementation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. Do not overwrite existing Spec Kit +scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Load the active spec, plan, tasks, constitution, and supporting docs. Treat + `tasks.md` as the execution source of truth. +3. Parse incomplete tasks in order. Respect dependencies and phase boundaries. + `[P]` tasks may be run in parallel only when they touch independent files and + the environment allows it. +4. Implement one coherent task or small dependency group at a time. Keep scope + limited to the task list and do not add new product behavior absent from the + spec. +5. Mark each task complete in `tasks.md` after its implementation and relevant + validation pass. +6. Run the smallest meaningful tests described by the plan, task, or repository + conventions. If a test cannot run, report the exact blocker. +7. Stop and report if the plan/spec/tasks conflict, a task is not actionable, or + implementation would require a decision not captured in the spec. +8. Finish with completed task ids, files changed, validation results, and any + remaining incomplete tasks. +SPECKIT_COMMAND_speckit_implement_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_plan_md <<'SPECKIT_COMMAND_speckit_plan_md_EOF' || true +--- +description: Create a technical implementation plan for the active specification. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Treat the input as the user's technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. Do not overwrite +existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-plan.sh --json` from the repo root. Stop if + the active feature or spec is missing; tell the user to run + `/speckit.specify` first. +2. Load `SPEC_FILE`, `PLAN_FILE`, `.specify/memory/constitution.md`, and any + existing docs under `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. Use the user's + input for stack and architecture choices. +4. Produce supporting docs when relevant: + `FEATURE_DIR/research.md`, `FEATURE_DIR/data-model.md`, + `FEATURE_DIR/contracts/`, and `FEATURE_DIR/quickstart.md`. +5. Keep the plan consistent with the spec. Do not add feature scope that the + spec did not request; send scope changes back to `/speckit.specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present so agent + context reflects the chosen stack. +7. Report created/updated files, unresolved risks, and readiness for + `/speckit.tasks`. +SPECKIT_COMMAND_speckit_plan_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_specify_md <<'SPECKIT_COMMAND_speckit_specify_md_EOF' || true +--- +description: Create or update a feature specification from a natural language description. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You MUST consider the user input before proceeding. If it is empty, stop and ask +for the feature description. + +## Bootstrap + +If `.specify` is absent, scaffold the minimal Spec Kit runtime before creating +the spec: + +- Create `.specify/memory`, `.specify/templates`, `.specify/scripts/bash`, and + `specs`. +- Add missing templates only when absent, especially + `.specify/templates/spec-template.md` and + `.specify/templates/checklist-template.md`. +- Add missing helper scripts only when absent: + `.specify/scripts/bash/create-new-feature.sh` must create a single + `specs/-/spec.md`, copy the spec template, write + `.specify/feature.json`, and print JSON with `FEATURE_DIR` and `SPEC_FILE`. + `.specify/scripts/bash/check-prerequisites.sh` must locate the active feature + from `.specify/feature.json`. +- Do not overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Generate a concise 2-4 word feature short name from the user description. + Prefer action-noun names such as `add-user-auth` or `analytics-dashboard`. +2. Run `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` if the + script supports it. Otherwise use the minimal script behavior described in + Bootstrap. Capture `FEATURE_DIR` and `SPEC_FILE`. +3. Load `.specify/templates/spec-template.md` and, if present, + `.specify/memory/constitution.md`. +4. Write `FEATURE_DIR/spec.md` from the user's description: + - Focus on what users need and why. + - Avoid implementation details, tech stacks, APIs, libraries, and code + structure. + - Include user scenarios, functional requirements, success criteria, + assumptions, edge cases, and key entities when relevant. + - Use at most three `[NEEDS CLARIFICATION: ...]` markers, only for decisions + that materially affect scope, security/privacy, or user experience and have + no reasonable default. +5. Create `FEATURE_DIR/checklists/requirements.md` from the checklist template. + Validate the spec against completeness, testability, measurable success + criteria, bounded scope, and absence of implementation details. Iterate the + spec up to three times for fixable failures. +6. If clarification markers remain, present all questions together with options + and wait for the user's answers before finalizing. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `/speckit.clarify` or `/speckit.plan`. +SPECKIT_COMMAND_speckit_specify_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_tasks_md <<'SPECKIT_COMMAND_speckit_tasks_md_EOF' || true +--- +description: Generate an actionable task list from the active implementation plan. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Use the input as task-generation focus if provided. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. Do not +overwrite existing Spec Kit scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/setup-tasks.sh --json` from the repo root. Stop if + `spec.md` or `plan.md` is missing; tell the user which Speckit command to run + first. +2. Load `FEATURE_DIR/spec.md`, `FEATURE_DIR/plan.md`, and available supporting + docs (`research.md`, `data-model.md`, `contracts/`, `quickstart.md`). +3. Generate `FEATURE_DIR/tasks.md` as executable Markdown: + - Number tasks `T001`, `T002`, and so on. + - Group by setup, foundations, user story phases, polish, and validation. + - Mark independent tasks with `[P]`. + - Include exact file paths where practical. + - If tests are required by the spec, constitution, or plan, put test tasks + before implementation tasks for the same behavior. + - Keep each user story independently implementable and testable. +4. Add dependency notes and parallel execution examples when they help execute + the plan. +5. Report task count, parallelizable count, and readiness for + `/speckit.analyze`, `/speckit.taskstoissues`, or `/speckit.implement`. +SPECKIT_COMMAND_speckit_tasks_md_EOF +read -r -d '' SPECKIT_COMMAND_speckit_taskstoissues_md <<'SPECKIT_COMMAND_speckit_taskstoissues_md_EOF' || true +--- +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +## User Input + +```text +$ARGUMENTS +``` + +Recognize `--dry-run`. In dry-run mode, print the `gh issue create` commands +that would run and do not create issues. + +## Bootstrap + +If `.specify` is absent, scaffold `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` if missing. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR` plus available +docs. Do not overwrite existing scripts or templates. + +## Procedure + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` + from the repo root. Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is available and authenticated unless this is `--dry-run`. +3. Read `FEATURE_DIR/tasks.md` and create one issue per task line that starts + with a Markdown task checkbox and contains a `T###` task id. +4. Use titles in this format: `[SpecKit] : `. +5. Before creating each issue, check all repository issues by exact title. If an + issue with the same title already exists, warn and skip it. +6. Assign every created issue with `--assignee ExtraToast`. +7. Pick exactly one best-fit label from the existing repository labels: + `bug`, `documentation`, or `enhancement`. + - Use `bug` for tasks about bugs, fixes, regressions, failures, or defects. + - Use `documentation` for docs, README, runbook, guide, or content tasks. + - Otherwise use `enhancement`. + - Never invent labels. If the chosen label does not exist in the repository, + warn and omit `--label`. +8. Use this helper shape inline; do not add scripts outside this command: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. +SPECKIT_COMMAND_speckit_taskstoissues_md_EOF +if [ "${INSTALL_CLAUDE}" = 1 ]; then + write_file "${COMMANDS_DIR}/speckit.analyze.md" 0644 "${SPECKIT_COMMAND_speckit_analyze_md}" + write_file "${COMMANDS_DIR}/speckit.checklist.md" 0644 "${SPECKIT_COMMAND_speckit_checklist_md}" + write_file "${COMMANDS_DIR}/speckit.clarify.md" 0644 "${SPECKIT_COMMAND_speckit_clarify_md}" + write_file "${COMMANDS_DIR}/speckit.constitution.md" 0644 "${SPECKIT_COMMAND_speckit_constitution_md}" + write_file "${COMMANDS_DIR}/speckit.implement.md" 0644 "${SPECKIT_COMMAND_speckit_implement_md}" + write_file "${COMMANDS_DIR}/speckit.plan.md" 0644 "${SPECKIT_COMMAND_speckit_plan_md}" + write_file "${COMMANDS_DIR}/speckit.specify.md" 0644 "${SPECKIT_COMMAND_speckit_specify_md}" + write_file "${COMMANDS_DIR}/speckit.tasks.md" 0644 "${SPECKIT_COMMAND_speckit_tasks_md}" + write_file "${COMMANDS_DIR}/speckit.taskstoissues.md" 0644 "${SPECKIT_COMMAND_speckit_taskstoissues_md}" +fi + # ----------------------------------------------------------------- # Skill: topics # ----------------------------------------------------------------- @@ -568,6 +1077,393 @@ if [ "${INSTALL_CLAUDE}" = 1 ]; then write_file "${SKILLS_DIR}/agent-session-bootstrap/SKILL.md" 0644 "${AGENT_SESSION_BOOTSTRAP_SKILL}" fi +# ----------------------------------------------------------------- +# Spec Kit Codex skills +# ----------------------------------------------------------------- +# Spec Kit Codex skills — generated by render-agent-kit.py from repo templates. +read -r -d '' CODEX_SPECKIT_speckit_analyze_SKILL_md <<'CODEX_SPECKIT_speckit_analyze_SKILL_md_EOF' || true +--- +name: speckit-analyze +description: Analyze consistency across the active spec, plan, and tasks. +--- + +# Speckit Analyze + +Use this skill for the `$speckit-analyze` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `spec.md`, `plan.md`, or `tasks.md` is missing. +2. Read the spec, plan, tasks, constitution if present, and referenced + supporting docs. +3. Perform read-only analysis. Do not modify files. +4. Report missing requirement coverage, spec-plan conflicts, task scope creep, + duplicated or vague tasks, ordering problems, and unverifiable acceptance + criteria. +5. Order findings by severity with file or section references. If no issues are + found, say so and identify residual risk. +6. Recommend the next Speckit command. +CODEX_SPECKIT_speckit_analyze_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_checklist_SKILL_md <<'CODEX_SPECKIT_speckit_checklist_SKILL_md_EOF' || true +--- +name: speckit-checklist +description: Generate a focused quality checklist for the active specification. +--- + +# Speckit Checklist + +Use this skill for the `$speckit-checklist` phase. Treat the user's prompt as +the checklist focus; infer a useful focus if none is provided. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add +`.specify/templates/checklist-template.md` and +`.specify/scripts/bash/check-prerequisites.sh` only when missing. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or spec exists. +2. Read the active spec, plan if present, and constitution if present. +3. Create `FEATURE_DIR/checklists/.md` from the checklist template when + available. +4. Write validation questions for requirements completeness, clarity, + consistency, and acceptance readiness. Do not write implementation tasks. +5. Mark items complete only when the user asked for validation and the current + artifacts satisfy the item. +6. Report the checklist path and high-risk gaps. +CODEX_SPECKIT_speckit_checklist_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_clarify_SKILL_md <<'CODEX_SPECKIT_speckit_clarify_SKILL_md_EOF' || true +--- +name: speckit-clarify +description: Clarify underspecified feature requirements before technical planning. +--- + +# Speckit Clarify + +Use this skill for the `$speckit-clarify` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must read +`.specify/feature.json`, print JSON with `FEATURE_DIR`, and honor required-file +checks. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json`. Stop if no active + feature or `spec.md` exists. +2. Read `FEATURE_DIR/spec.md` and the constitution if present. +3. Identify planning-blocking ambiguities in scope, actors, permissions, data, + compliance, failure behavior, and user experience. +4. Ask up to five concise questions, using multiple choice where possible. +5. Add a dated `## Clarifications` section to the spec, record answers, and + revise affected requirements. +6. Report clarification count and readiness for `$speckit-plan`. +CODEX_SPECKIT_speckit_clarify_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_constitution_SKILL_md <<'CODEX_SPECKIT_speckit_constitution_SKILL_md_EOF' || true +--- +name: speckit-constitution +description: Create or update the Spec Kit project constitution. +--- + +# Speckit Constitution + +Use this skill for the `$speckit-constitution` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing templates and minimal helper +scripts under `.specify/scripts/bash/` only when absent: +`check-prerequisites.sh`, `create-new-feature.sh`, `setup-plan.sh`, +`setup-tasks.sh`, and `update-agent-context.sh`. The helpers must support +active feature lookup from `.specify/feature.json`, JSON output, and required +file checks. Do not overwrite existing Spec Kit files. + +## Workflow + +1. Read `.specify/templates/constitution-template.md` and any existing + `.specify/memory/constitution.md`. +2. Derive concrete project principles, testing standards, delivery workflow, + review rules, governance, and amendment rules from the user request. +3. Write `.specify/memory/constitution.md` with no unresolved placeholders. +4. Check the spec, plan, and tasks templates for obvious constitution alignment + notes, but do not make unrelated edits. +5. Report the constitution path and readiness for `$speckit-specify`. +CODEX_SPECKIT_speckit_constitution_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_implement_SKILL_md <<'CODEX_SPECKIT_speckit_implement_SKILL_md_EOF' || true +--- +name: speckit-implement +description: Implement the active Spec Kit task list. +--- + +# Speckit Implement + +Use this skill for the `$speckit-implement` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`, +`SPEC_FILE`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if the spec, plan, or tasks file is missing. +2. Read the active spec, plan, tasks, constitution, and supporting docs. +3. Execute incomplete tasks from `tasks.md` in dependency order. Run `[P]` tasks + in parallel only when they touch independent files and the environment allows + it. +4. Keep implementation scope limited to the task list and active spec. +5. Mark tasks complete in `tasks.md` after implementation and relevant + validation. +6. Run the smallest meaningful tests required by the task, plan, or repository + conventions. Report exact blockers for checks that cannot run. +7. Stop for conflicts, unactionable tasks, or decisions missing from the spec. +8. Finish with completed task ids, changed files, validation results, and + remaining incomplete tasks. +CODEX_SPECKIT_speckit_implement_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_plan_SKILL_md <<'CODEX_SPECKIT_speckit_plan_SKILL_md_EOF' || true +--- +name: speckit-plan +description: Create a technical implementation plan for the active specification. +--- + +# Speckit Plan + +Use this skill for the `$speckit-plan` phase. Treat the user's prompt as +technical preferences and constraints. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh`, +`.specify/scripts/bash/setup-plan.sh`, and +`.specify/scripts/bash/update-agent-context.sh` only when absent. The minimal +`setup-plan.sh` must locate the active feature, copy +`.specify/templates/plan-template.md` to `FEATURE_DIR/plan.md` if needed, and +print JSON with `FEATURE_DIR`, `SPEC_FILE`, and `PLAN_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-plan.sh --json`. Stop if no active spec + exists. +2. Read the spec, plan template or existing plan, constitution, and supporting + docs in `FEATURE_DIR`. +3. Fill `FEATURE_DIR/plan.md` with implementation context, constraints, + constitution compliance, project structure, and phase gates. +4. Create supporting docs when relevant: `research.md`, `data-model.md`, + `contracts/`, and `quickstart.md`. +5. Keep scope aligned to the spec; send product-scope changes back to + `$speckit-specify`. +6. Run `.specify/scripts/bash/update-agent-context.sh` if present. +7. Report updated files, unresolved risks, and readiness for `$speckit-tasks`. +CODEX_SPECKIT_speckit_plan_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_specify_SKILL_md <<'CODEX_SPECKIT_speckit_specify_SKILL_md_EOF' || true +--- +name: speckit-specify +description: Create or update a feature specification from a natural language description. +--- + +# Speckit Specify + +Use this skill for the `$speckit-specify` phase. The user's prompt is the +feature description; if it is empty, ask for the description before proceeding. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/templates/spec-template.md`, +`.specify/templates/checklist-template.md`, +`.specify/scripts/bash/create-new-feature.sh`, and +`.specify/scripts/bash/check-prerequisites.sh` only when absent. The feature +script must create one `specs/-/spec.md`, copy the spec +template, write `.specify/feature.json`, and print JSON with `FEATURE_DIR` and +`SPEC_FILE`. + +## Workflow + +1. Generate a concise 2-4 word feature short name. +2. Run `.specify/scripts/bash/create-new-feature.sh --json ""` or + the minimal equivalent from Bootstrap. +3. Read `.specify/templates/spec-template.md` and + `.specify/memory/constitution.md` if present. +4. Write `FEATURE_DIR/spec.md` focused on what users need and why. Avoid + implementation details. Include user scenarios, functional requirements, + success criteria, assumptions, edge cases, and key entities when relevant. +5. Use at most three `[NEEDS CLARIFICATION: ...]` markers for critical unknowns + with no reasonable default. +6. Create `FEATURE_DIR/checklists/requirements.md`, validate the spec, and + iterate up to three times for fixable gaps. +7. Report `FEATURE_DIR`, `SPEC_FILE`, checklist status, and readiness for + `$speckit-clarify` or `$speckit-plan`. +CODEX_SPECKIT_speckit_specify_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_tasks_SKILL_md <<'CODEX_SPECKIT_speckit_tasks_SKILL_md_EOF' || true +--- +name: speckit-tasks +description: Generate an actionable task list from the active implementation plan. +--- + +# Speckit Tasks + +Use this skill for the `$speckit-tasks` phase. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add missing +`.specify/scripts/bash/check-prerequisites.sh` and +`.specify/scripts/bash/setup-tasks.sh` only when absent. The minimal +`setup-tasks.sh` must require `spec.md` and `plan.md`, create +`FEATURE_DIR/tasks.md` from `.specify/templates/tasks-template.md` if needed, +and print JSON with `FEATURE_DIR`, `PLAN_FILE`, and `TASKS_FILE`. + +## Workflow + +1. Run `.specify/scripts/bash/setup-tasks.sh --json`. Stop if the spec or plan + is missing. +2. Read the spec, plan, and supporting docs. +3. Generate `FEATURE_DIR/tasks.md` with `T001` style task ids, user-story + phases, dependency notes, and `[P]` markers for independent parallel tasks. +4. Include exact file paths where practical. Put test tasks before + implementation tasks whenever tests are required by the spec, constitution, + or plan. +5. Keep each user story independently implementable and testable. +6. Report task count, parallelizable count, and readiness for + `$speckit-analyze`, `$speckit-taskstoissues`, or `$speckit-implement`. +CODEX_SPECKIT_speckit_tasks_SKILL_md_EOF +read -r -d '' CODEX_SPECKIT_speckit_taskstoissues_SKILL_md <<'CODEX_SPECKIT_speckit_taskstoissues_SKILL_md_EOF' || true +--- +name: speckit-taskstoissues +description: Create GitHub issues from the active Spec Kit tasks list. +--- + +# Speckit Tasks To Issues + +Use this skill for the `$speckit-taskstoissues` phase. Support `--dry-run` by +printing the `gh issue create` commands without creating issues. + +## Bootstrap + +If `.specify` is absent, create `.specify/memory`, `.specify/templates`, +`.specify/scripts/bash`, and `specs`. Add a minimal +`.specify/scripts/bash/check-prerequisites.sh` only when absent. It must support +`--json --require-tasks --include-tasks` and return `FEATURE_DIR`. + +## Workflow + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks`. + Stop if `FEATURE_DIR/tasks.md` is missing. +2. Verify `gh` is installed and authenticated unless running `--dry-run`. +3. Parse task lines from `tasks.md` that start with a Markdown task checkbox and + contain a `T###` task id. +4. Create one issue per task with title + `[SpecKit] : `. +5. Before creating, check all repository issues by exact title; warn and skip + already-created issues. +6. Always include `--assignee ExtraToast`. +7. Pick exactly one best-fit existing repo label from + `enhancement`, `bug`, or `documentation`. Never invent labels; if the chosen + label does not exist, warn and omit `--label`. +8. Use this inline helper shape; do not add a script file: + +```bash +dry_run=false +case " $ARGUMENTS " in *" --dry-run "*) dry_run=true ;; esac + +feature_dir="" +tasks_file="$feature_dir/tasks.md" +feature_name="$(basename "$feature_dir")" + +label_exists() { + gh label list --limit 1000 --json name --jq '.[].name' | grep -Fxq "$1" +} + +issue_exists() { + gh issue list --state all --search "$1 in:title" --json title --jq '.[].title' | grep -Fxq "$1" +} + +while IFS= read -r task_line; do + task_id="$(printf '%s\n' "$task_line" | grep -Eo 'T[0-9]{3,}' | head -n 1)" + task_text="$(printf '%s\n' "$task_line" | + sed -E 's/^- \[[ xX]\][[:space:]]*//; s/\[P\][[:space:]]*//g; s/T[0-9]{3,}[[:space:]]*//; s/^[[:space:]]+//')" + [ -n "$task_id" ] || continue + [ -n "$task_text" ] || continue + + title="[SpecKit] ${feature_name}: ${task_id} ${task_text}" + body="$(cat <&2 + fi + + if issue_exists "$title"; then + printf 'WARN: issue already exists, skipping: %s\n' "$title" >&2 + continue + fi + + if [ "$dry_run" = true ]; then + printf 'gh issue create --title %q --body %q --assignee ExtraToast' "$title" "$body" + [ "${#label_args[@]}" -eq 0 ] || printf ' --label %q' "$label" + printf '\n' + else + gh issue create --title "$title" --body "$body" --assignee ExtraToast "${label_args[@]}" + fi +done < <(grep -E '^- \[[ xX]\].*T[0-9]{3,}' "$tasks_file") +``` + +9. Report created, skipped, and dry-run counts. +CODEX_SPECKIT_speckit_taskstoissues_SKILL_md_EOF +if [ "${INSTALL_CODEX}" = 1 ]; then + write_file "${CODEX_SKILLS_DIR}/speckit-analyze/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_analyze_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-checklist/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_checklist_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-clarify/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_clarify_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-constitution/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_constitution_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-implement/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_implement_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-plan/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_plan_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-specify/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_specify_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-tasks/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_tasks_SKILL_md}" + write_file "${CODEX_SKILLS_DIR}/speckit-taskstoissues/SKILL.md" 0644 "${CODEX_SPECKIT_speckit_taskstoissues_SKILL_md}" +fi + # ----------------------------------------------------------------- # Skill: council (multi-file toolkit — driver + prompts + schemas + # default config). Installs into ${SKILLS_DIR}/council and @@ -597,6 +1493,8 @@ import concurrent.futures import json import os import re +import shlex +import shutil import subprocess import sys import tempfile @@ -683,6 +1581,71 @@ WORKER_PERMISSION_MODE = "acceptEdits" # auto-accept edits; no command approva WORKER_TIMEOUT_S = int(os.environ.get("COUNCIL_WORKER_TIMEOUT_S", "1800")) VERIFY_TIMEOUT_S = int(os.environ.get("COUNCIL_VERIFY_TIMEOUT_S", "600")) WT_ROOT = Path(tempfile.gettempdir()) / "council-worktrees" +SPEC_DIR_RE = re.compile(r"^(\d{3})-([a-z0-9][a-z0-9-]*)$") +TASK_BLOCK_RE = re.compile( + r"^## (?P[^\n:]+)(?::[^\n]*)?\n" + r"\n" + r"```json\n(?P.*?)\n```", + re.MULTILINE | re.DOTALL, +) +MAX_CONSTITUTION_CHARS = 6000 +MAX_TEMPLATE_FIELD_CHARS = 12000 +EMBEDDED_CONSTITUTION = """# Constitution + +No project `.specify/memory/constitution.md` was found. Apply the repository's +agent guide, keep changes minimal, validate against real files, and preserve +human authorship. +""" +EMBEDDED_SPEC_TEMPLATE = """# Feature Specification: {{feature_name}} + +**Feature Branch**: `{{feature_id}}` +**Created**: {{date}} + +## User Brief + +{{brief}} + +## Requirements + +- Implement the brief as described, grounded in the real repository. +- Keep scope additive and preserve existing council canonical artifacts. + +## Success Criteria + +- `consolidated_plan.md` and `tasks.json` remain canonical. +- `tasks.md` round-trips exactly to `tasks.json` through council markers. +""" +EMBEDDED_PLAN_TEMPLATE = """# Implementation Plan: {{feature_name}} + +**Feature Branch**: `{{feature_id}}` +**Created**: {{date}} + +## Summary + +{{summary}} + +## Consolidated Plan + +{{consolidated_plan}} +""" +EMBEDDED_TASKS_TEMPLATE = """# Tasks: {{feature_id}} + + +""" + + +@dataclass(frozen=True) +class SpecRef: + number: int + slug: str + + @property + def name(self) -> str: + return f"{self.number:03d}-{self.slug}" + + @property + def relpath(self) -> str: + return f"specs/{self.name}" @dataclass @@ -931,6 +1894,335 @@ def _slugify(text: str) -> str: return (s[:48] or "run") +def _first_line(text: str) -> str: + for line in text.splitlines(): + if line.strip(): + return line.strip() + return "run" + + +def derive_feature_slug(brief: str, explicit_slug: Optional[str]) -> str: + return _slugify(explicit_slug or _first_line(brief)) + + +def _bounded(text: str, limit: int) -> str: + if len(text) <= limit: + return text + return text[:limit].rstrip() + "\n\n[truncated]" + + +def _constitution_path(repo: Path = REPO_ROOT) -> Path: + return repo / ".specify" / "memory" / "constitution.md" + + +def read_constitution_context(repo: Path = REPO_ROOT) -> str: + path = _constitution_path(repo) + text = path.read_text() if path.exists() else EMBEDDED_CONSTITUTION + return _bounded(text.strip(), MAX_CONSTITUTION_CHARS) + + +def _constitution_placeholder_reason(text: str) -> Optional[str]: + stripped = text.strip() + if not stripped: + return "constitution is empty" + lower = stripped.lower() + markers = ( + "[project name]", + "[insert", + "[fill", + "[todo", + "{{", + "todo:", + "tbd", + "placeholder", + ) + for marker in markers: + if marker in lower: + return f"constitution contains placeholder marker {marker!r}" + return None + + +def constitution_failure(repo: Path = REPO_ROOT) -> Optional[str]: + path = _constitution_path(repo) + if not path.exists(): + return f"missing constitution at {path}" + reason = _constitution_placeholder_reason(path.read_text()) + if reason: + return f"{reason} at {path}" + return None + + +def _spec_numbers(specs_root: Path) -> list[int]: + if not specs_root.exists(): + return [] + nums = [] + for child in specs_root.iterdir(): + m = SPEC_DIR_RE.match(child.name) + if m: + nums.append(int(m.group(1))) + return nums + + +def allocate_spec_ref(slug: str, specs_root: Path) -> SpecRef: + slug = _slugify(slug) + children = specs_root.iterdir() if specs_root.exists() else () + for child in children: + m = SPEC_DIR_RE.match(child.name) + if m and m.group(2) == slug: + raise ValueError(f"spec path already exists: {child}") + ref = SpecRef((max(_spec_numbers(specs_root)) if specs_root.exists() else 0) + 1, + slug) + path = specs_root / ref.name + if path.exists(): + raise ValueError(f"spec path already exists: {path}") + return ref + + +def spec_ref_from_state(state: dict) -> Optional[SpecRef]: + rel = state.get("spec_relpath") + if not isinstance(rel, str): + return None + name = Path(rel).name + m = SPEC_DIR_RE.match(name) + if not m: + return None + return SpecRef(int(m.group(1)), m.group(2)) + + +def prepare_spec_ref(run: Run, brief: str, explicit_slug: Optional[str]) -> SpecRef: + state = run.read_json("state.json") if run.has("state.json") else {} + existing = spec_ref_from_state(state) + if existing: + return existing + ref = allocate_spec_ref(derive_feature_slug(brief, explicit_slug), + REPO_ROOT / "specs") + run_target = run.path / ref.relpath + repo_target = REPO_ROOT / ref.relpath + if run_target.exists(): + raise ValueError(f"spec path already exists: {run_target}") + if repo_target.exists(): + raise ValueError(f"spec path already exists: {repo_target}") + run.set_state(spec_id=ref.name, spec_slug=ref.slug, spec_relpath=ref.relpath) + return ref + + +def read_spec_dir(path_s: Optional[str]) -> dict[str, str]: + if not path_s: + return {} + path = Path(path_s) + if not path.exists(): + raise SystemExit(f"spec dir not found: {path}") + if not path.is_dir(): + raise SystemExit(f"--spec-dir must be a directory: {path}") + out = {} + for name in ("spec.md", "plan.md", "tasks.md"): + p = path / name + if p.exists(): + out[name] = p.read_text() + return out + + +def load_sdd_template(name: str, fallback: str) -> str: + path = REPO_ROOT / ".specify" / "templates" / name + return path.read_text() if path.exists() else fallback + + +def render_sdd_template(template: str, values: dict[str, str]) -> str: + out = template + for key, val in values.items(): + bounded = _bounded(val, MAX_TEMPLATE_FIELD_CHARS) + out = out.replace("{{" + key + "}}", bounded) + out = out.replace("[" + key.upper() + "]", bounded) + out = out.replace("[" + key + "]", bounded) + return out + + +def render_tasks_md(tasks: list[dict], spec_ref: Optional[SpecRef] = None) -> str: + feature_id = spec_ref.name if spec_ref else "council" + template = load_sdd_template("tasks-template.md", EMBEDDED_TASKS_TEMPLATE) + header = render_sdd_template(template, { + "feature_id": feature_id, + "feature_name": feature_id, + }).strip() + if "" not in header: + header += "\n\n" + lines = [header, ""] + for task in tasks: + tid = str(task["id"]) + task_title = str(task.get("title", tid)).replace("\n", " ").strip() or tid + lines += [ + f"## {tid}: {task_title}", + f"", + "```json", + json.dumps(task, indent=2, sort_keys=True), + "```", + "", + ] + return "\n".join(lines).rstrip() + "\n" + + +def parse_tasks_md(text: str) -> list[dict]: + tasks: list[dict] = [] + for match in TASK_BLOCK_RE.finditer(text): + header_id = match.group("header_id").strip() + marker_id = match.group("marker_id").strip() + if header_id != marker_id: + raise ValueError(f"task marker mismatch: header {header_id!r}, " + f"marker {marker_id!r}") + try: + task = json.loads(match.group("body")) + except json.JSONDecodeError as exc: + raise ValueError(f"task {marker_id!r} JSON block is invalid: {exc}") from exc + if not isinstance(task, dict): + raise ValueError(f"task {marker_id!r} JSON block must be an object") + if str(task.get("id", "")).strip() != marker_id: + raise ValueError(f"task {marker_id!r} JSON id does not match marker") + tasks.append(task) + if not tasks: + raise ValueError("no council task JSON blocks found in tasks.md") + seen: set[str] = set() + for task in tasks: + tid = str(task["id"]) + if tid in seen: + raise ValueError(f"duplicate task id in tasks.md: {tid}") + seen.add(tid) + return tasks + + +def _normalise_tasks(tasks: list[dict]) -> object: + return json.loads(json.dumps(tasks, sort_keys=True)) + + +def assert_tasks_bijection(tasks: list[dict], tasks_md_text: str) -> None: + parsed = parse_tasks_md(tasks_md_text) + validate_tasks(parsed) + if _normalise_tasks(parsed) != _normalise_tasks(tasks): + raise ValueError("tasks.md does not match tasks.json") + + +def run_spec_dir(run: Run) -> Optional[Path]: + state = run.read_json("state.json") if run.has("state.json") else {} + ref = spec_ref_from_state(state) + if ref: + return run.path / ref.relpath + specs = run.path / "specs" + matches = sorted(p for p in specs.glob("[0-9][0-9][0-9]-*") if p.is_dir()) + return matches[-1] if matches else None + + +def regenerate_command(run: Run) -> str: + return ("council plan --run " + shlex.quote(str(run.path)) + + " --brief " + shlex.quote(str(run.path / "brief.md"))) + + +def analyze_checkpoint(run: Run, tasks: list[dict]) -> None: + failures = [] + constitution = constitution_failure() + if constitution: + failures.append(constitution) + spec_dir = run_spec_dir(run) + tasks_md = spec_dir / "tasks.md" if spec_dir else None + if not tasks_md or not tasks_md.exists(): + failures.append("missing tasks.md for tasks.json") + else: + try: + assert_tasks_bijection(tasks, tasks_md.read_text()) + except ValueError as exc: + failures.append(str(exc)) + if failures: + raise ValueError("analyze gate checkpoint 1 failed:\n- " + + "\n- ".join(failures) + + f"\nRegenerate with: {regenerate_command(run)}") + + +def analyze_tasks_file(tasks: list[dict], tasks_path: Path) -> None: + failures = [] + constitution = constitution_failure() + if constitution: + failures.append(constitution) + tasks_md = tasks_path.with_name("tasks.md") + if tasks_md.exists(): + try: + assert_tasks_bijection(tasks, tasks_md.read_text()) + except ValueError as exc: + failures.append(str(exc)) + if failures: + cmd = ("council plan --brief " + shlex.quote(str(tasks_path.parent / "spec.md")) + + " --spec-dir " + shlex.quote(str(tasks_path.parent))) + raise ValueError("analyze gate checkpoint 1 failed:\n- " + + "\n- ".join(failures) + + f"\nRegenerate with: {cmd}") + + +def build_spec_md(brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> str: + if seed.get("spec.md"): + return seed["spec.md"] + if obj.get("spec_markdown"): + return str(obj["spec_markdown"]).strip() + "\n" + template = load_sdd_template("spec-template.md", EMBEDDED_SPEC_TEMPLATE) + return render_sdd_template(template, { + "feature_name": ref.slug.replace("-", " ").title(), + "feature_id": ref.name, + "date": time.strftime("%Y-%m-%d"), + "brief": brief.strip(), + }).rstrip() + "\n" + + +def build_plan_md(brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> str: + if seed.get("plan.md"): + return seed["plan.md"] + for key in ("implementation_plan_markdown", "plan_markdown"): + if obj.get(key): + return str(obj[key]).strip() + "\n" + template = load_sdd_template("plan-template.md", EMBEDDED_PLAN_TEMPLATE) + return render_sdd_template(template, { + "feature_name": ref.slug.replace("-", " ").title(), + "feature_id": ref.name, + "date": time.strftime("%Y-%m-%d"), + "brief": brief.strip(), + "summary": str(obj.get("summary", "")).strip() or _first_line(brief), + "consolidated_plan": str(obj.get("consolidated_plan_markdown", "")).strip(), + }).rstrip() + "\n" + + +def write_sdd_artifacts(run: Run, brief: str, obj: dict, ref: SpecRef, + seed: dict[str, str]) -> None: + tasks = obj.get("tasks", []) + if seed.get("tasks.md"): + assert_tasks_bijection(tasks, seed["tasks.md"]) + spec_dir = run.path / ref.relpath + spec_dir.mkdir(parents=True, exist_ok=True) + (spec_dir / "spec.md").write_text(build_spec_md(brief, obj, ref, seed)) + (spec_dir / "plan.md").write_text(build_plan_md(brief, obj, ref, seed)) + (spec_dir / "tasks.md").write_text(render_tasks_md(tasks, ref)) + + +def copy_run_specs_to_worktree(run: Run, worktree: Path) -> None: + specs_root = run.path / "specs" + if not specs_root.exists(): + return + copied = [] + for src in sorted(p for p in specs_root.iterdir() + if p.is_dir() and SPEC_DIR_RE.match(p.name)): + dest = worktree / "specs" / src.name + if dest.exists(): + raise ValueError(f"spec path already exists in integration worktree: " + f"specs/{src.name}") + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(src, dest) + copied.append(f"specs/{src.name}") + if not copied: + return + git("add", "-A", "--", "specs", cwd=worktree) + dirty = git("status", "--porcelain", "--", "specs", cwd=worktree).stdout.strip() + if dirty: + git("-c", "user.name=council", "-c", "user.email=council@local", + "commit", "-q", "-m", "council: add spec artifacts", cwd=worktree) + log(f"committed Spec Kit artifacts: {', '.join(copied)}") + + def _split_dest_url(owner: str, name: str) -> str: """Canonical SSH remote for a GitHub owner/name. Pure (covered by --self-test). In runner workspaces git rewrites git@github.com: to https so @@ -942,7 +2234,8 @@ def _split_dest_url(owner: str, name: str) -> str: # stages 1-4 # -------------------------------------------------------------------------- -def stage_plan(run: Run, brief: str, a: Engine, b: Engine) -> tuple[dict, dict]: +def stage_plan(run: Run, brief: str, a: Engine, b: Engine, + constitution_context: str) -> tuple[dict, dict]: if run.has("planA.v1.json") and run.has("planB.v1.json"): log("stage 1: dual plans already present, skipping") return run.read_json("planA.v1.json"), run.read_json("planB.v1.json") @@ -953,7 +2246,8 @@ def stage_plan(run: Run, brief: str, a: Engine, b: Engine) -> tuple[dict, dict]: def mk(engine: Engine) -> Callable[[], EngineResult]: prompt = render(tmpl, engine_label=engine.label, brief=brief, repo_root=str(REPO_ROOT), schema=schema, - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(engine, prompt)) res_a, res_b = parallel([mk(a), mk(b)]) @@ -964,7 +2258,8 @@ def stage_plan(run: Run, brief: str, a: Engine, b: Engine) -> tuple[dict, dict]: def stage_critique_round(run: Run, brief: str, a: Engine, b: Engine, - plan_a: dict, plan_b: dict, rnd: int) -> tuple[dict, dict]: + plan_a: dict, plan_b: dict, rnd: int, + constitution_context: str) -> tuple[dict, dict]: out_a, out_b = f"planA.v{rnd + 1}.json", f"planB.v{rnd + 1}.json" if run.has(out_a) and run.has(out_b): log(f"stage 2: critique round {rnd} already present, skipping") @@ -978,7 +2273,8 @@ def stage_critique_round(run: Run, brief: str, a: Engine, b: Engine, prompt = render(critic_tmpl, engine_label=critic.label, brief=brief, repo_root=str(REPO_ROOT), plan=json.dumps(plan, indent=2), - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(critic, prompt)) crit_of_a, crit_of_b = parallel([crit(b, plan_a), crit(a, plan_b)]) @@ -992,7 +2288,8 @@ def stage_critique_round(run: Run, brief: str, a: Engine, b: Engine, prompt = render(rev_tmpl, engine_label=author.label, brief=brief, repo_root=str(REPO_ROOT), plan=json.dumps(plan, indent=2), critique=critique, schema=schema, - baseline=BASELINE_PROMPT) + baseline=BASELINE_PROMPT, + constitution=constitution_context) return lambda: run.record(run_engine(author, prompt)) rev_a, rev_b = parallel([rev(a, plan_a, crit_of_a.text), @@ -1004,10 +2301,19 @@ def stage_critique_round(run: Run, brief: str, a: Engine, b: Engine, def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, - rounds: int, consolidator: Engine) -> dict: + rounds: int, consolidator: Engine, + constitution_context: str, spec_ref: SpecRef, + spec_seed: dict[str, str]) -> dict: if run.has("tasks.json") and run.has("consolidated_plan.md"): log("stage 4: consolidation already present, skipping") - return run.read_json("tasks.json") + tasks = run.read_json("tasks.json") + obj = { + "consolidated_plan_markdown": (run.path / "consolidated_plan.md").read_text(), + "tasks": tasks, + } + write_sdd_artifacts(run, brief, obj, spec_ref, spec_seed) + analyze_checkpoint(run, tasks) + return tasks log(f"stage 4: consolidation ({consolidator.label})") history_parts = [] for r in range(1, rounds + 1): @@ -1022,6 +2328,7 @@ def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, history="\n\n".join(history_parts) or "(no critiques recorded)", schema=load_schema_text("consolidated"), baseline=BASELINE_PROMPT, + constitution=constitution_context, ) res = run.record(run_engine(consolidator, prompt)) obj = extract_json(res.text) @@ -1029,6 +2336,8 @@ def stage_consolidate(run: Run, brief: str, plan_a: dict, plan_b: dict, validate_tasks(tasks) run.write_text("consolidated_plan.md", obj.get("consolidated_plan_markdown", "")) run.write_json("tasks.json", tasks) + write_sdd_artifacts(run, brief, obj, spec_ref, spec_seed) + analyze_checkpoint(run, tasks) return tasks @@ -1227,6 +2536,7 @@ def execute_dag(run: Run, tasks: list[dict], worker_for: Callable[[str], Engine] git("worktree", "remove", "--force", str(integ_wt), check=False) git("branch", "-D", integ_branch, check=False) git("worktree", "add", "--force", "-b", integ_branch, str(integ_wt), base) + copy_run_specs_to_worktree(run, integ_wt) log(f"exec: {len(tasks)} task(s) in {len(waves)} wave(s); base {base[:8]}; " f"integration branch {integ_branch}; concurrency {cap}") @@ -1277,6 +2587,8 @@ def cmd_fanout(args: argparse.Namespace) -> int: if not run.has("tasks.json"): raise SystemExit(f"no tasks.json in {run.path}; run `plan` first") tasks = run.read_json("tasks.json") + validate_tasks(tasks) + analyze_checkpoint(run, tasks) waves = plan_waves(tasks) cfg = resolve_config({"intensity": args.intensity, "worker": args.worker, @@ -1349,6 +2661,7 @@ def cmd_fleet(args: argparse.Namespace) -> int: raise SystemExit(f"tasks file not found: {tasks_path}") tasks = json.loads(tasks_path.read_text()) validate_tasks(tasks) + analyze_tasks_file(tasks, tasks_path) waves = plan_waves(tasks) pool = parse_agents_pool(args.agents) @@ -1519,9 +2832,9 @@ def read_brief(arg: str) -> str: if arg == "-": return sys.stdin.read() p = Path(arg) - if not p.exists(): - raise SystemExit(f"brief file not found: {arg}") - return p.read_text() + if p.exists(): + return p.read_text() + return arg def cmd_plan(args: argparse.Namespace) -> int: @@ -1552,16 +2865,24 @@ def cmd_plan(args: argparse.Namespace) -> int: brief = read_brief(args.brief) run = Run.open(Path(args.run)) if args.run else Run.create(brief, args.slug) run.write_text("brief.md", brief) + spec_ref = prepare_spec_ref(run, brief, args.slug) + spec_seed = read_spec_dir(args.spec_dir) + constitution_context = read_constitution_context() run.set_state(stage="plan", intensity=cfg["intensity"], rounds=rounds, - planner_a=a.label, planner_b=b.label) + planner_a=a.label, planner_b=b.label, + spec_id=spec_ref.name, spec_slug=spec_ref.slug, + spec_relpath=spec_ref.relpath, + spec_dir=args.spec_dir) log(f"run dir: {run.path}") log(f"intensity {cfg['intensity']}: {a.label} ║ {b.label}, {rounds} round(s), " f"codex effort {cfg['codex_effort']}") - plan_a, plan_b = stage_plan(run, brief, a, b) + plan_a, plan_b = stage_plan(run, brief, a, b, constitution_context) for rnd in range(1, rounds + 1): - plan_a, plan_b = stage_critique_round(run, brief, a, b, plan_a, plan_b, rnd) - tasks = stage_consolidate(run, brief, plan_a, plan_b, rounds, consolidator) + plan_a, plan_b = stage_critique_round( + run, brief, a, b, plan_a, plan_b, rnd, constitution_context) + tasks = stage_consolidate(run, brief, plan_a, plan_b, rounds, consolidator, + constitution_context, spec_ref, spec_seed) run.set_state(stage="planned", task_count=len(tasks)) waves = plan_waves(tasks) @@ -1777,6 +3098,79 @@ def cmd_self_test(_args: argparse.Namespace) -> int: check("validate_tasks warns on empty verify", any("no verify" in m for m in captured)) + # Spec Kit task markdown + analyze helpers + sample_task = { + "id": "T1", + "title": "Implement one thing", + "objective": "Change exactly one thing", + "output_format": "Code edits", + "paths": ["platform/agents/council/council.py"], + "depends_on": [], + "difficulty": "moderate", + "model": "haiku", + "verify": "python3 platform/agents/council/council.py --self-test", + "boundaries": "Stay in scope", + } + rendered_tasks = render_tasks_md([sample_task], SpecRef(7, "sdd-aware-council")) + check("tasks.json -> tasks.md -> parse roundtrips", + parse_tasks_md(rendered_tasks) == [sample_task]) + edited_tasks = rendered_tasks.replace("Change exactly one thing", + "Change a different thing") + check("tasks.md bijection mismatch hard-fails", + _raises(lambda: assert_tasks_bijection([sample_task], edited_tasks))) + bad_marker = rendered_tasks.replace("", + "") + check("tasks.md parser rejects marker/header mismatch", + _raises(lambda: parse_tasks_md(bad_marker))) + gate_msg = "" + try: + analyze_checkpoint(Run(Path("/tmp/council-self-test-run")), [sample_task]) + except ValueError as exc: + gate_msg = str(exc) + check("analyze gate names checkpoint 1 and regenerate command", + "analyze gate checkpoint 1 failed" in gate_msg + and "Regenerate with: council plan --run" in gate_msg) + + # constitution handling is bounded and limited to reasoning roles. + check("constitution context is bounded", + len(read_constitution_context()) <= MAX_CONSTITUTION_CHARS + 20) + for role in ("planner", "critic", "reviser", "consolidator"): + check(f"constitution token present in {role}", + "{{constitution}}" in load_prompt(role)) + check("constitution token absent from worker", + "{{constitution}}" not in load_prompt("worker")) + check("constitution token absent from verifier", + "{{constitution}}" not in load_prompt("verifier")) + with tempfile.TemporaryDirectory() as td: + repo = Path(td) + check("constitution failure detects missing file", + "missing constitution" in (constitution_failure(repo) or "")) + cpath = repo / ".specify" / "memory" / "constitution.md" + cpath.parent.mkdir(parents=True) + cpath.write_text("# Constitution\n\n[PROJECT NAME]\n") + check("constitution failure detects placeholder", + "placeholder" in (constitution_failure(repo) or "")) + cpath.write_text("# Constitution\n\nShip small, verified changes.\n") + check("constitution failure accepts concrete file", + constitution_failure(repo) is None) + + # Spec numbering and slug derivation + check("free-text brief derives slug from first line", + derive_feature_slug("Fuse council with Spec Kit\nextra", None) + == "fuse-council-with-spec-kit") + check("explicit --slug is reused for spec slug", + derive_feature_slug("ignored", "My Feature") == "my-feature") + with tempfile.TemporaryDirectory() as td: + specs = Path(td) / "specs" + specs.mkdir() + (specs / "001-old").mkdir() + ref = allocate_spec_ref("new feature", specs) + check("NNN allocation uses max(existing)+1", + ref == SpecRef(2, "new-feature")) + (specs / "003-duplicate").mkdir() + check("NNN allocation fail-fast on existing slug", + _raises(lambda: allocate_spec_ref("duplicate", specs))) + # merge_config (intensity presets + precedence) std = merge_config({}, {}) check("default intensity is standard", @@ -1856,6 +3250,7 @@ def build_parser() -> argparse.ArgumentParser: pl.add_argument("--brief", help="brief file path, or - for stdin") pl.add_argument("--run", help="existing run dir to resume") pl.add_argument("--slug", help="slug for the run dir name") + pl.add_argument("--spec-dir", help="existing specs/NNN-slug dir to consume") pl.add_argument("--intensity", choices=list(PRESETS), help="preset (overrides council.toml for this run)") pl.add_argument("--rounds", type=int, default=None, help="override critique rounds") @@ -2034,9 +3429,15 @@ parallel execution. Each task must: Keep the task count proportional to the work: a handful for a focused change, more only when the work genuinely decomposes. Sequential, tightly-coupled work should be a single task with a clear ordering, not forced into false parallelism. +If useful, also include optional `spec_markdown` and +`implementation_plan_markdown` fields for Spec Kit artifacts; `tasks` remains +the canonical worker input. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY a JSON object — no prose, no code fences — matching this schema: @@ -2075,6 +3476,9 @@ parallel execution. {{baseline}} +# Constitution +{{constitution}} + # Output Return a concise Markdown critique: a bulleted list of concrete problems, each with WHY it matters and a suggested fix. End with one line: `VERDICT:` followed @@ -2102,6 +3506,9 @@ file will collide). Be concrete: name real files and real commands. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY a JSON object — no prose, no code fences — matching this schema: @@ -2138,6 +3545,9 @@ Re-check claims against the real code at {{repo_root}} as needed. {{baseline}} +# Constitution +{{constitution}} + # Output Return ONLY the revised JSON object — no prose, no code fences — matching this schema: @@ -2233,6 +3643,16 @@ read -r -d '' COUNCIL_FILE_schemas_consolidated_schema_json <<'COUNCIL_FILE_sche "required": ["consolidated_plan_markdown", "tasks"], "properties": { "consolidated_plan_markdown": { "type": "string" }, + "spec_markdown": { "type": "string" }, + "implementation_plan_markdown": { "type": "string" }, + "feature": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { "type": "string" }, + "slug": { "type": "string" } + } + }, "tasks": { "type": "array", "items": { @@ -2588,6 +4008,10 @@ if [ "${INSTALL_CLAUDE}" = 1 ] || [ "${INSTALL_CODEX}" = 1 ]; then # Re-running the installer never overwrites this file once you've # customised it — only the initial install seeds these defaults. +# SDD paths; git-commit-capture has no allowlist and stop-session-digest digests transcripts, so neither is path-suppressible. +**/.specify/** +**/specs/** + # Secrets-bearing files *.env .env* @@ -3333,6 +4757,853 @@ if [ "${INSTALL_CODEX}" = 1 ]; then write_file "${CODEX_HOOKS_CONFIG}" 0644 "${CODEX_HOOKS_JSON}" fi +# ----------------------------------------------------------------- +# Spec Kit project scaffold seed +# ----------------------------------------------------------------- +# Spec Kit project scaffold seed — generated by render-agent-kit.py from repo templates. +read -r -d '' SPECIFY_SEED__specify_memory_constitution_md <<'SPECIFY_SEED__specify_memory_constitution_md_EOF' || true +# personal-stack Constitution + +## Core Principles + +### I. Human Authorship and No Attribution + +All repository work is authored solely by the human driver. Do not add +`Co-Authored-By` trailers, generated-by footers, assistant names, model names, +or automation-attribution text to commits, PRs, code comments, docs, generated +files, or templates. + +### II. Validate Against Reality + +Claims about paths, APIs, config, cluster state, or tooling must be checked +against the real codebase and, where relevant, live state. If a fact is unknown, +search the repo, inspect the source, or run the narrowest safe command before +designing around it. Do not invent secret paths, resource names, commands, or +contracts. + +### III. Claude/Codex Parity + +Agent-facing behavior must stay equivalent across Claude and Codex surfaces. +Any skill, hook, memory rule, installer behavior, command, or project guidance +added for one agent must get the matching surface for the other in the same +branch, unless an explicit unsupported reason is recorded. + +### IV. Render and Validate Discipline + +Render-managed files are edited only at their source templates or inventory. +After touching a render source, run the owning renderer and commit the rendered +output with the source change. Run the smallest meaningful validation command +for the touched area, and state exactly what remains unverified if a check +cannot run. + +### V. Small Stacked PRs + +Every change should be reviewable, revertable, and scoped to one objective. +Prefer small stacked PRs over broad bundles. Avoid tangential cleanup, +speculative abstractions, unrelated refactors, and compatibility shims when a +direct local-pattern change is available. + +## Required Workflow + +1. Start from a spec for user-visible or cross-cutting changes. The spec must + describe outcomes, acceptance criteria, non-goals, and open questions. +2. Plan against existing repo patterns and real paths. Surface architectural + limitations before implementation begins. +3. Break work into tasks that preserve small PR boundaries and parallel safety. +4. Implement only the task scope. Never revert or overwrite unrelated parallel + edits. +5. Validate with the smallest meaningful command for the touched area: + `./gradlew :services::test` for Kotlin services, + `./gradlew :platform:tooling:test` for platform tooling, and + `npm run typecheck && npm run lint && npm run test` inside Vue UIs. +6. Capture durable lessons or decisions in the knowledge base when they affect + future repo behavior, without storing secrets, raw transcripts, or full + diffs. + +## Render-Managed Boundaries + +- `platform/inventory/fleet.yaml` is the source of truth for public service + routing, catalog, placement, exposure, and access intent. +- Generated Traefik routes, catalog ConfigMaps, agent-kit mirrors, and installer + artifacts must not be hand-edited. +- `.specify/memory/constitution.md` is committed and hand-edited for this repo. + The generic `.specify/templates/constitution-template.md` is only the + render-managed starter for future seed installs. + +## Governance + +This constitution overrides ad-hoc agent behavior. Amend it deliberately when +the governing workflow changes, and update `AGENTS.md`, `CLAUDE.md`, skills, or +templates in the same branch when parity requires it. + +**Version**: 1.0.0 +**Ratified**: 2026-06-08 +**Last Amended**: 2026-06-08 +SPECIFY_SEED__specify_memory_constitution_md_EOF +read -r -d '' SPECIFY_SEED__specify_scripts_bash_check_prerequisites_sh <<'SPECIFY_SEED__specify_scripts_bash_check_prerequisites_sh_EOF' || true +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +paths_only=false +require_tasks=false +include_tasks=false + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --paths-only) + paths_only=true + ;; + --require-tasks) + require_tasks=true + ;; + --include-tasks) + include_tasks=true + ;; + --help|-h) + printf 'Usage: %s [--json] [--paths-only] [--require-tasks] [--include-tasks]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +if [ "${paths_only}" = true ]; then + if [ "${json}" = true ]; then + specify_paths_json "${branch}" + else + printf 'REPO_ROOT: %s\n' "${REPO_ROOT}" + printf 'BRANCH: %s\n' "${branch}" + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'TASKS: %s\n' "${tasks_file}" + fi + exit 0 +fi + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${spec_file}" ] || specify_die "Feature spec not found: ${spec_file}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ "${require_tasks}" = true ] && [ ! -f "${tasks_file}" ]; then + specify_die "Tasks file not found: ${tasks_file}" +fi + +docs="" +append_doc() { + if [ -n "${docs}" ]; then + docs="${docs}," + fi + docs="${docs}\"$1\"" +} + +[ -f "${feature_dir}/research.md" ] && append_doc "research.md" +[ -f "${feature_dir}/data-model.md" ] && append_doc "data-model.md" +[ -d "${feature_dir}/contracts" ] && append_doc "contracts/" +[ -f "${feature_dir}/quickstart.md" ] && append_doc "quickstart.md" +if [ "${include_tasks}" = true ] && [ -f "${tasks_file}" ]; then + append_doc "tasks.md" +fi + +if [ "${json}" = true ]; then + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":[%s]}\n' "$(specify_json_escape "${feature_dir}")" "${docs}" +else + printf 'FEATURE_DIR: %s\n' "${feature_dir}" + printf 'AVAILABLE_DOCS:\n' + printf '%s\n' "${docs}" | tr ',' '\n' | sed 's/^/ /; s/"//g' +fi +SPECIFY_SEED__specify_scripts_bash_check_prerequisites_sh_EOF +read -r -d '' SPECIFY_SEED__specify_scripts_bash_common_sh <<'SPECIFY_SEED__specify_scripts_bash_common_sh_EOF' || true +#!/usr/bin/env bash + +set -euo pipefail + +if [ -z "${SPECIFY_SCRIPT_DIR:-}" ]; then + SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +fi + +specify_repo_root() { + if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + return 0 + fi + + CDPATH= cd -- "${SPECIFY_SCRIPT_DIR}/../../.." && pwd -P +} + +REPO_ROOT=$(specify_repo_root) +SPECIFY_DIR="${REPO_ROOT}/.specify" +SPECS_DIR="${REPO_ROOT}/specs" + +specify_has_git() { + command -v git >/dev/null 2>&1 && git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + +specify_current_branch() { + if specify_has_git; then + git -C "${REPO_ROOT}" rev-parse --abbrev-ref HEAD 2>/dev/null + else + basename "$(pwd)" + fi +} + +specify_is_feature_branch() { + case "$1" in + [0-9][0-9][0-9]-*) return 0 ;; + *) return 1 ;; + esac +} + +specify_feature_dir() { + printf '%s/specs/%s\n' "${REPO_ROOT}" "$1" +} + +specify_spec_file() { + printf '%s/specs/%s/spec.md\n' "${REPO_ROOT}" "$1" +} + +specify_plan_file() { + printf '%s/specs/%s/plan.md\n' "${REPO_ROOT}" "$1" +} + +specify_tasks_file() { + printf '%s/specs/%s/tasks.md\n' "${REPO_ROOT}" "$1" +} + +specify_template_file() { + printf '%s/templates/%s\n' "${SPECIFY_DIR}" "$1" +} + +specify_json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +specify_error() { + printf 'ERROR: %s\n' "$*" >&2 +} + +specify_die() { + specify_error "$*" + exit 1 +} + +specify_require_feature_branch() { + current_branch=$(specify_current_branch) + if [ "${current_branch}" = "main" ] || [ "${current_branch}" = "master" ]; then + specify_die "Current branch '${current_branch}' is not a feature branch. Run create-new-feature.sh first." + fi + if ! specify_is_feature_branch "${current_branch}"; then + specify_die "Current branch '${current_branch}' must start with a three-digit feature number, e.g. 001-my-feature." + fi + printf '%s\n' "${current_branch}" +} + +specify_copy_template() { + template_name=$1 + destination=$2 + feature_name=$3 + source_file=$(specify_template_file "${template_name}") + + [ -f "${source_file}" ] || specify_die "Template not found: ${source_file}" + + today=$(date +%F) + sed \ + -e "s/{{FEATURE_NAME}}/${feature_name}/g" \ + -e "s/{{feature_name}}/${feature_name}/g" \ + -e "s/{{DATE}}/${today}/g" \ + "${source_file}" > "${destination}" +} + +specify_paths_json() { + branch=$1 + feature_dir=$(specify_feature_dir "${branch}") + spec_file=$(specify_spec_file "${branch}") + plan_file=$(specify_plan_file "${branch}") + tasks_file=$(specify_tasks_file "${branch}") + has_git=false + if specify_has_git; then + has_git=true + fi + + printf '{"REPO_ROOT":"%s","BRANCH":"%s","HAS_GIT":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$(specify_json_escape "${REPO_ROOT}")" \ + "$(specify_json_escape "${branch}")" \ + "${has_git}" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${plan_file}")" \ + "$(specify_json_escape "${tasks_file}")" +} +SPECIFY_SEED__specify_scripts_bash_common_sh_EOF +read -r -d '' SPECIFY_SEED__specify_scripts_bash_create_new_feature_sh <<'SPECIFY_SEED__specify_scripts_bash_create_new_feature_sh_EOF' || true +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +feature_number="" +description="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --number) + shift + [ "$#" -gt 0 ] || specify_die "--number requires a value" + feature_number=$1 + ;; + --help|-h) + printf 'Usage: %s [--json] [--number N] \n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + *) + if [ -z "${description}" ]; then + description=$1 + else + description="${description} $1" + fi + ;; + esac + shift +done + +[ -n "${description}" ] || specify_die "Feature description is required" + +slug=$(printf '%s\n' "${description}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g' | awk ' +{ + count = 0 + for (i = 1; i <= NF; i++) { + word = $i + if (word == "the" || word == "and" || word == "for" || word == "with" || word == "from" || word == "that" || word == "this" || word == "into" || word == "onto") { + continue + } + if (length(word) < 2) { + continue + } + words[++count] = word + if (count == 5) { + break + } + } + if (count == 0) { + print "feature" + } else { + for (i = 1; i <= count; i++) { + printf "%s%s", (i == 1 ? "" : "-"), words[i] + } + printf "\n" + } +}') + +highest=0 +if specify_has_git; then + branches=$(git -C "${REPO_ROOT}" branch --all --format='%(refname:short)' 2>/dev/null || true) + for ref in ${branches}; do + ref=${ref#origin/} + number=${ref%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -d "${SPECS_DIR}" ]; then + for path in "${SPECS_DIR}"/[0-9][0-9][0-9]-*; do + [ -d "${path}" ] || continue + name=$(basename "${path}") + number=${name%%-*} + case "${number}" in + [0-9][0-9][0-9]) + if [ "${number}" -gt "${highest}" ]; then + highest=${number} + fi + ;; + esac + done +fi + +if [ -n "${feature_number}" ]; then + case "${feature_number}" in + *[!0-9]*) specify_die "--number must be numeric" ;; + esac + number=$(printf '%03d' "${feature_number}") +else + number=$(printf '%03d' $((highest + 1))) +fi + +branch_name="${number}-${slug}" +feature_dir=$(specify_feature_dir "${branch_name}") +spec_file=$(specify_spec_file "${branch_name}") + +if specify_has_git; then + if ! git -C "${REPO_ROOT}" rev-parse --verify --quiet "${branch_name}" >/dev/null; then + git -C "${REPO_ROOT}" checkout -b "${branch_name}" >/dev/null 2>&1 + else + git -C "${REPO_ROOT}" checkout "${branch_name}" >/dev/null 2>&1 + fi +fi + +mkdir -p "${feature_dir}" +if [ ! -f "${spec_file}" ]; then + specify_copy_template "spec-template.md" "${spec_file}" "${branch_name}" +fi + +if [ "${json}" = true ]; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_DIR":"%s","FEATURE_NUMBER":"%s"}\n' \ + "$(specify_json_escape "${branch_name}")" \ + "$(specify_json_escape "${spec_file}")" \ + "$(specify_json_escape "${feature_dir}")" \ + "$(specify_json_escape "${number}")" +else + printf 'BRANCH_NAME: %s\n' "${branch_name}" + printf 'SPEC_FILE: %s\n' "${spec_file}" +fi +SPECIFY_SEED__specify_scripts_bash_create_new_feature_sh_EOF +read -r -d '' SPECIFY_SEED__specify_scripts_bash_setup_plan_sh <<'SPECIFY_SEED__specify_scripts_bash_setup_plan_sh_EOF' || true +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +spec_file=$(specify_spec_file "${branch}") +plan_file=$(specify_plan_file "${branch}") + +mkdir -p "${feature_dir}" "${feature_dir}/contracts" +[ -f "${spec_file}" ] || specify_copy_template "spec-template.md" "${spec_file}" "${branch}" +[ -f "${plan_file}" ] || specify_copy_template "plan-template.md" "${plan_file}" "${branch}" + +touch "${feature_dir}/research.md" "${feature_dir}/data-model.md" "${feature_dir}/quickstart.md" + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'FEATURE_SPEC: %s\n' "${spec_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi +SPECIFY_SEED__specify_scripts_bash_setup_plan_sh_EOF +read -r -d '' SPECIFY_SEED__specify_scripts_bash_setup_tasks_sh <<'SPECIFY_SEED__specify_scripts_bash_setup_tasks_sh_EOF' || true +#!/usr/bin/env bash + +set -euo pipefail + +SPECIFY_SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +. "${SPECIFY_SCRIPT_DIR}/common.sh" + +json=false +while [ "$#" -gt 0 ]; do + case "$1" in + --json) + json=true + ;; + --help|-h) + printf 'Usage: %s [--json]\n' "$(basename "$0")" + exit 0 + ;; + --*) + specify_die "Unknown option: $1" + ;; + esac + shift +done + +branch=$(specify_require_feature_branch) +feature_dir=$(specify_feature_dir "${branch}") +plan_file=$(specify_plan_file "${branch}") +tasks_file=$(specify_tasks_file "${branch}") + +[ -d "${feature_dir}" ] || specify_die "Feature directory not found: ${feature_dir}" +[ -f "${plan_file}" ] || specify_die "Implementation plan not found: ${plan_file}" + +if [ ! -f "${tasks_file}" ]; then + specify_copy_template "tasks-template.md" "${tasks_file}" "${branch}" +fi + +if [ "${json}" = true ]; then + specify_paths_json "${branch}" +else + printf 'TASKS: %s\n' "${tasks_file}" + printf 'IMPL_PLAN: %s\n' "${plan_file}" + printf 'SPECS_DIR: %s\n' "${feature_dir}" + printf 'BRANCH: %s\n' "${branch}" +fi +SPECIFY_SEED__specify_scripts_bash_setup_tasks_sh_EOF +read -r -d '' SPECIFY_SEED__specify_templates_constitution_template_md <<'SPECIFY_SEED__specify_templates_constitution_template_md_EOF' || true +# Project Constitution + +## Core Principles + +### I. Outcome-First Specifications + +Every feature begins with a specification that describes user-visible outcomes, +acceptance scenarios, non-goals, and success criteria before implementation +details. Ambiguity must be marked explicitly with `NEEDS CLARIFICATION`. + +### II. Plan Before Implementation + +Implementation work starts only after the plan identifies real project paths, +dependencies, validation commands, rollback considerations, and risks. Plans +must prefer established local patterns over new abstractions. + +### III. Tests and Validation Are Mandatory + +Each feature defines the smallest meaningful verification command before work +begins. Changes are not complete until those checks pass or the remaining gap is +documented with the exact reason validation could not run. + +### IV. Small, Reviewable Changes + +Tasks and PRs must be independently reviewable, revertable, and scoped to one +behavioral objective. Unrelated cleanup, broad refactors, and speculative +flexibility are not allowed inside feature work. + +### V. Durable Context Stays Current + +Specifications, plans, tasks, and durable project memory must reflect decisions +that affect future work. Do not leave important behavior only in chat logs, +temporary notes, or uncommitted local state. + +## Workflow + +1. `/speckit.specify` creates or updates `specs//spec.md`. +2. `/speckit.plan` creates `plan.md` and supporting design artifacts. +3. `/speckit.tasks` creates `tasks.md` from the approved plan. +4. Implementation follows tasks in dependency order, with tests close to the + behavior being changed. +5. Completion requires validation evidence and any relevant documentation + updates. + +## Governance + +This constitution overrides informal conventions. Changes to these principles +must be reviewed deliberately, with downstream templates and instructions +updated in the same change. + +**Version**: 1.0.0 +**Ratified**: {{DATE}} +**Last Amended**: {{DATE}} +SPECIFY_SEED__specify_templates_constitution_template_md_EOF +read -r -d '' SPECIFY_SEED__specify_templates_plan_template_md <<'SPECIFY_SEED__specify_templates_plan_template_md_EOF' || true +# Implementation Plan: {{FEATURE_NAME}} + +**Branch**: `{{FEATURE_NAME}}` | **Date**: {{DATE}} | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/{{FEATURE_NAME}}/spec.md` + +## Summary + +[Extract from feature spec: primary requirement + technical approach] + +## Technical Context + +**Language/Version**: [e.g. Kotlin 2.x, TypeScript 5.x or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g. Spring Boot, Vue, Postgres or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g. PostgreSQL, Redis, files or N/A] +**Testing**: [e.g. Gradle unit tests, Vitest, Playwright or NEEDS CLARIFICATION] +**Target Platform**: [e.g. k3s, browser, JVM service or NEEDS CLARIFICATION] +**Project Type**: [service/ui/platform/mixed] +**Performance Goals**: [domain-specific target or N/A] +**Constraints**: [domain-specific constraints or N/A] +**Scale/Scope**: [domain-specific scale or N/A] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [ ] No attribution is introduced in files, comments, commit text, or PR text +- [ ] Claude/Codex parity is preserved for any agent-facing behavior +- [ ] Rendered artifacts are updated by the owning renderer when source changes require it +- [ ] Small stacked PR boundary is clear and unrelated cleanup is excluded +- [ ] Verification command is identified for each touched area + +## Project Structure + +### Documentation + +```text +specs/{{FEATURE_NAME}}/ +|-- plan.md +|-- research.md +|-- data-model.md +|-- quickstart.md +|-- contracts/ +`-- tasks.md +``` + +### Source Code + +```text +# Fill with the actual paths this feature will touch. +``` + +**Structure Decision**: [Document the chosen source layout and real paths] + +## Phase 0: Outline & Research + +1. Extract unknowns from Technical Context into research tasks. +2. Capture existing repo patterns for touched paths. +3. Resolve all NEEDS CLARIFICATION items before design. + +**Output**: `research.md` + +## Phase 1: Design & Contracts + +1. Derive entities from the feature spec and document them in `data-model.md`. +2. Produce or update API/CLI/config contracts in `contracts/`. +3. Write `quickstart.md` with validation steps for the feature. +4. Re-run Constitution Check. + +**Output**: `data-model.md`, `contracts/*`, `quickstart.md` + +## Phase 2: Task Planning Approach + +Describe how `/speckit.tasks` should convert this plan into ordered, independently executable tasks. Do not create `tasks.md` manually during `/speckit.plan`. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +| --- | --- | --- | +| [Only if a constitution gate is intentionally violated] | [reason] | [why simpler option does not work] | + +## Progress Tracking + +**Phase Status**: + +- [ ] Phase 0: Research complete +- [ ] Phase 1: Design complete +- [ ] Phase 2: Task planning approach complete + +**Gate Status**: + +- [ ] Initial Constitution Check: PASS +- [ ] Post-Design Constitution Check: PASS +- [ ] All NEEDS CLARIFICATION resolved +SPECIFY_SEED__specify_templates_plan_template_md_EOF +read -r -d '' SPECIFY_SEED__specify_templates_spec_template_md <<'SPECIFY_SEED__specify_templates_spec_template_md_EOF' || true +# Feature Specification: {{FEATURE_NAME}} + +**Feature Branch**: `{{FEATURE_NAME}}` +**Created**: {{DATE}} +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - [Short Title] (Priority: P1) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value and why it comes first] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] +2. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 2 - [Short Title] (Priority: P2) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +--- + +### User Story 3 - [Short Title] (Priority: P3) + +[Describe the user journey in plain language] + +**Why this priority**: [Explain the value] + +**Independent Test**: [Describe how to verify this story independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [observable outcome] + +### Edge Cases + +- What happens when [boundary condition]? +- How does the system handle [error condition]? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST [specific capability] +- **FR-002**: System MUST [specific capability] +- **FR-003**: Users MUST be able to [key interaction] +- **FR-004**: System MUST [data requirement] +- **FR-005**: System MUST [observable behavior] + +*Example of marking unclear requirements:* + +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified] +- **FR-007**: System MUST retain [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation details] +- **[Entity 2]**: [What it represents, relationships to other entities] + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: [Metric, e.g. "Users can complete primary task in under 2 minutes"] +- **SC-002**: [Metric, e.g. "System supports 1,000 concurrent users"] +- **SC-003**: [Metric, e.g. "95% of users complete the task without support"] +- **SC-004**: [Metric, e.g. "Reduce support tickets about X by 50%"] +SPECIFY_SEED__specify_templates_spec_template_md_EOF +read -r -d '' SPECIFY_SEED__specify_templates_tasks_template_md <<'SPECIFY_SEED__specify_templates_tasks_template_md_EOF' || true +# Tasks: {{FEATURE_NAME}} + +**Input**: Design documents from `/specs/{{FEATURE_NAME}}/` +**Prerequisites**: plan.md (required), research.md, data-model.md, contracts/ + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel with other tasks because it touches different files +- **[Story]**: User story label, for example US1, US2, US3 +- Include exact file paths in descriptions + +## Phase 1: Setup + +- [ ] T001 Create or verify project structure for this feature +- [ ] T002 Identify the smallest validation command for touched area + +## Phase 2: Foundational + +- [ ] T003 Implement shared models/configuration needed by all stories +- [ ] T004 Add or update base tests for cross-story behavior + +## Phase 3: User Story 1 (Priority: P1) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T005 [US1] Implement [specific behavior] in [path] +- [ ] T006 [US1] Add focused tests in [path] + +## Phase 4: User Story 2 (Priority: P2) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T007 [P] [US2] Implement [specific behavior] in [path] +- [ ] T008 [P] [US2] Add focused tests in [path] + +## Phase 5: User Story 3 (Priority: P3) + +**Goal**: [Brief value delivered by this story] + +**Independent Test**: [How to verify only this story] + +- [ ] T009 [P] [US3] Implement [specific behavior] in [path] +- [ ] T010 [P] [US3] Add focused tests in [path] + +## Phase 6: Polish + +- [ ] T011 Run the validation command identified in plan.md +- [ ] T012 Update docs or runbooks affected by this feature + +## Dependencies + +- Setup before foundational work +- Foundational work before user stories +- User stories may proceed in priority order, unless marked independent and parallel +- Polish after desired stories are complete + +## Parallel Example + +```text +T007 [P] [US2] ... +T009 [P] [US3] ... +``` +SPECIFY_SEED__specify_templates_tasks_template_md_EOF +if [ "${SCOPE}" = "project" ]; then + if [ ! -e "${PROJECT_ROOT}/.specify/memory/constitution.md" ]; then + write_file "${PROJECT_ROOT}/.specify/memory/constitution.md" 0644 "${SPECIFY_SEED__specify_memory_constitution_md}" + else + log "preserving existing ${PROJECT_ROOT}/.specify/memory/constitution.md" + fi + write_file "${PROJECT_ROOT}/.specify/scripts/bash/check-prerequisites.sh" 0755 "${SPECIFY_SEED__specify_scripts_bash_check_prerequisites_sh}" + write_file "${PROJECT_ROOT}/.specify/scripts/bash/common.sh" 0755 "${SPECIFY_SEED__specify_scripts_bash_common_sh}" + write_file "${PROJECT_ROOT}/.specify/scripts/bash/create-new-feature.sh" 0755 "${SPECIFY_SEED__specify_scripts_bash_create_new_feature_sh}" + write_file "${PROJECT_ROOT}/.specify/scripts/bash/setup-plan.sh" 0755 "${SPECIFY_SEED__specify_scripts_bash_setup_plan_sh}" + write_file "${PROJECT_ROOT}/.specify/scripts/bash/setup-tasks.sh" 0755 "${SPECIFY_SEED__specify_scripts_bash_setup_tasks_sh}" + write_file "${PROJECT_ROOT}/.specify/templates/constitution-template.md" 0644 "${SPECIFY_SEED__specify_templates_constitution_template_md}" + write_file "${PROJECT_ROOT}/.specify/templates/plan-template.md" 0644 "${SPECIFY_SEED__specify_templates_plan_template_md}" + write_file "${PROJECT_ROOT}/.specify/templates/spec-template.md" 0644 "${SPECIFY_SEED__specify_templates_spec_template_md}" + write_file "${PROJECT_ROOT}/.specify/templates/tasks-template.md" 0644 "${SPECIFY_SEED__specify_templates_tasks_template_md}" +fi + # ----------------------------------------------------------------- # Manifest # ----------------------------------------------------------------- diff --git a/specs/.gitkeep b/specs/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/specs/.gitkeep @@ -0,0 +1 @@ +