diff --git a/docs/project-changelog.md b/docs/project-changelog.md index ce7a2a41..772a830a 100644 --- a/docs/project-changelog.md +++ b/docs/project-changelog.md @@ -5,6 +5,35 @@ All notable changes to CafeKit are documented here, following ## [Unreleased] +## [0.13.2] - 2026-06-21 + +### Added — Enforce scaffold on task creation +- **`task-scaffold-guard.cjs` (PreToolUse hook)**: hard-blocks any `Write` to `specs//tasks/task-*.md`, forcing task files to be generated via `spec-scaffold.cjs` then `Edit`-filled. Fixes the field-test dodge where the model hand-wrote every task file and skipped the opt-in scaffold step. Only the `Write` tool on a task-file path is blocked — `Edit`/`MultiEdit`, other `Write`s, and the scaffold script itself are untouched. +- **Safety valves**: fail-open when the scaffold script is missing (no deadlock), an actionable block message with the exact command, and a `.claude/runtime.json` escape hatch (`"spec": { "scaffold_guard": false }`). +- **Validator placeholder gate**: `validate-spec-output.cjs` now hard-fails a task that still has an unfilled `{{...}}` scaffold placeholder (warns on a leftover `.../` path). Fill-side complement to the guard: the hook forces creation via scaffold, this proves the stub was completed. + +### Changed +- `settings.json`: guard registered under a dedicated `Write` matcher in `PreToolUse`. +- `migration-manifest.json`: hook declared in `runtime.files` so the installer ships it. +- Specs `SKILL.md` Step 7: scaffold is now mandatory (Write to a task file is blocked), not a suggestion. + +### Notes +- Buys process discipline and consistent `task_registry`/`task_files`, not a large token reduction — task content is still emitted via `Edit`. + +## [0.13.1] - 2026-06-21 + +### Added — Specs v2 (quality + grounding + output-cost) +- **Layer 2 grounding (`spec-ground.cjs`)**: new deterministic check that greps the real work tree to verify every `Related Files` path a task cites (Modify/Delete/Read) exists, or is Created earlier in the spec. Closes the gap where the structural validator checks spec *shape* but is blind to phantom file paths. Active-grep (not opt-in). Wired into Step 8.5 + the `--validate` gate. `--root` for monorepo/sibling-project specs. +- **Spec scaffolding (`spec-scaffold.cjs`)**: generates spec.json + doc templates + task stubs so the model Edit-fills placeholders instead of hand-Writing whole files (the dominant output-token cost). `--tasks-only` merges task stubs + task_files/task_registry into an existing spec without overwriting filled tasks. Wired into Step 7. +- **Execution Tier (Light/Standard/Deep)**: auto-scales research/discovery/review depth by complexity so small specs skip full-pipeline overhead. Recorded in `spec.json.design_context.execution_tier`. Quality floor (scope_lock, EARS, Layer 1 + Layer 2) never skips. +- **Evidence-gated red-team**: `review.md` Step 5.5 auto-rejects findings that don't cite a concrete task/section or verbatim quote (spec-level analogue of ck-plan's `file:line` gate). +- **Definition of a Complete Task (DoCT)**: explicit quality bar in SKILL.md mapping each completeness element (real paths, contract, measurable acceptance, real evidence commands, reachability, requirement mapping) to its enforcing mechanism. +- **Frontend Fidelity Rule**: when a frontend task has a provided visual reference (design image, Figma, screenshot, palette, design tokens, style guide), the task MUST reproduce it faithfully — extract concrete values (exact hex, font, spacing, verbatim UI text), state a `match ` constraint, and prove fidelity in Evidence. New components with a reference must cite its tokens; conditional so brownfield reuse is unaffected. (`tasks-generation.md` + DoCT) +- **Complexity Smell Check (by numbers)**: quantitative YAGNI tripwires in scope inquiry (>8 files / >2 new services / >12 tasks → challenge; >15 tasks → split into sibling specs), targeting the mega-spec failure mode. Surfaced for the user to decide (Expand/Hold/Reduce/Split), never silently built. (`scope-inquiry.md` + SKILL.md Step 3) + +### Changed — Specs v2 +- **Literal `R{N}.{M}` is now the default requirement format** (template + SKILL.md Step 5). Wakes per-criterion coverage (P1d), which the model previously dodged by writing a bare numbered list. NFR section uses literal IDs too. + ## [0.13.0] - 2026-06-18 ### Changed diff --git a/notes/v0.13.0-field-test-post-list-screen.md b/notes/v0.13.0-field-test-post-list-screen.md new file mode 100644 index 00000000..80b3e84b --- /dev/null +++ b/notes/v0.13.0-field-test-post-list-screen.md @@ -0,0 +1,67 @@ +# Field Test v0.13.0 — spec `post_list_screen` (e8194-ai-mining-sns) + +> Ghi chú đánh giá thực địa. Spec production thật, đa tầng (NestJS + Nuxt 3 + MySQL). +> Conversation: `~/.claude/projects/-Users-nghialuutrung-Desktop-e8194-ai-mining-sns/b6cfe865-b814-40d7-b82a-30874b2030ef/` +> Ngày: 2026-06-18. Chưa commit — memo nội bộ. + +## Bối cảnh test +- Yêu cầu: tính năng màn danh sách bài đăng (一覧画面), 5 tab trạng thái, bulk actions, filter, 9 ảnh references. +- Bản chạy: **đúng 0.13.0** (đã verify: Step 8 Hydration đã bỏ, không còn `task-hydration.md`/`tasks-parallel-analysis.md`, `cafekit.json` version=0.13.0). +- Kết quả: 16 task files, validator PASS, red-team 4-persona, `ready_for_implementation=true`. + +## 1. Đo lường vận hành (số cứng từ session JSONL) +| Chỉ số | Giá trị | So game spec (27m) | +|---|---|---| +| Thời gian | **1h38m04s** | 3.6× | +| Output tokens | **935.000** | 3× | +| cache_read đỉnh | **296.000** (vượt 200K, nhờ model 1M) | — | +| Timeout/API error | **8 lần** | rủi ro resume | +| Tool calls | 27 Read · 21 Write · 16 Edit · 12 TaskUpdate · 6 TaskCreate · 2 Agent | output-bound | + +→ Thủ phạm 1h38m = **spec quá lớn (16 task) + output 935K + 8 timeout**, KHÔNG phải context nạp. + +## 2. Cải tiến v0.13.0 phát huy thế nào — KẾT QUẢ HỖN HỢP +| Cải tiến | Thực địa | Bằng chứng | +|---|---|---| +| P1b timestamps | ✅ thành công (gián tiếp) | 6 mốc riêng biệt, không reuse init | +| **P1d sub-criteria** | ❌ **BỊ NÉ** | requirements dùng 90 numbered-list, 0 literal `Rx.y`. Log AI ghi thẳng: *"dạng list số — tránh kích hoạt check sub-criteria"* | +| **#3 contract drift** | ❌ **BỊ NÉ** | design 0 contract marker. Log: *"cố ý KHÔNG dùng marker... rủi ro lớn hơn lợi ích"* | +| P0 tollgate + #2 + A1 | ⚪ vô nghĩa ở case này | tiết kiệm vài K token, bị 935K output át | + +### PHÁT HIỆN GỐC: incentive ngược +- P1d/#3 là opt-in **CÓ downside** (dùng sai → validator FAIL). AI tính ra **né hoàn toàn là tối ưu** (pass chắc, 0 rủi ro). +- "Format rõ ràng" vô tình thành "gánh thêm rủi ro validator" → ai cũng tránh. +- Mỉa mai: spec này CHÍNH XÁC là loại đa-tầng BE↔FE mà #3 sinh ra để bảo vệ. RT-2 (red-team thủ công) đã bắt đúng 1 contract drift — nhưng bằng LLM, không phải cơ chế deterministic đã xây. + +## 3. Chất lượng SẢN PHẨM: xuất sắc (9/10) +- **Coverage 14/14 requirement** đều có task ref (R9 ít nhất 1, R6 nhiều nhất 4). Không mồ côi. +- Contract khóa từ **source thật**: PostStatus 1-8, CancelApproval=4, platform X=3/IG=2/FB=1, delete_flg=1. +- Evidence trung thực: ghi rõ "FE không có test runner → chỉ typecheck+lint+manual", không bịa test. +- Red-team bắt 4 lỗ thật, đáng giá nhất **RT-2**: endpoint `bulk-cancel-approval` xung đột cap với spec `cancel_bulk_approval` đang tồn tại → reconcile max 200 BE / cap 10 UI. + +## 4. Đọc ảnh references: CHÍNH XÁC xuất sắc +- Chép đúng **từng chữ JP** modal/button/field từ 6 ảnh đã verify tận mắt (R4.3/R4.5/R8.3/R8.6 khớp nguyên văn). +- **Bắt mâu thuẫn 2 ảnh cùng màn**: image_1 nút 「編集」 vs image_2 nút 「設定」 → Open Q1. Chứng minh nhìn+so sánh thật, không bịa. +- **Phân biệt nội dung vs nhiễu**: banner đỏ「予期しないエラー」 ở ảnh 7/8 (artifact lỗi screenshot) → không chép mù, ghi Open Q6 hỏi nguồn text. +- Lưu ý: tiêu đề màn thật trong ảnh là 「投稿カレンダー」, spec đặt route `/posts/list` theo mô tả — hợp lý nhưng cần PM xác nhận route. + +## 5. Chỉ dẫn UI trong task: đủ structure, thiếu pixel-style +- **CÓ (tầng structure/behavior)**: 14 component đặt tên (`PostListTable.vue`, `ModalRepost.vue`...), file path chính xác, Read/Create/Modify rõ, label JP nguyên văn, `#DAF1EE` ×5, tham chiếu "khớp ảnh 2/3", format `M月D日 HH:mm`. +- **THIẾU (tầng pixel)**: 0 spacing/padding/font-size/tailwind/token. Component **Create mới** (`PostListTable`, `ModalRepost`, `PostListPlatformFilter`) chỉ tả cấu trúc, không tả visual → dev tự suy từ ảnh + component anh em. +- Đánh giá: hợp lý cho **brownfield** (reuse design system sẵn thay tả pixel). Khác hẳn case game greenfield (0 hex/font/path = lỗ nghiêm trọng). Chỗ hở duy nhất: style component Create mới (badge color, button variant). + +## 6. KẾT LUẬN then chốt +1. **Chất lượng spec không nhờ P1d/#3** (cả 2 bị né) — nhờ **xương sống vốn có**: evidence gate (scout source thật) + red-team 4-persona + input đại ca chi tiết. → **0.12.0 cũng cho spec tương đương**. Giá trị thật của hệ nằm ở evidence+red-team, không ở 2 validator-check đã thêm. +2. **Công sức 0.13.0 đặt sai trọng tâm**: tối ưu *context nạp* + xây *cơ chế chặt opt-in*. Thực địa: (a) context bị 935K output át, (b) cơ chế chặt bị né vì opt-in-có-downside. Chỉ P1b "thắng" mà case này AI vốn đã tự đúng. +3. **Đòn bẩy thật chưa đụng**: (a) output sinh ra (thinking + viết lại), (b) spec-size quá lớn = nguồn 1h38m + 8 timeout. + +## 7. Việc đáng làm tiếp (theo thứ tự đòn bẩy thật) +1. **Sửa incentive P1d/#3** — đổi "opt-in im lặng" → SKILL.md chỉ thị BẮT BUỘC ("spec đa-tầng BE+FE PHẢI dùng contract marker"; "requirements PHẢI dùng literal Rx.y"). Biến né-được thành phải-dùng. Nếu không → gỡ luôn cho đỡ ảo tưởng an toàn. +2. **Chống spec-quá-lớn** — spec >10 task PHẢI tách hoặc song song hóa sinh task. Đây mới là thứ cắt 1h38m. +3. **Resume an toàn sau timeout** — 8 lần ngắt là rủi ro mất việc thật. +4. (phụ) Bổ sung style-reference 1 dòng cho component Create mới khi cần pixel-perfect. + +## Điểm tổng +- Sản phẩm spec: **9/10** +- Cải tiến-v0.13.0-phát-huy: **4/10** (2/3 validator-check bị né) +- Vận hành: **4/10** (1h38m, 8 timeout) diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 6b7eae30..831936ae 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.2] - 2026-06-21 + +### Added — Enforce scaffold on task creation +- **`task-scaffold-guard.cjs` (PreToolUse hook)**: hard-blocks any `Write` whose path matches `specs//tasks/task-*.md`, so task files can only be created via `spec-scaffold.cjs` and then `Edit`-filled. Closes the dodge where the model hand-`Write`s task files and bypasses the (previously opt-in) scaffold step. Narrow scope: only the `Write` tool on a task-file path is blocked; `Edit`/`MultiEdit` and `Write` to any other file are untouched, and the scaffold script (writing via Node fs through Bash) is never blocked. +- **Three safety valves**: fail-open when `spec-scaffold.cjs` is absent (a hook shipped without its script must not deadlock task creation); actionable block message carrying the exact scaffold command; escape hatch via `"spec": { "scaffold_guard": false }` in `.claude/runtime.json`. +- **Validator placeholder gate (`validate-spec-output.cjs`)**: a task file that still carries an unfilled `{{...}}` scaffold placeholder now hard-fails (previously a prompt-only DoCT rule). A leftover `.../` path fragment warns. This is the fill-side complement to the guard — the hook forces task files through the scaffold; this proves the resulting stubs were actually completed, closing the "stub created but not filled" gap. + +### Changed +- `settings/settings.json`: registered the guard under a dedicated `Write` matcher in `PreToolUse` (separate entry so the settings-merge dedupe does not swallow it). +- `migration-manifest.json`: declared `hooks/task-scaffold-guard.cjs` in `runtime.files` so the installer ships it alongside `spec-scaffold.cjs`. +- `skills/specs/SKILL.md` Step 7: scaffold is now stated as **mandatory** (raw `Write` to a task file is blocked), not a suggestion. + +### Notes +- Enforces *process discipline* (every spec goes through scaffold; `task_files`/`task_registry` stay consistent), not a large token cut — task content is still emitted via `Edit`. + +## [0.13.1] - 2026-06-21 + +### Added — Specs v2 (quality + grounding + output-cost) +- **Layer 2 grounding (`spec-ground.cjs`)**: deterministic check that greps the real work tree to verify every `Related Files` path a task cites (Modify/Delete/Read) exists, or is Created earlier in the spec. Active-grep, not opt-in. Wired into Step 8.5 + `--validate`. `--root` for monorepo specs. +- **Spec scaffolding (`spec-scaffold.cjs`)**: generates spec.json + doc templates + task stubs so the model Edit-fills placeholders instead of hand-Writing whole files (cuts the dominant output-token cost). `--tasks-only` merges into an existing spec without overwriting filled tasks. +- **Execution Tier (Light/Standard/Deep)**: auto-scales research/discovery/review depth so small specs skip full-pipeline overhead; recorded in `design_context.execution_tier`. Quality floor never skips. +- **Evidence-gated red-team**: findings without a concrete task/section citation are auto-rejected. +- **Definition of a Complete Task (DoCT)**: explicit quality bar mapping each completeness element to its enforcing mechanism. +- **Frontend Fidelity Rule**: FE tasks with a provided visual reference must reproduce it faithfully (concrete hex/font/spacing/verbatim text + `match ` constraint). Conditional — brownfield reuse unaffected. +- **Complexity Smell Check (by numbers)**: quantitative YAGNI tripwires (>8 files / >2 services / >12 tasks → challenge; >15 → split). + +### Changed +- **Literal `R{N}.{M}` is the default requirement format** (template + Step 5), waking per-criterion coverage that was previously dodged via bare numbered lists. + +### Notes +- All new validator/grounding checks are opt-in/progressive or active-grep — legacy specs are unaffected and continue to pass. Spec structure (spec.json + requirements/design/research/tasks) unchanged. + ## [0.13.0] - 2026-06-18 ### Changed diff --git a/packages/spec/package.json b/packages/spec/package.json index d843f2ef..88399223 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@haposoft/cafekit", - "version": "0.13.0", + "version": "0.13.2", "description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.", "author": "Haposoft ", "license": "MIT", diff --git a/packages/spec/scripts/run-skill-self-tests.mjs b/packages/spec/scripts/run-skill-self-tests.mjs index d57b8e37..fe07550a 100644 --- a/packages/spec/scripts/run-skill-self-tests.mjs +++ b/packages/spec/scripts/run-skill-self-tests.mjs @@ -250,7 +250,9 @@ async function runStaticSemanticTests() { file: "src/claude/skills/specs/SKILL.md", assert: (content) => content.includes("**MUST run deterministic validator.**") && - content.includes("Script failure overrides any LLM checklist result") && + // Substring is invariant to "Script failure" (single-layer) vs the + // Specs-v2 "Either script failing" (validator + grounding) wording. + content.includes("overrides any LLM checklist result") && content.includes("output MUST NOT suggest `/hapo:develop`"), }, { @@ -268,9 +270,10 @@ async function runStaticSemanticTests() { { label: "hapo:specs feature-description flow continues past init", file: "src/claude/skills/specs/SKILL.md", + // Specs-v2 reworded this invariant; assert the current phrasing that + // still guarantees Init is not a stop point. assert: (content) => - content.includes("After user confirms scope, continue through Init") && - content.includes("Do not stop after Init unless the user explicitly asks for init-only behavior."), + content.includes("Init is never a stop point"), }, { label: "legacy spec-init redirects to hapo specs resume", diff --git a/packages/spec/src/claude/hooks/task-scaffold-guard.cjs b/packages/spec/src/claude/hooks/task-scaffold-guard.cjs new file mode 100644 index 00000000..5eaf289b --- /dev/null +++ b/packages/spec/src/claude/hooks/task-scaffold-guard.cjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * Copyright (c) 2026 Haposoft. MIT License. + * + * PreToolUse Hook — task-scaffold-guard.cjs + * Implements: https://docs.anthropic.com/en/docs/claude-code/hooks + * + * Forces spec task files to be GENERATED via spec-scaffold.cjs instead of + * hand-written. Field tests showed the model dodges the (opt-in) scaffold step + * and raw-`Write`s every task file — the single biggest output-token cost of a + * spec run. This hook blocks `Write` to `specs//tasks/task-*.md`, so + * the only path to a task file is: scaffold (creates the stub) -> Edit (fills it). + * + * Scope is deliberately narrow — it only ever blocks the `Write` tool on a task + * file path. `Edit`/`MultiEdit` (filling a stub), and `Write` to any other file + * (requirements.md, design.md, source code) are never touched. The scaffold + * script itself writes via Node fs through a Bash call, not the `Write` tool, so + * it is never blocked. + * + * Safety valves: + * 1. fail-open when spec-scaffold.cjs is absent — you cannot force a tool that + * is not installed, so a hook shipped without its script must not deadlock. + * 2. actionable block message — prints the exact scaffold command to run. + * 3. escape hatch — "spec": { "scaffold_guard": false } in .claude/runtime.json. + * + * Exit: 0 = allow, 2 = block. + */ + +try { + const fs = require('fs'); + const path = require('path'); + + const stdin = fs.readFileSync(0, 'utf8').trim(); + if (!stdin) process.exit(0); + + const data = JSON.parse(stdin); + const toolName = data.tool_name || ''; + const toolInput = data.tool_input || {}; + const cwd = data.cwd || process.cwd(); + + // Only `Write` can create a file from scratch. Edit/MultiEdit require an + // existing file, so filling a scaffolded stub is always allowed. + if (toolName !== 'Write') process.exit(0); + + const filePath = toolInput.file_path || toolInput.path || ''; + if (!filePath) process.exit(0); + + // Match a spec task file: .../specs//tasks/task-<...>.md + const norm = filePath.replace(/\\/g, '/'); + const isTaskFile = /(^|\/)specs\/[^/]+\/tasks\/task-[^/]+\.md$/.test(norm); + if (!isTaskFile) process.exit(0); + + // Valve 3: explicit escape hatch via .claude/runtime.json (fail-closed: a + // missing/broken runtime.json keeps the guard ON). + let runtime = {}; + try { + const rp = path.join(cwd, '.claude', 'runtime.json'); + if (fs.existsSync(rp)) runtime = JSON.parse(fs.readFileSync(rp, 'utf8')); + } catch { /* malformed runtime.json -> keep guard on */ } + if (runtime.spec && runtime.spec.scaffold_guard === false) process.exit(0); + + // Valve 1: fail-open if the scaffold script is not installed next to this hook + // (hook lives in .claude/hooks/, script in .claude/scripts/). Forcing a tool + // that does not exist would deadlock task creation. + const scaffold = path.join(__dirname, '..', 'scripts', 'spec-scaffold.cjs'); + if (!fs.existsSync(scaffold)) process.exit(0); + + // Valve 2: block with an actionable message carrying the exact command. + const m = norm.match(/(^|\/)specs\/([^/]+)\/tasks\//); + const feature = m ? m[2] : ''; + console.log( + `TASK SCAFFOLD REQUIRED: task files must be generated, not hand-written.\n` + + `Blocked Write: ${filePath}\n\n` + + `Generate the stub(s), then Edit-fill the {{...}} placeholders:\n` + + ` node .claude/scripts/spec-scaffold.cjs ${feature} --tasks "R0-01-slug,R1-01-slug,..." --tasks-only\n` + + `Then use Edit (not Write) on each tasks/task-*.md stub.\n` + + `Override: set "spec": { "scaffold_guard": false } in .claude/runtime.json` + ); + process.exit(2); + +} catch (e) { + // Never let a hook crash block the user — log and fail-open. + try { + const fs = require('fs'), p = require('path'); + const d = p.join(__dirname, '.logs'); + if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); + fs.appendFileSync(p.join(d, 'hook-log.jsonl'), + JSON.stringify({ ts: new Date().toISOString(), hook: 'task-scaffold-guard', status: 'crash', error: e.message }) + '\n'); + } catch (_) {} + process.exit(0); +} diff --git a/packages/spec/src/claude/migration-manifest.json b/packages/spec/src/claude/migration-manifest.json index 93668092..dd9e5de1 100644 --- a/packages/spec/src/claude/migration-manifest.json +++ b/packages/spec/src/claude/migration-manifest.json @@ -90,6 +90,7 @@ "hooks/usage.cjs", "hooks/privacy-block.cjs", "hooks/inspect-block.cjs", + "hooks/task-scaffold-guard.cjs", "hooks/rules.cjs", "hooks/spec-state.cjs", "hooks/state.cjs", diff --git a/packages/spec/src/claude/scripts/spec-ground.cjs b/packages/spec/src/claude/scripts/spec-ground.cjs new file mode 100644 index 00000000..522bc197 --- /dev/null +++ b/packages/spec/src/claude/scripts/spec-ground.cjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +/** + * CafeKit spec GROUNDING checker (Specs v2, Layer 2). + * + * The deterministic validator (`validate-spec-output.cjs`) checks spec SHAPE + * (headings, registry sync, coverage). It is blind to whether the file paths a + * task cites actually EXIST. This script closes that gap: it parses every task's + * `Related Files` table and verifies, against the real work tree, that each + * Modify/Delete/Read path exists — unless another task in the same spec Creates + * it first (intra-spec resolution). + * + * Why active-grep instead of opt-in: field tests showed opt-in checks get + * skipped by the author. Grounding runs unconditionally over whatever paths the + * tasks already declare, so it cannot be dodged by choosing a different format. + * + * Usage: + * node spec-ground.cjs [--root ] + * + * : path to specs/ (relative or absolute) + * --root : work tree the paths resolve against. Default: two levels up from + * specDir (…//specs/ -> ). Override for + * monorepos where the spec lives apart from the code. + * + * Exit: 0 = GROUNDED, 1 = FAIL (missing path), 2 = usage error. + */ + +const fs = require('fs'); +const path = require('path'); + +function usage() { + console.error('Usage: node spec-ground.cjs [--root ]'); +} + +function parseArgs(argv) { + const args = { specDir: null, root: null }; + for (let i = 2; i < argv.length; i++) { + if (argv[i] === '--root') args.root = argv[++i]; + else if (!args.specDir) args.specDir = argv[i]; + } + return args; +} + +function listTaskFiles(specDir) { + const tasksDir = path.join(specDir, 'tasks'); + if (!fs.existsSync(tasksDir)) return []; + return fs + .readdirSync(tasksDir, { withFileTypes: true }) + .filter((e) => e.isFile() && e.name.endsWith('.md')) + .map((e) => path.join(tasksDir, e.name)) + .sort(); +} + +/** + * Pull rows out of a markdown `## Related Files` table. Returns + * [{ path, action, taskFile }]. Tables look like: + * | Path | Action | Description | + * | `front/x.vue` | Modify | ... | + */ +function parseRelatedFiles(content, taskFile) { + const rows = []; + const lines = content.split('\n'); + let inSection = false; + for (const line of lines) { + if (/^##+\s+Related Files\s*$/i.test(line)) { inSection = true; continue; } + if (inSection && /^##+\s+/.test(line)) break; // next heading ends section + if (!inSection) continue; + if (!line.trim().startsWith('|')) continue; + + const cells = line.split('|').map((c) => c.trim()); + // cells[0] is '' (leading pipe). path=cells[1], action=cells[2]. + const rawPath = (cells[1] || '').replace(/`/g, '').trim(); + const action = (cells[2] || '').toLowerCase().trim(); + if (!rawPath || rawPath === 'path') continue; // header row + if (/^-+$/.test(rawPath)) continue; // separator row + if (!action || !/(create|modify|delete|read)/.test(action)) continue; + rows.push({ path: rawPath, action, taskFile: path.basename(taskFile) }); + } + return rows; +} + +/** Lenient existence check with minimal glob handling. */ +function pathExists(root, p) { + // Glob-ish path (e.g. `assets/{x,instagram,facebook}.svg` or `dir/*.ts`): + // verify the parent directory exists rather than the literal name. + if (/[{*]/.test(p)) { + const parent = path.dirname(p.replace(/\{.*$/, '').replace(/\*.*$/, '')); + return fs.existsSync(path.join(root, parent)); + } + return fs.existsSync(path.join(root, p)); +} + +function main() { + const { specDir: specInput, root: rootInput } = parseArgs(process.argv); + if (!specInput) { usage(); process.exit(2); } + + const specDir = path.resolve(process.cwd(), specInput); + if (!fs.existsSync(specDir)) { + console.error(`FAIL ${specInput}\n- spec directory does not exist`); + process.exit(1); + } + // Default work-context: two levels up (…//specs/). + const root = rootInput + ? path.resolve(process.cwd(), rootInput) + : path.resolve(specDir, '..', '..'); + + const taskFiles = listTaskFiles(specDir); + const allRows = []; + for (const tf of taskFiles) { + allRows.push(...parseRelatedFiles(fs.readFileSync(tf, 'utf8'), tf)); + } + + // Paths that will exist because a task Creates them (intra-spec resolution). + const createdPaths = new Set( + allRows.filter((r) => r.action.includes('create')).map((r) => r.path), + ); + + const errors = []; + const warnings = []; + let checked = 0; + + for (const row of allRows) { + const onDisk = pathExists(root, row.path); + if (row.action.includes('create')) { + if (onDisk) warnings.push(`${row.taskFile}: Create path already exists (will overwrite): ${row.path}`); + continue; + } + // modify / delete / read must exist OR be created by another task in-spec. + checked++; + if (!onDisk && !createdPaths.has(row.path)) { + errors.push(`${row.taskFile}: ${row.action} path not found in work tree: ${row.path}`); + } + } + + for (const w of warnings) console.warn(`[WARN] ${w}`); + + const rel = path.relative(process.cwd(), specDir) || specDir; + if (errors.length > 0) { + console.error(`FAIL ${rel} (root: ${root})`); + for (const e of errors) console.error(`- ${e}`); + console.error(`\n${errors.length} missing / ${checked} checked path(s).`); + process.exit(1); + } + console.log(`GROUNDED ${rel} (${checked} path(s) verified against ${root})`); +} + +main(); diff --git a/packages/spec/src/claude/scripts/spec-scaffold.cjs b/packages/spec/src/claude/scripts/spec-scaffold.cjs new file mode 100644 index 00000000..b7d0d9e9 --- /dev/null +++ b/packages/spec/src/claude/scripts/spec-scaffold.cjs @@ -0,0 +1,187 @@ +#!/usr/bin/env node +/** + * CafeKit spec SCAFFOLD generator (Specs v2 — output-cost reducer). + * + * Field tests showed the dominant cost of a spec run is OUTPUT tokens: the + * model hand-Writes spec.json + every task file from scratch (a 16-task spec + * emitted ~935K output tokens). This script does the mechanical scaffolding so + * the model only has to Edit-fill content, not Write whole files. + * + * It creates spec.json (with task_files + task_registry pre-populated) and one + * stub per task from the canonical templates, leaving the `{{...}}` placeholders + * for the model to fill. It NEVER overwrites an existing spec directory. + * + * Usage: + * node spec-scaffold.cjs --tasks "R0-01-slug,R1-01-slug,..." \ + * [--lang en] [--title "..."] [--specs-root specs] + * + * Exit: 0 = scaffolded, 2 = usage/precondition error. + */ + +const fs = require('fs'); +const path = require('path'); + +const TEMPLATES = path.join(__dirname, '..', 'skills', 'specs', 'templates'); +const TASK_ID_RE = /^R(\d+)-(\d+)-([a-z0-9]+(?:-[a-z0-9]+)*)$/; + +function usage() { + console.error('Usage: node spec-scaffold.cjs --tasks "R0-01-slug,R1-01-slug" [--lang en] [--title "..."] [--specs-root specs]'); +} + +function parseArgs(argv) { + const a = { feature: null, tasks: null, lang: 'en', title: null, specsRoot: 'specs', tasksOnly: false }; + for (let i = 2; i < argv.length; i++) { + const v = argv[i]; + if (v === '--tasks') a.tasks = argv[++i]; + else if (v === '--lang') a.lang = argv[++i]; + else if (v === '--title') a.title = argv[++i]; + else if (v === '--specs-root') a.specsRoot = argv[++i]; + else if (v === '--tasks-only') a.tasksOnly = true; + else if (!a.feature) a.feature = v; + } + return a; +} + +function readTemplate(name) { + const p = path.join(TEMPLATES, name); + if (!fs.existsSync(p)) { + console.error(`precondition: template not found: ${p}`); + process.exit(2); + } + return fs.readFileSync(p, 'utf8'); +} + +function titleFromSlug(slug) { + const s = slug.replace(/-/g, ' '); + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function registryEntry(t) { + return { + id: `R${t.req}-${t.seq}`, + title: titleFromSlug(t.slug), + status: 'pending', + dependencies: [], + blocker: null, + started_at: null, + completed_at: null, + last_updated_at: null, + }; +} + +function fillTask(taskTpl, t, feature) { + return taskTpl + .replace(/\{\{REQ_NUMBER\}\}/g, t.req) + .replace(/\{\{SEQ\}\}/g, t.seq) + .replace(/\{\{TITLE\}\}/g, titleFromSlug(t.slug)) + .replace(/\{\{FEATURE_NAME\}\}/g, feature) + .replace(/\{\{PRIORITY\}\}/g, 'P2') + .replace(/\{\{EFFORT\}\}/g, 'TBD') + .replace(/\{\{DEPENDENCIES\}\}/g, 'none'); +} + +function nowIso() { + // Local ISO with offset, e.g. 2026-06-21T12:30:00+07:00 + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? '+' : '-'; + const oh = pad(Math.floor(Math.abs(off) / 60)); + const om = pad(Math.abs(off) % 60); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${oh}:${om}`; +} + +function main() { + const args = parseArgs(process.argv); + if (!args.feature || !args.tasks) { usage(); process.exit(2); } + + const ids = args.tasks.split(',').map((s) => s.trim()).filter(Boolean); + const parsed = []; + for (const id of ids) { + const m = id.match(TASK_ID_RE); + if (!m) { + console.error(`precondition: invalid task id "${id}" (want R{N}-{SEQ}-, SEQ 2 digits)`); + process.exit(2); + } + parsed.push({ raw: id, req: m[1], seq: m[2], slug: m[3], file: `tasks/task-${id}.md` }); + } + + const specDir = path.resolve(process.cwd(), args.specsRoot, args.feature); + const taskTpl = readTemplate('task.md'); + const ts = nowIso(); + const created = []; + + // --- --tasks-only: add task stubs + merge registry into an existing spec --- + if (args.tasksOnly) { + const specPath = path.join(specDir, 'spec.json'); + if (!fs.existsSync(specPath)) { + console.error(`precondition: --tasks-only needs an existing spec.json at ${specPath}`); + process.exit(2); + } + const spec = JSON.parse(fs.readFileSync(specPath, 'utf8')); + spec.task_files = spec.task_files || []; + spec.task_registry = spec.task_registry || {}; + fs.mkdirSync(path.join(specDir, 'tasks'), { recursive: true }); + for (const t of parsed) { + const abs = path.join(specDir, t.file); + if (fs.existsSync(abs)) continue; // never overwrite an already-filled task + fs.writeFileSync(abs, fillTask(taskTpl, t, args.feature)); + if (!spec.task_files.includes(t.file)) spec.task_files.push(t.file); + spec.task_registry[t.file] = registryEntry(t); + created.push(t.file); + } + spec.task_files.sort(); + spec.updated_at = ts; + fs.writeFileSync(specPath, JSON.stringify(spec, null, 2) + '\n'); + const relTo = path.relative(process.cwd(), specDir) || specDir; + console.log(`SCAFFOLDED (tasks-only) ${relTo}`); + console.log(`- ${created.length} new task stub(s); task_files + task_registry merged.`); + console.log(`- NEXT: Edit-fill {{...}} in each stub; set dependencies; run validator + spec-ground.`); + return; + } + + if (fs.existsSync(specDir)) { + console.error(`precondition: spec dir already exists, refusing to overwrite: ${specDir}`); + process.exit(2); + } + + fs.mkdirSync(path.join(specDir, 'tasks'), { recursive: true }); + fs.mkdirSync(path.join(specDir, 'reports'), { recursive: true }); + + // --- spec.json (from spec-state.json template) --- + const spec = JSON.parse(readTemplate('spec-state.json')); + spec.feature_name = args.feature; + spec.created_at = ts; + spec.updated_at = ts; + spec.language = args.lang; + spec.timestamps = spec.timestamps || {}; + spec.timestamps.init = ts; + spec.scope_lock = spec.scope_lock || {}; + spec.scope_lock.source = args.title || `{{PROJECT_DESCRIPTION}}`; + spec.task_files = parsed.map((t) => t.file); + spec.task_registry = {}; + for (const t of parsed) spec.task_registry[t.file] = registryEntry(t); + fs.writeFileSync(path.join(specDir, 'spec.json'), JSON.stringify(spec, null, 2) + '\n'); + created.push('spec.json'); + + // --- doc templates (placeholders left for the model to fill) --- + for (const [tpl, out] of [['requirements.md', 'requirements.md'], ['research.md', 'research.md'], ['design.md', 'design.md']]) { + const body = readTemplate(tpl).replace(/\{\{FEATURE_NAME\}\}/g, args.feature); + fs.writeFileSync(path.join(specDir, out), body); + created.push(out); + } + + // --- task stubs (fill the cheap placeholders; leave the rest) --- + for (const t of parsed) { + fs.writeFileSync(path.join(specDir, t.file), fillTask(taskTpl, t, args.feature)); + created.push(t.file); + } + + const rel = path.relative(process.cwd(), specDir) || specDir; + console.log(`SCAFFOLDED ${rel}`); + console.log(`- ${created.length} files created: spec.json + 3 docs + ${parsed.length} task stub(s)`); + console.log(`- task_files + task_registry pre-populated (${parsed.length} entries, all pending)`); + console.log(`- NEXT: Edit-fill the {{...}} placeholders (do NOT leave any); set dependencies in task_registry; run validator + spec-ground before ready.`); +} + +main(); diff --git a/packages/spec/src/claude/scripts/validate-spec-output.cjs b/packages/spec/src/claude/scripts/validate-spec-output.cjs index 093cc94d..8e0956af 100755 --- a/packages/spec/src/claude/scripts/validate-spec-output.cjs +++ b/packages/spec/src/claude/scripts/validate-spec-output.cjs @@ -132,6 +132,28 @@ function validateTaskSections(taskPath, content, errors) { } } +/** + * Task files are created from the scaffold template (the scaffold-guard hook + * forces creation through it), so every task starts as a stub full of `{{...}}` + * placeholders. The hook guarantees the stub is CREATED via scaffold, but + * nothing guaranteed the model FILLED it. An unfilled `{{...}}` is an + * incomplete task — SKILL.md: "Leave NO {{...}} placeholder ... fails DoCT" — + * so it is a hard error here. A `.../` path fragment is a not-yet-resolved path + * placeholder; it is only a warning, because it usually survives in prose Steps + * while the Related Files table (which spec-ground.cjs does verify) is already + * concrete. Matching `\.\.\.\/` (three dots + slash) avoids flagging a relative + * `../` path or a prose ellipsis. + */ +function validateTaskPlaceholders(taskPath, content, errors, warnings) { + const stub = content.match(/\{\{[^}\n]+\}\}/); + if (stub) { + errors.push(`${taskPath}: unfilled scaffold placeholder ${stub[0]} — task stub was not completed`); + } + if (/\.\.\.\//.test(content)) { + warnings.push(`${taskPath}: contains a '.../' path placeholder — replace with a concrete path`); + } +} + /** * Each phase completion must carry its own timestamp. Reusing `timestamps.init` * for a later phase is forbidden (SKILL.md spec.json Update Rules). This used to @@ -344,6 +366,7 @@ function validateSpec(specDir) { const fullPath = path.join(specDir, taskFile); const content = fs.readFileSync(fullPath, 'utf8'); validateTaskSections(taskFile, content, errors); + validateTaskPlaceholders(taskFile, content, errors, warnings); const idRe = /\b((?:REQ-\d+)|(?:R\d+))\b/gi; let match; diff --git a/packages/spec/src/claude/settings/settings.json b/packages/spec/src/claude/settings/settings.json index 34ca93c5..3e6aad4b 100644 --- a/packages/spec/src/claude/settings/settings.json +++ b/packages/spec/src/claude/settings/settings.json @@ -67,6 +67,15 @@ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/inspect-block.cjs\"" } ] + }, + { + "matcher": "Write", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/task-scaffold-guard.cjs\"" + } + ] } ], "PostToolUse": [ diff --git a/packages/spec/src/claude/skills/specs/SKILL.md b/packages/spec/src/claude/skills/specs/SKILL.md index ae709499..0dba5b72 100644 --- a/packages/spec/src/claude/skills/specs/SKILL.md +++ b/packages/spec/src/claude/skills/specs/SKILL.md @@ -258,9 +258,24 @@ Load: `references/scope-inquiry.md` - If Risk = **Chaotic** → exit spec workflow, redirect to `hapo:hotfix` - If Risk = **Complex** → include spike/prototype tasks in the spec - If Blast Radius = **Critical Path** → spec MUST include rollback strategy and test coverage requirements +- **Complexity smell check (by numbers)** — quick YAGNI tripwires: >8 files touched / >2 new classes-services / >12 task files → challenge or simplify; **>15 task files → split into sibling specs** (a mega-spec is slow + failure-prone, per field test). Surface tripwires in the scope summary; never silently build the mega-version. - User picks scope level: Expand / Hold / Reduce - **Skip if:** trivial task (< 20 words, 1 file, user says "just do it") +#### Execution Tier (auto-scale — set right after the 5-Dimension assessment) +Pick ONE tier from the assessment; it controls how deep the pipeline runs so small specs stay cheap. Record it in `spec.json.design_context.execution_tier`. + +| Tier | Trigger | Research (Step 5 external) | Discovery (Step 6) | Red-Team (Step 8) | Always runs | +|---|---|---|---|---|---| +| **Light** | Cynefin Clear + Blast Isolated + likely ≤2 tasks | skip (record skip rationale) | minimal | skip → Validate-only | scope_lock, EARS, **Layer 1 + Layer 2 grounding** | +| **Standard** | default (Complicated / Moderate blast / 3-4 tasks) | targeted | light | per Step 8 auto-decision | all of the above | +| **Deep** | Complex/Critical-path / security-migration / 5+ tasks | full (researchers + per-area scout) | full | Red-Team → Validate (mandatory) | all of the above | + +Rules: +- Grounding (Layer 2) + structural validator (Layer 1) + scope_lock **never skip**, any tier — they are the quality floor, not depth knobs. +- Light tier is the antidote to "small spec, full-pipeline overhead". Do NOT use Light for auth/payment/migration/schema/privacy work — those force Deep regardless of size. +- Tier only changes *research/discovery/review depth*; it never changes the Hard Output Contract or DoCT. + ### Step 4: Init - Check for duplicate slugs in `specs/` via Glob - Create directory `specs//` @@ -286,7 +301,7 @@ Load: `references/scope-inquiry.md` - External/current research must prefer official docs, standards, primary sources, or maintained upstream references. Record source links and the date/context of the finding. - Write `research.md` before final requirements. It MUST include an Evidence Summary with: codebase scout result, external research result or skip rationale, selected decision, rejected alternatives, remaining gaps, and downstream task/test implications. - If evidence exposes unresolved architecture choices, unclear acceptance criteria, or multiple viable approaches with no obvious winner, stop and route to `/hapo:brainstorm` instead of forcing a spec. -- Write requirements in **EARS** format (see `rules/ears-format.md`) +- Write requirements in **EARS** format (see `rules/ears-format.md`). Give each acceptance criterion an **explicit literal ID `R{N}.{M}`** (e.g. `- **R1.1** When ...`), NOT a bare numbered list. This activates per-criterion coverage at Layer 1 (each `R1.1` must be mapped by a task `_Requirements: 1.1_`). A bare `1. 2. 3.` list silently disables sub-criterion coverage — do not use it for non-trivial specs. - **Feasibility Check:** Cross-check each requirement against known technical constraints from `research.md`. - Each requirement gets a unique numeric ID - **Verify Quality:** Before proceeding, assert each requirement is: *Singular, Unambiguous, Testable, and has a numeric ID*. Include Non-Functional Requirements (Performance, Security, Scalability, Reliability, Accessibility). @@ -318,8 +333,11 @@ Load: `references/scope-inquiry.md` - Load `rules/phase-decision-matrix.md` before generating task files. Treat "phase" as an implementation slice/task cluster, not a `phase-XX.md` artifact. - Load `rules/task-scoring-rubric.md` for every candidate task to decide priority, split/merge, spike needs, dependencies, parallel eligibility, and evidence depth. - Load `references/ask-user-question-gates.md`; if scoring reveals unapproved scope expansion or an unresolved user-owned choice, pause before writing task files. -- Each task file follows template `templates/task.md` -- `Related Files` and test plans must inherit paths, contracts, and test targets from the codebase scout. If exact files/tests cannot be named for an enhancement, run targeted inspect before generating tasks. +- **Scaffold is mandatory — raw `Write` to a task file is blocked.** A PreToolUse hook (`task-scaffold-guard.cjs`) rejects any `Write` whose path matches `specs//tasks/task-*.md`. The only path to a task file is scaffold → Edit. Once the task list is decided, generate the stubs: + `node .claude/scripts/spec-scaffold.cjs --tasks "R0-01-slug,R1-01-slug,..." --tasks-only` + This creates each `tasks/task-R*.md` from `templates/task.md` and merges `task_files` + `task_registry` into the existing `spec.json` (pending, no overwrite of already-filled tasks). Then **Edit-fill** the `{{...}}` placeholders in each stub (`Edit`/`MultiEdit` are allowed; `Write` is not). (The guard fails open if the scaffold script is absent, and can be disabled via `"spec": { "scaffold_guard": false }` in `.claude/runtime.json`.) +- Each task file follows template `templates/task.md` (scaffold already applies it). **Leave NO `{{...}}` placeholder unfilled** — a stub with placeholders is an incomplete task. +- `Related Files` and test plans must inherit paths, contracts, and test targets from the codebase scout. If exact files/tests cannot be named for an enhancement, run targeted inspect before generating tasks. Every `Related Files` path will be grounded (Layer 2) at Step 8.5 — phantom paths hard-fail, so cite real paths. - Each task file MUST include `Completion Criteria` and `Evidence` sections detailed enough that a downstream quality gate can prove the task is truly done. Existing specs may use `Task Test Plan & Verification Evidence` or legacy `Verification & Evidence`. - Each task's `Evidence` MUST choose the right proof type for the touched surface: unit for pure logic, component/integration for UI or state wiring, E2E/UI flow for complete user workflows, visual/responsive checks for style/layout work, accessibility checks for interactive UI, smoke checks for scaffold/config, regression checks for bug fixes, and performance/security checks only when the requirement or risk calls for them. - Every task MUST preserve the approved `scope_lock`: implement all scoped acceptance criteria for its requirement, avoid out-of-scope features, and record any intentional deferral as a named later task rather than implicit omission. @@ -386,6 +404,21 @@ Each task file MUST be **self-contained and implementation-ready** — detailed **FORBIDDEN:** Task files with only vague checkboxes and no exact files, requirements, or evidence. Compact is good; vague is invalid. +#### Definition of a Complete Task (DoCT) — the quality bar +A task is "complete enough to implement without guessing" only when ALL hold. Each item is enforced by a named mechanism, not goodwill: + +| DoCT element | Enforced by | +|---|---| +| **Related Files** name exact real paths (Create/Modify/Delete) | Layer 2 grounding (`spec-ground.cjs`) — phantom path fails | +| **Contract** (API/DB/event shape) stated concretely | Layer 1 contract-drift check (``) | +| **Acceptance** measurable (no "fast/nice/safe" without a threshold) | EARS rule + reviewer judgment | +| **Evidence** uses commands that exist in the project (`package.json`) | Author + grounding spirit; never invent test commands | +| **Reachability** names a real entrypoint/caller | `Runtime reachability verification` (Layer 1 presence) + judgment | +| **Requirements mapping** present (`_Requirements: x.y_`) | Layer 1 coverage check | +| **FE fidelity** — if a visual reference (image/Figma/tokens/style guide) is provided, the task carries the concrete values (hex/font/spacing/verbatim text) + a `match ` constraint | `tasks-generation.md` Frontend Fidelity Rule + reviewer/visual check | + +A stub with unfilled `{{...}}` placeholders fails DoCT by definition. The two scripts (Layer 1 structural + Layer 2 grounding) are the floor; reviewer judgment covers the rest. + ### Step 8: Validation Review (Optional) Load: `references/review.md` + `rules/design-review.md` - Load `references/ask-user-question-gates.md` before applying validation or red-team changes. User approval is required when findings modify approved scope, requirements, canonical contracts, design decisions, or task behavior. @@ -398,14 +431,17 @@ Load: `references/review.md` + `rules/design-review.md` - **PROHIBITION:** The system MUST NOT skip Red Team because of a prior code-auditor review. Code review ≠ Spec review. - **PROHIBITION:** The system MUST NOT create `.ts`, `.js`, `.py` or any implementation files during validation. Spec-only outputs. - **Reconciliation Rule:** `validation.status = "completed"` is forbidden until all accepted findings and validation decisions are physically propagated into `requirements.md`, `design.md`, `tasks/*.md`, and `spec.json` where applicable. -- **Deterministic Gate:** Run `node .claude/scripts/validate-spec-output.cjs specs/` after all fixes and before final output. Script failure overrides any LLM checklist result and blocks `ready_for_implementation = true`. +- **Deterministic Gate (2 layers):** Run `node .claude/scripts/validate-spec-output.cjs specs/` (structural) AND `node .claude/scripts/spec-ground.cjs specs/ [--root ]` (grounding — paths exist) after all fixes and before final output. Either script failing overrides any LLM checklist result and blocks `ready_for_implementation = true`. ### Step 8.5: Finalization Audit (MANDATORY) - Re-scan the `tasks/` directory and rebuild `spec.json.task_files` + `task_registry` from the real filesystem (sorted relative paths; preserve task status when the path still matches). -- Run `node .claude/scripts/validate-spec-output.cjs specs/` and treat any non-zero exit as a blocking failure. +- **Layer 1 — Structural:** run `node .claude/scripts/validate-spec-output.cjs specs/` and treat any non-zero exit as a blocking failure. +- **Layer 2 — Grounding (MANDATORY):** run `node .claude/scripts/spec-ground.cjs specs/ [--root ]` and treat any non-zero exit as a blocking failure. This greps the real work tree to verify every `Related Files` path a task cites (Modify/Delete/Read) actually exists — or is Created by another task in the spec. Pass `--root` when the code lives apart from the spec (monorepo / sibling project). A spec that PASSES Layer 1 but FAILS Layer 2 is trolling phantom files and is NOT ready. **Validator-enforced (do not re-check by hand — a clean exit clears all of these):** task_files/task_registry synced to disk; task naming `tasks/task-R{N}-{SEQ}-.md`; no forbidden artifacts; research.md Evidence Summary present; every requirement **and sub-criterion** covered by a task; each task keeps the full template (Context, Constraints, Steps, Related Files, Completion Criteria, Evidence, Risk Assessment) plus Runtime reachability; numeric requirement IDs only; validation_recommended vs validation.status; timestamps not reused from init; ready_for_implementation blocked while any error exists. +**Grounding-enforced (spec-ground.cjs — do not re-check by hand):** every Modify/Delete/Read path in any task's `Related Files` exists in the work tree or is Created earlier in the spec. Phantom paths hard-fail. + **Judgment-only audit (validator cannot see these — assert manually):** - FAIL if a UI/app/runtime spec has multiple user-facing task outputs but no final integration/reachability task or section. - FAIL if accepted validation/red-team decisions exist in reports but are not reflected in implementation-facing sections (`Context`, `Steps`, `Requirements`, `Completion Criteria`, `Evidence`, canonical contracts, or requirements text). diff --git a/packages/spec/src/claude/skills/specs/references/review.md b/packages/spec/src/claude/skills/specs/references/review.md index 1f8a1697..6ae1419d 100644 --- a/packages/spec/src/claude/skills/specs/references/review.md +++ b/packages/spec/src/claude/skills/specs/references/review.md @@ -121,8 +121,13 @@ Rules: 3. Sort by severity: Critical → High → Medium 4. Cap at 15 findings maximum +#### Step 5.5: Evidence Filter (MANDATORY — auto-reject before merit) +Before adjudicating, drop any finding whose `Location`/`Evidence` does not point at a concrete spot in the spec: a task/section name (`task-R2-01... §Steps`) or a verbatim quote of the offending text. A finding that only asserts a problem in the abstract ("this might race", "auth could be bypassed") **without citing where in the spec** is auto-`Reject (no evidence)` and is NOT counted toward the cap or shown to the user. + +Rationale: this is the spec-level analogue of ck-plan's `file:line` evidence gate. It stops reviewers from inventing plausible-sounding flaws that aren't actually in the artifact. A real flaw can always be located; an imagined one cannot. + #### Step 6: Adjudicate -For each finding, evaluate and propose: **Accept** or **Reject** with rationale. +For each finding (that survived the Evidence Filter), evaluate and propose: **Accept** or **Reject** with rationale. #### Step 7: User Review Present via `AskUserQuestion`: diff --git a/packages/spec/src/claude/skills/specs/references/scope-inquiry.md b/packages/spec/src/claude/skills/specs/references/scope-inquiry.md index 23b5d19e..10e06768 100644 --- a/packages/spec/src/claude/skills/specs/references/scope-inquiry.md +++ b/packages/spec/src/claude/skills/specs/references/scope-inquiry.md @@ -48,6 +48,17 @@ Assess how many other parts of the system are affected if this change breaks or > **Rule**: If Blast Radius = Critical Path, the spec MUST include rollback strategy and explicit test coverage requirements in the task files. +### Complexity Smell Check (by numbers) + +After the qualitative 5-Dimension pass, run a quick quantitative gut-check. These are YAGNI tripwires, not hard limits — when one trips, pause and justify or simplify before locking scope: + +- **> 8 files touched** → challenge whether the same goal is reachable with fewer; flag accidental scope creep. +- **> 2 new classes / services / modules** → smell; justify each new abstraction (could an existing module absorb it?). +- **> 12 task files** → the spec is large; consider splitting by deliverable boundary into sibling specs (cross-spec `blockedBy`) rather than one mega-spec. +- **> 15 task files** → strong signal to split. A single mega-spec is slow to generate, prone to mid-run failure, and hard to review. Prefer 2-3 focused specs unless the work is genuinely one indivisible slice. + +If any tripwire fires, surface it in the Scope Inquiry summary and let the user decide (Expand / Hold / Reduce / Split). Do not silently build the mega-version. + ## Level Selection After completing the 5-Dimension Assessment, present via `AskUserQuestion`: @@ -77,6 +88,7 @@ Scope Inquiry: - Gap Size: [Small/Medium/Large] — [reason] - Risk Level: [Clear/Complicated/Complex/Chaotic] — [reason] - Blast Radius: [Isolated/Moderate/Critical Path] — [affected modules] +- Complexity smells: [none | >8 files | >2 new services | >12 tasks → consider split | >15 tasks → split] - Minimum Change: [Essential gap vs Niceties] -- User chose: [Expand / Hold / Reduce] +- User chose: [Expand / Hold / Reduce / Split] ``` diff --git a/packages/spec/src/claude/skills/specs/rules/tasks-generation.md b/packages/spec/src/claude/skills/specs/rules/tasks-generation.md index 9dfaba44..4e10f9a1 100644 --- a/packages/spec/src/claude/skills/specs/rules/tasks-generation.md +++ b/packages/spec/src/claude/skills/specs/rules/tasks-generation.md @@ -192,6 +192,21 @@ Choose verification by task risk and touched surface. Do not force every task to `hapo:specs` writes the expected proof into each task. `hapo:develop` executes the task-local proof before marking the task done. `hapo:test` runs the broader system pass after implementation or for a requested feature scope. +### Frontend Fidelity Rule (when a visual reference is provided) + +If the user/spec provides ANY visual reference for a frontend task — design image, Figma frame, screenshot, mockup, brand palette, design tokens, or a style guide — the task MUST reproduce it faithfully, not approximate it. Concretely, the task file MUST: + +1. **Extract concrete values into the task** (do NOT paraphrase as "make it look nice"): exact colors (`#DAF1EE`), font family + sizes, spacing/radius/shadow when the reference shows them, and verbatim UI text/labels (especially non-English copy, e.g. `「投稿を削除しました」`). +2. **State a `MUST: match ` constraint** naming the exact reference (e.g. `image_4.png` / Figma node / `tokens.css`), so fidelity is a requirement, not a suggestion. +3. **Prove fidelity in Evidence**: a visual/runtime check that compares the rendered UI against the named reference (screenshot diff or side-by-side inspection), plus accessibility/contrast if interactive. + +Reuse vs new component: +- **Reuse** an existing component → reference it by path; it carries the design system's style (no need to restate pixels). +- **New** component (`Create`) WITH a reference → the task MUST cite the concrete tokens/values from the reference (this is the gap that lets new components drift from the design). +- **New** component WITHOUT any reference → derive from a named sibling component or explicitly flag it as an open design question; never invent un-grounded styling silently. + +> Rationale: a spec that says "build the list screen" but omits the provided palette/text/spacing forces the implementer to guess, and the result drifts from what the user actually showed. When a reference exists, "looks roughly similar" is a failure — the task must carry the real values so the build matches the design. + ## Task Hierarchy Rules ### Maximum 2 Levels diff --git a/packages/spec/src/claude/skills/specs/templates/requirements.md b/packages/spec/src/claude/skills/specs/templates/requirements.md index 2c131614..18eb08bd 100644 --- a/packages/spec/src/claude/skills/specs/templates/requirements.md +++ b/packages/spec/src/claude/skills/specs/templates/requirements.md @@ -7,23 +7,24 @@ ### Requirement 1: {{REQUIREMENT_AREA_1}} + **Objective:** As a {{ROLE}}, I want {{CAPABILITY}}, so that {{BENEFIT}} #### Acceptance Criteria -1. When [event], the [system] shall [response/action] -2. If [trigger], then the [system] shall [response/action] -3. While [precondition], the [system] shall [response/action] -4. Where [feature is included], the [system] shall [response/action] -5. The [system] shall [response/action] +- **R1.1** When [event], the [system] shall [response/action] +- **R1.2** If [trigger], then the [system] shall [response/action] +- **R1.3** While [precondition], the [system] shall [response/action] +- **R1.4** Where [feature is included], the [system] shall [response/action] +- **R1.5** The [system] shall [response/action] ### Requirement 2: {{REQUIREMENT_AREA_2}} **Objective:** As a {{ROLE}}, I want {{CAPABILITY}}, so that {{BENEFIT}} #### Acceptance Criteria -1. When [event], the [system] shall [response/action] -2. When [event] and [condition], the [system] shall [response/action] +- **R2.1** When [event], the [system] shall [response/action] +- **R2.2** When [event] and [condition], the [system] shall [response/action] - + ## Non-Functional Requirements @@ -33,19 +34,19 @@ **Objective:** As a system owner, I want predictable performance characteristics, so that the feature remains usable under expected load. #### Acceptance Criteria -{{NEXT_REQ_NUMBER}}.1 The [system] shall [measurable performance metric, e.g. "respond within 500ms"] -{{NEXT_REQ_NUMBER}}.2 The [system] shall [measurable scale metric, e.g. "support 100 concurrent users"] +- **R{{NEXT_REQ_NUMBER}}.1** The [system] shall [measurable performance metric, e.g. "respond within 500ms"] +- **R{{NEXT_REQ_NUMBER}}.2** The [system] shall [measurable scale metric, e.g. "support 100 concurrent users"] ### Requirement {{NEXT_REQ_NUMBER_PLUS_ONE}}: Security & Privacy **Objective:** As a security/compliance stakeholder, I want the feature to protect sensitive data and enforce access boundaries, so that the system is safe to ship. #### Acceptance Criteria -{{NEXT_REQ_NUMBER_PLUS_ONE}}.1 The [system] shall [measurable security behavior, e.g. "encrypt data at rest using AES-256"] -{{NEXT_REQ_NUMBER_PLUS_ONE}}.2 If [unauthorized or invalid condition], the [system] shall [deny or recover with explicit behavior] +- **R{{NEXT_REQ_NUMBER_PLUS_ONE}}.1** The [system] shall [measurable security behavior, e.g. "encrypt data at rest using AES-256"] +- **R{{NEXT_REQ_NUMBER_PLUS_ONE}}.2** If [unauthorized or invalid condition], the [system] shall [deny or recover with explicit behavior] ### Requirement {{NEXT_REQ_NUMBER_PLUS_TWO}}: Reliability & Availability **Objective:** As an operator, I want predictable failure handling, so that incidents remain diagnosable and recoverable. #### Acceptance Criteria -{{NEXT_REQ_NUMBER_PLUS_TWO}}.1 If [failure condition], the [system] shall [recovery behavior] -{{NEXT_REQ_NUMBER_PLUS_TWO}}.2 The [system] shall [durability / retry / fallback expectation] +- **R{{NEXT_REQ_NUMBER_PLUS_TWO}}.1** If [failure condition], the [system] shall [recovery behavior] +- **R{{NEXT_REQ_NUMBER_PLUS_TWO}}.2** The [system] shall [durability / retry / fallback expectation] diff --git a/packages/spec/src/claude/skills/specs/templates/spec-state.json b/packages/spec/src/claude/skills/specs/templates/spec-state.json index 5e6a11a5..d778e0c9 100644 --- a/packages/spec/src/claude/skills/specs/templates/spec-state.json +++ b/packages/spec/src/claude/skills/specs/templates/spec-state.json @@ -60,6 +60,7 @@ "design_context": { "discovery_mode": null, "discovery_reason": null, + "execution_tier": "standard", "validation_recommended": false }, "validation": { diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 00000000..33bf9a37 --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1,8 @@ +# Git Ignore + +# CafeKit / Ecosystem +specs/_shared/ +plans/ +!plans/templates/ +.cafekit-backup/ +.cafekit.lock diff --git a/sample/CLAUDE.md b/sample/CLAUDE.md new file mode 100644 index 00000000..3d82a165 --- /dev/null +++ b/sample/CLAUDE.md @@ -0,0 +1,131 @@ +# CLAUDE.md + +Primary operating instructions for Claude Code or any AI agent using this CafeKit runtime. + +## Core Objective + +Act as the project orchestrator: understand the request, keep scope tight, use the right skills/agents, and deliver verified work that follows the project's architecture. + +## Core Behavior + +These rules reduce common agent coding failures: hidden assumptions, overbuilt solutions, unrelated edits, and unverified completion claims. They take priority over speed-oriented shortcuts. + +### 1. Think Before Coding + +- Do not assume silently. State assumptions when they affect the work. +- If multiple interpretations are plausible, surface them before implementation. +- If the simpler option is likely better, say so and push back. +- If the user asks a question about the project, use `/question` to answer from source evidence before planning. +- Before feature planning or coding, read `./README.md` for project context. + +### 2. Simplicity First + +- Solve the requested problem with the smallest maintainable change. +- Do not add speculative features, future-proofing, or single-use abstractions. +- Reuse existing modules before creating new ones. +- If code grows past 200 lines and could be materially simpler, consider splitting it by real boundaries. +- Prefer YAGNI, KISS, and DRY in that order. + +### 3. Surgical Changes + +- Touch only files required by the task. +- Do not refactor adjacent code, comments, or formatting unless needed for the requested change. +- Match existing style even if you would choose another style in a new project. +- Remove only dead code/imports created by your own change. +- Mention unrelated issues instead of fixing them opportunistically. + +### 4. Goal-Driven Execution + +- Convert requests into verifiable success criteria. +- For spec tasks, use `Completion Criteria` and `Evidence` as the source of truth. Existing task files may use `Task Test Plan & Verification Evidence` or legacy `Verification & Evidence`. +- For bugs, reproduce with a failing test or concrete evidence when feasible before fixing. +- Loop until verification passes or a real blocker is recorded. + +## CafeKit Operating Loop + +Use this loop for non-trivial work: + +1. **Understand** — read README, relevant docs, active spec/task, and existing code. +2. **Plan** — choose the smallest coherent path; use `/question` for evidence-backed project questions and `/specs` for feature specs when ready. +3. **Execute** — implement only the active task/scope; no placeholder completion. +4. **Verify** — run exact task commands first, then repo-level lint/test/build as needed. +5. **Sync** — mark task state only after proof exists. + +## Operating Discipline + +- If a CafeKit skill may apply, read and use that skill before acting. Do not improvise around an available skill workflow. +- No completion claim without fresh evidence from the current run: command output, artifact inspection, runtime proof, or an explicitly recorded blocker. +- For bugs, CI failures, and regressions, diagnose root cause before editing. Symptom patches are not completion. +- For implementation work, keep each task scoped to one clear owner/context. Reviewers should receive task files, diffs, and acceptance criteria, not chat history. +- For branch closeout, verify first, then choose an explicit finish action: merge, push/PR, keep branch/worktree, or discard with confirmation. +- If workflow tools such as `Agent` (legacy `Task`), `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet`, `AskUserQuestion`, `SendMessage`, or `TodoWrite` are unavailable in the current runtime, do not fail the workflow. Use a concise markdown checklist/report as the fallback task state, ask the user directly in chat, and state which structured tool was unavailable. + +## Definition Of Done + +A task is done only when all apply: + +- implementation satisfies `Completion Criteria` +- `Evidence` is satisfied with concrete proof +- preflight/build/test outcomes are passing or an explicit blocker is recorded +- code review has no critical issues +- a verification receipt exists before task state is synced to `done` + +`NO_TESTS` and `0 tests + exit 0` are not passing outcomes when the task requires automated tests. + +## Non-Negotiable Gates + +- Never bypass hooks. A hook block is an instruction boundary, not an obstacle. +- Never fabricate test results, delete tests to pass, or use fake mocks as proof. +- Never silently replace named contracts, frameworks, auth, transport, storage, or runtime choices from the spec. +- Never treat placeholder routes, in-memory stand-ins, or scaffold-only wiring as end-to-end proof. +- Never claim complete from stale evidence, memory, or a previous run. +- Never modify real `.env` secrets unless explicitly requested; update `.env.example` when env vars change. +- Never commit secrets. AI attribution is optional only when the user or project asks for it. + +## Rule References + +Consult these when the task touches the relevant area: + +- Primary workflow: `./.claude/rules/workflow.md` +- Development rules: `./.claude/rules/ai-dev-rules.md` +- Skill workflow routing: `./.claude/rules/skill-workflow-routing.md` +- Skill domain routing: `./.claude/rules/skill-domain-routing.md` +- Subagent coordination: `./.claude/rules/orchestrator.md` +- Docs maintenance: `./.claude/rules/manage-docs.md` +- State sync: `./.claude/rules/state-sync.md` +- Hook handling: `./.claude/rules/hook-protocols.md` +- Other protocols: `./.claude/rules/*` + +## Skill And Script Use + +- **IMPORTANT:** Analyze the skills catalog and activate the skills that are needed for the task during the process. +- **IMPORTANT:** DO NOT modify skills in `~/.claude/skills` directly. **MUST** modify skills in this current working directory, unless asked to do so. +- Use `./.claude/rules/skill-workflow-routing.md` and `./.claude/rules/skill-domain-routing.md` as advisory routing when choosing a skill. +- Run Python skill scripts with the skill venv: + - macOS/Linux: `.claude/skills/.venv/bin/python3 scripts/