From 66bcd5b97ffc5a8ec62219846573709e3c8ff95e Mon Sep 17 00:00:00 2001 From: HelloThisWorld Date: Wed, 15 Jul 2026 20:13:00 +0800 Subject: [PATCH] feat(templates): add reusable spec template system SpecBridge v0.7.0: secure, deterministic, offline-first templates for Kiro-compatible specs. - Versioned template manifest (specbridge-template.json, schema 1.0.0) with strict validation and stable error codes SBT001-SBT025 - Restricted one-pass {{variable}} renderer: no code execution, no expressions, no environment access, no network, no recursion - Ten built-in templates embedded at build time; project-local packs under .specbridge/templates with atomic script-free local install - template list/search/show/validate/preview/apply/install/uninstall/ scaffold CLI, spec new --template, append-only template records - MCP template_list/search/show/preview plus candidate-hash-bound template_apply; /specbridge:templates plugin skill (10 skills total) - Generated template gallery and embedded-pack module with CI drift checks; template documentation set and threat model --- .claude-plugin/marketplace.json | 2 +- .github/workflows/ci.yml | 6 + CHANGELOG.md | 84 + README.md | 67 +- docs/claude-code-plugin.md | 9 + docs/cli-mcp-parity.md | 5 +- docs/creating-templates.md | 113 + docs/mcp-server.md | 5 + docs/mcp-tools.md | 5 + docs/plugin-security.md | 2 +- docs/roadmap.md | 25 +- docs/security.md | 12 + docs/template-contribution-guide.md | 120 + docs/template-installation.md | 93 + docs/template-manifest.md | 247 + docs/template-rendering.md | 108 + docs/template-security.md | 157 + docs/templates.md | 96 + integrations/claude-code-plugin/package.json | 2 +- .../specbridge/.claude-plugin/plugin.json | 2 +- .../specbridge/dist/checksums.json | 10 +- .../specbridge/dist/cli.cjs | 5253 ++++++++++++++--- .../specbridge/dist/mcp-server.cjs | 2622 +++++++- .../specbridge/skills/templates/SKILL.md | 58 + package.json | 6 +- packages/cli/package.json | 3 +- packages/cli/src/cli.ts | 2 + packages/cli/src/commands/spec-new.ts | 109 +- packages/cli/src/commands/template.ts | 855 +++ packages/cli/src/version.ts | 2 +- packages/compat-kiro/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/errors.ts | 1 + packages/drift/package.json | 2 +- packages/evidence/package.json | 2 +- packages/execution/package.json | 2 +- packages/mcp-server/package.json | 3 +- packages/mcp-server/src/errors.ts | 3 + packages/mcp-server/src/tools/registry.ts | 15 + .../mcp-server/src/tools/template-apply.ts | 132 + .../mcp-server/src/tools/template-list.ts | 64 + .../mcp-server/src/tools/template-preview.ts | 137 + .../mcp-server/src/tools/template-search.ts | 50 + .../mcp-server/src/tools/template-shared.ts | 84 + .../mcp-server/src/tools/template-show.ts | 104 + packages/mcp-server/src/version.ts | 2 +- packages/reporting/package.json | 2 +- packages/runners/package.json | 2 +- .../builtins/authentication/README.md | 42 + .../authentication/files/design.md.template | 92 + .../files/requirements.md.template | 76 + .../authentication/files/tasks.md.template | 12 + .../authentication/specbridge-template.json | 56 + .../builtins/background-job/README.md | 39 + .../background-job/files/design.md.template | 87 + .../files/requirements.md.template | 66 + .../background-job/files/tasks.md.template | 12 + .../background-job/specbridge-template.json | 48 + .../builtins/bugfix-regression/README.md | 37 + .../files/bugfix.md.template | 49 + .../files/design.md.template | 35 + .../bugfix-regression/files/tasks.md.template | 11 + .../specbridge-template.json | 56 + .../templates/builtins/cli-tool/README.md | 39 + .../cli-tool/files/design.md.template | 74 + .../cli-tool/files/requirements.md.template | 75 + .../builtins/cli-tool/files/tasks.md.template | 12 + .../cli-tool/specbridge-template.json | 55 + .../builtins/database-migration/README.md | 40 + .../files/design.md.template | 94 + .../files/requirements.md.template | 77 + .../files/tasks.md.template | 12 + .../specbridge-template.json | 48 + .../builtins/event-driven-service/README.md | 40 + .../files/design.md.template | 99 + .../files/requirements.md.template | 67 + .../files/tasks.md.template | 12 + .../specbridge-template.json | 56 + .../performance-optimization/README.md | 39 + .../files/design.md.template | 94 + .../files/requirements.md.template | 66 + .../files/tasks.md.template | 12 + .../specbridge-template.json | 55 + .../templates/builtins/refactoring/README.md | 40 + .../refactoring/files/design.md.template | 103 + .../files/requirements.md.template | 75 + .../refactoring/files/tasks.md.template | 12 + .../refactoring/specbridge-template.json | 48 + .../templates/builtins/rest-api/README.md | 41 + .../rest-api/files/design.md.template | 83 + .../rest-api/files/requirements.md.template | 65 + .../builtins/rest-api/files/tasks.md.template | 12 + .../rest-api/specbridge-template.json | 62 + .../builtins/security-hardening/README.md | 42 + .../files/design.md.template | 95 + .../files/requirements.md.template | 76 + .../files/tasks.md.template | 12 + .../specbridge-template.json | 55 + packages/templates/package.json | 36 + packages/templates/src/apply-service.ts | 328 + .../templates/src/builtin-packs.generated.ts | 118 + packages/templates/src/catalog.ts | 245 + packages/templates/src/errors.ts | 69 + packages/templates/src/ids.ts | 90 + packages/templates/src/index.ts | 16 + packages/templates/src/install.ts | 297 + packages/templates/src/manifest.ts | 504 ++ packages/templates/src/pack.ts | 514 ++ packages/templates/src/records.ts | 182 + packages/templates/src/renderer.ts | 115 + packages/templates/src/scaffold.ts | 484 ++ packages/templates/src/search.ts | 77 + packages/templates/src/semver-range.ts | 87 + packages/templates/src/types.ts | 46 + packages/templates/src/variables.ts | 212 + packages/templates/src/version.ts | 8 + packages/templates/tsconfig.json | 7 + packages/templates/tsup.config.ts | 10 + packages/workflow/package.json | 2 +- packages/workflow/src/create-spec.ts | 65 +- pnpm-lock.yaml | 28 + scripts/generate-builtin-templates.mjs | 115 + scripts/generate-template-gallery.mjs | 100 + scripts/smoke.mjs | 2 +- scripts/validate-plugin.mjs | 2 +- scripts/verify-plugin-bundle.mjs | 43 +- tests/cli/cli-v07-template.test.ts | 352 ++ tests/helpers-templates.ts | 119 + tests/mcp/mcp-server.test.ts | 2 +- tests/mcp/mcp-stdio-process.test.ts | 4 +- tests/mcp/mcp-template-tools.test.ts | 229 + tests/plugin/plugin.test.ts | 43 +- tests/templates/apply.test.ts | 277 + tests/templates/builtin-packs.test.ts | 133 + tests/templates/catalog.test.ts | 188 + tests/templates/install-scaffold.test.ts | 250 + tests/templates/manifest.test.ts | 250 + tests/templates/pack-security.test.ts | 159 + tests/templates/renderer.test.ts | 172 + tsconfig.json | 1 + vitest.config.ts | 1 + 141 files changed, 18046 insertions(+), 1230 deletions(-) create mode 100644 docs/creating-templates.md create mode 100644 docs/template-contribution-guide.md create mode 100644 docs/template-installation.md create mode 100644 docs/template-manifest.md create mode 100644 docs/template-rendering.md create mode 100644 docs/template-security.md create mode 100644 docs/templates.md create mode 100644 integrations/claude-code-plugin/specbridge/skills/templates/SKILL.md create mode 100644 packages/cli/src/commands/template.ts create mode 100644 packages/mcp-server/src/tools/template-apply.ts create mode 100644 packages/mcp-server/src/tools/template-list.ts create mode 100644 packages/mcp-server/src/tools/template-preview.ts create mode 100644 packages/mcp-server/src/tools/template-search.ts create mode 100644 packages/mcp-server/src/tools/template-shared.ts create mode 100644 packages/mcp-server/src/tools/template-show.ts create mode 100644 packages/templates/builtins/authentication/README.md create mode 100644 packages/templates/builtins/authentication/files/design.md.template create mode 100644 packages/templates/builtins/authentication/files/requirements.md.template create mode 100644 packages/templates/builtins/authentication/files/tasks.md.template create mode 100644 packages/templates/builtins/authentication/specbridge-template.json create mode 100644 packages/templates/builtins/background-job/README.md create mode 100644 packages/templates/builtins/background-job/files/design.md.template create mode 100644 packages/templates/builtins/background-job/files/requirements.md.template create mode 100644 packages/templates/builtins/background-job/files/tasks.md.template create mode 100644 packages/templates/builtins/background-job/specbridge-template.json create mode 100644 packages/templates/builtins/bugfix-regression/README.md create mode 100644 packages/templates/builtins/bugfix-regression/files/bugfix.md.template create mode 100644 packages/templates/builtins/bugfix-regression/files/design.md.template create mode 100644 packages/templates/builtins/bugfix-regression/files/tasks.md.template create mode 100644 packages/templates/builtins/bugfix-regression/specbridge-template.json create mode 100644 packages/templates/builtins/cli-tool/README.md create mode 100644 packages/templates/builtins/cli-tool/files/design.md.template create mode 100644 packages/templates/builtins/cli-tool/files/requirements.md.template create mode 100644 packages/templates/builtins/cli-tool/files/tasks.md.template create mode 100644 packages/templates/builtins/cli-tool/specbridge-template.json create mode 100644 packages/templates/builtins/database-migration/README.md create mode 100644 packages/templates/builtins/database-migration/files/design.md.template create mode 100644 packages/templates/builtins/database-migration/files/requirements.md.template create mode 100644 packages/templates/builtins/database-migration/files/tasks.md.template create mode 100644 packages/templates/builtins/database-migration/specbridge-template.json create mode 100644 packages/templates/builtins/event-driven-service/README.md create mode 100644 packages/templates/builtins/event-driven-service/files/design.md.template create mode 100644 packages/templates/builtins/event-driven-service/files/requirements.md.template create mode 100644 packages/templates/builtins/event-driven-service/files/tasks.md.template create mode 100644 packages/templates/builtins/event-driven-service/specbridge-template.json create mode 100644 packages/templates/builtins/performance-optimization/README.md create mode 100644 packages/templates/builtins/performance-optimization/files/design.md.template create mode 100644 packages/templates/builtins/performance-optimization/files/requirements.md.template create mode 100644 packages/templates/builtins/performance-optimization/files/tasks.md.template create mode 100644 packages/templates/builtins/performance-optimization/specbridge-template.json create mode 100644 packages/templates/builtins/refactoring/README.md create mode 100644 packages/templates/builtins/refactoring/files/design.md.template create mode 100644 packages/templates/builtins/refactoring/files/requirements.md.template create mode 100644 packages/templates/builtins/refactoring/files/tasks.md.template create mode 100644 packages/templates/builtins/refactoring/specbridge-template.json create mode 100644 packages/templates/builtins/rest-api/README.md create mode 100644 packages/templates/builtins/rest-api/files/design.md.template create mode 100644 packages/templates/builtins/rest-api/files/requirements.md.template create mode 100644 packages/templates/builtins/rest-api/files/tasks.md.template create mode 100644 packages/templates/builtins/rest-api/specbridge-template.json create mode 100644 packages/templates/builtins/security-hardening/README.md create mode 100644 packages/templates/builtins/security-hardening/files/design.md.template create mode 100644 packages/templates/builtins/security-hardening/files/requirements.md.template create mode 100644 packages/templates/builtins/security-hardening/files/tasks.md.template create mode 100644 packages/templates/builtins/security-hardening/specbridge-template.json create mode 100644 packages/templates/package.json create mode 100644 packages/templates/src/apply-service.ts create mode 100644 packages/templates/src/builtin-packs.generated.ts create mode 100644 packages/templates/src/catalog.ts create mode 100644 packages/templates/src/errors.ts create mode 100644 packages/templates/src/ids.ts create mode 100644 packages/templates/src/index.ts create mode 100644 packages/templates/src/install.ts create mode 100644 packages/templates/src/manifest.ts create mode 100644 packages/templates/src/pack.ts create mode 100644 packages/templates/src/records.ts create mode 100644 packages/templates/src/renderer.ts create mode 100644 packages/templates/src/scaffold.ts create mode 100644 packages/templates/src/search.ts create mode 100644 packages/templates/src/semver-range.ts create mode 100644 packages/templates/src/types.ts create mode 100644 packages/templates/src/variables.ts create mode 100644 packages/templates/src/version.ts create mode 100644 packages/templates/tsconfig.json create mode 100644 packages/templates/tsup.config.ts create mode 100644 scripts/generate-builtin-templates.mjs create mode 100644 scripts/generate-template-gallery.mjs create mode 100644 tests/cli/cli-v07-template.test.ts create mode 100644 tests/helpers-templates.ts create mode 100644 tests/mcp/mcp-template-tools.test.ts create mode 100644 tests/templates/apply.test.ts create mode 100644 tests/templates/builtin-packs.test.ts create mode 100644 tests/templates/catalog.test.ts create mode 100644 tests/templates/install-scaffold.test.ts create mode 100644 tests/templates/manifest.test.ts create mode 100644 tests/templates/pack-security.test.ts create mode 100644 tests/templates/renderer.test.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b4a2ac9..71baa76 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "specbridge", "source": "./integrations/claude-code-plugin/specbridge", "description": "Kiro-compatible spec workflows, verified interactive task execution, and deterministic drift checks.", - "version": "0.6.1", + "version": "0.7.0", "license": "MIT", "keywords": [ "spec-driven-development", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c294e2..13d6872 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,12 @@ jobs: - name: CLI smoke test against the example Kiro workspace run: node scripts/smoke.mjs + - name: Built-in template module matches the packs on disk + run: pnpm check:builtin-templates + + - name: Template gallery matches the built-in manifests + run: pnpm check:template-gallery + - name: GitHub Action bundle is reproducible if: matrix.os == 'ubuntu-latest' run: git diff --exit-code integrations/github-action/dist diff --git a/CHANGELOG.md b/CHANGELOG.md index de5dbe2..61271c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,89 @@ # Changelog +## 0.7.0 + +Added: + +- Versioned template manifest (`specbridge-template.json`, schema 1.0.0) + with strict validation: template IDs, semver versions, kinds, workflow + modes, file sets, typed variables (string/boolean/integer/enum with + constraints), compatibility ranges, and safe optional metadata. +- Restricted deterministic template renderer: `{{variableName}}` + substitution only — one pass, no expressions, no conditionals, no + includes, no environment access, values never re-scanned. +- Built-in template catalog bundled with SpecBridge (immutable at runtime, + embedded at build time so every bundle ships it). +- Project-local template packs under `.specbridge/templates//`. +- Deterministic local template search over IDs, display names, + descriptions, and tags (exact ID > ID prefix > exact tag > display-name + token > description token; no model, no network). +- `template list | search | show | validate | preview | apply` CLI + commands; preview and `apply --dry-run` share the exact rendering path + with apply and write nothing. +- Local template installation and uninstallation + (`template install ` / `template uninstall project:`): + validated, script-free, atomic (temp directory + rename), never + overwriting; built-in templates cannot be uninstalled. +- `template scaffold` — generates a complete community-ready template pack + (manifest, README with validation instructions and a contribution + checklist, plain-Markdown template files); no TypeScript required. +- `spec new --template [--var key=value]`, delegating to the + same template application service (existing non-template `spec new` + behavior unchanged). +- Append-only template operation records in + `.specbridge/template-records.jsonl` (apply/install/uninstall/scaffold) + storing variable names and rendered-content hashes, never values. +- MCP template tools: `template_list`, `template_search`, `template_show`, + `template_preview` (read-only), and `template_apply` (candidate-hash + bound, acknowledgement-gated). Install/uninstall/scaffold remain + CLI-only. +- Claude Code `/specbridge:templates` Skill: list/search/show/preview, and + apply only after explicit confirmation with the previewed candidate + hash. +- Generated template gallery in `docs/templates.md` + (`pnpm generate:template-gallery`) with a CI drift check + (`pnpm check:template-gallery`); built-in packs are likewise embedded via + `pnpm generate:builtin-templates` with `pnpm check:builtin-templates`. +- Template contribution workflow and documentation + (`docs/creating-templates.md`, `docs/template-manifest.md`, + `docs/template-rendering.md`, `docs/template-security.md`, + `docs/template-installation.md`, `docs/template-contribution-guide.md`). +- Stable template error codes SBT001–SBT025 with remediation in every + message. + +Built-in templates: + +- REST API (`rest-api`) +- CLI tool (`cli-tool`) +- Database migration (`database-migration`) +- Authentication (`authentication`) +- Background job (`background-job`) +- Event-driven service (`event-driven-service`) +- Bugfix regression (`bugfix-regression`) +- Performance optimization (`performance-optimization`) +- Security hardening (`security-hardening`) +- Refactoring (`refactoring`) + +Security: + +- No executable template code, lifecycle scripts, or shell execution. +- No environment interpolation and no network access anywhere in the + template system (no remote registry, no URL or npm installation). +- Path traversal and symlinks rejected; targets restricted to the exact + Kiro spec file set; variables never allowed in target paths. +- One-pass rendering: substituted values are never re-rendered. +- Bounded packs and output (20 files, 256 KB manifest, 1 MB per template + file, 5 MB per pack, 1 MB per rendered document). +- Candidate-hash binding and an explicit acknowledgement for MCP apply. +- Atomic installation and atomic spec creation; existing specs are never + overwritten; generated stages always start unapproved. + +Deferred to v0.7.1: + +- Extension/plugin SDK, runner SDK distribution, analyzer/verifier/exporter + SDKs. +- Remote extension registry and community ecosystem index. + ## 0.6.1 Added: diff --git a/README.md b/README.md index ea0dda2..7781e18 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,33 @@ Codex, local models, or any supported coding agent. > Your `.kiro` specs remain the source of truth. -New in v0.6.1 — keep your existing `.kiro` specs and **choose a compatible +New in v0.7.0 — **reusable spec templates, without executable generators or +platform lock-in**: + +```bash +specbridge template search migration + +specbridge template preview database-migration \ + --name add-payment-status-index \ + --var tableName=payments + +specbridge template apply database-migration \ + --name add-payment-status-index \ + --var tableName=payments +``` + +Ten built-in templates ship with SpecBridge — `rest-api`, `cli-tool`, +`database-migration`, `authentication`, `background-job`, +`event-driven-service`, `bugfix-regression`, `performance-optimization`, +`security-hardening`, `refactoring` — plus project-local packs, local +installation, and `template scaffold` for community templates. Templates +are plain Markdown plus a JSON manifest: rendering is deterministic and +offline (`{{variable}}` substitution only — no scripts, no network, no +model), preview writes nothing, apply never overwrites an existing spec, +and generated stages start unapproved like every other spec. See +[docs/templates.md](docs/templates.md). + +From v0.6.1 — keep your existing `.kiro` specs and **choose a compatible coding agent or authoring model per operation**: ```text @@ -261,12 +287,16 @@ Working today (fully offline, no model, no API key): | `specbridge spec affected` | **v0.4** — which specs does this change set touch (read-only) | | `specbridge spec policy init / show / validate` | **v0.4** — per-spec verification policies (impact areas, required commands, rule overrides) | | `specbridge verify rules / explain ` | **v0.4** — inspect the stable rule registry SBV001–SBV025 | -| `specbridge mcp serve / doctor / manifest / tools` | **v0.5** — local stdio MCP server (25 tools since v0.6.1, 7 resources, 4 prompts) | +| `specbridge mcp serve / doctor / manifest / tools` | **v0.5** — local stdio MCP server (30 tools since v0.7.0, 7 resources, 4 prompts) | | `specbridge run recover-lock` | **v0.5** — diagnose and explicitly recover the interactive execution lock | | `specbridge runner list / matrix / show / doctor` | **v0.6** — profile-based runner diagnostics and the generated capability matrix (read-only) | | `specbridge runner test / conformance / models ` | **v0.6** — bounded structured-output probe (`--network`), conformance suite, provider-supported model listing | | `specbridge config doctor / migrate` | **v0.6** — configuration validation and the explicit v1 → v2 migration (dry-run by default, atomic apply with backup) | | `specbridge runner doctor gemini-default / openai-compatible-local / antigravity` | **v0.6.1** — diagnostics for the new adapters; MCP `runner_list` / `runner_show` / `runner_doctor` / `runner_matrix` expose the same read-only services | +| `specbridge template list / search / show / validate` | **v0.7.0** — deterministic, offline template discovery and validation (read-only) | +| `specbridge template preview / apply [--dry-run]` | **v0.7.0** — one rendering path; preview writes nothing, apply is atomic and never overwrites | +| `specbridge template install / uninstall / scaffold` | **v0.7.0** — local, script-free pack installation and community-template scaffolding | +| `specbridge spec new --template --var k=v` | **v0.7.0** — template-based spec creation through the same service | Planned commands (`spec sync/export`) are registered, marked "(planned)" in `--help`, and exit with an honest error — see the [roadmap](docs/roadmap.md). @@ -549,7 +579,7 @@ stores no credentials of any kind. secrets or environment variables. - Full model: [docs/security.md](docs/security.md). -## Limitations (v0.6.1) +## Limitations (v0.7.0) - The MCP server is stdio-only and local-only: no HTTP/SSE/WebSocket transport, no OAuth, no cloud hosting. One server process serves one @@ -570,6 +600,13 @@ stores no credentials of any kind. references, chore-task exclusion) are labelled and never default to error. - `spec sync` and `spec export` are not implemented yet (they fail honestly). SARIF output is deferred. +- Templates are local-only: there is no remote registry, no GitHub/npm/URL + installation, and no signed packs in v0.7.0. Template rendering is plain + `{{variable}}` substitution — no expressions, no conditionals, and + literal double braces are not supported in template files. Rendered + Markdown can still contain untrusted prose; SpecBridge control rules + (approvals, protected paths, verification) are never overridable by + template content. - Production runners are claude-code, codex-cli, gemini-cli, ollama (authoring-only), openai-compatible (authoring-only), and mock; the antigravity-cli adapter is experimental detection only. Provider usage @@ -623,13 +660,16 @@ deterministic drift verification (rule engine SBV001–SBV025, policies, affected-spec resolution, evidence freshness, four report formats) and the production GitHub Action. v0.5: the local stdio MCP server, direct interactive task execution, and the self-contained Claude Code plugin with -its repository-local marketplace. v0.6.0 (this release): the -capability-driven runner platform with a frozen adapter contract, runner -profiles and explicit configuration migration, deterministic selection and -bounded authoring fallback, the conformance framework, and the production -Codex CLI and Ollama (authoring-only) runners. Next — v0.6.1: Gemini CLI, -OpenAI-compatible authoring, Antigravity, MCP runner diagnostics, and the -runner-management Skill. v0.7: templates, plugin SDK, extension registry, +its repository-local marketplace. v0.6.0: the capability-driven runner +platform with a frozen adapter contract, runner profiles and explicit +configuration migration, deterministic selection and bounded authoring +fallback, the conformance framework, and the production Codex CLI and +Ollama (authoring-only) runners. v0.6.1: Gemini CLI, OpenAI-compatible +authoring, Antigravity, MCP runner diagnostics, and the runner-management +Skill. v0.7.0 (this release): the offline template system — versioned +manifests, a restricted deterministic renderer, ten built-in templates, +project-local packs, local installation, scaffolding, MCP template tools, +and the templates Skill. Next — v0.7.1: plugin SDK, extension registry, community ecosystem. Full detail: [docs/roadmap.md](docs/roadmap.md). ## Documentation @@ -638,6 +678,13 @@ community ecosystem. Full detail: [docs/roadmap.md](docs/roadmap.md). [Kiro compatibility](docs/kiro-compatibility.md) · [Spec authoring](docs/spec-authoring.md) · [Spec analysis](docs/spec-analysis.md) · +[Templates](docs/templates.md) · +[Creating templates](docs/creating-templates.md) · +[Template manifest](docs/template-manifest.md) · +[Template rendering](docs/template-rendering.md) · +[Template installation](docs/template-installation.md) · +[Template security](docs/template-security.md) · +[Template contribution guide](docs/template-contribution-guide.md) · [Approval workflow](docs/approval-workflow.md) · [Sidecar state](docs/sidecar-state.md) · [Runners (v0.6)](docs/runners.md) · diff --git a/docs/claude-code-plugin.md b/docs/claude-code-plugin.md index b8084c0..4eb8250 100644 --- a/docs/claude-code-plugin.md +++ b/docs/claude-code-plugin.md @@ -22,6 +22,8 @@ integrations/claude-code-plugin/specbridge/ │ ├── approve/SKILL.md /specbridge:approve (human-only) │ ├── implement/SKILL.md /specbridge:implement [task] │ ├── continue/SKILL.md /specbridge:continue +│ ├── runners/SKILL.md /specbridge:runners [profile] +│ ├── templates/SKILL.md /specbridge:templates [query | show … | apply …] │ └── verify/SKILL.md /specbridge:verify [spec] ├── bin/ │ ├── specbridge POSIX wrapper → dist/cli.cjs @@ -87,6 +89,13 @@ and controlled lifecycle operations and never duplicate core logic: never sends a network request itself, and never starts a login. The existing implementation workflow is unchanged: `task_begin` → the current Claude Code session edits → `task_complete`. +- `templates` (v0.7.0) discovers templates with `template_list`/ + `template_search`, inspects with `template_show`, always previews with + `template_preview`, and applies only after explicit user confirmation via + `template_apply` with the previewed `candidateHash` and the + `"apply-reviewed-template"` acknowledgement. It never installs, + uninstalls, or scaffolds templates (CLI-only operations), never renders + content itself, and never edits `.kiro` or `.specbridge` directly. No skill uses `bypassPermissions`, `dangerously-skip-permissions`, unrestricted `Bash(*)`, or unrestricted `Write`, and no skill instructs diff --git a/docs/cli-mcp-parity.md b/docs/cli-mcp-parity.md index 444aad0..e945784 100644 --- a/docs/cli-mcp-parity.md +++ b/docs/cli-mcp-parity.md @@ -11,7 +11,10 @@ over both. This table maps every capability to its surfaces: | List/read specs | `spec list/show` | `spec_list`, `spec_read` | `status` | | Spec status | `spec status` | `spec_status` | `status` | | Agent context | `spec context` | `spec_context` | (used internally) | -| Create templates | `spec new` | `spec_create` (preview→apply) | `new` | +| Create a spec | `spec new` | `spec_create` (preview→apply) | `new` | +| Discover templates | `template list/search/show/validate` | `template_list`, `template_search`, `template_show` | `templates` | +| Apply a template | `template preview/apply`, `spec new --template` | `template_preview` + `template_apply` (hash-bound) | `templates` (after confirmation) | +| Manage template packs | `template install/uninstall/scaffold` | **no** (deliberately CLI-only) | **no** | | Analyze spec | `spec analyze` | `spec_analyze` | `author`/`status` | | Apply authored stage | `spec generate/refine` (runner-drafted) | `spec_stage_validate` + `spec_stage_apply` (session-drafted) | `author` | | **Approve stage** | `spec approve` | **no direct model tool** | `approve` (human-invoked CLI) | diff --git a/docs/creating-templates.md b/docs/creating-templates.md new file mode 100644 index 0000000..ea90c05 --- /dev/null +++ b/docs/creating-templates.md @@ -0,0 +1,113 @@ +# Creating templates + +This is the contributor happy path: scaffold a template pack, edit plain +Markdown, validate, try it locally, share it. No TypeScript is ever +required — a template pack is a JSON manifest plus Markdown files, and +that is the whole format. For what templates are and what they cannot do, +start with [the template overview](templates.md). + +## 1. Scaffold a pack + +```bash +specbridge template scaffold my-template --kind feature --output ./my-template +``` + +This generates a complete, already-valid pack: + +``` +my-template/ +├── specbridge-template.json # the manifest +├── README.md # usage, variables table, checklist +└── files/ + ├── requirements.md.template + ├── design.md.template + └── tasks.md.template +``` + +A `--kind bugfix` scaffold generates `files/bugfix.md.template` instead of +`files/requirements.md.template` — the file set always mirrors the full +Kiro layout for the kind, nothing more and nothing less: + +| Kind | Rendered files | +| --- | --- | +| `feature` | `requirements.md`, `design.md`, `tasks.md` | +| `bugfix` | `bugfix.md`, `design.md`, `tasks.md` | + +Useful scaffold options: `--modes` (comma-separated workflow modes), +`--display-name`, `--description`, `--license` (default `MIT`), and +`--dry-run` to list the files without writing. Scaffolding works outside a +SpecBridge workspace too — you do not need a `.kiro` project to author a +template — and it never overwrites an existing directory. + +## 2. Edit the template files and manifest + +Template files are plain Markdown with `{{variable}}` placeholders. There +are no scripts, expressions, conditionals, or loops — the +[rendering rules](template-rendering.md) are deliberately small. The +scaffold starts you with the built-in variables (`{{title}}`, +`{{description}}`) and one example variable (`actor`); declare your own in +the manifest's `variables` array. Use `` placeholders for +content the spec author fills in by hand after applying. + +Everything the manifest can say — IDs, variables, constraints, file +entries, compatibility — is specified in the +[manifest reference](template-manifest.md). + +## 3. Validate + +```bash +specbridge template validate ./my-template + +# Treat warnings (missing README, stylistic render findings) as failures: +specbridge template validate ./my-template --strict +``` + +Validation checks the pack structure, the manifest, every declared file, +and a full render with deterministic sample values, and reports every +issue at once with a stable `SBT` code and a category (`manifest`, +`variables`, `rendering`, `kiro-layout`, …). Add `--json` for a +machine-readable report. + +## 4. Install and preview locally + +```bash +specbridge template install ./my-template + +specbridge template preview project:my-template --name example-spec +``` + +Install copies the validated pack into `.specbridge/templates/my-template/` +(atomically, never overwriting — see +[installation](template-installation.md)). Preview renders everything and +writes nothing; `template apply --dry-run` does the same through the exact +same rendering path. When the output looks right: + +```bash +specbridge template apply project:my-template --name my-first-real-spec +``` + +The `project:` prefix is only mandatory when the same ID also exists as a +built-in; an unambiguous ID works unqualified. + +## 5. Share it + +A template pack is just a directory of text files. To share it: + +- **Within a team**: commit the directory to your repository (anywhere + inside it) and let teammates run + `specbridge template install ./path/to/my-template`. There is no remote + registry, URL, or npm installation in v0.7.0 — installation reads a + local directory inside the repository, and nothing else. A community + index is deferred to v0.7.1+ per the [roadmap](roadmap.md). +- **As a built-in**: open a pull request adding the pack under + `packages/templates/builtins/` in this repository — see the + [contribution guide](template-contribution-guide.md). + +## Related documentation + +- [Template overview](templates.md) +- [Manifest reference](template-manifest.md) +- [Rendering rules](template-rendering.md) +- [Installation](template-installation.md) +- [Security](template-security.md) +- [Contribution guide](template-contribution-guide.md) diff --git a/docs/mcp-server.md b/docs/mcp-server.md index 34c01b0..6a04692 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -32,6 +32,11 @@ specbridge mcp manifest [--json] # identity + capability counts specbridge mcp tools [--json] [--verbose] # tool/resource/prompt catalog ``` +v0.7.0 adds five template tools: read-only `template_list`, +`template_search`, `template_show`, and `template_preview`, plus the +candidate-hash-bound `template_apply` (acknowledgement-gated, atomic, +never overwriting). Template install/uninstall/scaffold remain CLI-only. + v0.6.1 adds four read-only runner diagnostic tools (`runner_list`, `runner_show`, `runner_doctor`, `runner_matrix`) — thin adapters over the same shared runner services the CLI uses; see [mcp-tools.md](mcp-tools.md). diff --git a/docs/mcp-tools.md b/docs/mcp-tools.md index a514a1a..bc3b9dc 100644 --- a/docs/mcp-tools.md +++ b/docs/mcp-tools.md @@ -36,6 +36,10 @@ tool** (approval is a human CLI action — see | `runner_show` | v0.6.1: one profile in depth — redacted configuration, declared and detected capabilities, operation compatibility, invocation-free conformance summary, network boundary, limitations, remediation. | | `runner_doctor` | v0.6.1: runner diagnostics through the same shared detection the CLI uses. Never a model request, never a login, never a configuration change, never an interactive UI. | | `runner_matrix` | v0.6.1: the authoritative capability matrix (structured rows + Markdown), generated by the same shared implementation as `specbridge runner matrix`. | +| `template_list` | v0.7.0: built-in and project-local template summaries with validation status, filters (`source`, `kind`, `mode`, `tag`), and pagination. Deterministic and offline. | +| `template_search` | v0.7.0: deterministic local keyword search (exact ID > ID prefix > exact tag > display-name token > description token). No model, no network — not semantic search. | +| `template_show` | v0.7.0: one template in depth — metadata, typed variables, target files, README, validation issues. Qualified references only; no filesystem paths cross the boundary. | +| `template_preview` | v0.7.0: render a template application in memory — rendered files, diagnostics, target paths, the proposed sidecar state, and the binding `candidateHash`. Writes nothing. | The four runner tools are read-only adapters over the shared runner services: they log only to stderr, redact everything credential-shaped @@ -48,6 +52,7 @@ runner test request. | Tool | Purpose | | --- | --- | | `spec_create` | Offline Kiro-compatible spec templates. `apply: false` (default) is a pure preview; `apply: true` re-validates and creates atomically (temp dir + rename), refusing existing specs. | +| `template_apply` | v0.7.0: create a spec from a **previewed** template. Requires `expectedCandidateHash` from `template_preview` and the literal acknowledgement `"apply-reviewed-template"`; re-renders and refuses on hash mismatch (SBT023). Atomic creation through the shared spec-creation path, never overwrites (SBT020), all stages start unapproved, and an append-only template-apply record is written. Install/uninstall/scaffold are deliberately CLI-only. | | `spec_stage_apply` | Atomically apply a **previously validated** candidate. Requires `expectedCurrentHash`, `expectedCandidateHash`, and the literal acknowledgement `"apply-reviewed-candidate"`; refuses hash mismatches (SBMCP017/SBMCP002), analysis errors (SBMCP016), and approved stages. Invalidates dependent approvals per workflow rules, preserves line endings, records an append-only `interactive-authoring` run. There is **no force option**. The stage remains unapproved. | | `spec_run_verification` | Deterministic drift rules **plus** the trusted verification commands from `.specbridge/config.json` (argv arrays; never from tool arguments or spec content), with timeouts and output limits. `persistReport: true` opts into writing under `.specbridge/reports`. Not read-only (it executes trusted local commands) but never changes spec content, approvals, or evidence. | | `task_begin` | Begin an interactive task run (see [interactive-task-execution.md](interactive-task-execution.md)): validates approvals + tree, acquires the repository lock, snapshots Git, returns bounded context, boundaries, protected paths, expected verification commands, and explicit agent instructions. Modifies no source files, invokes no model. | diff --git a/docs/plugin-security.md b/docs/plugin-security.md index 6bdd530..e8de721 100644 --- a/docs/plugin-security.md +++ b/docs/plugin-security.md @@ -21,7 +21,7 @@ the v0.5 threat model in [security.md](security.md). ## The skills' permission surface -- Eight of the nine skills declare **no** `allowed-tools` at all: they use +- Nine of the ten skills declare **no** `allowed-tools` at all: they use the plugin's MCP tools under Claude Code's normal permission system. - The `approve` skill declares exactly one narrow allowance — `Bash("${CLAUDE_PLUGIN_ROOT}/bin/specbridge" spec approve *)` — for the diff --git a/docs/roadmap.md b/docs/roadmap.md index f8cf3b0..c34de0e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -23,6 +23,7 @@ implemented unless marked ✅ and covered by tests. | N — Capability-driven runner platform | versioned capability/operation/event/result/error contracts (frozen for v0.6.1 with snapshot tests), runner profiles, config schema v2 + explicit migration (`config doctor/migrate`), deterministic selection, explicit bounded authoring fallback, append-only attempt records, reusable conformance framework, `runner matrix/show/test/conformance/models` | ✅ v0.6.0 | | O — Production multi-runner | Codex CLI agent runner (read-only authoring sandbox, workspace-write execution, explicit-session resume, no unrestricted modes) and Ollama authoring runner (loopback-default model API, schema-validated structured output, bounded correction retry, authoring-only by capability); Claude Code runner migrated onto the shared contract unchanged | ✅ v0.6.0 | | P — Adapter expansion | Gemini CLI runner (plan-mode/allowlist authoring, capability-gated bounded-edit task execution, explicit-UUID resume, never YOLO), OpenAI-compatible authoring runner (chat-completions + responses, explicit structured-output modes, env-var-name credentials, safe redirects), experimental Antigravity capability adapter (detection only, no PTY/TUI automation), read-only MCP runner diagnostics (`runner_list/show/doctor/matrix`), `/specbridge:runners` plugin skill | ✅ v0.6.1 | +| Q — Templates | versioned template manifest (schema 1.0.0), restricted one-pass renderer, 10 built-in templates (embedded at build time), project-local packs, deterministic search, `template list/search/show/validate/preview/apply/install/uninstall/scaffold`, `spec new --template`, append-only template records, MCP `template_list/search/show/preview/apply` (hash-bound apply), `/specbridge:templates` skill, generated gallery with CI drift checks | ✅ v0.7.0 (local-only; no remote registry) | ## Command availability @@ -36,6 +37,7 @@ implemented unless marked ✅ and covered by tests. | `/specbridge:doctor·status·new·author·approve·implement·continue·verify` (plugin) | ✅ v0.5 | | `runner list/matrix/show/doctor/test/conformance/models`, `config doctor/migrate`, `spec generate/refine/run --runner `, `--show-runner-plan` | ✅ v0.6.0 — codex/ollama via your local installation; fake providers in CI | | `--runner gemini-default / openai-compatible-local`, `runner doctor antigravity`, MCP `runner_list/show/doctor/matrix`, `/specbridge:runners` | ✅ v0.6.1 — gemini/API endpoints via your own installation and accounts; fake providers in CI | +| `template list/search/show/validate/preview/apply/install/uninstall/scaffold`, `spec new --template`, MCP `template_list/search/show/preview/apply`, `/specbridge:templates` | ✅ v0.7.0 — fully offline and deterministic; local sources only | | `spec sync/export` | ❌ registered as "(planned)", exit 2 with an honest message | ## v0.6.1 (✅ implemented) @@ -58,9 +60,28 @@ values — every v0.6.0 snapshot test passes unchanged): - Claude Code `/specbridge:runners` Skill (MCP-diagnostics-driven, read-only). -## v0.7 (planned — not implemented) +## v0.7.0 (✅ implemented) + +Templates and template scaffolding — secure, deterministic, offline-first: + +- Versioned template manifest (`specbridge-template.json`, schema 1.0.0) + and a restricted one-pass `{{variable}}` renderer: no executable code, + no expressions, no environment access, no network, no recursion. +- Ten built-in templates (rest-api, cli-tool, database-migration, + authentication, background-job, event-driven-service, bugfix-regression, + performance-optimization, security-hardening, refactoring), embedded at + build time and validated end to end in CI. +- Project-local packs under `.specbridge/templates/` with validated, + atomic, script-free local installation — no remote registry, no URL or + npm installation. +- `template list/search/show/validate/preview/apply/install/uninstall/ + scaffold`, `spec new --template`, append-only template records, MCP + template tools with candidate-hash-bound apply, the + `/specbridge:templates` plugin skill, and a generated gallery + (`docs/templates.md`) with CI drift checks. + +## v0.7.1 (planned — not implemented) -- Spec templates and a template registry. - A plugin SDK and runner extension SDK distribution. - Analyzer and verifier SDKs. - An extension registry and community ecosystem documentation and diff --git a/docs/security.md b/docs/security.md index 87a1e23..6f8fd9c 100644 --- a/docs/security.md +++ b/docs/security.md @@ -165,3 +165,15 @@ same operations the CLI already gates, minus the human-only ones. Controls: | Malicious verifier output | Verifier stdout/stderr is captured with size limits, stored under the run directory, and only bounded tails ever appear in reports; output is never parsed as instructions. | | Oversized content (DoS) | 1 MB document/candidate caps, 2 MB structured-response cap, 500-diagnostic cap, list pagination, and SBMCP018/SBMCP019 failures before memory blowups. | | Plugin supply-chain integrity | Pinned SDK, reproducible bundles, SHA-256 checksum manifest verified in CI, license report, and a validator that rejects workspace imports or absolute paths in the shipped artifact. | + +## v0.7.0 template safety + +Templates are data, not code: no scripts, no shell, no environment access, +no network, one-pass `{{variable}}` rendering with bounded packs and +output, symlink and traversal rejection, atomic install and spec creation, +candidate-hash binding for MCP apply, and append-only operation records +that store variable names and hashes — never values. The full template +threat model (malicious manifests and placeholders, recursive injection, +traversal, symlink escape, oversized/binary payloads, ambiguous shadowing, +candidate substitution, and supply-chain limitations) lives in +[template-security.md](template-security.md). diff --git a/docs/template-contribution-guide.md b/docs/template-contribution-guide.md new file mode 100644 index 0000000..8f01468 --- /dev/null +++ b/docs/template-contribution-guide.md @@ -0,0 +1,120 @@ +# Template contribution guide + +How to contribute a built-in template to this repository. Contributors +never need to write TypeScript: a built-in template is pack files — +JSON and Markdown — plus a passing test suite. Start with +[creating templates](creating-templates.md) for the authoring workflow; +this page covers what is specific to built-ins. + +## Where built-ins live + +Each built-in pack is a directory under +`packages/templates/builtins//`: + +``` +packages/templates/builtins/rest-api/ +├── specbridge-template.json +├── README.md +└── files/ + ├── requirements.md.template + ├── design.md.template + └── tasks.md.template +``` + +The directory name must equal the manifest `id`. + +## Source of truth and generated artifacts + +The packs on disk are the single source of truth. Two artifacts are +generated from them and committed: + +- `packages/templates/src/builtin-packs.generated.ts` — the packs + embedded as string constants so every bundle (CLI, MCP standalone, + Claude Code plugin) ships the catalog without runtime filesystem + lookups. Regenerate with `pnpm generate:builtin-templates`. +- The gallery table in [docs/templates.md](templates.md). Regenerate + with `pnpm generate:template-gallery`. + +Never edit either by hand. After changing any pack file: + +```bash +pnpm generate:builtin-templates +pnpm generate:template-gallery +``` + +CI runs `pnpm check:builtin-templates` and `pnpm check:template-gallery` +and fails on drift. The generator also rejects symlinks, non-UTF-8 +content, and CRLF line endings in built-in packs. + +## The test gate + +```bash +npx vitest run tests/templates/builtin-packs.test.ts +``` + +This suite discovers every directory under `packages/templates/builtins/` +automatically — adding a pack adds its tests. Per pack it checks: + +1. **Manifest, README, and declared files are valid** — full pack + validation with README required, the directory name matching the + manifest ID, the license being `MIT`, and the compatibility range + being exactly `>=0.7.0 <1.0.0`. +2. **Render check is clean** — every file renders with deterministic + sample values and a fixed clock, with zero error-severity issues. +3. **End-to-end apply passes spec analysis** — the template is applied + into a fresh Kiro workspace; the resulting spec must analyze with no + error diagnostics, classify as `complete`, and every stage must pass + stage-level analysis. +4. **No vendor lock-in, employer terms, or absolute paths** — pack + content must not contain Windows drive paths, `/home/...` or + `/Users/...` paths, or CRLF line endings. + +A suite-level test additionally verifies the generated module matches +the packs on disk byte for byte. + +## Content quality bar + +- Vendor-neutral and generic: no company names, internal tools, cloud + products, or framework lock-in. The template must be useful in any + codebase. +- English, LF line endings, plain Markdown — no HTML, no front matter. +- `` placeholders for content the spec author fills in by + hand; spec analysis blocks approval until they are replaced. The + template gives structure; the engineering judgment stays with the + author. +- EARS-shaped acceptance criteria in requirements files: `WHEN , + THE SYSTEM SHALL .` and `IF , THEN THE + SYSTEM SHALL .` +- Concrete task verbs in `tasks.md.template` — "Implement", "Add + regression tests", "Verify" — not vague "handle" or "support". +- A README that documents every variable in a table and shows a + copy-pasteable `preview`/`apply` example (see + `packages/templates/builtins/rest-api/README.md` as the reference). + +## Pull request checklist + +- [ ] `specbridge template validate packages/templates/builtins/` + passes with no errors (ideally `--strict`). +- [ ] The pack README documents every variable and shows a working + usage example. +- [ ] Render check is clean: no error-severity rendering issues. +- [ ] End-to-end apply passes spec analysis + (`npx vitest run tests/templates/builtin-packs.test.ts`). +- [ ] `pnpm generate:builtin-templates` and + `pnpm generate:template-gallery` were run and the regenerated + files are committed. +- [ ] No employer-specific or vendor-locked content. +- [ ] No absolute paths anywhere in the pack. +- [ ] LF line endings throughout. +- [ ] An example is included: the manifest `examples` array carries a + copy-pasteable apply command (it appears in the generated + gallery). + +## Related documentation + +- [Template overview](templates.md) +- [Creating templates](creating-templates.md) +- [Manifest reference](template-manifest.md) +- [Rendering rules](template-rendering.md) +- [Installation](template-installation.md) +- [Security](template-security.md) diff --git a/docs/template-installation.md b/docs/template-installation.md new file mode 100644 index 0000000..70e67a9 --- /dev/null +++ b/docs/template-installation.md @@ -0,0 +1,93 @@ +# Template installation + +Templates come from exactly two sources in v0.7.0. There is no remote +registry, no URL installation, and no npm installation — install reads a +local directory inside your repository, and nothing else. A community +index is deferred to v0.7.1+ per the [roadmap](roadmap.md). For the +overall model see [the overview](templates.md). + +## Sources + +| Source | Reference | Location | Mutability | +| --- | --- | --- | --- | +| Built-in | `builtin:` | embedded in the SpecBridge binary at build time | immutable at runtime, versioned with SpecBridge | +| Project | `project:` | `.specbridge/templates//` | installed and uninstalled explicitly | + +## References and ambiguity + +An unqualified reference (`rest-api`) resolves only while exactly one +source provides that ID. If the same ID exists in both sources, every +command fails with an explicit ambiguity error (SBT002) listing the +qualified references — one source never silently shadows another. +`template install` warns up front when a pack's ID collides with a +built-in, because the unqualified reference becomes ambiguous the moment +the install completes. + +## Installing + +```bash +specbridge template install ./my-template +specbridge template install ./my-template --dry-run # validate + plan only +``` + +The source must be a local directory **inside the repository** (SBT007 +otherwise — copy the pack in first; installation only reads local, +inspectable paths). The flow: + +1. The pack is read with every filesystem check applied (no symlinks, no + binary files, size and count limits) and fully validated — an invalid + pack never installs. +2. The validated in-memory contents are written to a temp directory + under `.specbridge/tmp/`, so the copy can never pick up files that + validation did not see. +3. The copied pack is re-validated as an independent artifact. +4. One atomic rename moves it to `.specbridge/templates//`, and an + append-only install record is written. + +There is no `--force` and installs never overwrite: if +`project:` already exists, install fails with SBT021 and tells you +to uninstall first. A failed install leaves nothing behind. + +## Uninstalling + +```bash +specbridge template uninstall project:my-template +``` + +Uninstall requires a **qualified `project:` reference** — a bare ID is +refused so the command cannot accidentally target another source, and +built-ins are immutable (SBT022). The directory is atomically renamed +out of `.specbridge/templates/` before deletion, so the catalog never +sees a half-deleted pack. Specs generated from the template and past +template records are untouched — uninstalling a template never deletes +your work. + +## What `.specbridge` stores + +- `.specbridge/templates//` — one directory per installed pack, + byte-identical to what was validated. +- `.specbridge/template-records.jsonl` — append-only, one JSON line per + apply/install/uninstall/scaffold operation. Records hold safe + summaries only: template reference and version, manifest hash, + rendered-file hashes, and variable **names** — never variable values. + Previews are deliberately not recorded, because preview writes + nothing. + +See [sidecar state](sidecar-state.md) for the wider `.specbridge/` +layout and commit guidance. + +## Non-goals in v0.7.0 + +No remote registry, no `install `, no npm or GitHub installation, +no signed packs, no marketplace. These are honest gaps, not hidden +flags — see [security](template-security.md) for the supply-chain +implications and the [roadmap](roadmap.md) for what v0.7.1+ defers. + +## Related documentation + +- [Template overview](templates.md) +- [Creating templates](creating-templates.md) +- [Manifest reference](template-manifest.md) +- [Rendering rules](template-rendering.md) +- [Security](template-security.md) +- [Contribution guide](template-contribution-guide.md) diff --git a/docs/template-manifest.md b/docs/template-manifest.md new file mode 100644 index 0000000..0f61210 --- /dev/null +++ b/docs/template-manifest.md @@ -0,0 +1,247 @@ +# Template manifest reference + +Every template pack starts with `specbridge-template.json` at its root. +The manifest is inert data: no field is ever executed, fetched, or +interpreted as code. This page documents schema version 1.0.0 field by +field. For the surrounding concepts see [the overview](templates.md); for +how placeholders render see [rendering](template-rendering.md). + +## Strictness policy + +Unknown fields are **rejected**, not ignored. A manifest is an authored +artifact validated at install and apply time, not machine state that must +round-trip — failing loudly on a typo (`"varaibles"`) beats silently +dropping your variable declarations. If validation reports an unknown key, +fix the spelling; there is no escape hatch. + +Readers accept any `1.x` `schemaVersion`. A manifest declaring another +major version fails with SBT005 before any shape checks, so a +future-format pack produces one clear error instead of a wall of +unknown-field complaints. + +## Complete example + +This is the manifest of the built-in `rest-api` template +(`packages/templates/builtins/rest-api/specbridge-template.json`): + +```json +{ + "schemaVersion": "1.0.0", + "id": "rest-api", + "version": "1.0.0", + "displayName": "REST API", + "description": "A feature spec template for adding or changing a REST API endpoint: request/response contract, validation, status codes, authentication, idempotency, pagination, compatibility, observability, and rollout.", + "kind": "feature", + "supportedModes": ["requirements-first", "design-first", "quick"], + "defaultMode": "requirements-first", + "tags": ["api", "rest", "http", "backend"], + "files": [ + { + "source": "files/requirements.md.template", + "target": "requirements.md", + "stage": "requirements", + "required": true + }, + { + "source": "files/design.md.template", + "target": "design.md", + "stage": "design", + "required": true + }, + { + "source": "files/tasks.md.template", + "target": "tasks.md", + "stage": "tasks", + "required": true + } + ], + "variables": [ + { + "name": "actor", + "description": "Primary caller of the API (a user role or client system).", + "type": "string", + "required": false, + "default": "API client" + }, + { + "name": "resourceName", + "description": "Name of the primary resource the endpoint exposes (singular, e.g. \"order\").", + "type": "string", + "required": false, + "default": "resource" + }, + { + "name": "basePath", + "description": "Base URL path of the endpoint group (e.g. \"/api/v1/orders\").", + "type": "string", + "required": false, + "default": "/api/v1" + } + ], + "compatibility": { + "specbridge": ">=0.7.0 <1.0.0", + "kiroLayout": "1" + }, + "license": "MIT", + "examples": [ + "specbridge template apply rest-api --name orders-list-endpoint --var resourceName=order --var basePath=/api/v1/orders" + ] +} +``` + +## Required fields + +### `schemaVersion` + +Semver string (`x.y.z`). Must be a `1.x` version; anything else fails +with SBT005. + +### `id` + +The template's stable identifier, used as its directory name when +installed and in references like `builtin:rest-api`. IDs are strict so +they are always safe as directory names on every platform and can never +smuggle path segments: lowercase ASCII letters and digits, single hyphens +between runs, starting with a letter, at most 64 characters. No +underscores, dots, slashes, spaces, uppercase, or repeated hyphens. +Invalid IDs fail with SBT003. + +| Valid | Invalid | Why invalid | +| --- | --- | --- | +| `rest-api` | `REST-API` | uppercase | +| `database-migration` | `my_template` | underscore | +| `cli-tool-v2` | `api--v2` | repeated hyphen | +| `authentication` | `-api`, `api-` | leading/trailing hyphen | +| | `2fa-setup` | must start with a letter | +| | `api.v1`, `a/b` | dots and path separators | + +### `version` + +The template's own version, exact semver (`x.y.z`). Recorded in install +and apply records. + +### `displayName` and `description` + +Human-readable name (1–100 characters) and description (1–500 +characters). Both appear in `template list`, `template show`, and the +generated gallery in [templates.md](templates.md). + +### `kind` + +`feature` or `bugfix`. Determines the allowed target file set (below) and +the kind of spec that `template apply` creates. + +### `supportedModes` and `defaultMode` + +`supportedModes` is 1–3 distinct values from `requirements-first`, +`design-first`, `quick`. `defaultMode` must be one of them (SBT004 +otherwise); it is used when `--mode` is not passed. + +### `tags` + +Up to 12 distinct tags, each 1–32 characters matching +`[a-z0-9]+(-[a-z0-9]+)*` (lowercase, digits, single hyphens). Used by +`template list --tag` and `template search`. + +### `files` + +The rendered files, 1–20 entries. Each entry declares: + +- `source` — pack-relative path matching `files/.template` where + `` is lowercase letters, digits, dots, and hyphens (e.g. + `files/requirements.md.template`). Forward slashes only; absolute paths + and `.`/`..` segments are rejected (SBT007/SBT008). Sources live in a + flat `files/` directory — no nesting. +- `target` — the file name created inside `.kiro/specs//`. + Only the exact Kiro layout for the declared `kind` is allowed, and + every allowed target must appear **exactly once**: + - `feature`: `requirements.md`, `design.md`, `tasks.md` + - `bugfix`: `bugfix.md`, `design.md`, `tasks.md` + + Any other target fails with SBT011; a duplicate with SBT012; a missing + one with SBT004. Variables are never allowed in target paths. +- `stage` — must match the target (`requirements.md` → `requirements`, + `bugfix.md` → `bugfix`, `design.md` → `design`, `tasks.md` → `tasks`). +- `required` — must be `true`. Kiro layout 1 requires all files for the + kind; the field exists for forward compatibility, not for optional + files today. + +### `variables` + +Up to 30 declared variables. Each has: + +- `name` — 1–64 characters matching `[a-z][a-zA-Z0-9]*` (lower + camelCase), unique, and not one of the built-in names `specName`, + `title`, `description`, `kind`, `mode`, `generatedDate` — built-ins are + provided by SpecBridge and cannot be shadowed. +- `description` — 1–500 characters; shown by `template show` and quoted + in the error when a required variable is missing. +- `type` — `string`, `boolean`, `integer`, or `enum`. +- `required` (default `false`) and `default` — mutually exclusive: a + variable that is required must not also carry a default. An optional + variable without a default substitutes as empty text. +- `values` — required for and exclusive to `enum` variables: 1–50 + distinct strings, each 1–200 characters. +- `minLength` / `maxLength` — string variables only, non-negative, + `minLength <= maxLength`. +- `pattern` — string variables only; a **restricted safe regular + expression**: at most 200 characters, no backreferences (`\1`–`\9`), + and no quantified groups like `(…)+` — the shapes behind catastrophic + backtracking. This is a conservative subset by design; a template that + needs more should state the expectation in prose instead. Values + checked against a pattern are additionally capped at 2,000 characters. +- `minimum` / `maximum` — integer variables only, `minimum <= maximum`. + +Supplied values are validated and coerced at preview/apply time +(SBT013 missing required, SBT014 unknown or built-in name, SBT015 +invalid value). Any string value is capped at 100,000 characters. + +### `compatibility` + +```json +{ "specbridge": ">=0.7.0 <1.0.0", "kiroLayout": "1" } +``` + +- `specbridge` — a minimal range grammar: one or more space-separated + comparators that must all hold, using only `>=`, `<=`, `>`, `<`, `=`, + or a bare version (meaning `=`). No `^`, `~`, `||`, `x`-ranges, or + prerelease tags — state an explicit window. An incompatible SpecBridge + fails with SBT006. +- `kiroLayout` — must be `"1"`, the only supported layout (SBT006 + otherwise). + +### `license` + +License identifier, 1–50 characters (e.g. `MIT`). Required. Built-in +templates must be MIT. + +## Optional safe fields + +All optional fields are inert strings or booleans — never executed, +never fetched: + +- `author` (1–200 chars), `homepage` (1–500), `repository` (1–500) — + display metadata only. +- `examples` — up to 5 copy-pasteable command lines (each up to 500 + characters); the first one is shown by `template show` and in the + generated gallery. +- `deprecated` (boolean) and `replacement` (a valid template ID) — + applying a deprecated template emits an advisory warning naming the + replacement; it never blocks. +- `generatedDate` (boolean) — opt-in for the `{{generatedDate}}` + built-in variable (a `YYYY-MM-DD` date from an injectable clock). Off + by default so rendering is fully input-determined unless a template + explicitly asks for the date. See + [rendering](template-rendering.md). + +The manifest itself is limited to 256 KiB (SBT019); pack-wide limits are +listed in [security](template-security.md). + +## Related documentation + +- [Template overview](templates.md) +- [Creating templates](creating-templates.md) +- [Rendering rules](template-rendering.md) +- [Installation](template-installation.md) +- [Security](template-security.md) +- [Contribution guide](template-contribution-guide.md) diff --git a/docs/template-rendering.md b/docs/template-rendering.md new file mode 100644 index 0000000..bb07757 --- /dev/null +++ b/docs/template-rendering.md @@ -0,0 +1,108 @@ +# Template rendering + +SpecBridge renders templates with a deliberately restricted engine: +direct scalar substitution, one pass, nothing else. This page is the +complete description of that engine — if a capability is not listed +here, it does not exist. See [the overview](templates.md) for context +and [the manifest reference](template-manifest.md) for how variables are +declared. + +## Syntax + +The only supported syntax is `{{variableName}}`: two braces, a name +matching `[a-z][a-zA-Z0-9]*`, two braces. No inner whitespace, no dots, +no arguments. Every `{{…}}` occurrence in a template file must resolve +to a declared or built-in variable — a leftover or malformed placeholder +is an error (SBT016), never silently emitted into the output. + +Because any double-brace sequence is treated as a placeholder, **literal +double braces are not supported in template files**. A template that +needs to show `{{example}}` as text cannot; there is no escape syntax in +v0.7.0. Validation catches this early: `template validate` reports +malformed and undeclared placeholders per file. + +## One pass, values inserted verbatim + +Rendering makes a single pass over each template file. Substituted +values are inserted verbatim and **never rescanned**. If a spec author +passes: + +```bash +specbridge template apply rest-api --name demo --var resourceName="{{dangerous}}" +``` + +the rendered document contains the literal text `{{dangerous}}` — it is +not treated as a placeholder, looked up, or expanded. There is no second +pass and no recursion, which closes off placeholder-injection through +variable values entirely. + +## What is not supported + +- expressions, arithmetic, string operations +- conditionals and loops +- includes, partials, or cross-file references +- helpers, filters, or user-defined functions +- environment-variable access +- file reads or any filesystem access +- a second rendering pass or recursive expansion +- escape sequences for literal `{{` / `}}` + +These are permanent design constraints of the data-only template model, +not missing features — see [security](template-security.md). + +## Built-in variables + +| Variable | Value | +| --- | --- | +| `specName` | the `--name` argument | +| `title` | `--title`, or a title derived from the spec name | +| `description` | `--description`, or the standard placeholder text | +| `kind` | the manifest's `kind` (`feature` or `bugfix`) | +| `mode` | the effective workflow mode | +| `generatedDate` | opt-in only: today's date as `YYYY-MM-DD` (UTC) | + +Built-ins are always available (except `generatedDate`), cannot be +shadowed by manifest variables, and cannot be supplied with `--var` +(SBT014) — `title` and `description` have their own options. + +`generatedDate` requires `"generatedDate": true` in the manifest. It is +produced from an injectable clock, so tests and validation render +reproducibly; it is the only time-derived value in the whole pipeline, +and it is off by default so rendering stays fully input-determined. + +## Errors and limits + +- Unresolved or malformed placeholders: SBT016, naming the file and the + placeholder. +- Rendered output is capped at 1 MB (1,048,576 bytes) per document: + SBT018. +- Each rendered document must be non-empty and start with a top-level + `# ` heading (SBT017 otherwise); a missing trailing newline or CRLF + line endings are warnings. Rendered documents are also run through the + deterministic Kiro-compatible parsers, whose findings surface as + warnings. + +## Determinism + +The same template pack, variable values, spec name, title, description, +and mode always produce byte-identical output. No environment variables, +usernames, machine names, absolute paths, or network data ever enter a +rendered document. + +## One rendering path + +`template preview`, `template apply --dry-run`, the MCP +`template_preview` tool, and the real `template apply` all execute the +same planning function — there is no second renderer. What preview +showed is exactly what apply writes, and the MCP apply enforces that +with a candidate hash (see +[security](template-security.md#threat-model)). + +## Related documentation + +- [Template overview](templates.md) +- [Creating templates](creating-templates.md) +- [Manifest reference](template-manifest.md) +- [Installation](template-installation.md) +- [Security](template-security.md) +- [Contribution guide](template-contribution-guide.md) diff --git a/docs/template-security.md b/docs/template-security.md new file mode 100644 index 0000000..cf0bd1a --- /dev/null +++ b/docs/template-security.md @@ -0,0 +1,157 @@ +# Template security + +The template system is built on one principle: **templates are data, not +code**. A template pack is a JSON manifest plus plain Markdown files; +nothing in a pack is ever executed, evaluated, or fetched. Everything +else in this document follows from enforcing that principle at every +boundary. See [the overview](templates.md) for what templates are and +[rendering](template-rendering.md) for the substitution rules. + +## Enforced protections + +- **No scripts, lifecycle hooks, or shell.** There is no field in the + manifest that names a command, and no code path that spawns one. +- **No environment interpolation.** Environment variables, usernames, + machine names, and absolute paths never enter rendered output. +- **No recursive rendering.** One pass; substituted values are inserted + verbatim and never rescanned. +- **No expression evaluation.** `{{variableName}}` is the entire + syntax — no conditionals, loops, helpers, or includes. +- **No arbitrary filesystem access.** Sources must live in the pack's + flat `files/` directory; discovery only inspects the embedded built-in + catalog and `.specbridge/templates/`. +- **No variables in target filenames.** Targets are a fixed allowlist + per kind (`requirements.md`/`bugfix.md`, `design.md`, `tasks.md`), + each exactly once. +- **No path traversal.** Source paths reject `..`, `.`, absolute paths, + backslashes, and null bytes (SBT007/SBT008); writes go only to + `.kiro/specs//` and the sidecar. +- **No symlink following.** Every pack entry is `lstat`ed; any symlink + is rejected outright (SBT009), including the pack root. +- **No network.** Every template command is local and offline. +- **No spec overwrite.** Apply refuses an existing spec (SBT020); + install refuses an existing installed template (SBT021). +- **No approval bypass.** Generated stages start unapproved; templates + cannot mark anything approved, and `.kiro` files never carry template + metadata. +- **Bounded input and output.** From `TEMPLATE_PACK_LIMITS`, all + tested: at most 20 files per pack, manifest at most 256 KiB + (262,144 bytes), each template file at most 1 MiB (1,048,576 bytes), + total pack at most 5 MiB (5,242,880 bytes), each rendered document at + most 1 MiB, each supplied variable value at most 100,000 characters. + Packs nest at most 3 directories deep, and every file must be valid + UTF-8 with no null bytes. +- **Atomic install and spec creation.** Both go through a temp + directory and a single rename; a failure leaves nothing behind, and a + half-written pack or spec is never discoverable. +- **Candidate-hash binding for MCP apply.** The MCP `template_apply` + tool requires the `candidateHash` from `template_preview` plus an + explicit acknowledgement string, re-renders, and refuses on any + mismatch (SBT023). +- **Read-only preview.** `template preview` and `--dry-run` write + nothing and are deliberately not recorded. +- **No secret logging.** Append-only records in + `.specbridge/template-records.jsonl` store variable **names** and + rendered-content **hashes** only — never variable values. + +## Threat model + +**Malicious manifest.** The manifest is parsed with a strict schema: +unknown fields are rejected, every string is length-bounded, the file is +capped at 256 KiB, and no field is executable. `pattern` constraints are +vetted against a safe-regex subset (max 200 characters, no +backreferences, no quantified groups) to prevent catastrophic +backtracking. IDs are restricted to safe directory names. + +**Malicious placeholder.** Placeholder names must match +`[a-z][a-zA-Z0-9]*`; anything else is a malformed-placeholder error +(SBT016). A placeholder cannot express a path, an expression, or a +lookup — it can only name a declared or built-in variable. + +**Recursive placeholder injection.** A variable value containing +`{{other}}` is inserted as literal text; values are never rescanned, so +there is no second pass to exploit. + +**Traversal via source path.** Declared sources must match +`files/.template`; `..` and `.` segments, absolute paths, +backslashes, and null bytes are rejected (SBT007/SBT008), and undeclared +pack files can never be rendered (SBT010). + +**Traversal via target path.** Targets are compared against a fixed +allowlist per kind — not sanitized, allowlisted (SBT011). Variables are +never substituted into target paths, so no input can steer a write. + +**Symlink escape.** Pack reading uses `lstat` and rejects any symlink, +at the root or inside (SBT009). Install writes the already-validated +in-memory contents to a temp directory — the copy can never pick up +files or symlinks that validation did not see — and uninstall refuses a +symlinked install directory rather than following it. + +**Oversized pack.** File count, per-file size, total size, and nesting +depth are enforced before any content is parsed (SBT019), so a +multi-gigabyte or deeply nested directory fails fast. + +**Binary payload.** Every pack file must round-trip as UTF-8 and contain +no null bytes (SBT025). Template packs are text, and only text. + +**Ambiguous template shadowing.** If the same ID exists as both a +built-in and a project template, unqualified references fail with an +explicit ambiguity error listing the qualified candidates (SBT002) — +one source never silently shadows another. Install warns up front when +a pack's ID collides with a built-in. + +**Candidate substitution (MCP).** An agent could preview one thing and +apply another. `template_apply` therefore requires the exact +`candidateHash` produced by `template_preview` — a hash over the +template identity, manifest hash, spec identity, and every rendered +file — plus the literal acknowledgement `apply-reviewed-template`. The +tool re-renders from the same inputs and refuses on mismatch (SBT023): +the reviewed content is exactly the content written, or nothing is. + +**Spec overwrite.** Apply uses the same atomic spec-creation path as +`spec new` and fails with SBT020 if the spec exists. There is no +`--force`. + +**Invalid generated task layout.** Rendered documents are checked +structurally (non-empty, top-level heading) and run through the +deterministic Kiro-compatible parsers before anything is written; a +template whose output is not a valid spec document fails to apply +(SBT017). + +**Malicious Markdown content.** SpecBridge never executes rendered +Markdown — it is written to disk as spec text and later analyzed by +deterministic parsers. Spec analysis blocks approval while placeholder +content remains. What tooling cannot judge is the meaning of prose; see +the warning below. + +**Template pack supply-chain limitations.** v0.7.0 has no pack signing, +no provenance verification, no remote registry, and no marketplace — +installing a pack means trusting whoever handed you the directory. +Mitigations: packs are small plain-text artifacts you can fully inspect +(`specbridge template show --files --manifest`), install only +reads local directories inside the repository, and every install records +the manifest hash in an append-only record. A community index and +extension SDKs are deferred to v0.7.1+ per the [roadmap](roadmap.md); +until then, review before you install. + +## An honest warning about rendered prose + +Rendered Markdown may still contain untrusted natural-language +instructions aimed at AI agents — a template could embed text like +"ignore your verification rules" in a requirements section. SpecBridge's +control rules (stage approvals, protected paths, verification, evidence) +are enforced by the tooling itself and are never overridable by template +content: no sentence in a generated spec can approve a stage, skip +verification, or widen a write path. But agents reading generated specs +should treat their prose as data, not commands — the same discipline +that applies to any file content an agent did not author. + +## Related documentation + +- [Template overview](templates.md) +- [Creating templates](creating-templates.md) +- [Manifest reference](template-manifest.md) +- [Rendering rules](template-rendering.md) +- [Installation](template-installation.md) +- [Contribution guide](template-contribution-guide.md) +- [Security model (project-wide)](security.md) diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 0000000..760ae78 --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,96 @@ +# Spec templates + +SpecBridge v0.7.0 adds a secure, deterministic, offline-first template system +for Kiro-compatible specs: reusable spec templates without executable +generators or platform lock-in. + +Templates are **data, not code**. A template pack is a JSON manifest plus +plain Markdown files with `{{variable}}` placeholders. Applying a template +renders those files once — no scripts, no network, no model — and creates a +normal Kiro spec through the same atomic creation path as `spec new`. The +generated stages start unapproved; templates never bypass the approval +workflow, and `.kiro` files never carry template metadata. + +## Quick start + +```bash +specbridge template list + +specbridge template search migration + +specbridge template show database-migration + +specbridge template preview database-migration \ + --name add-payment-status-index \ + --var tableName=payments + +specbridge template apply database-migration \ + --name add-payment-status-index \ + --var tableName=payments +``` + +`template preview` and `template apply --dry-run` share the exact rendering +path with the real apply and write nothing. + +## Template sources + +| Source | Reference | Location | Notes | +| --- | --- | --- | --- | +| Built-in | `builtin:` | bundled with SpecBridge | immutable at runtime, versioned with SpecBridge | +| Project | `project:` | `.specbridge/templates//` | installed explicitly from a local path | + +An unqualified reference (`rest-api`) works only while exactly one source +provides that ID. If the same ID exists in both sources, SpecBridge returns +an ambiguity error (SBT002) listing the qualified references — one source +never silently shadows another. + +There is no remote registry, no URL installation, and no npm installation in +v0.7.0. Installation reads a local directory inside your repository, and +nothing else. See [docs/template-installation.md](template-installation.md). + +## Built-in template gallery + + + +10 built-in templates ship with SpecBridge. This table is generated from the +template manifests — do not edit it by hand. + +| Template | Description | Kind | Modes | Tags | Example | +| --- | --- | --- | --- | --- | --- | +| `authentication` — Authentication | A feature spec template for adding or changing authentication or authorization behavior: credential and session flow, authorization boundaries, wrong-credential and lockout behavior, expiry, revocation, replay protection, rate limiting, audit events, and security tests. | feature | requirements-first, design-first, quick | authentication, authorization, security, session, identity | `specbridge template apply authentication --name login-session-refresh --var actor=user --var sessionKind=token` | +| `background-job` — Background Job | A feature spec template for adding or changing an asynchronous background job or worker: trigger, scheduling, retries, idempotency, duplicate delivery, timeouts, dead-letter behavior, cancellation, observability, and tests. | feature | requirements-first, design-first, quick | background-job, worker, queue, async, scheduling | `specbridge template apply background-job --name invoice-export-worker --var jobName=invoice-export` | +| `bugfix-regression` — Bugfix Regression | A bugfix spec template built around evidence: current vs expected behavior, unchanged behavior, reproduction, root cause, the smallest safe fix, and the regression tests that keep it fixed. | bugfix | requirements-first, quick | bugfix, regression, debugging, quality | `specbridge template apply bugfix-regression --name checkout-total-rounding --var severity=high` | +| `cli-tool` — Command-Line Tool | A feature spec template for adding or changing a command-line tool or command: command surface, arguments and options, exit codes, stdout/stderr behavior, machine-readable output, non-interactive use, platform compatibility, error handling, and tests. | feature | requirements-first, design-first, quick | cli, command-line, tooling, developer-experience, ux | `specbridge template apply cli-tool --name my-tool --var commandName=mycli` | +| `database-migration` — Database Migration | A feature spec template for a schema or data migration: the schema change, batched backfill, backward compatibility, zero-downtime deployment order, indexes and locking, rollback limitations, validation, and observability. | feature | requirements-first, design-first, quick | database, migration, schema, sql, backfill | `specbridge template apply database-migration --name orders-status-backfill --var tableName=orders` | +| `event-driven-service` — Event-Driven Service | A feature spec template for a producer/consumer change on an event bus, queue, or stream: event contract, delivery semantics, ordering, idempotency, retries, dead-letter behavior, schema evolution, tracing, and rollout. | feature | requirements-first, design-first, quick | events, messaging, queue, streaming, async | `specbridge template apply event-driven-service --name order-events --var eventName=order-created` | +| `performance-optimization` — Performance Optimization | A feature spec template for a measurable performance improvement: numeric baseline and target, measurement method, workload definition, bottleneck evidence, constraints, regression risks, before/after validation, and rollback. | feature | requirements-first, design-first, quick | performance, optimization, profiling, latency, benchmarking | `specbridge template apply performance-optimization --name checkout-latency --var targetMetric="p95 latency"` | +| `refactoring` — Refactoring | A feature spec template for a behavior-preserving restructuring: an explicit inventory of behavior that must not change, refactor boundaries, an incremental plan with safe checkpoints, regression tests, rollback points, and measurable completion criteria. | feature | requirements-first, design-first, quick | refactoring, maintainability, tech-debt, restructuring, regression-safety | `specbridge template apply refactoring --name extract-billing-module --var componentName=billing` | +| `rest-api` — REST API | A feature spec template for adding or changing a REST API endpoint: request/response contract, validation, status codes, authentication, idempotency, pagination, compatibility, observability, and rollout. | feature | requirements-first, design-first, quick | api, rest, http, backend | `specbridge template apply rest-api --name orders-list-endpoint --var resourceName=order --var basePath=/api/v1/orders` | +| `security-hardening` — Security Hardening | A feature spec template for closing a specific security weakness or hardening a trust boundary: threat and assets at risk, abuse cases, required secure behavior, an explicit fail-closed decision, logging without secret leakage, negative tests, and rollout. | feature | requirements-first, design-first, quick | security, hardening, threat-model, abuse-case, defense | `specbridge template apply security-hardening --name harden-webhook-deserialization --var threatArea=deserialization` | + + + +Regenerate with `pnpm generate:template-gallery`; CI fails when the table +drifts from the manifests (`pnpm check:template-gallery`). + +## What templates cannot do + +- execute code, lifecycle scripts, or shell commands +- read environment variables or arbitrary files +- write outside `.kiro/specs//` +- use loops, conditionals, expressions, or includes in placeholders +- render recursively (one pass; values are never re-scanned) +- overwrite an existing spec +- mark any generated stage as approved + +See [docs/template-security.md](template-security.md) for the threat model +and the full list of enforced limits. + +## Related documentation + +- [Creating templates](creating-templates.md) — scaffold, edit, validate, share +- [Template manifest reference](template-manifest.md) +- [Template rendering rules](template-rendering.md) +- [Template installation](template-installation.md) +- [Template security](template-security.md) +- [Contribution guide](template-contribution-guide.md) diff --git a/integrations/claude-code-plugin/package.json b/integrations/claude-code-plugin/package.json index eb65c13..b84b4ce 100644 --- a/integrations/claude-code-plugin/package.json +++ b/integrations/claude-code-plugin/package.json @@ -1,6 +1,6 @@ { "name": "specbridge-claude-plugin", - "version": "0.6.1", + "version": "0.7.0", "private": true, "description": "Build harness for the self-contained SpecBridge Claude Code plugin (bundled CLI + MCP server + skills).", "license": "MIT", diff --git a/integrations/claude-code-plugin/specbridge/.claude-plugin/plugin.json b/integrations/claude-code-plugin/specbridge/.claude-plugin/plugin.json index 9de7a57..8221c4e 100644 --- a/integrations/claude-code-plugin/specbridge/.claude-plugin/plugin.json +++ b/integrations/claude-code-plugin/specbridge/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "specbridge", "displayName": "SpecBridge", "description": "Continue existing Kiro specs with Claude Code: validated stage authoring, verified interactive task execution, and deterministic spec drift checks.", - "version": "0.6.1", + "version": "0.7.0", "author": { "name": "HelloThisWorld" }, diff --git a/integrations/claude-code-plugin/specbridge/dist/checksums.json b/integrations/claude-code-plugin/specbridge/dist/checksums.json index 42452a2..d6ad3c6 100644 --- a/integrations/claude-code-plugin/specbridge/dist/checksums.json +++ b/integrations/claude-code-plugin/specbridge/dist/checksums.json @@ -1,18 +1,18 @@ { "schema": "specbridge.plugin-checksums/1", - "version": "0.6.1", + "version": "0.7.0", "files": { "THIRD_PARTY_LICENSES.txt": { "sha256": "c76d5d842432eac81300734011486b23740b4588d571cb813ec3113a09873289", "bytes": 153474 }, "cli.cjs": { - "sha256": "295bb26c0d360d41bd05b5987ad540abdccf535d74dc471712cd620aee1c419e", - "bytes": 2499815 + "sha256": "ea0bff1d7a4d717c154e2e65481689171d01515eae0d9392d55000f5727a97a6", + "bytes": 2742703 }, "mcp-server.cjs": { - "sha256": "5975483a1163e1fa9f7ab678487395c72e24fb78d92f667fe60de273e4fb05de", - "bytes": 2054540 + "sha256": "620f8d4778b4c6ab3c00e76b78203861d09a35ea0bd5e576c6e7715040aa3ec6", + "bytes": 2243836 } } } diff --git a/integrations/claude-code-plugin/specbridge/dist/cli.cjs b/integrations/claude-code-plugin/specbridge/dist/cli.cjs index 7841653..79e2b83 100644 --- a/integrations/claude-code-plugin/specbridge/dist/cli.cjs +++ b/integrations/claude-code-plugin/specbridge/dist/cli.cjs @@ -968,7 +968,7 @@ var require_command = __commonJS({ "use strict"; var EventEmitter2 = require("events").EventEmitter; var childProcess = require("child_process"); - var path31 = require("path"); + var path41 = require("path"); var fs = require("fs"); var process11 = require("process"); var { Argument: Argument2, humanReadableArgName } = require_argument(); @@ -1901,9 +1901,9 @@ Expecting one of '${allowedValues.join("', '")}'`); let launchWithNode = false; const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"]; function findFile(baseDir, baseName) { - const localBin = path31.resolve(baseDir, baseName); + const localBin = path41.resolve(baseDir, baseName); if (fs.existsSync(localBin)) return localBin; - if (sourceExt.includes(path31.extname(baseName))) return void 0; + if (sourceExt.includes(path41.extname(baseName))) return void 0; const foundExt = sourceExt.find( (ext) => fs.existsSync(`${localBin}${ext}`) ); @@ -1921,17 +1921,17 @@ Expecting one of '${allowedValues.join("', '")}'`); } catch (err) { resolvedScriptPath = this._scriptPath; } - executableDir = path31.resolve( - path31.dirname(resolvedScriptPath), + executableDir = path41.resolve( + path41.dirname(resolvedScriptPath), executableDir ); } if (executableDir) { let localFile = findFile(executableDir, executableFile); if (!localFile && !subcommand._executableFile && this._scriptPath) { - const legacyName = path31.basename( + const legacyName = path41.basename( this._scriptPath, - path31.extname(this._scriptPath) + path41.extname(this._scriptPath) ); if (legacyName !== this._name) { localFile = findFile( @@ -1942,7 +1942,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } executableFile = localFile || executableFile; } - launchWithNode = sourceExt.includes(path31.extname(executableFile)); + launchWithNode = sourceExt.includes(path41.extname(executableFile)); let proc; if (process11.platform !== "win32") { if (launchWithNode) { @@ -2782,7 +2782,7 @@ Expecting one of '${allowedValues.join("', '")}'`); * @return {Command} */ nameFromFilename(filename) { - this._name = path31.basename(filename, path31.extname(filename)); + this._name = path41.basename(filename, path41.extname(filename)); return this; } /** @@ -2796,9 +2796,9 @@ Expecting one of '${allowedValues.join("', '")}'`); * @param {string} [path] * @return {(string|null|Command)} */ - executableDir(path38) { - if (path38 === void 0) return this._executableDir; - this._executableDir = path38; + executableDir(path48) { + if (path48 === void 0) return this._executableDir; + this._executableDir = path48; return this; } /** @@ -3179,17 +3179,17 @@ var require_visit = __commonJS({ visit.BREAK = BREAK; visit.SKIP = SKIP; visit.REMOVE = REMOVE; - function visit_(key, node, visitor, path31) { - const ctrl = callVisitor(key, node, visitor, path31); + function visit_(key, node, visitor, path41) { + const ctrl = callVisitor(key, node, visitor, path41); if (identity3.isNode(ctrl) || identity3.isPair(ctrl)) { - replaceNode(key, path31, ctrl); - return visit_(key, ctrl, visitor, path31); + replaceNode(key, path41, ctrl); + return visit_(key, ctrl, visitor, path41); } if (typeof ctrl !== "symbol") { if (identity3.isCollection(node)) { - path31 = Object.freeze(path31.concat(node)); + path41 = Object.freeze(path41.concat(node)); for (let i2 = 0; i2 < node.items.length; ++i2) { - const ci = visit_(i2, node.items[i2], visitor, path31); + const ci = visit_(i2, node.items[i2], visitor, path41); if (typeof ci === "number") i2 = ci - 1; else if (ci === BREAK) @@ -3200,13 +3200,13 @@ var require_visit = __commonJS({ } } } else if (identity3.isPair(node)) { - path31 = Object.freeze(path31.concat(node)); - const ck = visit_("key", node.key, visitor, path31); + path41 = Object.freeze(path41.concat(node)); + const ck = visit_("key", node.key, visitor, path41); if (ck === BREAK) return BREAK; else if (ck === REMOVE) node.key = null; - const cv = visit_("value", node.value, visitor, path31); + const cv = visit_("value", node.value, visitor, path41); if (cv === BREAK) return BREAK; else if (cv === REMOVE) @@ -3227,17 +3227,17 @@ var require_visit = __commonJS({ visitAsync.BREAK = BREAK; visitAsync.SKIP = SKIP; visitAsync.REMOVE = REMOVE; - async function visitAsync_(key, node, visitor, path31) { - const ctrl = await callVisitor(key, node, visitor, path31); + async function visitAsync_(key, node, visitor, path41) { + const ctrl = await callVisitor(key, node, visitor, path41); if (identity3.isNode(ctrl) || identity3.isPair(ctrl)) { - replaceNode(key, path31, ctrl); - return visitAsync_(key, ctrl, visitor, path31); + replaceNode(key, path41, ctrl); + return visitAsync_(key, ctrl, visitor, path41); } if (typeof ctrl !== "symbol") { if (identity3.isCollection(node)) { - path31 = Object.freeze(path31.concat(node)); + path41 = Object.freeze(path41.concat(node)); for (let i2 = 0; i2 < node.items.length; ++i2) { - const ci = await visitAsync_(i2, node.items[i2], visitor, path31); + const ci = await visitAsync_(i2, node.items[i2], visitor, path41); if (typeof ci === "number") i2 = ci - 1; else if (ci === BREAK) @@ -3248,13 +3248,13 @@ var require_visit = __commonJS({ } } } else if (identity3.isPair(node)) { - path31 = Object.freeze(path31.concat(node)); - const ck = await visitAsync_("key", node.key, visitor, path31); + path41 = Object.freeze(path41.concat(node)); + const ck = await visitAsync_("key", node.key, visitor, path41); if (ck === BREAK) return BREAK; else if (ck === REMOVE) node.key = null; - const cv = await visitAsync_("value", node.value, visitor, path31); + const cv = await visitAsync_("value", node.value, visitor, path41); if (cv === BREAK) return BREAK; else if (cv === REMOVE) @@ -3281,23 +3281,23 @@ var require_visit = __commonJS({ } return visitor; } - function callVisitor(key, node, visitor, path31) { + function callVisitor(key, node, visitor, path41) { if (typeof visitor === "function") - return visitor(key, node, path31); + return visitor(key, node, path41); if (identity3.isMap(node)) - return visitor.Map?.(key, node, path31); + return visitor.Map?.(key, node, path41); if (identity3.isSeq(node)) - return visitor.Seq?.(key, node, path31); + return visitor.Seq?.(key, node, path41); if (identity3.isPair(node)) - return visitor.Pair?.(key, node, path31); + return visitor.Pair?.(key, node, path41); if (identity3.isScalar(node)) - return visitor.Scalar?.(key, node, path31); + return visitor.Scalar?.(key, node, path41); if (identity3.isAlias(node)) - return visitor.Alias?.(key, node, path31); + return visitor.Alias?.(key, node, path41); return void 0; } - function replaceNode(key, path31, node) { - const parent = path31[path31.length - 1]; + function replaceNode(key, path41, node) { + const parent = path41[path41.length - 1]; if (identity3.isCollection(parent)) { parent.items[key] = node; } else if (identity3.isPair(parent)) { @@ -3907,10 +3907,10 @@ var require_Collection = __commonJS({ var createNode = require_createNode(); var identity3 = require_identity(); var Node = require_Node(); - function collectionFromPath(schema, path31, value) { + function collectionFromPath(schema, path41, value) { let v = value; - for (let i2 = path31.length - 1; i2 >= 0; --i2) { - const k = path31[i2]; + for (let i2 = path41.length - 1; i2 >= 0; --i2) { + const k = path41[i2]; if (typeof k === "number" && Number.isInteger(k) && k >= 0) { const a2 = []; a2[k] = v; @@ -3929,7 +3929,7 @@ var require_Collection = __commonJS({ sourceObjects: /* @__PURE__ */ new Map() }); } - var isEmptyPath = (path31) => path31 == null || typeof path31 === "object" && !!path31[Symbol.iterator]().next().done; + var isEmptyPath = (path41) => path41 == null || typeof path41 === "object" && !!path41[Symbol.iterator]().next().done; var Collection = class extends Node.NodeBase { constructor(type, schema) { super(type); @@ -3959,11 +3959,11 @@ var require_Collection = __commonJS({ * be a Pair instance or a `{ key, value }` object, which may not have a key * that already exists in the map. */ - addIn(path31, value) { - if (isEmptyPath(path31)) + addIn(path41, value) { + if (isEmptyPath(path41)) this.add(value); else { - const [key, ...rest] = path31; + const [key, ...rest] = path41; const node = this.get(key, true); if (identity3.isCollection(node)) node.addIn(rest, value); @@ -3977,8 +3977,8 @@ var require_Collection = __commonJS({ * Removes a value from the collection. * @returns `true` if the item was found and removed. */ - deleteIn(path31) { - const [key, ...rest] = path31; + deleteIn(path41) { + const [key, ...rest] = path41; if (rest.length === 0) return this.delete(key); const node = this.get(key, true); @@ -3992,8 +3992,8 @@ var require_Collection = __commonJS({ * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). */ - getIn(path31, keepScalar) { - const [key, ...rest] = path31; + getIn(path41, keepScalar) { + const [key, ...rest] = path41; const node = this.get(key, true); if (rest.length === 0) return !keepScalar && identity3.isScalar(node) ? node.value : node; @@ -4011,8 +4011,8 @@ var require_Collection = __commonJS({ /** * Checks if the collection includes a value with the key `key`. */ - hasIn(path31) { - const [key, ...rest] = path31; + hasIn(path41) { + const [key, ...rest] = path41; if (rest.length === 0) return this.has(key); const node = this.get(key, true); @@ -4022,8 +4022,8 @@ var require_Collection = __commonJS({ * Sets a value in this collection. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - setIn(path31, value) { - const [key, ...rest] = path31; + setIn(path41, value) { + const [key, ...rest] = path41; if (rest.length === 0) { this.set(key, value); } else { @@ -4745,12 +4745,12 @@ var require_log = __commonJS({ if (logLevel === "debug") console.log(...messages); } - function warn(logLevel, warning) { + function warn(logLevel, warning2) { if (logLevel === "debug" || logLevel === "warn") { if (typeof node_process.emitWarning === "function") - node_process.emitWarning(warning); + node_process.emitWarning(warning2); else - console.warn(warning); + console.warn(warning2); } } exports2.debug = debug; @@ -6538,9 +6538,9 @@ var require_Document = __commonJS({ this.contents.add(value); } /** Adds a value to the document. */ - addIn(path31, value) { + addIn(path41, value) { if (assertCollection(this.contents)) - this.contents.addIn(path31, value); + this.contents.addIn(path41, value); } /** * Create a new `Alias` node, ensuring that the target `node` has the required anchor. @@ -6615,14 +6615,14 @@ var require_Document = __commonJS({ * Removes a value from the document. * @returns `true` if the item was found and removed. */ - deleteIn(path31) { - if (Collection.isEmptyPath(path31)) { + deleteIn(path41) { + if (Collection.isEmptyPath(path41)) { if (this.contents == null) return false; this.contents = null; return true; } - return assertCollection(this.contents) ? this.contents.deleteIn(path31) : false; + return assertCollection(this.contents) ? this.contents.deleteIn(path41) : false; } /** * Returns item at `key`, or `undefined` if not found. By default unwraps @@ -6637,10 +6637,10 @@ var require_Document = __commonJS({ * scalar values from their surrounding node; to disable set `keepScalar` to * `true` (collections are always returned intact). */ - getIn(path31, keepScalar) { - if (Collection.isEmptyPath(path31)) + getIn(path41, keepScalar) { + if (Collection.isEmptyPath(path41)) return !keepScalar && identity3.isScalar(this.contents) ? this.contents.value : this.contents; - return identity3.isCollection(this.contents) ? this.contents.getIn(path31, keepScalar) : void 0; + return identity3.isCollection(this.contents) ? this.contents.getIn(path41, keepScalar) : void 0; } /** * Checks if the document includes a value with the key `key`. @@ -6651,10 +6651,10 @@ var require_Document = __commonJS({ /** * Checks if the document includes a value at `path`. */ - hasIn(path31) { - if (Collection.isEmptyPath(path31)) + hasIn(path41) { + if (Collection.isEmptyPath(path41)) return this.contents !== void 0; - return identity3.isCollection(this.contents) ? this.contents.hasIn(path31) : false; + return identity3.isCollection(this.contents) ? this.contents.hasIn(path41) : false; } /** * Sets a value in this document. For `!!set`, `value` needs to be a @@ -6671,13 +6671,13 @@ var require_Document = __commonJS({ * Sets a value in this document. For `!!set`, `value` needs to be a * boolean to add/remove the item from the set. */ - setIn(path31, value) { - if (Collection.isEmptyPath(path31)) { + setIn(path41, value) { + if (Collection.isEmptyPath(path41)) { this.contents = value; } else if (this.contents == null) { - this.contents = Collection.collectionFromPath(this.schema, Array.from(path31), value); + this.contents = Collection.collectionFromPath(this.schema, Array.from(path41), value); } else if (assertCollection(this.contents)) { - this.contents.setIn(path31, value); + this.contents.setIn(path41, value); } } /** @@ -8219,9 +8219,9 @@ var require_composer = __commonJS({ this.prelude = []; this.errors = []; this.warnings = []; - this.onError = (source, code2, message, warning) => { + this.onError = (source, code2, message, warning2) => { const pos = getErrorPos(source); - if (warning) + if (warning2) this.warnings.push(new errors.YAMLWarning(pos, code2, message)); else this.errors.push(new errors.YAMLParseError(pos, code2, message)); @@ -8294,10 +8294,10 @@ ${cb}` : comment; console.dir(token, { depth: null }); switch (token.type) { case "directive": - this.directives.add(token.source, (offset, message, warning) => { + this.directives.add(token.source, (offset, message, warning2) => { const pos = getErrorPos(token); pos[0] += offset; - this.onError(pos, "BAD_DIRECTIVE", message, warning); + this.onError(pos, "BAD_DIRECTIVE", message, warning2); }); this.prelude.push(token.source); this.atDirectives = true; @@ -8637,9 +8637,9 @@ var require_cst_visit = __commonJS({ visit.BREAK = BREAK; visit.SKIP = SKIP; visit.REMOVE = REMOVE; - visit.itemAtPath = (cst, path31) => { + visit.itemAtPath = (cst, path41) => { let item = cst; - for (const [field, index] of path31) { + for (const [field, index] of path41) { const tok = item?.[field]; if (tok && "items" in tok) { item = tok.items[index]; @@ -8648,23 +8648,23 @@ var require_cst_visit = __commonJS({ } return item; }; - visit.parentCollection = (cst, path31) => { - const parent = visit.itemAtPath(cst, path31.slice(0, -1)); - const field = path31[path31.length - 1][0]; + visit.parentCollection = (cst, path41) => { + const parent = visit.itemAtPath(cst, path41.slice(0, -1)); + const field = path41[path41.length - 1][0]; const coll = parent?.[field]; if (coll && "items" in coll) return coll; throw new Error("Parent collection not found"); }; - function _visit(path31, item, visitor) { - let ctrl = visitor(item, path31); + function _visit(path41, item, visitor) { + let ctrl = visitor(item, path41); if (typeof ctrl === "symbol") return ctrl; for (const field of ["key", "value"]) { const token = item[field]; if (token && "items" in token) { for (let i2 = 0; i2 < token.items.length; ++i2) { - const ci = _visit(Object.freeze(path31.concat([[field, i2]])), token.items[i2], visitor); + const ci = _visit(Object.freeze(path41.concat([[field, i2]])), token.items[i2], visitor); if (typeof ci === "number") i2 = ci - 1; else if (ci === BREAK) @@ -8675,10 +8675,10 @@ var require_cst_visit = __commonJS({ } } if (typeof ctrl === "function" && field === "key") - ctrl = ctrl(item, path31); + ctrl = ctrl(item, path41); } } - return typeof ctrl === "function" ? ctrl(item, path31) : ctrl; + return typeof ctrl === "function" ? ctrl(item, path41) : ctrl; } exports2.visit = visit; } @@ -10339,7 +10339,7 @@ var require_public_api = __commonJS({ const doc = parseDocument(src, options); if (!doc) return null; - doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning)); + doc.warnings.forEach((warning2) => log.warn(doc.options.logLevel, warning2)); if (doc.errors.length > 0) { if (doc.options.logLevel !== "silent") throw doc.errors[0]; @@ -10436,7 +10436,7 @@ var require_windows = __commonJS({ module2.exports = isexe; isexe.sync = sync; var fs = require("fs"); - function checkPathExt(path31, options) { + function checkPathExt(path41, options) { var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT; if (!pathext) { return true; @@ -10447,25 +10447,25 @@ var require_windows = __commonJS({ } for (var i2 = 0; i2 < pathext.length; i2++) { var p = pathext[i2].toLowerCase(); - if (p && path31.substr(-p.length).toLowerCase() === p) { + if (p && path41.substr(-p.length).toLowerCase() === p) { return true; } } return false; } - function checkStat(stat, path31, options) { + function checkStat(stat, path41, options) { if (!stat.isSymbolicLink() && !stat.isFile()) { return false; } - return checkPathExt(path31, options); + return checkPathExt(path41, options); } - function isexe(path31, options, cb) { - fs.stat(path31, function(er, stat) { - cb(er, er ? false : checkStat(stat, path31, options)); + function isexe(path41, options, cb) { + fs.stat(path41, function(er, stat) { + cb(er, er ? false : checkStat(stat, path41, options)); }); } - function sync(path31, options) { - return checkStat(fs.statSync(path31), path31, options); + function sync(path41, options) { + return checkStat(fs.statSync(path41), path41, options); } } }); @@ -10477,13 +10477,13 @@ var require_mode = __commonJS({ module2.exports = isexe; isexe.sync = sync; var fs = require("fs"); - function isexe(path31, options, cb) { - fs.stat(path31, function(er, stat) { + function isexe(path41, options, cb) { + fs.stat(path41, function(er, stat) { cb(er, er ? false : checkStat(stat, options)); }); } - function sync(path31, options) { - return checkStat(fs.statSync(path31), options); + function sync(path41, options) { + return checkStat(fs.statSync(path41), options); } function checkStat(stat, options) { return stat.isFile() && checkMode(stat, options); @@ -10517,7 +10517,7 @@ var require_isexe = __commonJS({ } module2.exports = isexe; isexe.sync = sync; - function isexe(path31, options, cb) { + function isexe(path41, options, cb) { if (typeof options === "function") { cb = options; options = {}; @@ -10527,7 +10527,7 @@ var require_isexe = __commonJS({ throw new TypeError("callback not provided"); } return new Promise(function(resolve, reject) { - isexe(path31, options || {}, function(er, is) { + isexe(path41, options || {}, function(er, is) { if (er) { reject(er); } else { @@ -10536,7 +10536,7 @@ var require_isexe = __commonJS({ }); }); } - core(path31, options || {}, function(er, is) { + core(path41, options || {}, function(er, is) { if (er) { if (er.code === "EACCES" || options && options.ignoreErrors) { er = null; @@ -10546,9 +10546,9 @@ var require_isexe = __commonJS({ cb(er, is); }); } - function sync(path31, options) { + function sync(path41, options) { try { - return core.sync(path31, options || {}); + return core.sync(path41, options || {}); } catch (er) { if (options && options.ignoreErrors || er.code === "EACCES") { return false; @@ -10565,7 +10565,7 @@ var require_which = __commonJS({ "../../node_modules/.pnpm/which@2.0.2/node_modules/which/which.js"(exports2, module2) { "use strict"; var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys"; - var path31 = require("path"); + var path41 = require("path"); var COLON = isWindows ? ";" : ":"; var isexe = require_isexe(); var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" }); @@ -10603,7 +10603,7 @@ var require_which = __commonJS({ return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd)); const ppRaw = pathEnv[i2]; const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw; - const pCmd = path31.join(pathPart, cmd); + const pCmd = path41.join(pathPart, cmd); const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd; resolve(subStep(p, i2, 0)); }); @@ -10630,7 +10630,7 @@ var require_which = __commonJS({ for (let i2 = 0; i2 < pathEnv.length; i2++) { const ppRaw = pathEnv[i2]; const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw; - const pCmd = path31.join(pathPart, cmd); + const pCmd = path41.join(pathPart, cmd); const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd; for (let j = 0; j < pathExt.length; j++) { const cur = p + pathExt[j]; @@ -10678,7 +10678,7 @@ var require_path_key = __commonJS({ var require_resolveCommand = __commonJS({ "../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/util/resolveCommand.js"(exports2, module2) { "use strict"; - var path31 = require("path"); + var path41 = require("path"); var which = require_which(); var getPathKey = require_path_key(); function resolveCommandAttempt(parsed, withoutPathExt) { @@ -10696,7 +10696,7 @@ var require_resolveCommand = __commonJS({ try { resolved = which.sync(parsed.command, { path: env[getPathKey({ env })], - pathExt: withoutPathExt ? path31.delimiter : void 0 + pathExt: withoutPathExt ? path41.delimiter : void 0 }); } catch (e) { } finally { @@ -10705,7 +10705,7 @@ var require_resolveCommand = __commonJS({ } } if (resolved) { - resolved = path31.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved); + resolved = path41.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved); } return resolved; } @@ -10759,8 +10759,8 @@ var require_shebang_command = __commonJS({ if (!match) { return null; } - const [path31, argument] = match[0].replace(/#! ?/, "").split(" "); - const binary = path31.split("/").pop(); + const [path41, argument] = match[0].replace(/#! ?/, "").split(" "); + const binary = path41.split("/").pop(); if (binary === "env") { return argument; } @@ -10795,7 +10795,7 @@ var require_readShebang = __commonJS({ var require_parse = __commonJS({ "../../node_modules/.pnpm/cross-spawn@7.0.6/node_modules/cross-spawn/lib/parse.js"(exports2, module2) { "use strict"; - var path31 = require("path"); + var path41 = require("path"); var resolveCommand = require_resolveCommand(); var escape2 = require_escape(); var readShebang = require_readShebang(); @@ -10820,7 +10820,7 @@ var require_parse = __commonJS({ const needsShell = !isExecutableRegExp.test(commandFile); if (parsed.options.forceShell || needsShell) { const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); - parsed.command = path31.normalize(parsed.command); + parsed.command = path41.normalize(parsed.command); parsed.command = escape2.command(parsed.command); parsed.args = parsed.args.map((arg) => escape2.argument(arg, needsDoubleEscapeMetaChars)); const shellCommand = [parsed.command].concat(parsed.args).join(" "); @@ -11185,8 +11185,8 @@ var require_utils = __commonJS({ } return output; }; - exports2.basename = (path31, { windows } = {}) => { - const segs = path31.split(windows ? /[\\/]/ : "/"); + exports2.basename = (path41, { windows } = {}) => { + const segs = path41.split(windows ? /[\\/]/ : "/"); const last = segs[segs.length - 1]; if (last === "") { return segs[segs.length - 2]; @@ -15893,8 +15893,8 @@ var require_utils2 = __commonJS({ } return ind; } - function removeDotSegments(path31) { - let input = path31; + function removeDotSegments(path41) { + let input = path41; const output = []; let nextSlash = -1; let len = 0; @@ -16146,8 +16146,8 @@ var require_schemes = __commonJS({ wsComponent.secure = void 0; } if (wsComponent.resourceName) { - const [path31, query] = wsComponent.resourceName.split("?"); - wsComponent.path = path31 && path31 !== "/" ? path31 : void 0; + const [path41, query] = wsComponent.resourceName.split("?"); + wsComponent.path = path41 && path41 !== "/" ? path41 : void 0; wsComponent.query = query; wsComponent.resourceName = void 0; } @@ -19870,31 +19870,31 @@ var ZodError = class _ZodError extends Error { this.issues = issues; } format(_mapper) { - const mapper = _mapper || function(issue2) { - return issue2.message; + const mapper = _mapper || function(issue4) { + return issue4.message; }; const fieldErrors = { _errors: [] }; const processError = (error2) => { - for (const issue2 of error2.issues) { - if (issue2.code === "invalid_union") { - issue2.unionErrors.map(processError); - } else if (issue2.code === "invalid_return_type") { - processError(issue2.returnTypeError); - } else if (issue2.code === "invalid_arguments") { - processError(issue2.argumentsError); - } else if (issue2.path.length === 0) { - fieldErrors._errors.push(mapper(issue2)); + for (const issue4 of error2.issues) { + if (issue4.code === "invalid_union") { + issue4.unionErrors.map(processError); + } else if (issue4.code === "invalid_return_type") { + processError(issue4.returnTypeError); + } else if (issue4.code === "invalid_arguments") { + processError(issue4.argumentsError); + } else if (issue4.path.length === 0) { + fieldErrors._errors.push(mapper(issue4)); } else { let curr = fieldErrors; let i2 = 0; - while (i2 < issue2.path.length) { - const el = issue2.path[i2]; - const terminal = i2 === issue2.path.length - 1; + while (i2 < issue4.path.length) { + const el = issue4.path[i2]; + const terminal = i2 === issue4.path.length - 1; if (!terminal) { curr[el] = curr[el] || { _errors: [] }; } else { curr[el] = curr[el] || { _errors: [] }; - curr[el]._errors.push(mapper(issue2)); + curr[el]._errors.push(mapper(issue4)); } curr = curr[el]; i2++; @@ -19919,7 +19919,7 @@ var ZodError = class _ZodError extends Error { get isEmpty() { return this.issues.length === 0; } - flatten(mapper = (issue2) => issue2.message) { + flatten(mapper = (issue4) => issue4.message) { const fieldErrors = {}; const formErrors = []; for (const sub of this.issues) { @@ -19943,30 +19943,30 @@ ZodError.create = (issues) => { }; // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js -var errorMap = (issue2, _ctx) => { +var errorMap = (issue4, _ctx) => { let message; - switch (issue2.code) { + switch (issue4.code) { case ZodIssueCode.invalid_type: - if (issue2.received === ZodParsedType.undefined) { + if (issue4.received === ZodParsedType.undefined) { message = "Required"; } else { - message = `Expected ${issue2.expected}, received ${issue2.received}`; + message = `Expected ${issue4.expected}, received ${issue4.received}`; } break; case ZodIssueCode.invalid_literal: - message = `Invalid literal value, expected ${JSON.stringify(issue2.expected, util.jsonStringifyReplacer)}`; + message = `Invalid literal value, expected ${JSON.stringify(issue4.expected, util.jsonStringifyReplacer)}`; break; case ZodIssueCode.unrecognized_keys: - message = `Unrecognized key(s) in object: ${util.joinValues(issue2.keys, ", ")}`; + message = `Unrecognized key(s) in object: ${util.joinValues(issue4.keys, ", ")}`; break; case ZodIssueCode.invalid_union: message = `Invalid input`; break; case ZodIssueCode.invalid_union_discriminator: - message = `Invalid discriminator value. Expected ${util.joinValues(issue2.options)}`; + message = `Invalid discriminator value. Expected ${util.joinValues(issue4.options)}`; break; case ZodIssueCode.invalid_enum_value: - message = `Invalid enum value. Expected ${util.joinValues(issue2.options)}, received '${issue2.received}'`; + message = `Invalid enum value. Expected ${util.joinValues(issue4.options)}, received '${issue4.received}'`; break; case ZodIssueCode.invalid_arguments: message = `Invalid function arguments`; @@ -19978,50 +19978,50 @@ var errorMap = (issue2, _ctx) => { message = `Invalid date`; break; case ZodIssueCode.invalid_string: - if (typeof issue2.validation === "object") { - if ("includes" in issue2.validation) { - message = `Invalid input: must include "${issue2.validation.includes}"`; - if (typeof issue2.validation.position === "number") { - message = `${message} at one or more positions greater than or equal to ${issue2.validation.position}`; + if (typeof issue4.validation === "object") { + if ("includes" in issue4.validation) { + message = `Invalid input: must include "${issue4.validation.includes}"`; + if (typeof issue4.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue4.validation.position}`; } - } else if ("startsWith" in issue2.validation) { - message = `Invalid input: must start with "${issue2.validation.startsWith}"`; - } else if ("endsWith" in issue2.validation) { - message = `Invalid input: must end with "${issue2.validation.endsWith}"`; + } else if ("startsWith" in issue4.validation) { + message = `Invalid input: must start with "${issue4.validation.startsWith}"`; + } else if ("endsWith" in issue4.validation) { + message = `Invalid input: must end with "${issue4.validation.endsWith}"`; } else { - util.assertNever(issue2.validation); + util.assertNever(issue4.validation); } - } else if (issue2.validation !== "regex") { - message = `Invalid ${issue2.validation}`; + } else if (issue4.validation !== "regex") { + message = `Invalid ${issue4.validation}`; } else { message = "Invalid"; } break; case ZodIssueCode.too_small: - if (issue2.type === "array") - message = `Array must contain ${issue2.exact ? "exactly" : issue2.inclusive ? `at least` : `more than`} ${issue2.minimum} element(s)`; - else if (issue2.type === "string") - message = `String must contain ${issue2.exact ? "exactly" : issue2.inclusive ? `at least` : `over`} ${issue2.minimum} character(s)`; - else if (issue2.type === "number") - message = `Number must be ${issue2.exact ? `exactly equal to ` : issue2.inclusive ? `greater than or equal to ` : `greater than `}${issue2.minimum}`; - else if (issue2.type === "bigint") - message = `Number must be ${issue2.exact ? `exactly equal to ` : issue2.inclusive ? `greater than or equal to ` : `greater than `}${issue2.minimum}`; - else if (issue2.type === "date") - message = `Date must be ${issue2.exact ? `exactly equal to ` : issue2.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue2.minimum))}`; + if (issue4.type === "array") + message = `Array must contain ${issue4.exact ? "exactly" : issue4.inclusive ? `at least` : `more than`} ${issue4.minimum} element(s)`; + else if (issue4.type === "string") + message = `String must contain ${issue4.exact ? "exactly" : issue4.inclusive ? `at least` : `over`} ${issue4.minimum} character(s)`; + else if (issue4.type === "number") + message = `Number must be ${issue4.exact ? `exactly equal to ` : issue4.inclusive ? `greater than or equal to ` : `greater than `}${issue4.minimum}`; + else if (issue4.type === "bigint") + message = `Number must be ${issue4.exact ? `exactly equal to ` : issue4.inclusive ? `greater than or equal to ` : `greater than `}${issue4.minimum}`; + else if (issue4.type === "date") + message = `Date must be ${issue4.exact ? `exactly equal to ` : issue4.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue4.minimum))}`; else message = "Invalid input"; break; case ZodIssueCode.too_big: - if (issue2.type === "array") - message = `Array must contain ${issue2.exact ? `exactly` : issue2.inclusive ? `at most` : `less than`} ${issue2.maximum} element(s)`; - else if (issue2.type === "string") - message = `String must contain ${issue2.exact ? `exactly` : issue2.inclusive ? `at most` : `under`} ${issue2.maximum} character(s)`; - else if (issue2.type === "number") - message = `Number must be ${issue2.exact ? `exactly` : issue2.inclusive ? `less than or equal to` : `less than`} ${issue2.maximum}`; - else if (issue2.type === "bigint") - message = `BigInt must be ${issue2.exact ? `exactly` : issue2.inclusive ? `less than or equal to` : `less than`} ${issue2.maximum}`; - else if (issue2.type === "date") - message = `Date must be ${issue2.exact ? `exactly` : issue2.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue2.maximum))}`; + if (issue4.type === "array") + message = `Array must contain ${issue4.exact ? `exactly` : issue4.inclusive ? `at most` : `less than`} ${issue4.maximum} element(s)`; + else if (issue4.type === "string") + message = `String must contain ${issue4.exact ? `exactly` : issue4.inclusive ? `at most` : `under`} ${issue4.maximum} character(s)`; + else if (issue4.type === "number") + message = `Number must be ${issue4.exact ? `exactly` : issue4.inclusive ? `less than or equal to` : `less than`} ${issue4.maximum}`; + else if (issue4.type === "bigint") + message = `BigInt must be ${issue4.exact ? `exactly` : issue4.inclusive ? `less than or equal to` : `less than`} ${issue4.maximum}`; + else if (issue4.type === "date") + message = `Date must be ${issue4.exact ? `exactly` : issue4.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue4.maximum))}`; else message = "Invalid input"; break; @@ -20032,14 +20032,14 @@ var errorMap = (issue2, _ctx) => { message = `Intersection results could not be merged`; break; case ZodIssueCode.not_multiple_of: - message = `Number must be a multiple of ${issue2.multipleOf}`; + message = `Number must be a multiple of ${issue4.multipleOf}`; break; case ZodIssueCode.not_finite: message = "Number must be finite"; break; default: message = _ctx.defaultError; - util.assertNever(issue2); + util.assertNever(issue4); } return { message }; }; @@ -20056,8 +20056,8 @@ function getErrorMap() { // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js var makeIssue = (params) => { - const { data, path: path31, errorMaps, issueData } = params; - const fullPath = [...path31, ...issueData.path || []]; + const { data, path: path41, errorMaps, issueData } = params; + const fullPath = [...path41, ...issueData.path || []]; const fullIssue = { ...issueData, path: fullPath @@ -20083,7 +20083,7 @@ var makeIssue = (params) => { var EMPTY_PATH = []; function addIssueToContext(ctx, issueData) { const overrideMap = getErrorMap(); - const issue2 = makeIssue({ + const issue4 = makeIssue({ issueData, data: ctx.data, path: ctx.path, @@ -20098,7 +20098,7 @@ function addIssueToContext(ctx, issueData) { // then global default map ].filter((x) => !!x) }); - ctx.common.issues.push(issue2); + ctx.common.issues.push(issue4); } var ParseStatus = class _ParseStatus { constructor() { @@ -20173,11 +20173,11 @@ var errorUtil; // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js var ParseInputLazyPath = class { - constructor(parent, value, path31, key) { + constructor(parent, value, path41, key) { this._cachedPath = []; this.parent = parent; this.data = value; - this._path = path31; + this._path = path41; this._key = key; } get path() { @@ -22056,9 +22056,9 @@ var ZodObject = class _ZodObject extends ZodType { ...this._def, unknownKeys: "strict", ...message !== void 0 ? { - errorMap: (issue2, ctx) => { - const defaultError = this._def.errorMap?.(issue2, ctx).message ?? ctx.defaultError; - if (issue2.code === "unrecognized_keys") + errorMap: (issue4, ctx) => { + const defaultError = this._def.errorMap?.(issue4, ctx).message ?? ctx.defaultError; + if (issue4.code === "unrecognized_keys") return { message: errorUtil.errToObj(message).message ?? defaultError }; @@ -24150,7 +24150,7 @@ function parseReport(raw, schema) { } const result = schema.safeParse(parsed); if (!result.success) { - const issues = result.error.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; "); + const issues = result.error.issues.map((issue4) => `${issue4.path.join(".") || "(root)"}: ${issue4.message}`).join("; "); return { ok: false, reason: `runner output does not match the report schema: ${issues}` }; } return { ok: true, report: result.data }; @@ -24939,7 +24939,7 @@ function fileSchemaVersion(parsed) { return typeof version2 === "string" ? version2 : void 0; } function zodIssueSummary(error2) { - return error2.issues.map((issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}`).join("; "); + return error2.issues.map((issue4) => `${issue4.path.join(".") || "(root)"}: ${issue4.message}`).join("; "); } function readAgentConfig(workspace) { const configPath = import_path4.default.join(workspace.sidecarDir, "config.json"); @@ -25083,7 +25083,7 @@ function planConfigMigration(raw) { return { kind: "invalid", problems: check4.error.issues.map( - (issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}` + (issue4) => `${issue4.path.join(".") || "(root)"}: ${issue4.message}` ) }; } @@ -25094,7 +25094,7 @@ function planConfigMigration(raw) { return { kind: "invalid", problems: parsed.error.issues.map( - (issue2) => `${issue2.path.join(".") || "(root)"}: ${issue2.message}` + (issue4) => `${issue4.path.join(".") || "(root)"}: ${issue4.message}` ) }; } @@ -25144,7 +25144,7 @@ function planConfigMigration(raw) { return { kind: "invalid", problems: validated.error.issues.map( - (issue2) => `migrated configuration would be invalid \u2014 ${issue2.path.join(".") || "(root)"}: ${issue2.message}` + (issue4) => `migrated configuration would be invalid \u2014 ${issue4.path.join(".") || "(root)"}: ${issue4.message}` ) }; } @@ -25746,7 +25746,7 @@ function fullCommandPath(command) { } // ../../packages/cli/src/version.ts -var VERSION = "0.6.1"; +var VERSION = "0.7.0"; // ../../packages/cli/src/commands/doctor.ts var import_node_path2 = __toESM(require("path"), 1); @@ -27341,35 +27341,35 @@ function extractPathReferences(document) { for (const match of text.matchAll(BACKTICK_SPAN)) { const raw = match[1]; if (raw === void 0) continue; - const path55 = normalizePathCandidate(raw); - if (path55 === void 0) continue; - const key = `${path55} ${i2}`; + const path56 = normalizePathCandidate(raw); + if (path56 === void 0) continue; + const key = `${path56} ${i2}`; if (seen.has(key)) continue; seen.add(key); references.push({ raw, - path: path55, + path: path56, line: i2, method: "backtick-path", confidence: "deterministic", - isGlob: GLOB_CHARS.test(path55) + isGlob: GLOB_CHARS.test(path56) }); } for (const match of text.matchAll(MARKDOWN_LINK)) { const raw = match[1]; if (raw === void 0) continue; - const path55 = normalizePathCandidate(raw); - if (path55 === void 0) continue; - const key = `${path55} ${i2}`; + const path56 = normalizePathCandidate(raw); + if (path56 === void 0) continue; + const key = `${path56} ${i2}`; if (seen.has(key)) continue; seen.add(key); references.push({ raw, - path: path55, + path: path56, line: i2, method: "markdown-link", confidence: "deterministic", - isGlob: GLOB_CHARS.test(path55) + isGlob: GLOB_CHARS.test(path56) }); } } @@ -29063,9 +29063,26 @@ Valid examples: notification-preferences, auth-v2, payment-retry.` } const requestedTitle = request.title?.trim(); const title = requestedTitle !== void 0 && requestedTitle.length > 0 ? requestedTitle : titleFromSpecName(request.name); + const files = renderSpecTemplates(specType, mode, { title, description }); + return planSpecCreationFromFiles( + workspace, + { name: request.name, specType, mode, title, description, descriptionIsPlaceholder, files }, + clock + ); +} +function planSpecCreationFromFiles(workspace, prepared, clock = systemClock) { + const nameCheck = validateSpecName(prepared.name); + if (!nameCheck.valid) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Invalid spec name "${prepared.name}": +${nameCheck.problems.map((p) => ` - ${p}`).join("\n")} +Valid examples: notification-preferences, auth-v2, payment-retry.` + ); + } const dir = assertInsideWorkspace( workspace.rootDir, - import_path10.default.join(workspace.rootDir, KIRO_DIR_NAME, KIRO_SPECS_DIR, request.name) + import_path10.default.join(workspace.rootDir, KIRO_DIR_NAME, KIRO_SPECS_DIR, prepared.name) ); if ((0, import_fs10.existsSync)(dir)) { let entries = []; @@ -29075,24 +29092,23 @@ Valid examples: notification-preferences, auth-v2, payment-retry.` } throw new SpecBridgeError( "SPEC_ALREADY_EXISTS", - `Spec "${request.name}" already exists at ${dir}. + `Spec "${prepared.name}" already exists at ${dir}. ` + (entries.length > 0 ? `Existing files: ${entries.join(", ")}. -` : "") + `SpecBridge never overwrites an existing spec. Inspect it with "spec show ${request.name}", or choose a different name.` +` : "") + `SpecBridge never overwrites an existing spec. Inspect it with "spec show ${prepared.name}", or choose a different name.` ); } - const files = renderSpecTemplates(specType, mode, { title, description }); - const state = newSpecState(request.name, specType, mode, clock); + const state = newSpecState(prepared.name, prepared.specType, prepared.mode, clock); return { - specName: request.name, - specType, - mode, - title, - description, - descriptionIsPlaceholder, + specName: prepared.name, + specType: prepared.specType, + mode: prepared.mode, + title: prepared.title, + description: prepared.description, + descriptionIsPlaceholder: prepared.descriptionIsPlaceholder, dir, - files, + files: prepared.files, state, - statePath: specStatePath(workspace, request.name) + statePath: specStatePath(workspace, prepared.name) }; } function executeSpecCreation(workspace, plan) { @@ -29958,359 +29974,3377 @@ Examples: ); } -// ../../packages/cli/src/commands/spec-new.ts -var SPEC_TYPES = ["feature", "bugfix"]; -var WORKFLOW_MODES = ["requirements-first", "design-first", "quick"]; -function planToJson(plan, dryRun) { - return createJsonReport("specbridge.spec-new/1", `${CLI_BIN} ${VERSION}`, { - dryRun, - created: !dryRun, - specName: plan.specName, - specType: plan.specType, - workflowMode: plan.mode, - title: plan.title, - dir: plan.dir, - files: plan.files.map((file) => ({ - fileName: file.fileName, - stage: file.stage, - bytes: Buffer.byteLength(file.content, "utf8"), - content: file.content - })), - state: plan.state, - statePath: plan.statePath - }); +// ../../packages/templates/dist/index.js +var import_fs12 = require("fs"); +var import_path11 = __toESM(require("path"), 1); +var import_fs13 = require("fs"); +var import_path12 = __toESM(require("path"), 1); +var import_fs14 = require("fs"); +var import_path13 = __toESM(require("path"), 1); +var import_path14 = __toESM(require("path"), 1); +var import_fs15 = require("fs"); +var import_path15 = __toESM(require("path"), 1); +var import_fs16 = require("fs"); +var import_os = require("os"); +var import_path16 = __toESM(require("path"), 1); +var SPECBRIDGE_VERSION = "0.7.0"; +var TEMPLATE_ERROR_CODES = { + SBT001: "template not found", + SBT002: "ambiguous template reference", + SBT003: "invalid template ID", + SBT004: "invalid manifest", + SBT005: "unsupported template schema", + SBT006: "incompatible SpecBridge version", + SBT007: "invalid source path", + SBT008: "path traversal detected", + SBT009: "symlink rejected", + SBT010: "undeclared template file", + SBT011: "invalid target file", + SBT012: "duplicate target file", + SBT013: "missing required variable", + SBT014: "unknown variable", + SBT015: "invalid variable value", + SBT016: "unresolved placeholder", + SBT017: "rendered output invalid", + SBT018: "rendered output too large", + SBT019: "template pack too large", + SBT020: "spec already exists", + SBT021: "template already installed", + SBT022: "built-in template cannot be uninstalled", + SBT023: "candidate hash mismatch", + SBT024: "acknowledgement required", + SBT025: "template operation failed" +}; +var TemplateError = class extends SpecBridgeError { + templateCode; + /** Actionable next step, always present. */ + remediation; + constructor(templateCode, detail, remediation, details) { + super("TEMPLATE_ERROR", `${templateCode} (${TEMPLATE_ERROR_CODES[templateCode]}): ${detail} ${remediation}`, { + ...details, + templateCode + }); + this.name = "TemplateError"; + this.templateCode = templateCode; + this.remediation = remediation; + } +}; +var MAX_TEMPLATE_ID_LENGTH = 64; +var TEMPLATE_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/; +function validateTemplateId(id) { + const problems = []; + if (id.length === 0) { + problems.push("ID must not be empty."); + return { valid: false, problems }; + } + if (id.includes("\0")) { + problems.push("ID must not contain null bytes."); + return { valid: false, problems }; + } + if (id.length > MAX_TEMPLATE_ID_LENGTH) { + problems.push(`ID must be at most ${MAX_TEMPLATE_ID_LENGTH} characters (got ${id.length}).`); + } + if (/[A-Z]/.test(id)) { + problems.push("ID must use lowercase letters only."); + } + if (/_/.test(id)) { + problems.push("ID must use hyphens, not underscores."); + } + if (/[\\/]/.test(id) || id.includes("..")) { + problems.push('ID must not contain path separators or "..".'); + } + if (/\s/.test(id)) { + problems.push("ID must not contain spaces."); + } + if (id.startsWith("-") || id.endsWith("-")) { + problems.push("ID must not start or end with a hyphen."); + } + if (id.includes("--")) { + problems.push("ID must not contain repeated hyphens."); + } + if (problems.length === 0 && !TEMPLATE_ID_PATTERN.test(id)) { + problems.push( + "ID must start with a lowercase letter and contain only lowercase letters, digits, and single hyphens." + ); + } + return { valid: problems.length === 0, problems }; } -function printPlanSummary(runtime, plan, dryRun) { - const workspace = runtime.workspace(); - runtime.out(reportTitle(dryRun ? `Dry run \u2014 nothing was written` : `Created spec: ${plan.specName}`)); - runtime.out(); - runtime.out(` Name: ${plan.specName}`); - runtime.out(` Type: ${plan.specType}`); - runtime.out(` Mode: ${plan.mode}`); - runtime.out(` Title: ${plan.title}`); - runtime.out(` Dir: ${relPath(workspace, plan.dir)}`); - runtime.out(); - runtime.out(sectionTitle(dryRun ? "Files that would be created" : "Files created")); - for (const file of plan.files) { - runtime.out(okLine(`${relPath(workspace, plan.dir)}/${file.fileName}`, `(${Buffer.byteLength(file.content, "utf8")} B)`)); +function formatTemplateReference(source, id) { + return `${source}:${id}`; +} +function parseTemplateReference(raw) { + const trimmed = raw.trim(); + const colon = trimmed.indexOf(":"); + if (colon === -1) { + return validateTemplateId(trimmed).valid ? { source: void 0, id: trimmed } : void 0; } - runtime.out(okLine(relPath(workspace, plan.statePath), "(sidecar workflow state)")); - runtime.out(); - if (dryRun) { - runtime.out(sectionTitle("Rendered content")); - for (const file of plan.files) { - runtime.out(dim(`--- ${file.fileName} ---`)); - runtime.outRaw(file.content); + const source = trimmed.slice(0, colon); + const id = trimmed.slice(colon + 1); + if (source !== "builtin" && source !== "project") { + return void 0; + } + return validateTemplateId(id).valid ? { source, id } : void 0; +} +var TEMPLATE_PACK_LIMITS = { + /** Maximum number of files in a pack (manifest and README included). */ + maxPackFiles: 20, + /** Maximum size of specbridge-template.json in bytes. */ + maxManifestBytes: 256 * 1024, + /** Maximum size of a single template file in bytes. */ + maxTemplateFileBytes: 1024 * 1024, + /** Maximum total pack size in bytes. */ + maxTotalPackBytes: 5 * 1024 * 1024, + /** Maximum size of one rendered document in bytes. */ + maxRenderedFileBytes: 1024 * 1024, + /** Maximum length of a supplied variable value in characters. */ + maxVariableValueLength: 1e5 +}; +var VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/; +var COMPARATOR_PATTERN = /^(>=|<=|>|<|=)?(\d+)\.(\d+)\.(\d+)$/; +function parseSemver(version2) { + const match = VERSION_PATTERN.exec(version2); + if (!match) return void 0; + return [Number(match[1]), Number(match[2]), Number(match[3])]; +} +function compareSemver(a2, b) { + for (let i2 = 0; i2 < 3; i2 += 1) { + const left = a2[i2] ?? 0; + const right = b[i2] ?? 0; + if (left !== right) return left < right ? -1 : 1; + } + return 0; +} +function validateSemverRange(range) { + const parts = range.trim().split(/\s+/); + if (parts.length === 0 || parts.length === 1 && parts[0] === "") { + return { valid: false, problem: "range must not be empty" }; + } + for (const part of parts) { + if (!COMPARATOR_PATTERN.test(part)) { + return { + valid: false, + problem: `unsupported comparator "${part}" \u2014 use space-separated comparators like ">=0.7.0 <1.0.0" with operators >=, <=, >, <, or =` + }; } - runtime.out(dim(`--- sidecar state (${relPath(workspace, plan.statePath)}) ---`)); - runtime.outRaw(`${JSON.stringify(plan.state, null, 2)} -`); - return; } - runtime.out(sectionTitle("Next steps")); - const firstStage = plan.state.specType === "bugfix" ? "bugfix" : plan.mode === "design-first" ? "design" : "requirements"; - runtime.out(` 1. Replace the template placeholders in ${firstStage}.md with real content.`); - runtime.out(` 2. ${CLI_BIN} spec analyze ${plan.specName} --stage ${firstStage}`); - runtime.out(` 3. ${CLI_BIN} spec approve ${plan.specName} --stage ${firstStage}`); - runtime.out(` 4. ${CLI_BIN} spec status ${plan.specName}`); + return { valid: true }; +} +function semverSatisfies(version2, range) { + const target = parseSemver(version2); + if (!target) return false; + const parts = range.trim().split(/\s+/); + for (const part of parts) { + const match = COMPARATOR_PATTERN.exec(part); + if (!match) return false; + const operator = match[1] ?? "="; + const bound = [Number(match[2]), Number(match[3]), Number(match[4])]; + const cmp = compareSemver(target, bound); + switch (operator) { + case ">=": + if (cmp < 0) return false; + break; + case "<=": + if (cmp > 0) return false; + break; + case ">": + if (cmp <= 0) return false; + break; + case "<": + if (cmp >= 0) return false; + break; + case "=": + if (cmp !== 0) return false; + break; + default: + return false; + } + } + return true; } -function registerSpecNewCommand(spec, runtime) { - spec.command("new ").description("Create a new Kiro-compatible spec from offline templates (no model required)").option("--type ", `spec type: ${SPEC_TYPES.join(" | ")}`, "feature").option( - "--mode ", - `workflow mode: ${WORKFLOW_MODES.join(" | ")}`, - "requirements-first" - ).option("--title ", "human-readable title (default: derived from the spec name)").option("--description ", "initial description inserted into the first document").option("--from-file ", "read the description from a UTF-8 file inside the workspace").option("--dry-run", "print everything that would be created without writing any file").option("--json", "output a machine-readable JSON report").addHelpText( - "after", - ` -The spec is created under .kiro/specs// and stays fully Kiro-compatible: -no front matter, no tool metadata. Workflow state (approvals) lives in -.specbridge/state/specs/.json. - -Spec names use lowercase words separated by single hyphens: notification-preferences, -auth-v2, payment-retry. - -Examples: - ${CLI_BIN} spec new notification-preferences - ${CLI_BIN} spec new notification-preferences --mode requirements-first --title "Notification Preferences" - ${CLI_BIN} spec new cache-fallback --type bugfix --description "Fix stale cache fallback after upstream timeout" - ${CLI_BIN} spec new payment-retry --mode quick --from-file feature-description.md - ${CLI_BIN} spec new payment-retry --dry-run` - ).action((name, options) => { - if (!SPEC_TYPES.includes(options.type)) { - throw new SpecBridgeError( - "INVALID_ARGUMENT", - `Unknown --type "${options.type}". Valid types: ${SPEC_TYPES.join(", ")}.` +var TEMPLATE_MANIFEST_FILE_NAME = "specbridge-template.json"; +var SUPPORTED_KIRO_LAYOUT = "1"; +var BUILTIN_VARIABLE_NAMES = [ + "specName", + "title", + "description", + "kind", + "mode", + "generatedDate" +]; +var ALLOWED_TARGETS = { + feature: ["requirements.md", "design.md", "tasks.md"], + bugfix: ["bugfix.md", "design.md", "tasks.md"] +}; +var TARGET_STAGES = { + "requirements.md": "requirements", + "bugfix.md": "bugfix", + "design.md": "design", + "tasks.md": "tasks" +}; +var VARIABLE_NAME_PATTERN = /^[a-z][a-zA-Z0-9]*$/; +var TAG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +var SEMVER_PATTERN = /^\d+\.\d+\.\d+$/; +var SOURCE_PATH_PATTERN = /^files\/[a-z0-9][a-z0-9.-]*\.template$/; +var MAX_VARIABLE_NAME_LENGTH = 64; +var MAX_PATTERN_LENGTH = 200; +function checkSafePattern(pattern) { + if (pattern.length > MAX_PATTERN_LENGTH) { + return `pattern exceeds ${MAX_PATTERN_LENGTH} characters`; + } + if (/\\[1-9]/.test(pattern)) { + return "backreferences (\\1\u2013\\9) are not allowed"; + } + if (/\)[*+?{]/.test(pattern)) { + return "quantified groups like (\u2026)+ are not allowed"; + } + try { + new RegExp(pattern, "u"); + } catch (cause) { + return `not a valid regular expression: ${cause instanceof Error ? cause.message : String(cause)}`; + } + return void 0; +} +var templateVariableSchema = external_exports.object({ + name: external_exports.string().min(1).max(MAX_VARIABLE_NAME_LENGTH), + description: external_exports.string().min(1).max(500), + type: external_exports.enum(["string", "boolean", "integer", "enum"]), + required: external_exports.boolean().default(false), + default: external_exports.union([external_exports.string(), external_exports.boolean(), external_exports.number()]).optional(), + /** Allowed values; required for and exclusive to `enum` variables. */ + values: external_exports.array(external_exports.string().min(1).max(200)).min(1).max(50).optional(), + minLength: external_exports.number().int().min(0).optional(), + maxLength: external_exports.number().int().min(0).optional(), + minimum: external_exports.number().int().optional(), + maximum: external_exports.number().int().optional(), + /** Restricted safe regular expression the (string) value must match. */ + pattern: external_exports.string().optional() +}).strict(); +var templateFileSchema = external_exports.object({ + source: external_exports.string().min(1), + target: external_exports.string().min(1), + stage: external_exports.enum(["requirements", "bugfix", "design", "tasks"]), + required: external_exports.boolean().default(true) +}).strict(); +var templateCompatibilitySchema = external_exports.object({ + specbridge: external_exports.string().min(1), + kiroLayout: external_exports.string().min(1) +}).strict(); +var templateManifestSchema = external_exports.object({ + schemaVersion: external_exports.string().regex(SEMVER_PATTERN), + id: external_exports.string().min(1), + version: external_exports.string().regex(SEMVER_PATTERN), + displayName: external_exports.string().min(1).max(100), + description: external_exports.string().min(1).max(500), + kind: external_exports.enum(["feature", "bugfix"]), + supportedModes: external_exports.array(external_exports.enum(["requirements-first", "design-first", "quick"])).min(1).max(3), + defaultMode: external_exports.enum(["requirements-first", "design-first", "quick"]), + tags: external_exports.array(external_exports.string().min(1).max(32)).max(12), + files: external_exports.array(templateFileSchema).min(1).max(TEMPLATE_PACK_LIMITS.maxPackFiles), + variables: external_exports.array(templateVariableSchema).max(30), + compatibility: templateCompatibilitySchema, + license: external_exports.string().min(1).max(50), + /** Optional safe metadata — inert strings, never executed or fetched. */ + author: external_exports.string().min(1).max(200).optional(), + homepage: external_exports.string().min(1).max(500).optional(), + repository: external_exports.string().min(1).max(500).optional(), + examples: external_exports.array(external_exports.string().min(1).max(500)).max(5).optional(), + deprecated: external_exports.boolean().optional(), + replacement: external_exports.string().min(1).optional(), + /** + * Opt-in for the deterministic `generatedDate` built-in variable + * (YYYY-MM-DD from an injectable clock). Off by default so rendering is + * fully input-determined unless a template explicitly asks for the date. + */ + generatedDate: external_exports.boolean().optional() +}).strict(); +function issue(code2, category, message) { + return { code: code2, category, severity: "error", message }; +} +function checkSourcePath(source) { + if (source.includes("\0")) return "contains a null byte"; + if (source.includes("\\")) return "must use forward slashes"; + if (source.startsWith("/") || /^[A-Za-z]:/.test(source)) return "must not be an absolute path"; + if (source.split("/").includes("..") || source.split("/").includes(".")) { + return 'must not contain "." or ".." segments'; + } + if (!SOURCE_PATH_PATTERN.test(source)) { + return "must match files/.template (e.g. files/requirements.md.template)"; + } + return void 0; +} +function checkManifestSemantics(manifest) { + const issues = []; + const idCheck = validateTemplateId(manifest.id); + if (!idCheck.valid) { + for (const problem of idCheck.problems) { + issues.push(issue("SBT003", "manifest", `Template ID "${manifest.id}": ${problem}`)); + } + } + const majorVersion = manifest.schemaVersion.split(".")[0]; + if (majorVersion !== "1") { + issues.push( + issue( + "SBT005", + "manifest", + `schemaVersion ${manifest.schemaVersion} is not supported; this SpecBridge understands schema 1.x.` + ) + ); + } + if (!manifest.supportedModes.includes(manifest.defaultMode)) { + issues.push( + issue( + "SBT004", + "manifest", + `defaultMode "${manifest.defaultMode}" is not in supportedModes [${manifest.supportedModes.join(", ")}].` + ) + ); + } + if (new Set(manifest.supportedModes).size !== manifest.supportedModes.length) { + issues.push(issue("SBT004", "manifest", "supportedModes contains duplicates.")); + } + for (const tag of manifest.tags) { + if (!TAG_PATTERN.test(tag)) { + issues.push( + issue("SBT004", "manifest", `Tag "${tag}" must be lowercase letters/digits with single hyphens.`) ); } - if (!WORKFLOW_MODES.includes(options.mode)) { - throw new SpecBridgeError( - "INVALID_ARGUMENT", - `Unknown --mode "${options.mode}". Valid modes: ${WORKFLOW_MODES.join(", ")}.` + } + if (new Set(manifest.tags).size !== manifest.tags.length) { + issues.push(issue("SBT004", "manifest", "tags contains duplicates.")); + } + const allowed = ALLOWED_TARGETS[manifest.kind]; + const seenTargets = /* @__PURE__ */ new Set(); + const seenSources = /* @__PURE__ */ new Set(); + for (const file of manifest.files) { + const sourceProblem = checkSourcePath(file.source); + if (sourceProblem !== void 0) { + const code2 = /absolute/.test(sourceProblem) || /"\.\."/.test(sourceProblem) ? "SBT008" : "SBT007"; + issues.push(issue(code2, "paths", `File source "${file.source}" ${sourceProblem}.`)); + } + if (seenSources.has(file.source)) { + issues.push(issue("SBT004", "files", `File source "${file.source}" is declared twice.`)); + } + seenSources.add(file.source); + if (!allowed.includes(file.target)) { + issues.push( + issue( + "SBT011", + "kiro-layout", + `Target "${file.target}" is not an allowed ${manifest.kind} spec file. Allowed targets: ${allowed.join(", ")}. Variables are never allowed in target paths.` + ) ); + continue; } - const workspace = runtime.workspace(); - const request = { - name, - specType: options.type, - mode: options.mode, - ...options.title !== void 0 ? { title: options.title } : {}, - ...options.description !== void 0 ? { description: options.description } : {}, - ...options.fromFile !== void 0 ? { fromFile: options.fromFile } : {}, - cwd: runtime.cwd - }; - const clock = () => runtime.now(); - if (options.dryRun === true) { - const plan = planSpecCreation(workspace, request, clock); - if (options.json === true) { - runtime.outRaw(serializeJsonReport(planToJson(plan, true))); - } else { - printPlanSummary(runtime, plan, true); - } - return; + if (seenTargets.has(file.target)) { + issues.push(issue("SBT012", "files", `Target "${file.target}" is declared more than once.`)); } - const result = createSpec(workspace, request, clock); - if (options.json === true) { - runtime.outRaw(serializeJsonReport(planToJson(result.plan, false))); - return; + seenTargets.add(file.target); + const expectedStage = TARGET_STAGES[file.target]; + if (expectedStage !== void 0 && file.stage !== expectedStage) { + issues.push( + issue( + "SBT004", + "files", + `Target "${file.target}" must declare stage "${expectedStage}" (got "${file.stage}").` + ) + ); } - printPlanSummary(runtime, result.plan, false); - }); -} - -// ../../packages/cli/src/commands/spec-analyze.ts -var STAGE_CHOICES = [...STAGE_NAMES, "all"]; -function registerSpecAnalyzeCommand(spec, runtime) { - spec.command("analyze ").description("Analyze a spec for structural and consistency problems (deterministic, offline)").option("--stage ", `stage to analyze: ${STAGE_CHOICES.join(" | ")}`, "all").option("--strict", "treat warnings as failures (exit 1)").option("--json", "output a machine-readable JSON report").addHelpText( - "after", - ` -Findings come in three levels: error (blocks approval), warning (reported, -never blocks unless --strict), and info. Placeholders left over from -generated templates are errors for active stages and warnings for stages -still blocked behind an unapproved prerequisite. - -Exit codes: 0 no errors \xB7 1 errors found (or warnings with --strict) \xB7 2 usage/runtime error. - -Examples: - ${CLI_BIN} spec analyze notification-preferences - ${CLI_BIN} spec analyze notification-preferences --stage requirements - ${CLI_BIN} spec analyze login-timeout-fix --stage bugfix --json - ${CLI_BIN} spec analyze notification-preferences --strict` - ).action((name, options) => { - if (!STAGE_CHOICES.includes(options.stage)) { - throw new SpecBridgeError( - "INVALID_ARGUMENT", - `Unknown --stage "${options.stage}". Valid stages: ${STAGE_CHOICES.join(", ")}.` + if (!file.required) { + issues.push( + issue( + "SBT004", + "kiro-layout", + `Target "${file.target}" is marked optional, but Kiro layout ${SUPPORTED_KIRO_LAYOUT} requires all ${manifest.kind} files. Set "required": true.` + ) ); } - const workspace = runtime.workspace(); - const folder = requireSpec(workspace, name); - const spec2 = analyzeSpec(workspace, folder); - const stateRead = readSpecState(workspace, folder.name); - let evaluation; - if (stateRead.state !== void 0) { - evaluation = evaluateWorkflow(workspace, stateRead.state); + } + for (const target of allowed) { + if (!seenTargets.has(target)) { + issues.push( + issue( + "SBT004", + "kiro-layout", + `A ${manifest.kind} template must render "${target}", but no file declares it as a target.` + ) + ); } - let stages; - if (options.stage !== "all") { - const stage = options.stage; - const specType = stateRead.state?.specType ?? (spec2.classification.type === "bugfix" ? "bugfix" : "feature"); - if (!isStageApplicable(specType, stage)) { - throw new SpecBridgeError( - "INVALID_ARGUMENT", - `Stage "${stage}" does not apply to a ${specType} spec. Applicable stages: ${applicableStages(specType).join(", ")}.` + } + const seenVariables = /* @__PURE__ */ new Set(); + for (const variable of manifest.variables) { + if (!VARIABLE_NAME_PATTERN.test(variable.name)) { + issues.push( + issue( + "SBT004", + "variables", + `Variable name "${variable.name}" must match [a-z][a-zA-Z0-9]* (lower camelCase).` + ) + ); + } + if (BUILTIN_VARIABLE_NAMES.includes(variable.name)) { + issues.push( + issue( + "SBT004", + "variables", + `Variable "${variable.name}" shadows a built-in variable. Built-ins (${BUILTIN_VARIABLE_NAMES.join(", ")}) are provided by SpecBridge and cannot be redeclared.` + ) + ); + } + if (seenVariables.has(variable.name)) { + issues.push(issue("SBT004", "variables", `Variable "${variable.name}" is declared twice.`)); + } + seenVariables.add(variable.name); + if (variable.type === "enum") { + if (variable.values === void 0) { + issues.push( + issue("SBT004", "variables", `Enum variable "${variable.name}" must declare "values".`) + ); + } else if (new Set(variable.values).size !== variable.values.length) { + issues.push( + issue("SBT004", "variables", `Enum variable "${variable.name}" has duplicate values.`) ); } - stages = [stage]; + } else if (variable.values !== void 0) { + issues.push( + issue("SBT004", "variables", `"values" is only allowed on enum variables ("${variable.name}").`) + ); } - const result = analyzeSpecWorkflow(spec2, evaluation, stages); - const failed = result.hasErrors || options.strict === true && result.warningCount > 0; - if (options.json === true) { - runtime.outRaw( - serializeJsonReport( - createJsonReport("specbridge.spec-analyze/1", `${CLI_BIN} ${VERSION}`, { - specName: result.specName, - strict: options.strict === true, - managed: evaluation !== void 0, - stages: result.stages.map((stage) => ({ - stage: stage.stage, - fileName: stage.fileName, - fileExists: stage.fileExists, - diagnostics: stage.diagnostics - })), - errorCount: result.errorCount, - warningCount: result.warningCount, - failed - }) + if (variable.type !== "string" && (variable.minLength !== void 0 || variable.maxLength !== void 0 || variable.pattern !== void 0)) { + issues.push( + issue( + "SBT004", + "variables", + `minLength/maxLength/pattern are only allowed on string variables ("${variable.name}").` ) ); - runtime.exitCode = failed ? 1 : 0; - return; } - runtime.out(reportTitle(`Analysis: ${folder.name}`)); - if (stateRead.diagnostics.length > 0) { - for (const diagnostic of stateRead.diagnostics) { - runtime.out(severityLine(diagnostic.severity, diagnostic.message)); - } + if (variable.type !== "integer" && (variable.minimum !== void 0 || variable.maximum !== void 0)) { + issues.push( + issue("SBT004", "variables", `minimum/maximum are only allowed on integer variables ("${variable.name}").`) + ); } - if (evaluation === void 0) { - runtime.out(dim(" Approval state: unmanaged (no sidecar state) \u2014 analyzing all present stages at full strictness.")); + if (variable.minLength !== void 0 && variable.maxLength !== void 0 && variable.minLength > variable.maxLength) { + issues.push( + issue("SBT004", "variables", `Variable "${variable.name}": minLength exceeds maxLength.`) + ); } - runtime.out(); - for (const stage of result.stages) { - runtime.out(sectionTitle(`${stage.stage} (${stage.fileName})`)); - if (!stage.fileExists && stage.diagnostics.length === 0) { - runtime.out(dim(" not present")); - } else if (stage.diagnostics.length === 0) { - runtime.out(okLine("no findings")); - } else { - for (const diagnostic of stage.diagnostics) { - const location = diagnostic.file !== void 0 ? ` [${relPath(workspace, diagnostic.file)}${diagnostic.line !== void 0 ? `:${diagnostic.line}` : ""}]` : ""; - runtime.out(severityLine(diagnostic.severity, `${diagnostic.message}${location}`)); - } + if (variable.minimum !== void 0 && variable.maximum !== void 0 && variable.minimum > variable.maximum) { + issues.push(issue("SBT004", "variables", `Variable "${variable.name}": minimum exceeds maximum.`)); + } + if (variable.pattern !== void 0) { + const patternProblem = checkSafePattern(variable.pattern); + if (patternProblem !== void 0) { + issues.push( + issue("SBT004", "variables", `Variable "${variable.name}" pattern rejected: ${patternProblem}.`) + ); } - runtime.out(); } - const summary = `${result.errorCount} error${result.errorCount === 1 ? "" : "s"}, ${result.warningCount} warning${result.warningCount === 1 ? "" : "s"}`; - if (failed) { - runtime.out(`Result: ${reportTitle("FAIL")} \u2014 ${summary}${options.strict === true && !result.hasErrors ? " (strict mode)" : ""}`); - } else { - runtime.out(`Result: ${reportTitle("OK")} \u2014 ${summary}`); + if (variable.default !== void 0) { + const defaultType = typeof variable.default; + const expected = { + string: "string", + boolean: "boolean", + integer: "number", + enum: "string" + }; + if (defaultType !== expected[variable.type]) { + issues.push( + issue( + "SBT004", + "variables", + `Variable "${variable.name}" default must be a ${expected[variable.type]} (got ${defaultType}).` + ) + ); + } + if (variable.required) { + issues.push( + issue( + "SBT004", + "variables", + `Variable "${variable.name}" is required and also has a default \u2014 pick one.` + ) + ); + } } - runtime.exitCode = failed ? 1 : 0; - }); + } + const rangeCheck = validateSemverRange(manifest.compatibility.specbridge); + if (!rangeCheck.valid) { + issues.push( + issue( + "SBT004", + "compatibility", + `compatibility.specbridge is invalid: ${rangeCheck.problem ?? "unparseable range"}.` + ) + ); + } + if (manifest.compatibility.kiroLayout !== SUPPORTED_KIRO_LAYOUT) { + issues.push( + issue( + "SBT006", + "compatibility", + `compatibility.kiroLayout "${manifest.compatibility.kiroLayout}" is not supported (this SpecBridge supports layout "${SUPPORTED_KIRO_LAYOUT}").` + ) + ); + } + if (manifest.replacement !== void 0 && !validateTemplateId(manifest.replacement).valid) { + issues.push( + issue("SBT004", "manifest", `replacement "${manifest.replacement}" is not a valid template ID.`) + ); + } + return issues; } - -// ../../packages/cli/src/commands/spec-approve.ts -function resultToJson(specName, result) { - const base = { specName }; - if (result.ok && result.action === "approved") { - return createJsonReport("specbridge.spec-approve/1", `${CLI_BIN} ${VERSION}`, { - ...base, - action: "approved", - stage: result.stage, - hash: result.hash, - reapproved: result.reapproved, - invalidated: result.invalidated, - initialized: result.initialized, - status: result.state.status, - statePath: result.statePath, - warnings: result.analysis.diagnostics.filter((d) => d.severity === "warning"), - diagnostics: result.diagnostics - }); +function parseTemplateManifest(text) { + if (Buffer.byteLength(text, "utf8") > TEMPLATE_PACK_LIMITS.maxManifestBytes) { + return { + issues: [ + issue( + "SBT019", + "limits", + `Manifest exceeds ${TEMPLATE_PACK_LIMITS.maxManifestBytes} bytes. Manifests are metadata, not content.` + ) + ] + }; } - if (result.ok) { - return createJsonReport("specbridge.spec-approve/1", `${CLI_BIN} ${VERSION}`, { - ...base, - action: "revoked", - stage: result.stage, - invalidated: result.invalidated, - status: result.state.status, - statePath: result.statePath, - diagnostics: result.diagnostics - }); + let parsed; + try { + parsed = JSON.parse(text); + } catch (cause) { + return { + issues: [ + issue( + "SBT004", + "manifest", + `Manifest is not valid JSON: ${cause instanceof Error ? cause.message : String(cause)}.` + ) + ] + }; } - return createJsonReport("specbridge.spec-approve/1", `${CLI_BIN} ${VERSION}`, { - ...base, - action: "blocked", - reason: result.reason, - message: result.message, - missingPrerequisites: result.missingPrerequisites ?? [], - stalePrerequisites: result.stalePrerequisites ?? [], - analysis: result.analysis !== void 0 ? { - errorCount: result.analysis.errorCount, - warningCount: result.analysis.warningCount, - diagnostics: result.analysis.diagnostics - } : null, - diagnostics: result.diagnostics - }); + if (typeof parsed === "object" && parsed !== null) { + const declared = parsed["schemaVersion"]; + if (typeof declared === "string" && SEMVER_PATTERN.test(declared) && declared.split(".")[0] !== "1") { + return { + issues: [ + issue( + "SBT005", + "manifest", + `schemaVersion ${declared} is not supported; this SpecBridge understands schema 1.x. Upgrade SpecBridge or use a 1.x template.` + ) + ] + }; + } + } + const result = templateManifestSchema.safeParse(parsed); + if (!result.success) { + return { + issues: result.error.issues.slice(0, 25).map( + (zodIssue) => issue( + "SBT004", + "manifest", + `${zodIssue.path.length > 0 ? zodIssue.path.join(".") : "manifest"}: ${zodIssue.message}` + ) + ) + }; + } + const semanticIssues = checkManifestSemantics(result.data); + if (semanticIssues.some((entry) => entry.severity === "error")) { + return { manifest: result.data, issues: semanticIssues }; + } + return { manifest: result.data, issues: semanticIssues }; } -function registerSpecApproveCommand(spec, runtime) { - spec.command("approve ").description("Approve (or revoke) a workflow stage; approvals live in .specbridge, never in .kiro").requiredOption("--stage ", `stage to approve: ${STAGE_NAMES.join(" | ")}`).option("--revoke", "revoke the stage approval (dependent approvals are invalidated too)").option("--json", "output a machine-readable JSON report").addHelpText( - "after", - ` -Approval records the SHA-256 of the exact file bytes plus a timestamp in -.specbridge/state/specs/.json. The Markdown file itself is never -rewritten. If an approved file changes later, the approval is reported as -stale; re-approving updates the hash (and invalidates dependent approvals, -because they were made against different content). - -Prerequisites by workflow: - requirements-first requirements -> design -> tasks - design-first design -> requirements -> tasks - quick requirements + design in any order, then tasks - bugfix bugfix -> design -> tasks - -For an existing Kiro spec without SpecBridge state, the first successful -approval initializes the sidecar state (origin: existing-kiro-workspace). - -Exit codes: 0 approved/revoked \xB7 1 blocked (prerequisites or analysis errors) \xB7 2 usage error. - -Examples: - ${CLI_BIN} spec approve notification-preferences --stage requirements - ${CLI_BIN} spec approve notification-preferences --stage design - ${CLI_BIN} spec approve login-timeout-fix --stage bugfix - ${CLI_BIN} spec approve notification-preferences --stage requirements --revoke` - ).action((name, options) => { - const stage = options.stage; - if (stage === void 0 || !STAGE_NAMES.includes(stage)) { - throw new SpecBridgeError( - "INVALID_ARGUMENT", - `Unknown --stage "${options.stage ?? ""}". Valid stages: ${STAGE_NAMES.join(", ")}.` +var PLACEHOLDER_PATTERN = /\{\{([^{}\r\n]*)\}\}/g; +var VALID_PLACEHOLDER_NAME = /^[a-z][a-zA-Z0-9]*$/; +function renderTemplateText(sourceLabel, text, values) { + const parts = []; + let lastIndex = 0; + PLACEHOLDER_PATTERN.lastIndex = 0; + let match; + while ((match = PLACEHOLDER_PATTERN.exec(text)) !== null) { + parts.push(text.slice(lastIndex, match.index)); + lastIndex = match.index + match[0].length; + const inner = match[1] ?? ""; + if (!VALID_PLACEHOLDER_NAME.test(inner)) { + throw new TemplateError( + "SBT016", + `${sourceLabel} contains a malformed placeholder "${truncatePlaceholder(match[0])}".`, + "Placeholders must be exactly {{variableName}} with a name matching [a-z][a-zA-Z0-9]*. Literal double braces are not supported in template files.", + { source: sourceLabel } ); } - const workspace = runtime.workspace(); - const folder = requireSpec(workspace, name); - const spec2 = analyzeSpec(workspace, folder); - const result = approveStage( - workspace, - spec2, - { stage, ...options.revoke === true ? { revoke: true } : {} }, - { clock: () => runtime.now() } + const value = values.get(inner); + if (value === void 0) { + throw new TemplateError( + "SBT016", + `${sourceLabel} references "{{${inner}}}", which is not a declared or built-in variable.`, + 'Declare the variable in the manifest "variables" array, or remove the placeholder.', + { source: sourceLabel, variable: inner } + ); + } + parts.push(value); + } + parts.push(text.slice(lastIndex)); + const rendered = parts.join(""); + const renderedBytes = Buffer.byteLength(rendered, "utf8"); + if (renderedBytes > TEMPLATE_PACK_LIMITS.maxRenderedFileBytes) { + throw new TemplateError( + "SBT018", + `Rendering ${sourceLabel} produced ${renderedBytes} bytes (limit ${TEMPLATE_PACK_LIMITS.maxRenderedFileBytes}).`, + "Shorten the template or the supplied variable values.", + { source: sourceLabel, bytes: renderedBytes } ); - if (options.json === true) { - runtime.outRaw(serializeJsonReport(resultToJson(folder.name, result))); - runtime.exitCode = result.ok ? 0 : result.failure === "usage" ? 2 : 1; - return; + } + return rendered; +} +function truncatePlaceholder(raw) { + return raw.length > 40 ? `${raw.slice(0, 40)}\u2026` : raw; +} +function collectPlaceholders(text) { + const names = []; + const malformed = []; + const seen = /* @__PURE__ */ new Set(); + PLACEHOLDER_PATTERN.lastIndex = 0; + let match; + while ((match = PLACEHOLDER_PATTERN.exec(text)) !== null) { + const inner = match[1] ?? ""; + if (!VALID_PLACEHOLDER_NAME.test(inner)) { + malformed.push(truncatePlaceholder(match[0])); + continue; } - if (!result.ok) { - runtime.err(result.message); - if (result.reason === "prerequisites-unmet") { - const nextStage = result.missingPrerequisites?.[0] ?? result.stalePrerequisites?.[0]; - if (nextStage !== void 0) { - runtime.err(""); - runtime.err("Run:"); - runtime.err(` ${CLI_BIN} spec analyze ${folder.name} --stage ${nextStage}`); - runtime.err(` ${CLI_BIN} spec approve ${folder.name} --stage ${nextStage}`); - } + if (!seen.has(inner)) { + seen.add(inner); + names.push(inner); + } + } + return { names, malformed }; +} +function rejectNullBytes(name, value) { + if (value.includes("\0")) { + throw new TemplateError( + "SBT015", + `Variable "${name}" contains a null byte.`, + "Remove the null byte from the value.", + { variable: name } + ); + } +} +function coerceValue(variable, raw) { + const name = variable.name; + switch (variable.type) { + case "string": { + if (typeof raw !== "string") { + throw invalidValue(name, `expected a string, got ${typeof raw}`); } - if (result.reason === "analysis-errors" && result.analysis !== void 0) { - runtime.err(""); - for (const diagnostic of result.analysis.diagnostics.filter((d) => d.severity === "error")) { - const location = diagnostic.file !== void 0 ? ` [${relPath(workspace, diagnostic.file)}${diagnostic.line !== void 0 ? `:${diagnostic.line}` : ""}]` : ""; - runtime.err(` ${diagnostic.severity === "error" ? "\u2717" : "!"} ${diagnostic.message}${location}`); + if (raw.length > TEMPLATE_PACK_LIMITS.maxVariableValueLength) { + throw invalidValue(name, `value exceeds ${TEMPLATE_PACK_LIMITS.maxVariableValueLength} characters`); + } + if (variable.minLength !== void 0 && raw.length < variable.minLength) { + throw invalidValue(name, `value is shorter than minLength ${variable.minLength}`); + } + if (variable.maxLength !== void 0 && raw.length > variable.maxLength) { + throw invalidValue(name, `value is longer than maxLength ${variable.maxLength}`); + } + if (variable.pattern !== void 0) { + if (raw.length > 2e3) { + throw invalidValue(name, "values checked against a pattern must be at most 2000 characters"); + } + if (!new RegExp(variable.pattern, "u").test(raw)) { + throw invalidValue(name, `value does not match pattern ${variable.pattern}`); } - runtime.err(""); - runtime.err(dim(`Full report: ${CLI_BIN} spec analyze ${folder.name} --stage ${stage}`)); } - runtime.exitCode = result.failure === "usage" ? 2 : 1; - return; + return raw; } - if (result.action === "revoked") { - runtime.out(reportTitle(`Revoked: ${folder.name} \u2014 ${result.stage}`)); - runtime.out(); - runtime.out(okLine(`${result.stage} approval revoked (files were not touched)`)); - for (const invalidated of result.invalidated) { - runtime.out(warnLine(`${invalidated} approval invalidated (it depended on ${result.stage})`)); + case "boolean": { + if (typeof raw === "boolean") return raw ? "true" : "false"; + if (raw === "true") return "true"; + if (raw === "false") return "false"; + throw invalidValue(name, `expected true or false, got "${String(raw)}"`); + } + case "integer": { + let value; + if (typeof raw === "number") { + value = raw; + } else if (typeof raw === "string" && /^-?\d+$/.test(raw.trim())) { + value = Number(raw.trim()); + } else { + throw invalidValue(name, `expected an integer, got "${String(raw)}"`); } - runtime.out(); - runtime.out(` Status: ${result.state.status}`); - runtime.out(dim(` State: ${relPath(workspace, result.statePath)}`)); - return; + if (!Number.isSafeInteger(value)) { + throw invalidValue(name, "value is not a safe integer"); + } + if (variable.minimum !== void 0 && value < variable.minimum) { + throw invalidValue(name, `value is below minimum ${variable.minimum}`); + } + if (variable.maximum !== void 0 && value > variable.maximum) { + throw invalidValue(name, `value is above maximum ${variable.maximum}`); + } + return String(value); } - runtime.out(reportTitle(`Approved: ${folder.name} \u2014 ${result.stage}`)); - runtime.out(); - if (result.initialized) { - runtime.out(okLine("Sidecar state initialized for this existing Kiro spec", "(origin: existing-kiro-workspace)")); + case "enum": { + if (typeof raw !== "string") { + throw invalidValue(name, `expected one of ${(variable.values ?? []).join(", ")}`); + } + if (!(variable.values ?? []).includes(raw)) { + throw invalidValue( + name, + `"${raw}" is not an allowed value. Allowed: ${(variable.values ?? []).join(", ")}` + ); + } + return raw; + } + } +} +function invalidValue(name, detail) { + return new TemplateError("SBT015", `Variable "${name}": ${detail}.`, "Fix the value and retry.", { + variable: name + }); +} +function formatGeneratedDate(clock) { + return clock().toISOString().slice(0, 10); +} +function resolveVariables(manifest, supplied, builtins) { + const values = /* @__PURE__ */ new Map(); + for (const [name, raw] of [ + ["specName", builtins.specName], + ["title", builtins.title], + ["description", builtins.description], + ["kind", builtins.kind], + ["mode", builtins.mode] + ]) { + rejectNullBytes(name, raw); + values.set(name, raw); + } + if (manifest.generatedDate === true) { + values.set("generatedDate", formatGeneratedDate(builtins.clock)); + } + const declared = new Map(manifest.variables.map((variable) => [variable.name, variable])); + for (const name of Object.keys(supplied)) { + if (BUILTIN_VARIABLE_NAMES.includes(name)) { + throw new TemplateError( + "SBT014", + `"${name}" is a built-in variable and cannot be supplied with --var.`, + name === "title" || name === "description" ? `Use the --${name} option instead.` : "Built-in values are derived from the spec name, kind, and mode.", + { variable: name } + ); + } + if (!declared.has(name)) { + const known = [...declared.keys()]; + throw new TemplateError( + "SBT014", + `Variable "${name}" is not declared by this template.`, + known.length > 0 ? `Declared variables: ${known.join(", ")}.` : "This template declares no variables.", + { variable: name } + ); + } + } + const variableNames = []; + for (const variable of manifest.variables) { + const raw = supplied[variable.name]; + if (raw === void 0) { + if (variable.required) { + throw new TemplateError( + "SBT013", + `Required variable "${variable.name}" was not supplied.`, + `Pass --var ${variable.name}=. ${variable.description}`, + { variable: variable.name } + ); + } + if (variable.default !== void 0) { + const coerced2 = coerceValue(variable, variable.default); + rejectNullBytes(variable.name, coerced2); + values.set(variable.name, coerced2); + variableNames.push(variable.name); + } else { + values.set(variable.name, ""); + variableNames.push(variable.name); + } + continue; + } + if (typeof raw === "string") rejectNullBytes(variable.name, raw); + const coerced = coerceValue(variable, raw); + rejectNullBytes(variable.name, coerced); + values.set(variable.name, coerced); + variableNames.push(variable.name); + } + return { values, variableNames }; +} +var EXTRA_ALLOWED_FILES = ["README.md", "LICENSE"]; +var MAX_PACK_DEPTH = 3; +function issue2(code2, category, message, file) { + return file === void 0 ? { code: code2, category, severity: "error", message } : { code: code2, category, severity: "error", message, file }; +} +function warning(code2, category, message, file) { + return file === void 0 ? { code: code2, category, severity: "warning", message } : { code: code2, category, severity: "warning", message, file }; +} +function readTemplatePackDirectory(dir) { + const rootStat = statNoFollow(dir); + if (rootStat.isSymbolicLink()) { + throw new TemplateError("SBT009", `Template pack path is a symlink: ${dir}.`, "Point at the real directory.", { + path: dir + }); + } + if (!rootStat.isDirectory()) { + throw new TemplateError( + "SBT007", + `Template pack path is not a directory: ${dir}.`, + "Point at a directory containing specbridge-template.json.", + { path: dir } + ); + } + const files = /* @__PURE__ */ new Map(); + let totalBytes = 0; + const walk = (currentDir, relative, depth) => { + if (depth > MAX_PACK_DEPTH) { + throw new TemplateError( + "SBT019", + `Template pack nests deeper than ${MAX_PACK_DEPTH} directories at ${relative}.`, + "Template packs are flat: a manifest, README.md, and a files/ directory.", + { path: currentDir } + ); + } + const entries = (0, import_fs12.readdirSync)(currentDir, { withFileTypes: true }).sort( + (a2, b) => a2.name.localeCompare(b.name, "en") + ); + for (const entry of entries) { + const entryPath = import_path11.default.join(currentDir, entry.name); + const entryRelative = relative === "" ? entry.name : `${relative}/${entry.name}`; + const stat = statNoFollow(entryPath); + if (stat.isSymbolicLink()) { + throw new TemplateError( + "SBT009", + `Template pack contains a symlink: ${entryRelative}.`, + "Remove the symlink; packs must contain regular files only.", + { path: entryPath } + ); + } + if (stat.isDirectory()) { + walk(entryPath, entryRelative, depth + 1); + continue; + } + if (!stat.isFile()) { + throw new TemplateError( + "SBT007", + `Template pack contains a non-regular file: ${entryRelative}.`, + "Packs may contain regular text files only.", + { path: entryPath } + ); + } + if (files.size >= TEMPLATE_PACK_LIMITS.maxPackFiles) { + throw new TemplateError( + "SBT019", + `Template pack has more than ${TEMPLATE_PACK_LIMITS.maxPackFiles} files.`, + "Remove files that are not the manifest, README.md, LICENSE, or declared template files.", + { path: dir } + ); + } + const perFileLimit = Math.max( + TEMPLATE_PACK_LIMITS.maxTemplateFileBytes, + TEMPLATE_PACK_LIMITS.maxManifestBytes + ); + if (stat.size > perFileLimit) { + throw new TemplateError( + "SBT019", + `${entryRelative} is ${stat.size} bytes (per-file limit ${perFileLimit}).`, + "Template files are Markdown documents, not data payloads.", + { path: entryPath } + ); + } + totalBytes += stat.size; + if (totalBytes > TEMPLATE_PACK_LIMITS.maxTotalPackBytes) { + throw new TemplateError( + "SBT019", + `Template pack exceeds ${TEMPLATE_PACK_LIMITS.maxTotalPackBytes} bytes in total.`, + "Reduce the pack size.", + { path: dir } + ); + } + const buffer = (0, import_fs12.readFileSync)(entryPath); + const text = buffer.toString("utf8"); + if (!Buffer.from(text, "utf8").equals(buffer)) { + throw new TemplateError( + "SBT025", + `${entryRelative} is not valid UTF-8 text.`, + "Template packs contain UTF-8 text files only; binary content is rejected.", + { path: entryPath } + ); + } + if (text.includes("\0")) { + throw new TemplateError( + "SBT025", + `${entryRelative} contains binary (null-byte) content.`, + "Template packs contain plain text files only.", + { path: entryPath } + ); + } + files.set(entryRelative, text); + } + }; + walk(dir, "", 0); + return { origin: dir, files }; +} +function statNoFollow(target) { + try { + return (0, import_fs12.lstatSync)(target); + } catch (cause) { + throw new TemplateError( + "SBT007", + `Cannot read template pack path ${target}: ${cause instanceof Error ? cause.message : String(cause)}.`, + "Check that the path exists and is readable.", + { path: target } + ); + } +} +function loadTemplatePack(data, options = {}) { + const issues = []; + const manifestText = data.files.get(TEMPLATE_MANIFEST_FILE_NAME); + const readme = data.files.get("README.md"); + let manifest; + if (manifestText === void 0) { + issues.push( + issue2( + "SBT004", + "manifest", + `Pack has no ${TEMPLATE_MANIFEST_FILE_NAME} at its root. Every template pack starts with a manifest.` + ) + ); + } else { + const parsed = parseTemplateManifest(manifestText); + manifest = parsed.manifest; + issues.push(...parsed.issues); + } + if (readme === void 0) { + const message = "Pack has no README.md. A README with usage instructions is required for built-in and community-ready templates."; + issues.push( + options.requireReadme === true ? issue2("SBT004", "documentation", message) : warning("SBT004", "documentation", message) + ); + } + if (manifest !== void 0) { + const declaredSources = new Set(manifest.files.map((file) => file.source)); + for (const file of manifest.files) { + if (!data.files.has(file.source)) { + issues.push( + issue2("SBT007", "paths", `Declared source "${file.source}" does not exist in the pack.`, file.source) + ); + } + } + for (const packFile of data.files.keys()) { + if (packFile === TEMPLATE_MANIFEST_FILE_NAME) continue; + if (EXTRA_ALLOWED_FILES.includes(packFile)) continue; + if (declaredSources.has(packFile)) continue; + issues.push( + issue2( + "SBT010", + "files", + `"${packFile}" is not the manifest, README.md, LICENSE, or a declared template file. Undeclared files are rejected \u2014 nothing outside the manifest can ever be rendered.`, + packFile + ) + ); + } + for (const file of manifest.files) { + const content = data.files.get(file.source); + if (content === void 0) continue; + const bytes = Buffer.byteLength(content, "utf8"); + if (bytes > TEMPLATE_PACK_LIMITS.maxTemplateFileBytes) { + issues.push( + issue2( + "SBT019", + "limits", + `"${file.source}" is ${bytes} bytes (limit ${TEMPLATE_PACK_LIMITS.maxTemplateFileBytes}).`, + file.source + ) + ); + } + } + const declaredVariables = new Set(manifest.variables.map((variable) => variable.name)); + const availableBuiltins = new Set( + BUILTIN_VARIABLE_NAMES.filter( + (name) => name !== "generatedDate" || manifest?.generatedDate === true + ) + ); + for (const file of manifest.files) { + const content = data.files.get(file.source); + if (content === void 0) continue; + const { names, malformed } = collectPlaceholders(content); + for (const bad of malformed) { + issues.push( + issue2( + "SBT016", + "rendering", + `"${file.source}" contains a malformed placeholder "${bad}". Placeholders must be exactly {{variableName}}.`, + file.source + ) + ); + } + for (const name of names) { + if (!declaredVariables.has(name) && !availableBuiltins.has(name)) { + const hint = name === "generatedDate" ? 'Set "generatedDate": true in the manifest to opt in to the generated date.' : 'Declare it in the manifest "variables" array or remove the placeholder.'; + issues.push( + issue2( + "SBT016", + "rendering", + `"${file.source}" references undeclared variable "{{${name}}}". ${hint}`, + file.source + ) + ); + } + } + } + const version2 = options.specbridgeVersion ?? SPECBRIDGE_VERSION; + if (!semverSatisfies(version2, manifest.compatibility.specbridge)) { + issues.push( + issue2( + "SBT006", + "compatibility", + `Template requires SpecBridge ${manifest.compatibility.specbridge}, but this is ${version2}.` + ) + ); + } + } + return { + origin: data.origin, + manifest, + manifestText, + readme, + files: data.files, + issues, + valid: !issues.some((entry) => entry.severity === "error") + }; +} +function sampleValue(variable) { + if (variable.default !== void 0) return variable.default; + switch (variable.type) { + case "string": { + if (variable.pattern !== void 0) return void 0; + let sample = `Example ${variable.name} value`; + if (variable.maxLength !== void 0 && sample.length > variable.maxLength) { + sample = sample.slice(0, variable.maxLength); + } + if (variable.minLength !== void 0 && sample.length < variable.minLength) { + sample = sample.padEnd(variable.minLength, "x"); + } + return sample; + } + case "boolean": + return true; + case "integer": + return variable.minimum ?? variable.maximum ?? 1; + case "enum": + return variable.values?.[0]; + } +} +function checkPackRendering(pack, clock) { + const manifest = pack.manifest; + if (manifest === void 0) return []; + const issues = []; + const supplied = {}; + let renderable = true; + for (const variable of manifest.variables) { + if (!variable.required && variable.default === void 0 && variable.type === "string") continue; + const sample = sampleValue(variable); + if (sample === void 0) { + issues.push( + warning( + "SBT015", + "rendering", + `Render check could not synthesize a sample for variable "${variable.name}" (pattern-constrained without a default); rendering was checked with the remaining variables.` + ) + ); + if (variable.required) renderable = false; + continue; + } + supplied[variable.name] = sample; + } + if (!renderable) return issues; + let resolved; + try { + resolved = resolveVariables(manifest, supplied, { + specName: "example-spec", + title: "Example Spec", + description: "Example description used by template validation.", + kind: manifest.kind, + mode: manifest.defaultMode, + clock + }); + } catch (cause) { + issues.push( + issue2( + "SBT015", + "rendering", + `Render check failed while resolving variables: ${cause instanceof Error ? cause.message : String(cause)}` + ) + ); + return issues; + } + for (const file of manifest.files) { + const content = pack.files.get(file.source); + if (content === void 0) continue; + let rendered; + try { + rendered = renderTemplateText(file.source, content, resolved.values); + } catch (cause) { + issues.push( + issue2( + "SBT017", + "rendering", + `Rendering "${file.source}" failed: ${cause instanceof Error ? cause.message : String(cause)}`, + file.source + ) + ); + continue; + } + issues.push(...checkRenderedDocument(file.source, file.target, rendered)); + } + return issues; +} +function checkRenderedDocument(sourceLabel, target, rendered) { + const issues = []; + if (rendered.trim().length === 0) { + issues.push(issue2("SBT017", "rendering", `"${sourceLabel}" renders to an empty document.`, sourceLabel)); + return issues; + } + if (!/^#\s+\S/m.test(rendered)) { + issues.push( + issue2( + "SBT017", + "rendering", + `"${sourceLabel}" renders without a top-level "# " heading; Kiro spec files start with one.`, + sourceLabel + ) + ); + } + if (!rendered.endsWith("\n")) { + issues.push( + warning("SBT017", "rendering", `"${sourceLabel}" does not end with a newline.`, sourceLabel) + ); + } + if (rendered.includes("\r\n")) { + issues.push( + warning("SBT017", "rendering", `"${sourceLabel}" renders with CRLF line endings; generated files use LF.`, sourceLabel) + ); + } + issues.push( + ...parserDiagnostics(target, rendered).map( + (diagnostic) => warning("SBT017", "rendering", `${target}: ${diagnostic.message}`, sourceLabel) + ) + ); + return issues; +} +function parserDiagnostics(target, rendered) { + const document = MarkdownDocument.fromText(rendered, target); + const stage = TARGET_STAGES[target]; + switch (stage) { + case "requirements": + return parseRequirements(document).diagnostics; + case "design": + return parseDesign(document).diagnostics; + case "tasks": + return parseTasks(document).diagnostics; + case "bugfix": + return parseBugfix(document).diagnostics; + default: + return []; + } +} +var BUILTIN_TEMPLATE_PACKS = [ + { + id: "authentication", + files: { + "files/design.md.template": '# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers a {{sessionKind}}-based authentication and authorization\nflow for the primary actor "{{actor}}".\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe where authentication sits: the , the\n, the , and every service that validates the\n{{sessionKind}}.\n\n## Components and Interfaces\n\n- Credential verification: .\n- Issuance: .\n- Validation: .\n- Authorization: .\n- Revocation: .\n\n## Credential and Session Flow\n\nDescribe the main flow here: credential presentation, verification,\n{{sessionKind}} issuance, validation on subsequent requests, renewal, and\nsign-out.\n\n## Session Lifecycle\n\n- Expiry: .\n- Renewal: .\n- Revocation: .\n\n## Authorization Model\n\n- \n\n## Replay Protection\n\n- \n\n## Rate Limiting and Lockout\n\n- \n\n## Audit Events\n\n- \n\n## Failure Handling\n\n- Wrong credentials: generic failure response; no account-existence leak; the attempt counts toward lockout.\n- Identity or session store unavailable: .\n- Lockout: .\n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Security tests: .\n- Integration tests: .\n- Regression tests: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n', + "files/requirements.md.template": '# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers an authentication or authorization change for the primary\nactor "{{actor}}", using a {{sessionKind}}-based mechanism issued after\nsuccessful sign-in.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{sessionKind}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a {{actor}}, I want , so that .\n\n#### Acceptance Criteria\n\n1. WHEN valid credentials are presented via , THE SYSTEM SHALL issue a {{sessionKind}} scoped to the authenticated {{actor}} and record a sign-in success audit event.\n2. IF the presented credentials are wrong, THEN THE SYSTEM SHALL reject the attempt with a generic failure response that does not reveal whether the account exists.\n3. WHEN consecutive failed attempts occur for one account, THE SYSTEM SHALL lock further attempts for and record a lockout audit event.\n4. IF sign-in attempts from one exceed , THEN THE SYSTEM SHALL reject further attempts until the window resets.\n\n### Requirement 2: \n\n**User Story:** As a {{actor}}, I want my {{sessionKind}} to stop working when it should, so that a stolen or stale credential cannot be reused.\n\n#### Acceptance Criteria\n\n1. WHEN a {{sessionKind}} older than is presented, THE SYSTEM SHALL reject it and require re-authentication.\n2. WHEN a {{sessionKind}} is revoked by , THE SYSTEM SHALL reject it within even though its expiry has not passed.\n3. IF a one-time credential or renewal artifact is presented a second time, THEN THE SYSTEM SHALL reject the replay and record an audit event.\n\n### Requirement 3: \n\n**User Story:** As a {{actor}}, I want my access limited to what my permissions allow, so that protected operations stay protected.\n\n#### Acceptance Criteria\n\n1. WHEN an authenticated {{actor}} requests an operation inside their authorization boundary, THE SYSTEM SHALL allow the operation.\n2. IF an authenticated {{actor}} requests an operation outside their authorization boundary, THEN THE SYSTEM SHALL deny it with without revealing protected details.\n3. WHEN the permissions of a {{actor}} change, THE SYSTEM SHALL enforce the new boundary on subsequent requests within .\n\n### Requirement 4: \n\n**User Story:** As a , I want authentication events recorded, so that incidents can be investigated after the fact.\n\n#### Acceptance Criteria\n\n1. WHEN a sign-in succeeds or fails, a {{sessionKind}} is issued or revoked, or a lockout starts, THE SYSTEM SHALL record an audit event containing and never the credential itself.\n2. IF the audit destination is unavailable, THEN THE SYSTEM SHALL instead of silently dropping events.\n\n## Non-Functional Requirements\n\n- Performance: .\n- Security: credentials are verified via ; credential material never appears in logs; every authentication endpoint is rate limited.\n- Reliability: .\n- Observability: sign-in outcomes, lockouts, and revocations emit without credential material.\n- Compatibility: existing authenticated sessions ; no {{actor}} is silently signed out without .\n\n## Edge Cases\n\n- Add edge cases here (clock skew between issuing and validating services, concurrent sign-ins for one account, revocation racing an in-flight request, expiry during a long-running operation).\n\n## Out of Scope\n\n- Add explicitly excluded behavior here (account registration, credential recovery, or federation changes, if unchanged).\n', + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the credential verification flow and the {{sessionKind}} format, lifetime, and issuance path.\n- [ ] 2. Implement credential verification with a generic failure response that does not reveal account existence.\n- [ ] 3. Implement {{sessionKind}} validation at every protected entry point, including expiry checks.\n- [ ] 4. Implement revocation so a revoked {{sessionKind}} is rejected before its expiry.\n- [ ] 5. Implement lockout and rate limiting for repeated failed sign-in attempts.\n- [ ] 6. Implement the authorization boundary check and its deny-by-default behavior.\n- [ ] 7. Add replay protection for one-time credentials and renewal requests.\n- [ ] 8. Add audit events for sign-in success and failure, issuance, revocation, and lockout without recording credential material.\n- [ ] 9. Write security tests covering wrong credentials, lockout, expired and revoked {{sessionKind}} rejection, replay, and authorization boundary probes.\n- [ ] 10. Verify existing authenticated flows keep passing and document the rollout and rollback steps.\n", + "README.md": '# Authentication template\n\nA feature spec template for adding or changing authentication or\nauthorization behavior.\n\nIt pre-structures the spec around the questions authentication changes\nalways raise: the credential and session flow, authorization boundaries,\nwrong-credential and lockout behavior, expiry, revocation, replay\nprotection, rate limiting, audit events, and security tests. It is\nvendor-neutral by design \u2014 it names mechanisms, not products.\n\n## Usage\n\n```bash\nspecbridge template preview authentication \\\n --name login-session-refresh \\\n --var actor=user \\\n --var sessionKind=token\n\nspecbridge template apply authentication \\\n --name login-session-refresh \\\n --var actor=user \\\n --var sessionKind=token\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `actor` | string | `user` | Primary actor who authenticates (a user role or client system). |\n| `sessionKind` | enum: `token`, `cookie`, `session` | `token` | Kind of credential artifact issued after successful authentication. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe engineering judgment stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "authentication",\n "version": "1.0.0",\n "displayName": "Authentication",\n "description": "A feature spec template for adding or changing authentication or authorization behavior: credential and session flow, authorization boundaries, wrong-credential and lockout behavior, expiry, revocation, replay protection, rate limiting, audit events, and security tests.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["authentication", "authorization", "security", "session", "identity"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "actor",\n "description": "Primary actor who authenticates (a user role or client system).",\n "type": "string",\n "required": false,\n "default": "user"\n },\n {\n "name": "sessionKind",\n "description": "Kind of credential artifact issued after successful authentication.",\n "type": "enum",\n "required": false,\n "default": "token",\n "values": ["token", "cookie", "session"]\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply authentication --name login-session-refresh --var actor=user --var sessionKind=token"\n ]\n}\n' + } + }, + { + id: "background-job", + files: { + "files/design.md.template": '# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers the asynchronous background job "{{jobName}}" from\ntrigger to completion.\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe where {{jobName}} sits: the , the\n, the , and the downstream systems the\njob reads and writes.\n\n## Components and Interfaces\n\n- Trigger: .\n- Work item contract: .\n- Worker: .\n- Status surface: .\n- Dead-letter destination: .\n\n## Trigger and Scheduling\n\nDescribe how work is produced here: , batching,\nordering guarantees, and the concurrency policy when triggers overlap.\n\n## Execution Model\n\n- \n\n## Idempotency and Duplicate Delivery\n\n- \n\n## Retries and Dead Letters\n\n- \n- \n\n## Timeout and Cancellation\n\n- \n\n## Failure Handling\n\n- Retryable failure: .\n- Non-retryable failure: routed to the dead-letter destination with the failure reason; no further retries.\n- Crash mid-execution: .\n- Downstream dependency unavailable: .\n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Unit tests: .\n- Integration tests: .\n- Regression tests: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n', + "files/requirements.md.template": '# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers the asynchronous background job "{{jobName}}": what\ntriggers it, how it is scheduled, and how it behaves under retries,\nduplicate delivery, timeouts, and cancellation.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{jobName}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a , I want {{jobName}} to run when , so that .\n\n#### Acceptance Criteria\n\n1. WHEN , THE SYSTEM SHALL enqueue one execution of {{jobName}} carrying .\n2. WHEN an execution of {{jobName}} starts, THE SYSTEM SHALL record including the trigger that caused it.\n3. IF the trigger fires while a previous execution is still running, THEN THE SYSTEM SHALL according to .\n\n### Requirement 2: \n\n**User Story:** As a , I want repeated or duplicated executions to be safe, so that retries and redelivery never corrupt data.\n\n#### Acceptance Criteria\n\n1. WHEN an execution of {{jobName}} fails with a retryable error, THE SYSTEM SHALL retry it up to times with .\n2. WHEN the same work item is delivered more than once, THE SYSTEM SHALL produce the same observable outcome as exactly one successful execution.\n3. IF an execution fails with a non-retryable error, THEN THE SYSTEM SHALL stop retrying and route the work item to with the failure reason.\n\n### Requirement 3: \n\n**User Story:** As a , I want stuck or cancelled work bounded and visible, so that failures stay recoverable.\n\n#### Acceptance Criteria\n\n1. WHEN an execution of {{jobName}} exceeds , THE SYSTEM SHALL stop it and count the attempt as a retryable failure.\n2. WHEN cancellation is requested, THE SYSTEM SHALL stop the execution at and leave persisted data in a consistent state.\n3. IF a work item reaches , THEN THE SYSTEM SHALL emit so an operator can inspect and reprocess it.\n\n## Non-Functional Requirements\n\n- Performance: .\n- Security: the worker runs with ; sensitive payload fields are and never logged.\n- Reliability: .\n- Observability: every execution emits including attempt number and outcome.\n- Compatibility: existing consumers of the output of {{jobName}} are not broken; .\n\n## Edge Cases\n\n- Add edge cases here (clock skew around scheduled runs, redelivery after a crash mid-execution, poison messages, cancellation racing a retry, partial downstream failures).\n\n## Out of Scope\n\n- Add explicitly excluded behavior here.\n', + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the work item contract for {{jobName}}: payload schema, idempotency key, and size limits.\n- [ ] 2. Implement the trigger and scheduling path that enqueues {{jobName}} work items.\n- [ ] 3. Implement the worker execution logic with explicit transaction boundaries.\n- [ ] 4. Implement idempotent processing so duplicate delivery produces the same outcome as one execution.\n- [ ] 5. Implement retries with backoff and route exhausted work items to the dead-letter destination.\n- [ ] 6. Implement the per-attempt timeout and the cancellation path.\n- [ ] 7. Add observability: metrics, structured logs with attempt number, and trace propagation from trigger to execution.\n- [ ] 8. Write integration tests for retry, duplicate delivery, timeout, cancellation, and dead-letter routing.\n- [ ] 9. Measure throughput and completion latency against the non-functional requirements.\n- [ ] 10. Verify existing consumers of the job output keep passing and document the rollout and rollback steps.\n", + "README.md": '# Background Job template\n\nA feature spec template for adding or changing an asynchronous background\njob or worker.\n\nIt pre-structures the spec around the questions background jobs always\nraise: the trigger and scheduling policy, retries and backoff, idempotency\nunder duplicate delivery, timeouts, dead-letter behavior, cancellation,\nobservability, and tests. It is vendor-neutral by design \u2014 any queue,\nscheduler, or worker runtime fits.\n\n## Usage\n\n```bash\nspecbridge template preview background-job \\\n --name invoice-export-worker \\\n --var jobName=invoice-export\n\nspecbridge template apply background-job \\\n --name invoice-export-worker \\\n --var jobName=invoice-export\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `jobName` | string | `background-job` | Short name of the job or worker (lowercase-hyphen). |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe engineering judgment stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "background-job",\n "version": "1.0.0",\n "displayName": "Background Job",\n "description": "A feature spec template for adding or changing an asynchronous background job or worker: trigger, scheduling, retries, idempotency, duplicate delivery, timeouts, dead-letter behavior, cancellation, observability, and tests.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["background-job", "worker", "queue", "async", "scheduling"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "jobName",\n "description": "Short name of the job or worker (lowercase-hyphen, e.g. \\"invoice-export\\").",\n "type": "string",\n "required": false,\n "default": "background-job"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply background-job --name invoice-export-worker --var jobName=invoice-export"\n ]\n}\n' + } + }, + { + id: "bugfix-regression", + files: { + "files/bugfix.md.template": "# Bugfix Document\n\n## Summary\n\n**{{title}}**\n\n{{description}}\n\nSeverity: {{severity}}. Affected area: {{affectedArea}}.\n\n## Current Behavior\n\nDescribe the observed incorrect behavior here, exactly as it happens.\n\n## Expected Behavior\n\nDescribe the correct behavior here, with the authoritative source for why\nit is correct (spec, documentation, prior release behavior).\n\n## Unchanged Behavior\n\n- List behavior in {{affectedArea}} that must remain unchanged here.\n- List adjacent features that share code with the fix here.\n\n## Reproduction\n\n1. Add deterministic reproduction steps here.\n2. \n3. \n4. \n\n## Evidence\n\n- Logs: add relevant log lines here.\n- Error messages: add exact error text here.\n- Screenshots: add links or paths here.\n- Failing tests: add failing test names here.\n- Relevant source locations: add file paths here.\n- First known bad version or commit: .\n\n## Constraints\n\n- Add implementation or compatibility constraints here.\n- \n\n## Regression Risks\n\n- Add behavior that could regress here.\n- \n", + "files/design.md.template": "# Fix Design\n\n## Root Cause\n\nDocument the confirmed root cause here, with the evidence that confirms it.\nIf the root cause is still suspected rather than confirmed, say so\nexplicitly and list what would confirm it.\n\n## Proposed Fix\n\nDescribe the smallest safe fix here. State why a smaller fix is not\npossible.\n\n## Affected Components\n\n- Add affected files and components in {{affectedArea}} here.\n\n## Failure Handling\n\n- Add failure modes introduced or fixed by this change here.\n- \n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n\n## Regression Protection\n\n- Add the regression test that fails before the fix and passes after it here.\n- Add tests pinning the unchanged behavior listed in the bugfix document here.\n\n## Validation Strategy\n\n- Add the checks that prove the fix works here: the reproduction steps from\n the bugfix document, the regression tests, and any data verification.\n", + "files/tasks.md.template": "# Bugfix Implementation Plan\n\n- [ ] 1. Reproduce the bug with the deterministic steps from the bugfix document.\n- [ ] 2. Capture evidence (logs, failing test, source locations) before changing code.\n- [ ] 3. Confirm the root cause and record it in the fix design.\n- [ ] 4. Write a regression test that fails on the current behavior.\n- [ ] 5. Implement the smallest safe fix.\n- [ ] 6. Verify the regression test passes and the reproduction no longer occurs.\n- [ ] 7. Verify the unchanged behavior listed in the bugfix document still holds.\n- [ ] 8. Check for data written by the buggy behavior and repair it if required.\n- [ ] 9. Run the full validation checks and document remaining risks.\n", + "README.md": '# Bugfix Regression template\n\nA bugfix spec template that keeps the fix honest: evidence before root\ncause, root cause before fix, and regression tests before done.\n\nIt follows the bugfix format SpecBridge analyzes: current behavior,\nexpected behavior, unchanged behavior, reproduction, evidence, root cause,\nthe smallest safe fix, regression tests, and verification.\n\n## Usage\n\n```bash\nspecbridge template preview bugfix-regression \\\n --name checkout-total-rounding \\\n --var severity=high\n\nspecbridge template apply bugfix-regression \\\n --name checkout-total-rounding \\\n --var severity=high\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `affectedArea` | string | `the affected component` | Where the bug appears. |\n| `severity` | enum (`low`, `medium`, `high`, `critical`) | `medium` | Impact severity. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real reproduction steps and evidence.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "bugfix-regression",\n "version": "1.0.0",\n "displayName": "Bugfix Regression",\n "description": "A bugfix spec template built around evidence: current vs expected behavior, unchanged behavior, reproduction, root cause, the smallest safe fix, and the regression tests that keep it fixed.",\n "kind": "bugfix",\n "supportedModes": ["requirements-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["bugfix", "regression", "debugging", "quality"],\n "files": [\n {\n "source": "files/bugfix.md.template",\n "target": "bugfix.md",\n "stage": "bugfix",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "affectedArea",\n "description": "Component, module, or user-facing surface where the bug appears.",\n "type": "string",\n "required": false,\n "default": "the affected component"\n },\n {\n "name": "severity",\n "description": "Impact severity of the bug.",\n "type": "enum",\n "required": false,\n "default": "medium",\n "values": ["low", "medium", "high", "critical"]\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply bugfix-regression --name checkout-total-rounding --var severity=high"\n ]\n}\n' + } + }, + { + id: "cli-tool", + files: { + "files/design.md.template": '# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers the `{{commandName}}` command surface and its output\ncontract for the primary actor "{{actor}}".\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe where the command sits: , ,\n, and .\n\n## Components and Interfaces\n\n- Command(s): `{{commandName}} ` \u2014 .\n- Arguments and options: .\n- Exit codes: .\n- Output contract: and .\n- Environment: .\n\n## Command-Line UX\n\n- Help text: .\n- Prompts: .\n- Progress and color: .\n\n## Control Flow\n\nDescribe the main invocation path here: parse, validate, execute, format,\nexit.\n\n## Failure Handling\n\n- Validation failure: usage error on stderr, nonzero exit code, no side effects.\n- Runtime failure: .\n- Interruption: .\n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Unit tests: .\n- Integration tests: .\n- Platform tests: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n', + "files/requirements.md.template": '# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers a command-line change to `{{commandName}}` used by the\nprimary actor "{{actor}}", in interactive terminals and in scripts.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{commandName}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a {{actor}}, I want , so that .\n\n#### Acceptance Criteria\n\n1. WHEN `{{commandName}} ` is invoked with valid arguments, THE SYSTEM SHALL and exit with code 0.\n2. WHEN a required argument is missing, THE SYSTEM SHALL print a usage message naming the argument to stderr and exit with a nonzero code.\n3. IF an unknown option or a malformed value is supplied, THEN THE SYSTEM SHALL reject the invocation with an error on stderr naming the option and exit with a nonzero code.\n4. WHEN the command succeeds, THE SYSTEM SHALL write results to stdout only and keep diagnostics on stderr.\n\n### Requirement 2: \n\n**User Story:** As a {{actor}}, I want structured output behind a flag, so that scripts consume results without parsing prose.\n\n#### Acceptance Criteria\n\n1. WHEN the structured output flag (e.g. `--json`) is supplied, THE SYSTEM SHALL emit to stdout with a stable, documented shape.\n2. IF an error occurs while structured output is requested, THEN THE SYSTEM SHALL emit a machine-readable error with and exit with a nonzero code.\n3. WHEN structured output is requested, THE SYSTEM SHALL keep human-oriented messages and progress output off stdout.\n\n### Requirement 3: \n\n**User Story:** As a {{actor}}, I want `{{commandName}}` to run unattended, so that CI jobs and scripts never hang on a prompt.\n\n#### Acceptance Criteria\n\n1. WHEN stdin is not a terminal, THE SYSTEM SHALL run without prompting and SHALL fail with a nonzero exit code when required input is missing.\n2. IF a destructive action normally asks for confirmation, THEN THE SYSTEM SHALL require in non-interactive runs instead of prompting.\n3. WHEN stdout is piped to another process, THE SYSTEM SHALL disable .\n\n### Requirement 4: \n\n**User Story:** As a {{actor}}, I want documented exit codes, so that scripts branch on failure classes reliably.\n\n#### Acceptance Criteria\n\n1. WHEN the command completes, THE SYSTEM SHALL exit with a code from the documented set: 0 for success and .\n2. IF an internal error occurs, THEN THE SYSTEM SHALL print a diagnostic message to stderr without a stack trace by default and exit with .\n\n## Non-Functional Requirements\n\n- Performance: .\n- Security: arguments and environment input are validated before use; secrets never appear in stdout, stderr, or help examples.\n- Reliability: .\n- Observability: writes diagnostics to stderr; normal runs stay quiet.\n- Compatibility: behavior is consistent across ; documented output shapes stay stable for existing scripts.\n\n## Edge Cases\n\n- Add edge cases here (broken pipe on stdout, very large input or output, non-UTF-8 arguments, missing locale, conflicting options, concurrent invocations sharing state).\n\n## Out of Scope\n\n- Add explicitly excluded behavior here.\n', + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the command surface for `{{commandName}}`: subcommands, arguments, options, and the documented exit-code set.\n- [ ] 2. Implement argument and option parsing with usage errors written to stderr and a nonzero exit code.\n- [ ] 3. Implement the core command behavior with results on stdout and diagnostics on stderr.\n- [ ] 4. Implement the machine-readable output mode behind a flag (e.g. `--json`) with a stable, documented shape.\n- [ ] 5. Implement non-interactive behavior: no prompts when stdin is not a terminal, with an explicit confirmation flag for destructive actions.\n- [ ] 6. Add exit-code mapping so every failure class exits with its documented code.\n- [ ] 7. Write unit tests for parsing, validation, and exit-code mapping.\n- [ ] 8. Write integration tests that run `{{commandName}}` end to end, including piped stdout and structured output.\n- [ ] 9. Verify behavior on every supported platform and shell, and record any differences.\n- [ ] 10. Document the command: help text, examples for interactive and scripted use, and the exit-code table.\n", + "README.md": '# Command-Line Tool template\n\nA feature spec template for adding or changing a command-line tool or\ncommand.\n\nIt pre-structures the spec around the questions CLI changes always raise:\nthe command surface (subcommands, arguments, options), exit codes,\nstdout/stderr discipline, machine-readable output, non-interactive and\nscripted use, platform compatibility, error handling, and tests.\n\n## Usage\n\n```bash\nspecbridge template preview cli-tool \\\n --name my-tool \\\n --var commandName=mycli\n\nspecbridge template apply cli-tool \\\n --name my-tool \\\n --var commandName=mycli\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `commandName` | string | `mycli` | Name of the command the spec covers. |\n| `actor` | string | `developer` | Primary user of the command. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe engineering judgment stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "cli-tool",\n "version": "1.0.0",\n "displayName": "Command-Line Tool",\n "description": "A feature spec template for adding or changing a command-line tool or command: command surface, arguments and options, exit codes, stdout/stderr behavior, machine-readable output, non-interactive use, platform compatibility, error handling, and tests.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["cli", "command-line", "tooling", "developer-experience", "ux"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "commandName",\n "description": "Name of the command the spec covers (e.g. \\"mycli\\").",\n "type": "string",\n "required": false,\n "default": "mycli"\n },\n {\n "name": "actor",\n "description": "Primary user of the command (a role, e.g. \\"developer\\").",\n "type": "string",\n "required": false,\n "default": "developer"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply cli-tool --name my-tool --var commandName=mycli"\n ]\n}\n' + } + }, + { + id: "database-migration", + files: { + "files/design.md.template": "# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers the schema change and backfill for the `{{tableName}}`\ntable and the deployment order around them.\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe how the migration is orchestrated: , the phase split (expand, backfill, contract), and .\n\n## Schema Change\n\n- DDL: .\n- Constraints and defaults: .\n- Data types: .\n\n## Data Migration and Backfill\n\n- Batching: .\n- Checkpointing: .\n- Idempotency: .\n- Live writes: .\n\n## Indexes and Locking\n\n- Index builds: .\n- Lock scope: .\n- Timeouts: .\n\n## Backward Compatibility\n\n- \n\n## Affected Components\n\n- Add every code path that reads or writes `{{tableName}}` here, with the change each one needs.\n\n## Rollback Limitations\n\nNot every step of this migration is reversible. Identify each irreversible\nstep explicitly and pair it with a mitigation instead of assuming a reverse\nmigration exists.\n\n- Reversible steps: .\n- Irreversible steps: .\n- Mitigations: .\n\n## Failure Handling\n\n- Failed schema statement: .\n- Failed or stuck backfill batch: .\n- Exceeded lock or duration budget: .\n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Migration tests: .\n- Interruption tests: .\n- Compatibility tests: .\n- Validation queries: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n", + "files/requirements.md.template": "# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers a schema and/or data migration for the `{{tableName}}`\ntable, including the backfill and the deployment order that keeps the\nrunning application working throughout.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{tableName}} | |\n| Backfill | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a , I want , so that .\n\n#### Acceptance Criteria\n\n1. WHEN the migration is applied to , THE SYSTEM SHALL produce on the `{{tableName}}` table without losing or altering existing rows.\n2. IF the migration is applied a second time, THEN THE SYSTEM SHALL make no further changes and report success (idempotent or guarded by ).\n3. WHEN the migration runs at , THE SYSTEM SHALL hold locks on `{{tableName}}` no longer than .\n\n### Requirement 2: \n\n**User Story:** As a , I want existing rows migrated in bounded batches, so that the database stays responsive during the backfill.\n\n#### Acceptance Criteria\n\n1. WHEN the backfill runs, THE SYSTEM SHALL migrate all existing `{{tableName}}` rows in batches of at most .\n2. IF the backfill is interrupted, THEN THE SYSTEM SHALL resume from without corrupting or double-writing rows.\n3. WHEN the backfill completes, THE SYSTEM SHALL report showing zero unmigrated rows.\n4. WHEN rows are written during the backfill window, THE SYSTEM SHALL migrate or preserve those writes so none are lost.\n\n### Requirement 3: \n\n**User Story:** As a , I want the previous and the new application release to work against the migrated schema, so that the rollout needs no downtime.\n\n#### Acceptance Criteria\n\n1. WHEN the schema change is deployed before the application change, THE SYSTEM SHALL keep the currently released application version working unchanged.\n2. IF the application is rolled back after the schema change, THEN THE SYSTEM SHALL keep the previous application release functional against the new schema.\n3. WHEN reads and writes occur during the migration window, THE SYSTEM SHALL return consistent results for .\n\n### Requirement 4: \n\n**User Story:** As a , I want an honest, documented rollback path, so that a bad migration is contained without guesswork.\n\n#### Acceptance Criteria\n\n1. WHEN a rollback is executed before the backfill starts, THE SYSTEM SHALL restore the previous schema of the `{{tableName}}` table.\n2. IF a rollback is requested after an irreversible step has run (dropped columns, destructive rewrites, lost precision), THEN THE SYSTEM SHALL refuse an automatic reverse migration and report the documented manual recovery path.\n\n## Non-Functional Requirements\n\n- Performance: the migration and backfill complete within at without exceeding on the database.\n- Security: the migration runs with ; production data is never copied outside .\n- Reliability: every step is idempotent or guarded by migration version tracking; an interrupted run leaves the database in a recoverable state.\n- Observability: progress, batch counts, row counts, and errors are emitted to throughout the run.\n- Compatibility: the previous and the new application release both operate against the migrated schema for the whole rollout window.\n\n## Edge Cases\n\n- Add edge cases here (rows inserted or updated mid-backfill, null or out-of-range values in existing data, replication lag on read replicas, long-running transactions blocking schema changes, retries after a partial batch failure).\n\n## Out of Scope\n\n- Add explicitly excluded behavior here.\n", + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the target schema for `{{tableName}}` and split the migration into expand, backfill, and contract phases.\n- [ ] 2. Write the expand-phase migration script with additive schema changes only.\n- [ ] 3. Implement the backfill as batched, checkpointed, idempotent steps with progress logging.\n- [ ] 4. Add index builds using an online strategy where available, with lock and statement timeouts set.\n- [ ] 5. Implement compatibility in the application (dual-write, tolerant reads, or a feature flag) for the rollout window.\n- [ ] 6. Document every irreversible step with its point of no return and the manual recovery path for each.\n- [ ] 7. Write validation queries that prove row counts and data integrity before and after the backfill.\n- [ ] 8. Measure migration and backfill duration and lock impact on a production-like dataset.\n- [ ] 9. Add automated tests covering an empty database, a populated database, and an interrupted backfill.\n- [ ] 10. Verify the full deployment order in a staging rehearsal, including the rollback path, and record the results.\n", + "README.md": '# Database Migration template\n\nA feature spec template for a schema and/or data migration.\n\nIt pre-structures the spec around the questions migrations always raise:\nthe schema change itself, the batched data backfill, backward compatibility\nand zero-downtime deployment order, indexes and locking, rollback\nlimitations (including steps that cannot be reversed), performance,\nvalidation, and observability.\n\n## Usage\n\n```bash\nspecbridge template preview database-migration \\\n --name orders-status-backfill \\\n --var tableName=orders\n\nspecbridge template apply database-migration \\\n --name orders-status-backfill \\\n --var tableName=orders\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `tableName` | string | `records` | Primary table the migration changes. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. In particular, the "Rollback\nLimitations" section only stays honest if you identify the genuinely\nirreversible steps yourself. The template gives structure; the engineering\njudgment stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "database-migration",\n "version": "1.0.0",\n "displayName": "Database Migration",\n "description": "A feature spec template for a schema or data migration: the schema change, batched backfill, backward compatibility, zero-downtime deployment order, indexes and locking, rollback limitations, validation, and observability.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["database", "migration", "schema", "sql", "backfill"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "tableName",\n "description": "Primary table the migration changes (e.g. \\"orders\\").",\n "type": "string",\n "required": false,\n "default": "records"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply database-migration --name orders-status-backfill --var tableName=orders"\n ]\n}\n' + } + }, + { + id: "event-driven-service", + files: { + "files/design.md.template": "# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers the `{{eventName}}` event from producer to consumers,\nwith {{deliverySemantics}} delivery and idempotent consumption as the\ndefault posture.\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe the event flow end to end: publishes\n`{{eventName}}` to , and \nsubscribe via . Name the kind of channel in use\n(e.g. a log-based stream or a work queue) and where this change sits in it.\n\n## Components and Interfaces\n\n- Producer: .\n- Consumer(s): .\n- Channel: .\n- Dead-letter destination: .\n\n## Event Contract\n\n- Name and version: `{{eventName}}` .\n- Payload schema: .\n- Envelope metadata: .\n- Ordering key: .\n\n## Delivery Semantics\n\nThis design commits to {{deliverySemantics}} delivery. Exactly-once\ndelivery is deliberately not claimed \u2014 no broker provides it end to end \u2014\nso the consumer side carries the burden:\n\n- At-least-once: duplicates are possible; every consumer effect must be idempotent (see Idempotency).\n- At-most-once: loss is possible; state the business justification for tolerating dropped events.\n- \n\n## Ordering\n\n- \n\n## Idempotency\n\n- \n\n## Retries and Dead-Letter Behavior\n\n- Retry policy: .\n- Dead-letter behavior: .\n\n## Schema Evolution\n\n- \n\n## Failure Handling\n\n- Broker unavailable at publish time: .\n- Consumer crash mid-processing: .\n- Poison message: .\n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Contract tests: .\n- Idempotency tests: .\n- Failure tests: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n", + "files/requirements.md.template": "# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers an event-driven change centered on the `{{eventName}}`\nevent, with {{deliverySemantics}} delivery between the producer and its\nconsumers.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{eventName}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a , I want a `{{eventName}}` event published when , so that .\n\n#### Acceptance Criteria\n\n1. WHEN is committed, THE SYSTEM SHALL publish a `{{eventName}}` event containing within |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a consumer of {{componentName}}, I want its observable behavior verified unchanged after the restructuring, so that nothing built on it breaks.\n\n#### Acceptance Criteria\n\n1. WHEN any input from the behavior inventory is applied after a refactor step, THE SYSTEM SHALL produce the same observable output as the pre-refactor baseline.\n2. IF an inventoried behavior differs after a step, THEN THE SYSTEM SHALL be reverted to the previous checkpoint before any further steps proceed.\n\n### Requirement 2: \n\n**User Story:** As the engineer performing the refactor, I want the restructuring split into steps with a releasable checkpoint after each, so that any step can be shipped or reverted on its own.\n\n#### Acceptance Criteria\n\n1. WHEN a refactor step completes, THE SYSTEM SHALL build cleanly and pass the full regression suite at that checkpoint.\n2. IF a checkpoint fails, THEN THE SYSTEM SHALL be restored to the previous checkpoint using without loss of data or history.\n\n### Requirement 3: \n\n**User Story:** As an external caller of {{componentName}}, I want its public interface contract unchanged, so that no caller has to change because of the refactor.\n\n#### Acceptance Criteria\n\n1. WHEN an external caller invokes {{componentName}} through its public interface, THE SYSTEM SHALL accept the same inputs and honor the same contract as before the refactor.\n2. IF an interface change is unavoidable, THEN THE SYSTEM SHALL provide so existing callers keep working during the transition.\n\n### Requirement 4: \n\n**User Story:** As an operator, I want performance and timing drift from the refactor measured against a baseline, so that a "pure" refactor does not quietly degrade the service.\n\n#### Acceptance Criteria\n\n1. WHEN the post-refactor {{componentName}} runs , THE SYSTEM SHALL stay within of the pre-refactor baseline.\n2. IF measured drift exceeds the budget, THEN THE SYSTEM SHALL be reverted to the last good checkpoint unless the drift is explicitly accepted in this spec.\n\n## Non-Functional Requirements\n\n- Performance: post-refactor performance stays within of the measured pre-refactor baseline.\n- Security: the refactor introduces no new external surface; validation and permission checks in {{componentName}} keep their current coverage.\n- Reliability: every checkpoint is releasable; a failed step is revertible via .\n- Observability: existing logs and metrics for {{componentName}} keep their meaning, or renames are documented for dashboards and alerts.\n- Compatibility: external callers and persisted data formats are unaffected, or a documented migration accompanies the change.\n\n## Edge Cases\n\n- Add edge cases here (error-message text that callers or tests match on, timing-sensitive consumers, reflection or serialization that depends on internal names, hidden couplings discovered mid-refactor).\n\n## Out of Scope\n\n- Behavior changes and new features: anything that alters the behavior inventory belongs in its own spec.\n- Add other explicitly excluded work here.\n', + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the behavior inventory for {{componentName}}: every externally observable behavior that must not change.\n- [ ] 2. Write characterization tests that pin the inventoried behavior, including error messages and edge cases, before any restructuring begins.\n- [ ] 3. Measure the pre-refactor baseline for performance, timing, and error output.\n- [ ] 4. Define the incremental steps, with a releasable checkpoint and a rollback point after each step.\n- [ ] 5. Implement the first restructuring step and run the full regression suite at its checkpoint.\n- [ ] 6. Implement the remaining steps one checkpoint at a time, keeping the build releasable after each.\n- [ ] 7. Verify interface compatibility for all external callers of {{componentName}}.\n- [ ] 8. Measure post-refactor performance and timing against the baseline and record any drift.\n- [ ] 9. Remove dead code and temporary seams left behind by the restructuring.\n- [ ] 10. Verify the measurable completion criteria from the design document and record any accepted deviations.\n", + "README.md": '# Refactoring template\n\nA feature spec template for a behavior-preserving restructuring.\n\nIt pre-structures the spec around the questions refactors always raise: the\nexplicit inventory of behavior that must not change, the motivation, the\nboundaries of the refactor, affected components and interfaces,\ncompatibility, an incremental plan with safe checkpoints, regression tests,\nrollback points, and measurable completion criteria. It treats "unchanged\nbehavior" as a claim to verify with tests, not an assumption \u2014 even a pure\nrefactor can change performance, timing, and error messages.\n\n## Usage\n\n```bash\nspecbridge template preview refactoring \\\n --name extract-billing-module \\\n --var componentName=billing\n\nspecbridge template apply refactoring \\\n --name extract-billing-module \\\n --var componentName=billing\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `componentName` | string | `the component` | The component, module, or subsystem being restructured. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe judgment about what must not change stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "refactoring",\n "version": "1.0.0",\n "displayName": "Refactoring",\n "description": "A feature spec template for a behavior-preserving restructuring: an explicit inventory of behavior that must not change, refactor boundaries, an incremental plan with safe checkpoints, regression tests, rollback points, and measurable completion criteria.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["refactoring", "maintainability", "tech-debt", "restructuring", "regression-safety"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "componentName",\n "description": "The component, module, or subsystem being restructured (e.g. \\"billing\\").",\n "type": "string",\n "required": false,\n "default": "the component"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply refactoring --name extract-billing-module --var componentName=billing"\n ]\n}\n' + } + }, + { + id: "rest-api", + files: { + "files/design.md.template": "# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design covers the `{{resourceName}}` endpoints under `{{basePath}}`.\n\n## Goals\n\n- Add concrete goals here.\n\n## Non-Goals\n\n- Add explicitly excluded goals here.\n\n## Architecture\n\nDescribe where the endpoint sits: , ,\n, and any upstream/downstream services it calls.\n\n## Components and Interfaces\n\n- Endpoint(s): `{{basePath}}/` \u2014 .\n- Request contract: .\n- Response contract: .\n- Error model: .\n- Authentication and authorization: .\n\n## Data Model\n\n- Add new or changed data structures for the {{resourceName}} resource here.\n\n## Control Flow\n\nDescribe the main request path here: validation, authorization, business\nlogic, persistence, response mapping.\n\n## Failure Handling\n\n- Validation failure: 400 with field-level detail; no state change.\n- Downstream dependency failure: .\n- Concurrent modification: .\n\n## Idempotency\n\n- \n\n## Pagination\n\n- \n\n## Compatibility\n\n- \n\n## Security Considerations\n\n- \n\n## Observability\n\n- \n\n## Testing Strategy\n\n- Contract tests: .\n- Integration tests: .\n- Regression tests: .\n\n## Rollout\n\n- \n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here.\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n", + "files/requirements.md.template": '# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec covers a REST API change under `{{basePath}}` exposing the\n`{{resourceName}}` resource to the primary actor "{{actor}}".\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| {{resourceName}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a {{actor}}, I want , so that .\n\n#### Acceptance Criteria\n\n1. WHEN a valid request is received at `{{basePath}}/`, THE SYSTEM SHALL respond with and .\n2. WHEN the request body fails validation, THE SYSTEM SHALL respond with 400 and a machine-readable error body naming each invalid field.\n3. IF the caller is not authenticated, THEN THE SYSTEM SHALL respond with 401 without leaking whether the {{resourceName}} exists.\n4. IF the caller is authenticated but not authorized for the {{resourceName}}, THEN THE SYSTEM SHALL respond with 403 or 404 according to .\n5. WHEN the requested {{resourceName}} does not exist, THE SYSTEM SHALL respond with 404 and a stable error code.\n\n### Requirement 2: \n\n**User Story:** As a {{actor}}, I want repeated identical requests to be safe, so that retries never corrupt state.\n\n#### Acceptance Criteria\n\n1. WHEN the same is submitted twice, THE SYSTEM SHALL without duplicating side effects.\n2. IF a request is retried after a timeout, THEN THE SYSTEM SHALL produce the same observable outcome as a single successful request.\n\n### Requirement 3: \n\n**User Story:** As a {{actor}}, I want bounded list responses, so that large collections stay usable.\n\n#### Acceptance Criteria\n\n1. WHEN a list request exceeds the page size, THE SYSTEM SHALL return at most items and a cursor or link to the next page.\n2. WHEN an invalid cursor is supplied, THE SYSTEM SHALL respond with 400 and a stable error code.\n\n## Non-Functional Requirements\n\n- Performance: .\n- Security: requests are authenticated via ; authorization is enforced per {{resourceName}}; input is validated before any state change.\n- Reliability: .\n- Observability: every request emits without logging secrets or full payloads.\n- Compatibility: existing consumers of `{{basePath}}` are not broken; additive changes only, or a documented versioning step.\n\n## Edge Cases\n\n- Add edge cases here (oversized payloads, concurrent writes to the same {{resourceName}}, clock skew on expiry fields, partial downstream failures).\n\n## Out of Scope\n\n- Add explicitly excluded behavior here.\n', + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the request and response contract for `{{basePath}}/`, including error bodies and status codes.\n- [ ] 2. Implement request validation and the machine-readable validation error body.\n- [ ] 3. Implement authentication and per-{{resourceName}} authorization checks.\n- [ ] 4. Implement the endpoint business logic and persistence path.\n- [ ] 5. Implement idempotency behavior for repeated requests.\n- [ ] 6. Implement pagination for list responses, if applicable.\n- [ ] 7. Add contract tests covering every documented status code.\n- [ ] 8. Add integration tests for authentication, authorization, validation, and not-found paths.\n- [ ] 9. Add observability: structured logs, metrics, and trace spans without secret leakage.\n- [ ] 10. Verify backward compatibility with existing consumers and document the rollout and rollback steps.\n", + "README.md": '# REST API template\n\nA feature spec template for adding or changing a REST API endpoint.\n\nIt pre-structures the spec around the questions REST changes always raise:\nthe request/response contract, validation and status codes, authentication\nand authorization, idempotency, pagination, backward compatibility,\nobservability, contract tests, and rollout.\n\n## Usage\n\n```bash\nspecbridge template preview rest-api \\\n --name orders-list-endpoint \\\n --var resourceName=order \\\n --var basePath=/api/v1/orders\n\nspecbridge template apply rest-api \\\n --name orders-list-endpoint \\\n --var resourceName=order \\\n --var basePath=/api/v1/orders\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `actor` | string | `API client` | Primary caller of the API. |\n| `resourceName` | string | `resource` | Primary resource the endpoint exposes (singular). |\n| `basePath` | string | `/api/v1` | Base URL path of the endpoint group. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe engineering judgment stays with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "rest-api",\n "version": "1.0.0",\n "displayName": "REST API",\n "description": "A feature spec template for adding or changing a REST API endpoint: request/response contract, validation, status codes, authentication, idempotency, pagination, compatibility, observability, and rollout.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["api", "rest", "http", "backend"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "actor",\n "description": "Primary caller of the API (a user role or client system).",\n "type": "string",\n "required": false,\n "default": "API client"\n },\n {\n "name": "resourceName",\n "description": "Name of the primary resource the endpoint exposes (singular, e.g. \\"order\\").",\n "type": "string",\n "required": false,\n "default": "resource"\n },\n {\n "name": "basePath",\n "description": "Base URL path of the endpoint group (e.g. \\"/api/v1/orders\\").",\n "type": "string",\n "required": false,\n "default": "/api/v1"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply rest-api --name orders-list-endpoint --var resourceName=order --var basePath=/api/v1/orders"\n ]\n}\n' + } + }, + { + id: "security-hardening", + files: { + "files/design.md.template": "# Design Document\n\n## Overview\n\n**{{title}}**\n\n{{description}}\n\nThis design closes a specific weakness in {{threatArea}} that puts\n{{assetName}} at risk. Its claims stay scoped to that weakness: shipping\nthis change hardens one boundary; it does not make the system secure as a\nwhole.\n\n## Goals\n\n- Add concrete goals here (the weakness closed and the boundary hardened).\n\n## Non-Goals\n\n- Certifying the system as secure: only the named weakness is addressed.\n- Add other explicitly excluded goals here.\n\n## Architecture\n\nDescribe where the enforcement sits: , , , and the assets behind it. Explain\nwhy the check cannot be bypassed by any path that crosses the boundary.\n\n## Threat Model\n\n- Weakness: .\n- Assets at risk: {{assetName}} and .\n- Attacker: .\n- Impact if exploited: .\n\n## Trust Boundary\n\n- Boundary: .\n- Entry points crossing it: .\n- Enforcement point: .\n\n## Abuse Cases\n\n- .\n- .\n\n## Required Secure Behavior\n\n- .\n- .\n\n## Affected Components\n\n- .\n- .\n\n## Dependency Implications\n\n- .\n- .\n\n## Failure Handling\n\n- Default decision: the enforcement path fails closed \u2014 if the check cannot run, the operation is denied.\n- .\n- .\n\n## Security Considerations\n\n- .\n- .\n\n## Observability\n\n- .\n- What never appears in logs: secrets, credentials, tokens, raw payloads.\n- .\n\n## Testing Strategy\n\n- Negative tests: .\n- Regression tests: .\n- Failure-path tests: .\n\n## Rollout\n\n- .\n\n## Risks and Trade-offs\n\n- Add known risks and accepted trade-offs here (false positives on legitimate traffic, added latency, operational load).\n\n## Alternatives Considered\n\n- Add rejected alternatives and why they were rejected here.\n", + "files/requirements.md.template": "# Requirements Document\n\n## Introduction\n\n**{{title}}**\n\n{{description}}\n\nThis spec closes a specific weakness in {{threatArea}} that puts\n{{assetName}} at risk. Its claims are scoped to that weakness: the change\nhardens one boundary and does not certify the system as secure.\n\n## Glossary\n\n| Term | Definition |\n| --- | --- |\n| Trust boundary | |\n| {{threatArea}} | |\n| | |\n\n## Requirements\n\n### Requirement 1: \n\n**User Story:** As a security engineer, I want {{threatArea}} enforced at , so that {{assetName}} is no longer exposed to the identified weakness.\n\n#### Acceptance Criteria\n\n1. WHEN data or a request crosses , THE SYSTEM SHALL validate it against before any further processing.\n2. WHEN validation rejects an input, THE SYSTEM SHALL leave {{assetName}} and all other state unchanged.\n3. IF the input fails validation, THEN THE SYSTEM SHALL return without revealing internal detail an attacker could use.\n\n### Requirement 2: \n\n**User Story:** As an operator, I want the enforcement path to fail closed, so that an outage in the check never becomes a bypass.\n\n#### Acceptance Criteria\n\n1. IF the enforcement component is unavailable or times out, THEN THE SYSTEM SHALL deny the operation instead of continuing without the check.\n2. WHEN a deliberate fail-open exception applies to , THE SYSTEM SHALL emit an audit event every time the exception is exercised.\n\n### Requirement 3: \n\n**User Story:** As an incident responder, I want rejections and enforcement failures logged with stable reason codes, so that abuse is investigable without leaking secrets.\n\n#### Acceptance Criteria\n\n1. WHEN a request is rejected at the boundary, THE SYSTEM SHALL log with a stable reason code.\n2. THE SYSTEM SHALL keep secrets, credentials, tokens, and raw payloads out of security log events.\n3. IF writing the log event fails, THEN THE SYSTEM SHALL still enforce the boundary decision.\n\n### Requirement 4: \n\n**User Story:** As a legitimate caller, I want valid requests to keep succeeding after the hardening, so that closing the weakness does not become an outage.\n\n#### Acceptance Criteria\n\n1. WHEN a well-formed request within documented limits crosses the boundary, THE SYSTEM SHALL produce the same observable outcome as before the hardening.\n2. IF monitoring shows legitimate traffic being rejected during rollout, THEN THE SYSTEM SHALL be rolled back or the rule corrected before rollout continues.\n\n## Non-Functional Requirements\n\n- Performance: the enforcement check stays within per request at .\n- Security: enforcement runs on every path across the trust boundary with no bypass route; the enforcement component itself runs with least privilege.\n- Reliability: the fail-closed decision is explicit; .\n- Observability: rejections, enforcement failures, and exercised fail-open exceptions are visible in without secret leakage.\n- Compatibility: legitimate callers documented in keep working unchanged.\n\n## Edge Cases\n\n- Add edge cases here (oversized or deeply nested inputs, encoding tricks, replayed requests, partial enforcement-component failure, the boundary crossed via a background job).\n\n## Out of Scope\n\n- Weaknesses outside {{threatArea}}: this spec closes one named weakness and makes no claim that the system as a whole is secure.\n- Add other explicitly excluded behavior here.\n", + "files/tasks.md.template": "# Implementation Plan\n\n- [ ] 1. Define the validation rules or enforcement policy for {{threatArea}} at the trust boundary.\n- [ ] 2. Implement the enforcement check on every entry point that crosses the boundary.\n- [ ] 3. Implement fail-closed behavior when the enforcement component is unavailable, with any deliberate fail-open exception documented and audited.\n- [ ] 4. Add rejection responses that reveal no internal detail an attacker could use.\n- [ ] 5. Add security event logging with stable reason codes and no secrets, credentials, or raw payloads.\n- [ ] 6. Write negative tests that reproduce each abuse case and prove the attack path is closed.\n- [ ] 7. Write regression tests proving legitimate request patterns still succeed.\n- [ ] 8. Verify dependency versions and configuration implicated in the weakness, and update or pin them.\n- [ ] 9. Measure the performance overhead of the new checks against the latency budget.\n- [ ] 10. Document the rollout plan, the monitoring to watch during rollout, and the rollback trigger.\n", + "README.md": '# Security Hardening template\n\nA feature spec template for closing a specific security weakness or\nhardening a trust boundary.\n\nIt pre-structures the spec around the questions hardening work always\nraises: the threat and the assets at risk, the trust boundary and its entry\npoints, abuse cases, the required secure behavior, an explicit fail-closed\nor fail-open decision, logging without secret leakage, dependency\nimplications, negative tests that prove the attack no longer works, and\nrollout. Its claims stay scoped to the weakness being closed \u2014 applying the\ntemplate does not certify a system as secure.\n\n## Usage\n\n```bash\nspecbridge template preview security-hardening \\\n --name harden-webhook-deserialization \\\n --var threatArea=deserialization\n\nspecbridge template apply security-hardening \\\n --name harden-webhook-deserialization \\\n --var threatArea=deserialization\n```\n\n## Variables\n\n| Variable | Type | Default | Purpose |\n| --- | --- | --- | --- |\n| `threatArea` | string | `input validation` | The class of weakness being closed. |\n| `assetName` | string | `the protected data` | The primary asset at risk behind the boundary. |\n\nThe built-in variables `specName`, `title`, `description`, `kind`, and\n`mode` are always available and are set by `--name`, `--title`, and\n`--description`.\n\n## What you still fill in\n\nThe rendered documents contain `` placeholders and\n"Add \u2026 here." lines by design. `specbridge spec analyze` blocks approval\nuntil they are replaced with real content. The template gives structure;\nthe threat analysis and the engineering judgment stay with you.\n', + "specbridge-template.json": '{\n "schemaVersion": "1.0.0",\n "id": "security-hardening",\n "version": "1.0.0",\n "displayName": "Security Hardening",\n "description": "A feature spec template for closing a specific security weakness or hardening a trust boundary: threat and assets at risk, abuse cases, required secure behavior, an explicit fail-closed decision, logging without secret leakage, negative tests, and rollout.",\n "kind": "feature",\n "supportedModes": ["requirements-first", "design-first", "quick"],\n "defaultMode": "requirements-first",\n "tags": ["security", "hardening", "threat-model", "abuse-case", "defense"],\n "files": [\n {\n "source": "files/requirements.md.template",\n "target": "requirements.md",\n "stage": "requirements",\n "required": true\n },\n {\n "source": "files/design.md.template",\n "target": "design.md",\n "stage": "design",\n "required": true\n },\n {\n "source": "files/tasks.md.template",\n "target": "tasks.md",\n "stage": "tasks",\n "required": true\n }\n ],\n "variables": [\n {\n "name": "threatArea",\n "description": "The class of weakness being closed (e.g. \\"input validation\\", \\"deserialization\\", \\"authentication\\").",\n "type": "string",\n "required": false,\n "default": "input validation"\n },\n {\n "name": "assetName",\n "description": "The primary asset at risk behind the boundary being hardened (e.g. \\"customer records\\").",\n "type": "string",\n "required": false,\n "default": "the protected data"\n }\n ],\n "compatibility": {\n "specbridge": ">=0.7.0 <1.0.0",\n "kiroLayout": "1"\n },\n "license": "MIT",\n "examples": [\n "specbridge template apply security-hardening --name harden-webhook-deserialization --var threatArea=deserialization"\n ]\n}\n' + } + } +]; +function projectTemplatesDir(workspace) { + return import_path12.default.join(workspace.sidecarDir, "templates"); +} +function builtinEntries(options) { + const entries = []; + for (const packData of BUILTIN_TEMPLATE_PACKS) { + const pack = loadTemplatePack( + { origin: `builtin:${packData.id}`, files: new Map(Object.entries(packData.files)) }, + { + requireReadme: true, + ...options.specbridgeVersion !== void 0 ? { specbridgeVersion: options.specbridgeVersion } : {} + } + ); + entries.push({ + source: "builtin", + id: packData.id, + ref: formatTemplateReference("builtin", packData.id), + pack, + valid: pack.valid && pack.manifest?.id === packData.id + }); + } + return entries; +} +function projectEntries(workspace, options, diagnostics) { + if (workspace === void 0) return []; + const dir = projectTemplatesDir(workspace); + if (!(0, import_fs13.existsSync)(dir)) return []; + const entries = []; + let names; + try { + names = (0, import_fs13.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).sort((a2, b) => a2.localeCompare(b, "en")); + } catch (cause) { + diagnostics.push({ + severity: "warning", + code: "TEMPLATE_DIR_UNREADABLE", + message: `Cannot read ${dir}: ${cause instanceof Error ? cause.message : String(cause)}` + }); + return []; + } + for (const name of names) { + const packDir = import_path12.default.join(dir, name); + let pack; + try { + const data = readTemplatePackDirectory(packDir); + pack = loadTemplatePack( + data, + options.specbridgeVersion !== void 0 ? { specbridgeVersion: options.specbridgeVersion } : {} + ); + } catch (cause) { + const message = cause instanceof Error ? cause.message : String(cause); + const failure = { + code: cause instanceof TemplateError ? cause.templateCode : "SBT025", + category: "files", + severity: "error", + message + }; + pack = { + origin: packDir, + manifest: void 0, + manifestText: void 0, + readme: void 0, + files: /* @__PURE__ */ new Map(), + issues: [failure], + valid: false + }; + } + const manifestMismatch = pack.manifest !== void 0 && pack.manifest.id !== name; + if (manifestMismatch) { + pack.issues.push({ + code: "SBT004", + category: "manifest", + severity: "error", + message: `Installed directory "${name}" does not match manifest id "${pack.manifest?.id}".` + }); + } + entries.push({ + source: "project", + id: name, + ref: formatTemplateReference("project", name), + pack, + valid: pack.valid && !manifestMismatch + }); + } + return entries; +} +function loadTemplateCatalog(workspace, options = {}) { + const diagnostics = []; + const source = options.source ?? "all"; + const entries = []; + if (source === "all" || source === "builtin") { + entries.push(...builtinEntries(options)); + } + if (source === "all" || source === "project") { + entries.push(...projectEntries(workspace, options, diagnostics)); + } + entries.sort( + (a2, b) => a2.source === b.source ? a2.id.localeCompare(b.id, "en") : a2.source === "builtin" ? -1 : 1 + ); + return { entries, diagnostics }; +} +function resolveTemplate(catalog, rawReference) { + const reference = parseTemplateReference(rawReference); + if (reference === void 0) { + throw new TemplateError( + "SBT003", + `"${rawReference}" is not a valid template reference.`, + 'Use a template ID like "rest-api" or a qualified reference like "builtin:rest-api" or "project:my-template".', + { reference: rawReference } + ); + } + const matches = catalog.entries.filter( + (entry) => entry.id === reference.id && (reference.source === void 0 || entry.source === reference.source) + ); + if (matches.length === 0) { + const suggestions = catalog.entries.filter((entry) => entry.id.includes(reference.id) || reference.id.includes(entry.id)).map((entry) => entry.ref).slice(0, 5); + throw new TemplateError( + "SBT001", + `Template "${rawReference}" was not found.`, + suggestions.length > 0 ? `Did you mean: ${suggestions.join(", ")}? Run "specbridge template list" to see all templates.` : 'Run "specbridge template list" to see available templates.', + { reference: rawReference } + ); + } + if (matches.length > 1) { + throw new TemplateError( + "SBT002", + `Template ID "${reference.id}" exists in multiple sources.`, + `Use a qualified reference: ${matches.map((entry) => entry.ref).join(" or ")}.`, + { reference: rawReference, candidates: matches.map((entry) => entry.ref) } + ); + } + const match = matches[0]; + if (match === void 0) { + throw new TemplateError("SBT001", `Template "${rawReference}" was not found.`, 'Run "specbridge template list".'); + } + return match; +} +function resolveValidTemplate(catalog, rawReference) { + const entry = resolveTemplate(catalog, rawReference); + if (!entry.valid || entry.pack.manifest === void 0) { + const problems = entry.pack.issues.filter((item) => item.severity === "error").slice(0, 5).map((item) => `${item.code}: ${item.message}`); + throw new TemplateError( + "SBT004", + `Template ${entry.ref} failed validation and cannot be used.` + (problems.length > 0 ? ` Problems: ${problems.join(" | ")}` : ""), + `Run "specbridge template validate ${entry.ref}" for the full report.`, + { reference: entry.ref } + ); + } + return entry; +} +var DEFAULT_SEARCH_LIMIT = 20; +var MAX_SEARCH_LIMIT = 50; +var SCORE_EXACT_ID = 1e3; +var SCORE_ID_PREFIX = 800; +var SCORE_EXACT_TAG = 600; +var SCORE_DISPLAY_NAME_TOKEN = 400; +var SCORE_DESCRIPTION_TOKEN = 200; +function tokenize(text) { + return text.toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 0); +} +function clampSearchLimit(requested) { + if (requested === void 0 || !Number.isFinite(requested)) return DEFAULT_SEARCH_LIMIT; + return Math.max(1, Math.min(Math.trunc(requested), MAX_SEARCH_LIMIT)); +} +function scoreEntry(entry, query, queryTokens) { + const manifest = entry.pack.manifest; + const id = entry.id.toLowerCase(); + const tags = (manifest?.tags ?? []).map((tag) => tag.toLowerCase()); + const displayTokens = tokenize(manifest?.displayName ?? ""); + const descriptionTokens = new Set(tokenize(manifest?.description ?? "")); + let score = 0; + if (id === query) score += SCORE_EXACT_ID; + else if (id.startsWith(query)) score += SCORE_ID_PREFIX; + for (const token of queryTokens) { + if (tags.includes(token)) score += SCORE_EXACT_TAG; + if (displayTokens.includes(token)) score += SCORE_DISPLAY_NAME_TOKEN; + if (descriptionTokens.has(token)) score += SCORE_DESCRIPTION_TOKEN; + if (token !== query && id.split("-").includes(token)) score += SCORE_ID_PREFIX / 2; + } + return score; +} +function searchTemplates(catalog, rawQuery, options = {}) { + const query = rawQuery.trim().toLowerCase(); + if (query.length === 0) return []; + const queryTokens = tokenize(query); + const limit = clampSearchLimit(options.limit); + return catalog.entries.map((entry) => ({ entry, score: scoreEntry(entry, query, queryTokens) })).filter((result) => result.score > 0).sort((a2, b) => a2.score !== b.score ? b.score - a2.score : a2.entry.ref.localeCompare(b.entry.ref, "en")).slice(0, limit); +} +var TEMPLATE_RECORDS_FILE_NAME = "template-records.jsonl"; +var TEMPLATE_RECORD_TYPES = [ + "template-apply", + "template-install", + "template-uninstall", + "template-scaffold" +]; +var baseRecordShape = { + schemaVersion: external_exports.string().regex(/^\d+\.\d+\.\d+$/), + recordId: external_exports.string().min(1).max(100), + type: external_exports.enum(TEMPLATE_RECORD_TYPES), + createdAt: external_exports.string().datetime(), + result: external_exports.enum(["ok", "failed"]) +}; +var templateApplyRecordSchema = external_exports.object({ + ...baseRecordShape, + type: external_exports.literal("template-apply"), + templateRef: external_exports.string(), + templateId: external_exports.string(), + templateVersion: external_exports.string(), + templateSource: external_exports.enum(["builtin", "project"]), + manifestHash: external_exports.string(), + specName: external_exports.string(), + specKind: external_exports.enum(["feature", "bugfix"]), + workflowMode: external_exports.enum(["requirements-first", "design-first", "quick"]), + /** Workspace-relative POSIX target path -> sha256 of rendered bytes. */ + renderedFiles: external_exports.array(external_exports.object({ target: external_exports.string(), hash: external_exports.string() })), + /** Safe variable NAMES only; values are never stored. */ + variableNames: external_exports.array(external_exports.string()), + createdPaths: external_exports.array(external_exports.string()) +}).passthrough(); +var templateInstallRecordSchema = external_exports.object({ + ...baseRecordShape, + type: external_exports.literal("template-install"), + templateRef: external_exports.string(), + templateId: external_exports.string(), + templateVersion: external_exports.string(), + manifestHash: external_exports.string(), + /** Workspace-relative source the pack was copied from. */ + sourcePath: external_exports.string(), + installedPath: external_exports.string() +}).passthrough(); +var templateUninstallRecordSchema = external_exports.object({ + ...baseRecordShape, + type: external_exports.literal("template-uninstall"), + templateRef: external_exports.string(), + templateId: external_exports.string(), + uninstalledPath: external_exports.string() +}).passthrough(); +var templateScaffoldRecordSchema = external_exports.object({ + ...baseRecordShape, + type: external_exports.literal("template-scaffold"), + templateId: external_exports.string(), + kind: external_exports.enum(["feature", "bugfix"]), + outputPath: external_exports.string() +}).passthrough(); +var templateRecordSchema = external_exports.discriminatedUnion("type", [ + templateApplyRecordSchema, + templateInstallRecordSchema, + templateUninstallRecordSchema, + templateScaffoldRecordSchema +]); +function templateRecordsPath(workspace) { + return import_path13.default.join(workspace.sidecarDir, TEMPLATE_RECORDS_FILE_NAME); +} +var recordCounter = 0; +function newTemplateRecordId(clock = systemClock) { + recordCounter += 1; + return `template-${clock().getTime().toString(36)}-${process.pid.toString(36)}-${recordCounter}`; +} +function appendTemplateRecord(workspace, record2) { + const validated = templateRecordSchema.parse(record2); + const filePath = templateRecordsPath(workspace); + try { + (0, import_fs14.mkdirSync)(workspace.sidecarDir, { recursive: true }); + (0, import_fs14.appendFileSync)(filePath, `${JSON.stringify(validated)} +`, "utf8"); + } catch (cause) { + throw ioError("append template record to", filePath, cause); + } +} +function nowIso(clock) { + return isoNow(clock); +} +function rethrowSpecExists(cause, specName) { + if (isSpecBridgeError(cause) && cause.code === "SPEC_ALREADY_EXISTS") { + throw new TemplateError( + "SBT020", + `Spec "${specName}" already exists.`, + `SpecBridge never overwrites an existing spec. Choose a different --name or inspect the existing spec with "specbridge spec show ${specName}".`, + { specName } + ); + } + throw cause; +} +function candidateHashForFiles(identity3, files) { + const payload = { + schema: "specbridge.template-candidate/1", + ...identity3, + files: files.map((file) => ({ target: file.fileName, hash: sha256Hex(file.content) })) + }; + return sha256Hex(JSON.stringify(payload)); +} +function planTemplateApplication(workspace, catalog, request, clock = systemClock) { + const entry = resolveValidTemplate(catalog, request.reference); + const manifest = entry.pack.manifest; + const manifestText = entry.pack.manifestText; + if (manifest === void 0 || manifestText === void 0) { + throw new TemplateError("SBT004", `Template ${entry.ref} has no readable manifest.`, "Re-install the template."); + } + const diagnostics = []; + if (manifest.deprecated === true) { + diagnostics.push({ + severity: "warning", + code: "TEMPLATE_DEPRECATED", + message: `Template ${entry.ref} is deprecated.` + (manifest.replacement !== void 0 ? ` Consider "${manifest.replacement}" instead.` : "") + }); + } + const mode = request.mode ?? manifest.defaultMode; + if (!manifest.supportedModes.includes(mode)) { + throw new TemplateError( + "SBT015", + `Template ${entry.ref} does not support mode "${mode}".`, + `Supported modes: ${manifest.supportedModes.join(", ")} (default: ${manifest.defaultMode}).`, + { reference: entry.ref, mode } + ); + } + try { + planSpecCreationFromFiles( + workspace, + { + name: request.specName, + specType: manifest.kind, + mode, + title: "placeholder", + description: "placeholder", + descriptionIsPlaceholder: true, + files: [] + }, + clock + ); + } catch (cause) { + rethrowSpecExists(cause, request.specName); + } + const requestedTitle = request.title?.trim(); + const title = requestedTitle !== void 0 && requestedTitle.length > 0 ? requestedTitle : titleFromSpecName(request.specName); + const requestedDescription = request.description?.trim(); + const descriptionIsPlaceholder = requestedDescription === void 0 || requestedDescription.length === 0; + const description = descriptionIsPlaceholder ? manifest.kind === "bugfix" ? DEFAULT_BUGFIX_DESCRIPTION : DEFAULT_FEATURE_DESCRIPTION : requestedDescription; + const resolved = resolveVariables(manifest, request.variables ?? {}, { + specName: request.specName, + title, + description, + kind: manifest.kind, + mode, + clock + }); + const files = []; + for (const file of manifest.files) { + const source = entry.pack.files.get(file.source); + if (source === void 0) { + throw new TemplateError( + "SBT007", + `Template ${entry.ref} declares "${file.source}" but the pack does not contain it.`, + `Run "specbridge template validate ${entry.ref}".`, + { reference: entry.ref, source: file.source } + ); + } + const content = renderTemplateText(file.source, source, resolved.values); + const stage = TARGET_STAGES[file.target]; + if (stage === void 0) { + throw new TemplateError( + "SBT011", + `Template ${entry.ref} declares invalid target "${file.target}".`, + `Run "specbridge template validate ${entry.ref}".`, + { reference: entry.ref, target: file.target } + ); + } + const structural = checkRenderedDocument(file.source, file.target, content); + const errors = structural.filter((issueItem) => issueItem.severity === "error"); + if (errors.length > 0) { + throw new TemplateError( + "SBT017", + `Rendered "${file.target}" is not a valid spec document: ${errors.map((issueItem) => issueItem.message).join(" | ")}`, + "Fix the template file or the supplied variable values, then preview again.", + { reference: entry.ref, target: file.target } + ); + } + for (const warningItem of structural) { + diagnostics.push({ + severity: "warning", + code: "TEMPLATE_RENDER_WARNING", + message: warningItem.message + }); + } + files.push({ fileName: file.target, stage, content }); + } + let specPlan; + try { + specPlan = planSpecCreationFromFiles( + workspace, + { + name: request.specName, + specType: manifest.kind, + mode, + title, + description, + descriptionIsPlaceholder, + files + }, + clock + ); + } catch (cause) { + rethrowSpecExists(cause, request.specName); + } + const manifestHash = sha256Hex(manifestText); + const candidateHash = candidateHashForFiles( + { + templateRef: entry.ref, + templateVersion: manifest.version, + manifestHash, + specName: request.specName, + kind: manifest.kind, + mode + }, + files + ); + return { + templateRef: entry.ref, + templateId: entry.id, + templateVersion: manifest.version, + templateSource: entry.source, + manifest, + manifestHash, + mode, + variableNames: resolved.variableNames, + specPlan, + candidateHash, + diagnostics + }; +} +function toPosix(relative) { + return relative.split(import_path14.default.sep).join("/"); +} +function executeTemplateApplication(workspace, plan, clock = systemClock, recordId) { + let creation; + try { + creation = executeSpecCreation(workspace, plan.specPlan); + } catch (cause) { + rethrowSpecExists(cause, plan.specPlan.specName); + } + const id = recordId ?? newTemplateRecordId(clock); + const record2 = { + schemaVersion: "1.0.0", + recordId: id, + type: "template-apply", + createdAt: nowIso(clock), + result: "ok", + templateRef: plan.templateRef, + templateId: plan.templateId, + templateVersion: plan.templateVersion, + templateSource: plan.templateSource, + manifestHash: plan.manifestHash, + specName: plan.specPlan.specName, + specKind: plan.specPlan.specType, + workflowMode: plan.mode, + renderedFiles: plan.specPlan.files.map((file) => ({ + target: file.fileName, + hash: sha256Hex(file.content) + })), + variableNames: plan.variableNames, + createdPaths: [ + ...creation.writtenFiles.map((file) => toPosix(import_path14.default.relative(workspace.rootDir, file))), + toPosix(import_path14.default.relative(workspace.rootDir, creation.statePath)) + ] + }; + appendTemplateRecord(workspace, record2); + return { plan, creation, recordId: id }; +} +function planTemplateInstall(workspace, catalog, request) { + const sourceDir = import_path15.default.resolve(request.cwd ?? workspace.rootDir, request.sourcePath); + try { + assertInsideWorkspace(workspace.rootDir, sourceDir); + } catch (cause) { + if (isSpecBridgeError(cause) && cause.code === "PATH_OUTSIDE_WORKSPACE") { + throw new TemplateError( + "SBT007", + `Install source ${sourceDir} is outside the repository.`, + "Copy the template pack into the repository first; installation only reads local, inspectable paths.", + { path: sourceDir } + ); + } + throw cause; + } + const data = readTemplatePackDirectory(sourceDir); + const pack = loadTemplatePack(data); + if (!pack.valid || pack.manifest === void 0 || pack.manifestText === void 0) { + const problems = pack.issues.filter((issue32) => issue32.severity === "error").slice(0, 5).map((issue32) => `${issue32.code}: ${issue32.message}`); + throw new TemplateError( + "SBT004", + `Template pack at ${sourceDir} failed validation: ${problems.join(" | ")}`, + `Run "specbridge template validate ${request.sourcePath}" for the full report and fix the pack before installing.`, + { path: sourceDir } + ); + } + const templateId = pack.manifest.id; + const targetDir = import_path15.default.join(projectTemplatesDir(workspace), templateId); + if ((0, import_fs15.existsSync)(targetDir)) { + throw new TemplateError( + "SBT021", + `Template "project:${templateId}" is already installed at ${targetDir}.`, + `Uninstall it first with "specbridge template uninstall project:${templateId}" \u2014 installs never overwrite.`, + { path: targetDir } + ); + } + const warnings = []; + if (catalog.entries.some((entry) => entry.source === "builtin" && entry.id === templateId)) { + warnings.push( + `A built-in template with ID "${templateId}" exists. After installation the unqualified reference "${templateId}" becomes ambiguous and every command will require "builtin:${templateId}" or "project:${templateId}".` + ); + } + return { + templateId, + ref: `project:${templateId}`, + templateVersion: pack.manifest.version, + manifestHash: sha256Hex(pack.manifestText), + sourceDir, + targetDir, + pack, + warnings + }; +} +function executeTemplateInstall(workspace, plan, clock = systemClock, recordId) { + const tmpParent = import_path15.default.join(workspace.sidecarDir, "tmp"); + const tempDir = import_path15.default.join( + tmpParent, + `template-install-${plan.templateId}-${process.pid}-${Math.random().toString(36).slice(2, 8)}` + ); + try { + (0, import_fs15.mkdirSync)(tempDir, { recursive: true }); + for (const [relative, content] of plan.pack.files) { + const target = import_path15.default.join(tempDir, relative); + (0, import_fs15.mkdirSync)(import_path15.default.dirname(target), { recursive: true }); + writeFileAtomic(target, content); + } + const copied = loadTemplatePack(readTemplatePackDirectory(tempDir)); + if (!copied.valid) { + throw new TemplateError( + "SBT025", + `Copied template pack failed re-validation; installation was aborted.`, + "Retry the install; if this persists, the source pack is changing while being read.", + { path: plan.sourceDir } + ); + } + (0, import_fs15.mkdirSync)(import_path15.default.dirname(plan.targetDir), { recursive: true }); + if ((0, import_fs15.existsSync)(plan.targetDir)) { + throw new TemplateError( + "SBT021", + `Template "project:${plan.templateId}" was installed by another process.`, + `Nothing was overwritten. Inspect the installed template with "specbridge template show project:${plan.templateId}".`, + { path: plan.targetDir } + ); + } + (0, import_fs15.renameSync)(tempDir, plan.targetDir); + } finally { + (0, import_fs15.rmSync)(tempDir, { recursive: true, force: true }); + try { + (0, import_fs15.rmdirSync)(tmpParent); + } catch { + } + } + const id = recordId ?? newTemplateRecordId(clock); + appendTemplateRecord(workspace, { + schemaVersion: "1.0.0", + recordId: id, + type: "template-install", + createdAt: nowIso(clock), + result: "ok", + templateRef: plan.ref, + templateId: plan.templateId, + templateVersion: plan.templateVersion, + manifestHash: plan.manifestHash, + sourcePath: import_path15.default.relative(workspace.rootDir, plan.sourceDir).split(import_path15.default.sep).join("/"), + installedPath: import_path15.default.relative(workspace.rootDir, plan.targetDir).split(import_path15.default.sep).join("/") + }); + return { plan, installedPath: plan.targetDir, recordId: id }; +} +function planTemplateUninstall(workspace, rawReference) { + const reference = parseTemplateReference(rawReference); + if (reference === void 0) { + throw new TemplateError( + "SBT003", + `"${rawReference}" is not a valid template reference.`, + 'Use a qualified project reference like "project:my-template".', + { reference: rawReference } + ); + } + if (reference.source === "builtin") { + throw new TemplateError( + "SBT022", + `Built-in template "${reference.id}" cannot be uninstalled.`, + "Built-in templates are bundled with SpecBridge and are immutable at runtime.", + { reference: rawReference } + ); + } + if (reference.source !== "project") { + throw new TemplateError( + "SBT025", + `Uninstall requires a qualified project reference (got "${rawReference}").`, + `Use "project:${reference.id}" so the command cannot accidentally target another source.`, + { reference: rawReference } + ); + } + const dir = import_path15.default.join(projectTemplatesDir(workspace), reference.id); + let stat; + try { + stat = (0, import_fs15.lstatSync)(dir); + } catch { + throw new TemplateError( + "SBT001", + `Template "project:${reference.id}" is not installed.`, + 'Run "specbridge template list --source project" to see installed templates.', + { reference: rawReference } + ); + } + if (stat.isSymbolicLink() || !stat.isDirectory()) { + throw new TemplateError( + "SBT009", + `Installed template path ${dir} is not a regular directory.`, + "Remove the entry manually after inspecting it; SpecBridge will not follow symlinks.", + { path: dir } + ); + } + return { templateId: reference.id, ref: `project:${reference.id}`, dir }; +} +function executeTemplateUninstall(workspace, plan, clock = systemClock, recordId) { + const tmpParent = import_path15.default.join(workspace.sidecarDir, "tmp"); + const tempDir = import_path15.default.join( + tmpParent, + `template-uninstall-${plan.templateId}-${process.pid}-${Math.random().toString(36).slice(2, 8)}` + ); + (0, import_fs15.mkdirSync)(tmpParent, { recursive: true }); + (0, import_fs15.renameSync)(plan.dir, tempDir); + try { + (0, import_fs15.rmSync)(tempDir, { recursive: true, force: true }); + } finally { + try { + (0, import_fs15.rmdirSync)(tmpParent); + } catch { + } + } + const id = recordId ?? newTemplateRecordId(clock); + appendTemplateRecord(workspace, { + schemaVersion: "1.0.0", + recordId: id, + type: "template-uninstall", + createdAt: nowIso(clock), + result: "ok", + templateRef: plan.ref, + templateId: plan.templateId, + uninstalledPath: import_path15.default.relative(workspace.rootDir, plan.dir).split(import_path15.default.sep).join("/") + }); + return { plan, recordId: id }; +} +function titleCase(id) { + return id.split("-").map((part) => part.length > 0 ? part[0]?.toUpperCase() + part.slice(1) : part).join(" "); +} +var CONTRIBUTION_CHECKLIST = `## Contribution checklist + +Before sharing this template (or opening a pull request to add it as a +SpecBridge built-in): + +- [ ] \`specbridge template validate ./\` passes with no errors. +- [ ] \`specbridge template preview\` output reads well with realistic variable values. +- [ ] The README documents every variable and shows a copy-pasteable usage example. +- [ ] Template files are plain Markdown \u2014 no scripts, no HTML, no front matter. +- [ ] No employer-specific, vendor-locked, or machine-specific content. +- [ ] The manifest "examples" array shows at least one real, working command. +`; +function scaffoldManifest(request, modes) { + const targets = ALLOWED_TARGETS[request.kind]; + const manifest = { + schemaVersion: "1.0.0", + id: request.templateId, + version: "1.0.0", + displayName: request.displayName ?? titleCase(request.templateId), + description: request.description ?? `A ${request.kind} spec template. Describe what kind of change this template is for.`, + kind: request.kind, + supportedModes: modes, + defaultMode: modes[0], + tags: [request.kind === "bugfix" ? "bugfix" : "feature"], + files: targets.map((target) => ({ + source: `files/${target}.template`, + target, + stage: target === "requirements.md" ? "requirements" : target === "bugfix.md" ? "bugfix" : target === "design.md" ? "design" : "tasks", + required: true + })), + variables: [ + { + name: "actor", + description: "Primary user or system actor.", + type: "string", + required: false, + default: "user" + } + ], + compatibility: { + specbridge: ">=0.7.0 <1.0.0", + kiroLayout: "1" + }, + license: request.license ?? "MIT" + }; + return `${JSON.stringify(manifest, null, 2)} +`; +} +function scaffoldReadme(request) { + const display = request.displayName ?? titleCase(request.templateId); + return `# ${display} template + +${request.description ?? `A ${request.kind} spec template. Describe what kind of change this template is for.`} + +## Usage + +\`\`\`bash +specbridge template preview ${request.templateId} \\ + --name my-new-spec \\ + --var actor=user + +specbridge template apply ${request.templateId} \\ + --name my-new-spec \\ + --var actor=user +\`\`\` + +## Variables + +| Variable | Type | Default | Purpose | +| --- | --- | --- | --- | +| \`actor\` | string | \`user\` | Primary user or system actor. | + +The built-in variables \`specName\`, \`title\`, \`description\`, \`kind\`, and +\`mode\` are always available. + +## Validate locally + +\`\`\`bash +# From the directory containing this template pack: +specbridge template validate ./${import_path16.default.basename(request.outputPath)} + +# Then install it into a project for a real preview: +specbridge template install ./${import_path16.default.basename(request.outputPath)} +specbridge template preview project:${request.templateId} --name example-spec +\`\`\` + +${CONTRIBUTION_CHECKLIST}`; +} +function featureRequirementsTemplate() { + return `# Requirements Document + +## Introduction + +**{{title}}** + +{{description}} + +## Glossary + +| Term | Definition | +| --- | --- | +| | | + +## Requirements + +### Requirement 1: + +**User Story:** As a {{actor}}, I want , so that . + +#### Acceptance Criteria + +1. WHEN , THE SYSTEM SHALL . +2. IF , THEN THE SYSTEM SHALL . + +## Non-Functional Requirements + +- Performance: add measurable performance expectations here. +- Security: add authentication, authorization, and data-handling expectations here. +- Reliability: add availability and failure-recovery expectations here. +- Observability: add logging, metrics, and alerting expectations here. +- Compatibility: add platform and integration constraints here. + +## Edge Cases + +- Add edge cases here. + +## Out of Scope + +- Add explicitly excluded behavior here. +`; +} +function featureDesignTemplate() { + return `# Design Document + +## Overview + +**{{title}}** + +{{description}} + +## Goals + +- Add concrete goals here. + +## Non-Goals + +- Add explicitly excluded goals here. + +## Architecture + +Describe the overall approach here. + +## Components and Interfaces + +- Add affected components and their interfaces here. + +## Data Model + +- Add new or changed data structures here. + +## Control Flow + +Describe the main control flow here. + +## Failure Handling + +- Add failure modes and how the system handles them here. + +## Security Considerations + +- Add authentication, authorization, and data-protection concerns here. + +## Observability + +- Add logging, metrics, and tracing decisions here. + +## Testing Strategy + +- Add unit, integration, and regression testing plans here. + +## Risks and Trade-offs + +- Add known risks and accepted trade-offs here. + +## Alternatives Considered + +- Add rejected alternatives and why they were rejected here. +`; +} +function featureTasksTemplate() { + return `# Implementation Plan + +- [ ] 1. +- [ ] 2. +- [ ] 3. Add automated tests for the acceptance criteria. +- [ ] 4. Verify error handling and edge cases. +- [ ] 5. Update documentation where required. +`; +} +function bugfixDocumentTemplate() { + return `# Bugfix Document + +## Summary + +**{{title}}** + +{{description}} + +## Current Behavior + +Describe the observed incorrect behavior here. + +## Expected Behavior + +Describe the correct behavior here. + +## Unchanged Behavior + +- List behavior that must remain unchanged here. + +## Reproduction + +1. Add reproduction steps here. + +## Evidence + +- Logs: add relevant log lines here. +- Error messages: add exact error text here. +- Failing tests: add failing test names here. +- Relevant source locations: add file paths here. + +## Constraints + +- Add implementation or compatibility constraints here. + +## Regression Risks + +- Add behavior that could regress here. +`; +} +function bugfixDesignTemplate() { + return `# Fix Design + +## Root Cause + +Document the confirmed or suspected root cause here. + +## Proposed Fix + +Describe the smallest safe fix here. + +## Affected Components + +- Add affected files and components here. + +## Failure Handling + +- Add failure modes introduced or fixed by this change here. + +## Alternatives Considered + +- Add rejected alternatives and why they were rejected here. + +## Regression Protection + +- Add the regression tests that will guard this fix here. + +## Validation Strategy + +- Add the checks that prove the fix works here. +`; +} +function bugfixTasksTemplate() { + return `# Bugfix Implementation Plan + +- [ ] 1. Reproduce the bug with deterministic evidence. +- [ ] 2. Confirm the root cause. +- [ ] 3. Implement the smallest safe fix. +- [ ] 4. Add regression tests. +- [ ] 5. Verify unchanged behavior. +- [ ] 6. Run the required validation checks. +`; +} +var DEFAULT_MODES = { + feature: ["requirements-first", "design-first", "quick"], + bugfix: ["requirements-first", "quick"] +}; +function planTemplateScaffold(request) { + const idCheck = validateTemplateId(request.templateId); + if (!idCheck.valid) { + throw new TemplateError( + "SBT003", + `"${request.templateId}" is not a valid template ID: +${idCheck.problems.map((p) => ` - ${p}`).join("\n")}`, + "Valid examples: rest-api, database-migration, cli-tool-v2.", + { templateId: request.templateId } + ); + } + const modes = request.modes !== void 0 && request.modes.length > 0 ? request.modes : DEFAULT_MODES[request.kind]; + if (new Set(modes).size !== modes.length) { + throw new TemplateError("SBT015", "--modes contains duplicates.", "List each mode once.", {}); + } + const outputDir = import_path16.default.resolve(request.cwd, request.outputPath); + const relative = import_path16.default.relative(import_path16.default.resolve(request.cwd), outputDir); + if (relative.startsWith("..") || import_path16.default.isAbsolute(relative)) { + throw new TemplateError( + "SBT007", + `Scaffold output ${outputDir} is outside the current directory.`, + "Scaffold into the directory you are working in, e.g. --output ./my-template.", + { path: outputDir } + ); + } + if ((0, import_fs16.existsSync)(outputDir)) { + throw new TemplateError( + "SBT025", + `Scaffold output directory already exists: ${outputDir}.`, + "Choose a different --output path; scaffolding never overwrites existing files.", + { path: outputDir } + ); + } + const files = /* @__PURE__ */ new Map(); + files.set(TEMPLATE_MANIFEST_FILE_NAME, scaffoldManifest(request, modes)); + files.set("README.md", scaffoldReadme(request)); + if (request.kind === "feature") { + files.set("files/requirements.md.template", featureRequirementsTemplate()); + files.set("files/design.md.template", featureDesignTemplate()); + files.set("files/tasks.md.template", featureTasksTemplate()); + } else { + files.set("files/bugfix.md.template", bugfixDocumentTemplate()); + files.set("files/design.md.template", bugfixDesignTemplate()); + files.set("files/tasks.md.template", bugfixTasksTemplate()); + } + const selfCheck = loadTemplatePack({ origin: outputDir, files }, { requireReadme: true }); + if (!selfCheck.valid) { + throw new TemplateError( + "SBT025", + `Internal error: scaffolded pack failed validation: ${selfCheck.issues.filter((issue32) => issue32.severity === "error").map((issue32) => issue32.message).join(" | ")}`, + "This is a SpecBridge bug \u2014 please report it.", + {} + ); + } + return { templateId: request.templateId, kind: request.kind, outputDir, files }; +} +function executeTemplateScaffold(plan, workspace, clock = systemClock, recordId) { + const tmpParent = workspace !== void 0 ? import_path16.default.join(workspace.sidecarDir, "tmp") : import_path16.default.join((0, import_os.tmpdir)(), "specbridge-scaffold"); + const tempDir = import_path16.default.join( + tmpParent, + `template-scaffold-${plan.templateId}-${process.pid}-${Math.random().toString(36).slice(2, 8)}` + ); + const writtenFiles = []; + try { + (0, import_fs16.mkdirSync)(tempDir, { recursive: true }); + for (const [relative, content] of plan.files) { + const target = import_path16.default.join(tempDir, relative); + (0, import_fs16.mkdirSync)(import_path16.default.dirname(target), { recursive: true }); + writeFileAtomic(target, content); + } + (0, import_fs16.mkdirSync)(import_path16.default.dirname(plan.outputDir), { recursive: true }); + if ((0, import_fs16.existsSync)(plan.outputDir)) { + throw new TemplateError( + "SBT025", + `Scaffold output directory was created by another process: ${plan.outputDir}.`, + "Choose a different --output path; scaffolding never overwrites existing files.", + { path: plan.outputDir } + ); + } + (0, import_fs16.renameSync)(tempDir, plan.outputDir); + for (const relative of plan.files.keys()) { + writtenFiles.push(import_path16.default.join(plan.outputDir, relative)); + } + } finally { + (0, import_fs16.rmSync)(tempDir, { recursive: true, force: true }); + try { + (0, import_fs16.rmdirSync)(tmpParent); + } catch { + } + } + let id; + if (workspace !== void 0) { + id = recordId ?? newTemplateRecordId(clock); + appendTemplateRecord(workspace, { + schemaVersion: "1.0.0", + recordId: id, + type: "template-scaffold", + createdAt: nowIso(clock), + result: "ok", + templateId: plan.templateId, + kind: plan.kind, + outputPath: import_path16.default.relative(workspace.rootDir, plan.outputDir).split(import_path16.default.sep).join("/") + }); + } + return { plan, writtenFiles, recordId: id }; +} + +// ../../packages/cli/src/commands/template.ts +var import_node_fs = require("fs"); +var import_node_path4 = __toESM(require("path"), 1); +var WORKFLOW_MODES = ["requirements-first", "design-first", "quick"]; +var SPEC_TYPES = ["feature", "bugfix"]; +function collectVar(value, previous = []) { + return [...previous, value]; +} +function parseVars(options) { + const variables = {}; + for (const raw of options.var ?? []) { + const eq = raw.indexOf("="); + if (eq <= 0) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Invalid --var "${raw}". Use --var key=value (e.g. --var tableName=payments).` + ); + } + const key = raw.slice(0, eq); + if (key in variables) { + throw new SpecBridgeError("INVALID_ARGUMENT", `--var "${key}" was supplied more than once.`); + } + variables[key] = raw.slice(eq + 1); + } + return variables; +} +function splitTemplateInputs(options) { + const { title: varTitle, description: varDescription, ...variables } = parseVars(options); + for (const [name, optionValue, varValue] of [ + ["title", options.title, varTitle], + ["description", options.description, varDescription] + ]) { + if (optionValue !== void 0 && varValue !== void 0) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Both --${name} and --var ${name}=\u2026 were supplied. Use one of them.` + ); + } + } + return { + title: options.title ?? varTitle, + description: options.description ?? varDescription, + variables + }; +} +function requireMode(value) { + if (value === void 0) return void 0; + if (!WORKFLOW_MODES.includes(value)) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Unknown --mode "${value}". Valid modes: ${WORKFLOW_MODES.join(", ")}.` + ); + } + return value; +} +function catalogFor(runtime, source) { + if (source !== void 0 && !["builtin", "project", "all"].includes(source)) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Unknown --source "${source}". Valid sources: builtin, project, all.` + ); + } + return loadTemplateCatalog(runtime.tryWorkspace(), { + source: source ?? "all" + }); +} +function entryToJson(entry) { + const manifest = entry.pack.manifest; + return { + ref: entry.ref, + id: entry.id, + source: entry.source, + valid: entry.valid, + displayName: manifest?.displayName ?? null, + version: manifest?.version ?? null, + description: manifest?.description ?? null, + kind: manifest?.kind ?? null, + supportedModes: manifest?.supportedModes ?? [], + defaultMode: manifest?.defaultMode ?? null, + tags: manifest?.tags ?? [], + compatibility: manifest?.compatibility ?? null, + deprecated: manifest?.deprecated ?? false, + errors: entry.pack.issues.filter((issue4) => issue4.severity === "error").map((issue4) => `${issue4.code}: ${issue4.message}`) + }; +} +function applyFilters(entries, filters) { + let result = entries; + if (filters.kind !== void 0) { + if (!SPEC_TYPES.includes(filters.kind)) { + throw new SpecBridgeError( + "INVALID_ARGUMENT", + `Unknown --kind "${filters.kind}". Valid kinds: ${SPEC_TYPES.join(", ")}.` + ); + } + result = result.filter((entry) => entry.pack.manifest?.kind === filters.kind); + } + if (filters.mode !== void 0) { + const mode = requireMode(filters.mode); + result = result.filter((entry) => entry.pack.manifest?.supportedModes.includes(mode) === true); + } + if (filters.tag !== void 0) { + result = result.filter((entry) => entry.pack.manifest?.tags.includes(filters.tag) === true); + } + return result; +} +function printEntryLine(runtime, entry) { + const manifest = entry.pack.manifest; + if (!entry.valid || manifest === void 0) { + runtime.out(failLine(`${entry.ref}`, '(invalid \u2014 run "template validate" for details)')); + return; + } + const deprecated = manifest.deprecated === true ? " [deprecated]" : ""; + runtime.out(okLine(`${entry.ref} \u2014 ${manifest.displayName} v${manifest.version}${deprecated}`)); + runtime.out( + dim( + ` ${manifest.kind} | modes: ${manifest.supportedModes.join(", ")} | tags: ${manifest.tags.join(", ")}` + ) + ); + runtime.out(dim(` ${manifest.description}`)); +} +function printIssues(runtime, issues) { + for (const issue4 of issues) { + const location = issue4.file !== void 0 ? ` [${issue4.file}]` : ""; + const line = `${issue4.code} (${issue4.category})${location}: ${issue4.message}`; + runtime.out(issue4.severity === "error" ? failLine(line) : warnLine(line)); + } +} +function printApplicationPlan(runtime, plan, heading, showContent) { + const workspace = runtime.workspace(); + runtime.out(reportTitle(heading)); + runtime.out(); + runtime.out(` Template: ${plan.templateRef} v${plan.templateVersion}`); + runtime.out(` Spec name: ${plan.specPlan.specName}`); + runtime.out(` Kind: ${plan.specPlan.specType}`); + runtime.out(` Mode: ${plan.mode}`); + runtime.out(` Title: ${plan.specPlan.title}`); + runtime.out(` Dir: ${relPath(workspace, plan.specPlan.dir)}`); + runtime.out(` Candidate: ${plan.candidateHash}`); + runtime.out(); + for (const diagnostic of plan.diagnostics) { + runtime.out(warnLine(diagnostic.message)); + } + runtime.out(sectionTitle("Target files")); + for (const file of plan.specPlan.files) { + runtime.out( + okLine( + `${relPath(workspace, plan.specPlan.dir)}/${file.fileName}`, + `(${file.stage}, ${Buffer.byteLength(file.content, "utf8")} B, unapproved)` + ) + ); + } + runtime.out(okLine(relPath(workspace, plan.specPlan.statePath), "(sidecar workflow state)")); + if (showContent) { + runtime.out(); + runtime.out(sectionTitle("Rendered content")); + for (const file of plan.specPlan.files) { + runtime.out(dim(`--- ${file.fileName} ---`)); + runtime.outRaw(file.content); + } + runtime.out(dim("--- sidecar state proposal ---")); + runtime.outRaw(`${JSON.stringify(plan.specPlan.state, null, 2)} +`); + } +} +function applicationPlanJson(plan, extra) { + return { + template: { + ref: plan.templateRef, + id: plan.templateId, + version: plan.templateVersion, + source: plan.templateSource, + manifestHash: plan.manifestHash + }, + specName: plan.specPlan.specName, + specKind: plan.specPlan.specType, + workflowMode: plan.mode, + title: plan.specPlan.title, + dir: plan.specPlan.dir, + candidateHash: plan.candidateHash, + variableNames: plan.variableNames, + diagnostics: plan.diagnostics, + files: plan.specPlan.files.map((file) => ({ + fileName: file.fileName, + stage: file.stage, + bytes: Buffer.byteLength(file.content, "utf8"), + content: file.content + })), + state: plan.specPlan.state, + statePath: plan.specPlan.statePath, + ...extra + }; +} +function registerTemplateCommands(program2, runtime) { + const template = program2.command("template").description("Discover, preview, and apply reusable spec templates (offline, deterministic)"); + template.command("list").description("List available templates from the built-in catalog and project-local packs").option("--source ", "template source: builtin | project | all", "all").option("--kind ", `filter by spec kind: ${SPEC_TYPES.join(" | ")}`).option("--mode ", `filter by supported workflow mode: ${WORKFLOW_MODES.join(" | ")}`).option("--tag ", "filter by tag").option("--json", "output a machine-readable JSON report").action((options) => { + const catalog = catalogFor(runtime, options.source); + const entries = applyFilters(catalog.entries, options); + if (options.json === true) { + runtime.outRaw( + serializeJsonReport( + createJsonReport("specbridge.template-list/1", `${CLI_BIN} ${VERSION}`, { + source: options.source ?? "all", + count: entries.length, + templates: entries.map(entryToJson), + diagnostics: catalog.diagnostics + }) + ) + ); + return; + } + runtime.out(reportTitle(`Templates (${entries.length})`)); + runtime.out(); + if (entries.length === 0) { + runtime.out(dim(" No templates match the given filters.")); + return; + } + for (const entry of entries) { + printEntryLine(runtime, entry); + } + runtime.out(); + runtime.out(dim(`Apply one with: ${CLI_BIN} template apply