@haposoft/cafekit 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
5
5
  "author": "Haposoft <nghialt@haposoft.com>",
6
6
  "license": "MIT",
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2026 Haposoft. MIT License.
4
+ *
5
+ * PreToolUse Hook — task-scaffold-guard.cjs
6
+ * Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
7
+ *
8
+ * Forces spec task files to be GENERATED via spec-scaffold.cjs instead of
9
+ * hand-written. Field tests showed the model dodges the (opt-in) scaffold step
10
+ * and raw-`Write`s every task file — the single biggest output-token cost of a
11
+ * spec run. This hook blocks `Write` to `specs/<feature>/tasks/task-*.md`, so
12
+ * the only path to a task file is: scaffold (creates the stub) -> Edit (fills it).
13
+ *
14
+ * Scope is deliberately narrow — it only ever blocks the `Write` tool on a task
15
+ * file path. `Edit`/`MultiEdit` (filling a stub), and `Write` to any other file
16
+ * (requirements.md, design.md, source code) are never touched. The scaffold
17
+ * script itself writes via Node fs through a Bash call, not the `Write` tool, so
18
+ * it is never blocked.
19
+ *
20
+ * Safety valves:
21
+ * 1. fail-open when spec-scaffold.cjs is absent — you cannot force a tool that
22
+ * is not installed, so a hook shipped without its script must not deadlock.
23
+ * 2. actionable block message — prints the exact scaffold command to run.
24
+ * 3. escape hatch — "spec": { "scaffold_guard": false } in .claude/runtime.json.
25
+ *
26
+ * Exit: 0 = allow, 2 = block.
27
+ */
28
+
29
+ try {
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const stdin = fs.readFileSync(0, 'utf8').trim();
34
+ if (!stdin) process.exit(0);
35
+
36
+ const data = JSON.parse(stdin);
37
+ const toolName = data.tool_name || '';
38
+ const toolInput = data.tool_input || {};
39
+ const cwd = data.cwd || process.cwd();
40
+
41
+ // Only `Write` can create a file from scratch. Edit/MultiEdit require an
42
+ // existing file, so filling a scaffolded stub is always allowed.
43
+ if (toolName !== 'Write') process.exit(0);
44
+
45
+ const filePath = toolInput.file_path || toolInput.path || '';
46
+ if (!filePath) process.exit(0);
47
+
48
+ // Match a spec task file: .../specs/<feature>/tasks/task-<...>.md
49
+ const norm = filePath.replace(/\\/g, '/');
50
+ const isTaskFile = /(^|\/)specs\/[^/]+\/tasks\/task-[^/]+\.md$/.test(norm);
51
+ if (!isTaskFile) process.exit(0);
52
+
53
+ // Valve 3: explicit escape hatch via .claude/runtime.json (fail-closed: a
54
+ // missing/broken runtime.json keeps the guard ON).
55
+ let runtime = {};
56
+ try {
57
+ const rp = path.join(cwd, '.claude', 'runtime.json');
58
+ if (fs.existsSync(rp)) runtime = JSON.parse(fs.readFileSync(rp, 'utf8'));
59
+ } catch { /* malformed runtime.json -> keep guard on */ }
60
+ if (runtime.spec && runtime.spec.scaffold_guard === false) process.exit(0);
61
+
62
+ // Valve 1: fail-open if the scaffold script is not installed next to this hook
63
+ // (hook lives in .claude/hooks/, script in .claude/scripts/). Forcing a tool
64
+ // that does not exist would deadlock task creation.
65
+ const scaffold = path.join(__dirname, '..', 'scripts', 'spec-scaffold.cjs');
66
+ if (!fs.existsSync(scaffold)) process.exit(0);
67
+
68
+ // Valve 2: block with an actionable message carrying the exact command.
69
+ const m = norm.match(/(^|\/)specs\/([^/]+)\/tasks\//);
70
+ const feature = m ? m[2] : '<feature>';
71
+ console.log(
72
+ `TASK SCAFFOLD REQUIRED: task files must be generated, not hand-written.\n` +
73
+ `Blocked Write: ${filePath}\n\n` +
74
+ `Generate the stub(s), then Edit-fill the {{...}} placeholders:\n` +
75
+ ` node .claude/scripts/spec-scaffold.cjs ${feature} --tasks "R0-01-slug,R1-01-slug,..." --tasks-only\n` +
76
+ `Then use Edit (not Write) on each tasks/task-*.md stub.\n` +
77
+ `Override: set "spec": { "scaffold_guard": false } in .claude/runtime.json`
78
+ );
79
+ process.exit(2);
80
+
81
+ } catch (e) {
82
+ // Never let a hook crash block the user — log and fail-open.
83
+ try {
84
+ const fs = require('fs'), p = require('path');
85
+ const d = p.join(__dirname, '.logs');
86
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
87
+ fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
88
+ JSON.stringify({ ts: new Date().toISOString(), hook: 'task-scaffold-guard', status: 'crash', error: e.message }) + '\n');
89
+ } catch (_) {}
90
+ process.exit(0);
91
+ }
@@ -90,6 +90,7 @@
90
90
  "hooks/usage.cjs",
91
91
  "hooks/privacy-block.cjs",
92
92
  "hooks/inspect-block.cjs",
93
+ "hooks/task-scaffold-guard.cjs",
93
94
  "hooks/rules.cjs",
94
95
  "hooks/spec-state.cjs",
95
96
  "hooks/state.cjs",
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CafeKit spec GROUNDING checker (Specs v2, Layer 2).
4
+ *
5
+ * The deterministic validator (`validate-spec-output.cjs`) checks spec SHAPE
6
+ * (headings, registry sync, coverage). It is blind to whether the file paths a
7
+ * task cites actually EXIST. This script closes that gap: it parses every task's
8
+ * `Related Files` table and verifies, against the real work tree, that each
9
+ * Modify/Delete/Read path exists — unless another task in the same spec Creates
10
+ * it first (intra-spec resolution).
11
+ *
12
+ * Why active-grep instead of opt-in: field tests showed opt-in checks get
13
+ * skipped by the author. Grounding runs unconditionally over whatever paths the
14
+ * tasks already declare, so it cannot be dodged by choosing a different format.
15
+ *
16
+ * Usage:
17
+ * node spec-ground.cjs <specDir> [--root <work-context>]
18
+ *
19
+ * <specDir> : path to specs/<feature> (relative or absolute)
20
+ * --root : work tree the paths resolve against. Default: two levels up from
21
+ * specDir (…/<root>/specs/<feature> -> <root>). Override for
22
+ * monorepos where the spec lives apart from the code.
23
+ *
24
+ * Exit: 0 = GROUNDED, 1 = FAIL (missing path), 2 = usage error.
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ function usage() {
31
+ console.error('Usage: node spec-ground.cjs <specDir> [--root <work-context>]');
32
+ }
33
+
34
+ function parseArgs(argv) {
35
+ const args = { specDir: null, root: null };
36
+ for (let i = 2; i < argv.length; i++) {
37
+ if (argv[i] === '--root') args.root = argv[++i];
38
+ else if (!args.specDir) args.specDir = argv[i];
39
+ }
40
+ return args;
41
+ }
42
+
43
+ function listTaskFiles(specDir) {
44
+ const tasksDir = path.join(specDir, 'tasks');
45
+ if (!fs.existsSync(tasksDir)) return [];
46
+ return fs
47
+ .readdirSync(tasksDir, { withFileTypes: true })
48
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
49
+ .map((e) => path.join(tasksDir, e.name))
50
+ .sort();
51
+ }
52
+
53
+ /**
54
+ * Pull rows out of a markdown `## Related Files` table. Returns
55
+ * [{ path, action, taskFile }]. Tables look like:
56
+ * | Path | Action | Description |
57
+ * | `front/x.vue` | Modify | ... |
58
+ */
59
+ function parseRelatedFiles(content, taskFile) {
60
+ const rows = [];
61
+ const lines = content.split('\n');
62
+ let inSection = false;
63
+ for (const line of lines) {
64
+ if (/^##+\s+Related Files\s*$/i.test(line)) { inSection = true; continue; }
65
+ if (inSection && /^##+\s+/.test(line)) break; // next heading ends section
66
+ if (!inSection) continue;
67
+ if (!line.trim().startsWith('|')) continue;
68
+
69
+ const cells = line.split('|').map((c) => c.trim());
70
+ // cells[0] is '' (leading pipe). path=cells[1], action=cells[2].
71
+ const rawPath = (cells[1] || '').replace(/`/g, '').trim();
72
+ const action = (cells[2] || '').toLowerCase().trim();
73
+ if (!rawPath || rawPath === 'path') continue; // header row
74
+ if (/^-+$/.test(rawPath)) continue; // separator row
75
+ if (!action || !/(create|modify|delete|read)/.test(action)) continue;
76
+ rows.push({ path: rawPath, action, taskFile: path.basename(taskFile) });
77
+ }
78
+ return rows;
79
+ }
80
+
81
+ /** Lenient existence check with minimal glob handling. */
82
+ function pathExists(root, p) {
83
+ // Glob-ish path (e.g. `assets/{x,instagram,facebook}.svg` or `dir/*.ts`):
84
+ // verify the parent directory exists rather than the literal name.
85
+ if (/[{*]/.test(p)) {
86
+ const parent = path.dirname(p.replace(/\{.*$/, '').replace(/\*.*$/, ''));
87
+ return fs.existsSync(path.join(root, parent));
88
+ }
89
+ return fs.existsSync(path.join(root, p));
90
+ }
91
+
92
+ function main() {
93
+ const { specDir: specInput, root: rootInput } = parseArgs(process.argv);
94
+ if (!specInput) { usage(); process.exit(2); }
95
+
96
+ const specDir = path.resolve(process.cwd(), specInput);
97
+ if (!fs.existsSync(specDir)) {
98
+ console.error(`FAIL ${specInput}\n- spec directory does not exist`);
99
+ process.exit(1);
100
+ }
101
+ // Default work-context: two levels up (…/<root>/specs/<feature>).
102
+ const root = rootInput
103
+ ? path.resolve(process.cwd(), rootInput)
104
+ : path.resolve(specDir, '..', '..');
105
+
106
+ const taskFiles = listTaskFiles(specDir);
107
+ const allRows = [];
108
+ for (const tf of taskFiles) {
109
+ allRows.push(...parseRelatedFiles(fs.readFileSync(tf, 'utf8'), tf));
110
+ }
111
+
112
+ // Paths that will exist because a task Creates them (intra-spec resolution).
113
+ const createdPaths = new Set(
114
+ allRows.filter((r) => r.action.includes('create')).map((r) => r.path),
115
+ );
116
+
117
+ const errors = [];
118
+ const warnings = [];
119
+ let checked = 0;
120
+
121
+ for (const row of allRows) {
122
+ const onDisk = pathExists(root, row.path);
123
+ if (row.action.includes('create')) {
124
+ if (onDisk) warnings.push(`${row.taskFile}: Create path already exists (will overwrite): ${row.path}`);
125
+ continue;
126
+ }
127
+ // modify / delete / read must exist OR be created by another task in-spec.
128
+ checked++;
129
+ if (!onDisk && !createdPaths.has(row.path)) {
130
+ errors.push(`${row.taskFile}: ${row.action} path not found in work tree: ${row.path}`);
131
+ }
132
+ }
133
+
134
+ for (const w of warnings) console.warn(`[WARN] ${w}`);
135
+
136
+ const rel = path.relative(process.cwd(), specDir) || specDir;
137
+ if (errors.length > 0) {
138
+ console.error(`FAIL ${rel} (root: ${root})`);
139
+ for (const e of errors) console.error(`- ${e}`);
140
+ console.error(`\n${errors.length} missing / ${checked} checked path(s).`);
141
+ process.exit(1);
142
+ }
143
+ console.log(`GROUNDED ${rel} (${checked} path(s) verified against ${root})`);
144
+ }
145
+
146
+ main();
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CafeKit spec SCAFFOLD generator (Specs v2 — output-cost reducer).
4
+ *
5
+ * Field tests showed the dominant cost of a spec run is OUTPUT tokens: the
6
+ * model hand-Writes spec.json + every task file from scratch (a 16-task spec
7
+ * emitted ~935K output tokens). This script does the mechanical scaffolding so
8
+ * the model only has to Edit-fill content, not Write whole files.
9
+ *
10
+ * It creates spec.json (with task_files + task_registry pre-populated) and one
11
+ * stub per task from the canonical templates, leaving the `{{...}}` placeholders
12
+ * for the model to fill. It NEVER overwrites an existing spec directory.
13
+ *
14
+ * Usage:
15
+ * node spec-scaffold.cjs <feature> --tasks "R0-01-slug,R1-01-slug,..." \
16
+ * [--lang en] [--title "..."] [--specs-root specs]
17
+ *
18
+ * Exit: 0 = scaffolded, 2 = usage/precondition error.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const TEMPLATES = path.join(__dirname, '..', 'skills', 'specs', 'templates');
25
+ const TASK_ID_RE = /^R(\d+)-(\d+)-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
26
+
27
+ function usage() {
28
+ console.error('Usage: node spec-scaffold.cjs <feature> --tasks "R0-01-slug,R1-01-slug" [--lang en] [--title "..."] [--specs-root specs]');
29
+ }
30
+
31
+ function parseArgs(argv) {
32
+ const a = { feature: null, tasks: null, lang: 'en', title: null, specsRoot: 'specs', tasksOnly: false };
33
+ for (let i = 2; i < argv.length; i++) {
34
+ const v = argv[i];
35
+ if (v === '--tasks') a.tasks = argv[++i];
36
+ else if (v === '--lang') a.lang = argv[++i];
37
+ else if (v === '--title') a.title = argv[++i];
38
+ else if (v === '--specs-root') a.specsRoot = argv[++i];
39
+ else if (v === '--tasks-only') a.tasksOnly = true;
40
+ else if (!a.feature) a.feature = v;
41
+ }
42
+ return a;
43
+ }
44
+
45
+ function readTemplate(name) {
46
+ const p = path.join(TEMPLATES, name);
47
+ if (!fs.existsSync(p)) {
48
+ console.error(`precondition: template not found: ${p}`);
49
+ process.exit(2);
50
+ }
51
+ return fs.readFileSync(p, 'utf8');
52
+ }
53
+
54
+ function titleFromSlug(slug) {
55
+ const s = slug.replace(/-/g, ' ');
56
+ return s.charAt(0).toUpperCase() + s.slice(1);
57
+ }
58
+
59
+ function registryEntry(t) {
60
+ return {
61
+ id: `R${t.req}-${t.seq}`,
62
+ title: titleFromSlug(t.slug),
63
+ status: 'pending',
64
+ dependencies: [],
65
+ blocker: null,
66
+ started_at: null,
67
+ completed_at: null,
68
+ last_updated_at: null,
69
+ };
70
+ }
71
+
72
+ function fillTask(taskTpl, t, feature) {
73
+ return taskTpl
74
+ .replace(/\{\{REQ_NUMBER\}\}/g, t.req)
75
+ .replace(/\{\{SEQ\}\}/g, t.seq)
76
+ .replace(/\{\{TITLE\}\}/g, titleFromSlug(t.slug))
77
+ .replace(/\{\{FEATURE_NAME\}\}/g, feature)
78
+ .replace(/\{\{PRIORITY\}\}/g, 'P2')
79
+ .replace(/\{\{EFFORT\}\}/g, 'TBD')
80
+ .replace(/\{\{DEPENDENCIES\}\}/g, 'none');
81
+ }
82
+
83
+ function nowIso() {
84
+ // Local ISO with offset, e.g. 2026-06-21T12:30:00+07:00
85
+ const d = new Date();
86
+ const pad = (n) => String(n).padStart(2, '0');
87
+ const off = -d.getTimezoneOffset();
88
+ const sign = off >= 0 ? '+' : '-';
89
+ const oh = pad(Math.floor(Math.abs(off) / 60));
90
+ const om = pad(Math.abs(off) % 60);
91
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${sign}${oh}:${om}`;
92
+ }
93
+
94
+ function main() {
95
+ const args = parseArgs(process.argv);
96
+ if (!args.feature || !args.tasks) { usage(); process.exit(2); }
97
+
98
+ const ids = args.tasks.split(',').map((s) => s.trim()).filter(Boolean);
99
+ const parsed = [];
100
+ for (const id of ids) {
101
+ const m = id.match(TASK_ID_RE);
102
+ if (!m) {
103
+ console.error(`precondition: invalid task id "${id}" (want R{N}-{SEQ}-<slug>, SEQ 2 digits)`);
104
+ process.exit(2);
105
+ }
106
+ parsed.push({ raw: id, req: m[1], seq: m[2], slug: m[3], file: `tasks/task-${id}.md` });
107
+ }
108
+
109
+ const specDir = path.resolve(process.cwd(), args.specsRoot, args.feature);
110
+ const taskTpl = readTemplate('task.md');
111
+ const ts = nowIso();
112
+ const created = [];
113
+
114
+ // --- --tasks-only: add task stubs + merge registry into an existing spec ---
115
+ if (args.tasksOnly) {
116
+ const specPath = path.join(specDir, 'spec.json');
117
+ if (!fs.existsSync(specPath)) {
118
+ console.error(`precondition: --tasks-only needs an existing spec.json at ${specPath}`);
119
+ process.exit(2);
120
+ }
121
+ const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
122
+ spec.task_files = spec.task_files || [];
123
+ spec.task_registry = spec.task_registry || {};
124
+ fs.mkdirSync(path.join(specDir, 'tasks'), { recursive: true });
125
+ for (const t of parsed) {
126
+ const abs = path.join(specDir, t.file);
127
+ if (fs.existsSync(abs)) continue; // never overwrite an already-filled task
128
+ fs.writeFileSync(abs, fillTask(taskTpl, t, args.feature));
129
+ if (!spec.task_files.includes(t.file)) spec.task_files.push(t.file);
130
+ spec.task_registry[t.file] = registryEntry(t);
131
+ created.push(t.file);
132
+ }
133
+ spec.task_files.sort();
134
+ spec.updated_at = ts;
135
+ fs.writeFileSync(specPath, JSON.stringify(spec, null, 2) + '\n');
136
+ const relTo = path.relative(process.cwd(), specDir) || specDir;
137
+ console.log(`SCAFFOLDED (tasks-only) ${relTo}`);
138
+ console.log(`- ${created.length} new task stub(s); task_files + task_registry merged.`);
139
+ console.log(`- NEXT: Edit-fill {{...}} in each stub; set dependencies; run validator + spec-ground.`);
140
+ return;
141
+ }
142
+
143
+ if (fs.existsSync(specDir)) {
144
+ console.error(`precondition: spec dir already exists, refusing to overwrite: ${specDir}`);
145
+ process.exit(2);
146
+ }
147
+
148
+ fs.mkdirSync(path.join(specDir, 'tasks'), { recursive: true });
149
+ fs.mkdirSync(path.join(specDir, 'reports'), { recursive: true });
150
+
151
+ // --- spec.json (from spec-state.json template) ---
152
+ const spec = JSON.parse(readTemplate('spec-state.json'));
153
+ spec.feature_name = args.feature;
154
+ spec.created_at = ts;
155
+ spec.updated_at = ts;
156
+ spec.language = args.lang;
157
+ spec.timestamps = spec.timestamps || {};
158
+ spec.timestamps.init = ts;
159
+ spec.scope_lock = spec.scope_lock || {};
160
+ spec.scope_lock.source = args.title || `{{PROJECT_DESCRIPTION}}`;
161
+ spec.task_files = parsed.map((t) => t.file);
162
+ spec.task_registry = {};
163
+ for (const t of parsed) spec.task_registry[t.file] = registryEntry(t);
164
+ fs.writeFileSync(path.join(specDir, 'spec.json'), JSON.stringify(spec, null, 2) + '\n');
165
+ created.push('spec.json');
166
+
167
+ // --- doc templates (placeholders left for the model to fill) ---
168
+ for (const [tpl, out] of [['requirements.md', 'requirements.md'], ['research.md', 'research.md'], ['design.md', 'design.md']]) {
169
+ const body = readTemplate(tpl).replace(/\{\{FEATURE_NAME\}\}/g, args.feature);
170
+ fs.writeFileSync(path.join(specDir, out), body);
171
+ created.push(out);
172
+ }
173
+
174
+ // --- task stubs (fill the cheap placeholders; leave the rest) ---
175
+ for (const t of parsed) {
176
+ fs.writeFileSync(path.join(specDir, t.file), fillTask(taskTpl, t, args.feature));
177
+ created.push(t.file);
178
+ }
179
+
180
+ const rel = path.relative(process.cwd(), specDir) || specDir;
181
+ console.log(`SCAFFOLDED ${rel}`);
182
+ console.log(`- ${created.length} files created: spec.json + 3 docs + ${parsed.length} task stub(s)`);
183
+ console.log(`- task_files + task_registry pre-populated (${parsed.length} entries, all pending)`);
184
+ console.log(`- NEXT: Edit-fill the {{...}} placeholders (do NOT leave any); set dependencies in task_registry; run validator + spec-ground before ready.`);
185
+ }
186
+
187
+ main();
@@ -67,6 +67,15 @@
67
67
  "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/inspect-block.cjs\""
68
68
  }
69
69
  ]
70
+ },
71
+ {
72
+ "matcher": "Write",
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/task-scaffold-guard.cjs\""
77
+ }
78
+ ]
70
79
  }
71
80
  ],
72
81
  "PostToolUse": [
@@ -258,9 +258,24 @@ Load: `references/scope-inquiry.md`
258
258
  - If Risk = **Chaotic** → exit spec workflow, redirect to `hapo:hotfix`
259
259
  - If Risk = **Complex** → include spike/prototype tasks in the spec
260
260
  - If Blast Radius = **Critical Path** → spec MUST include rollback strategy and test coverage requirements
261
+ - **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.
261
262
  - User picks scope level: Expand / Hold / Reduce
262
263
  - **Skip if:** trivial task (< 20 words, 1 file, user says "just do it")
263
264
 
265
+ #### Execution Tier (auto-scale — set right after the 5-Dimension assessment)
266
+ 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`.
267
+
268
+ | Tier | Trigger | Research (Step 5 external) | Discovery (Step 6) | Red-Team (Step 8) | Always runs |
269
+ |---|---|---|---|---|---|
270
+ | **Light** | Cynefin Clear + Blast Isolated + likely ≤2 tasks | skip (record skip rationale) | minimal | skip → Validate-only | scope_lock, EARS, **Layer 1 + Layer 2 grounding** |
271
+ | **Standard** | default (Complicated / Moderate blast / 3-4 tasks) | targeted | light | per Step 8 auto-decision | all of the above |
272
+ | **Deep** | Complex/Critical-path / security-migration / 5+ tasks | full (researchers + per-area scout) | full | Red-Team → Validate (mandatory) | all of the above |
273
+
274
+ Rules:
275
+ - Grounding (Layer 2) + structural validator (Layer 1) + scope_lock **never skip**, any tier — they are the quality floor, not depth knobs.
276
+ - 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.
277
+ - Tier only changes *research/discovery/review depth*; it never changes the Hard Output Contract or DoCT.
278
+
264
279
  ### Step 4: Init
265
280
  - Check for duplicate slugs in `specs/` via Glob
266
281
  - Create directory `specs/<feature-name>/`
@@ -286,7 +301,7 @@ Load: `references/scope-inquiry.md`
286
301
  - External/current research must prefer official docs, standards, primary sources, or maintained upstream references. Record source links and the date/context of the finding.
287
302
  - 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.
288
303
  - 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.
289
- - Write requirements in **EARS** format (see `rules/ears-format.md`)
304
+ - 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.
290
305
  - **Feasibility Check:** Cross-check each requirement against known technical constraints from `research.md`.
291
306
  - Each requirement gets a unique numeric ID
292
307
  - **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`
318
333
  - Load `rules/phase-decision-matrix.md` before generating task files. Treat "phase" as an implementation slice/task cluster, not a `phase-XX.md` artifact.
319
334
  - Load `rules/task-scoring-rubric.md` for every candidate task to decide priority, split/merge, spike needs, dependencies, parallel eligibility, and evidence depth.
320
335
  - Load `references/ask-user-question-gates.md`; if scoring reveals unapproved scope expansion or an unresolved user-owned choice, pause before writing task files.
321
- - Each task file follows template `templates/task.md`
322
- - `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.
336
+ - **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/<feature>/tasks/task-*.md`. The only path to a task file is scaffold → Edit. Once the task list is decided, generate the stubs:
337
+ `node .claude/scripts/spec-scaffold.cjs <feature> --tasks "R0-01-slug,R1-01-slug,..." --tasks-only`
338
+ 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). Hand-writing whole task files is the single biggest output-token cost — scaffold + fill cuts it. (The guard fails open if the scaffold script is absent, and can be disabled via `"spec": { "scaffold_guard": false }` in `.claude/runtime.json`.)
339
+ - Each task file follows template `templates/task.md` (scaffold already applies it). **Leave NO `{{...}}` placeholder unfilled** — a stub with placeholders is an incomplete task.
340
+ - `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.
323
341
  - 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`.
324
342
  - 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.
325
343
  - 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
386
404
 
387
405
  **FORBIDDEN:** Task files with only vague checkboxes and no exact files, requirements, or evidence. Compact is good; vague is invalid.
388
406
 
407
+ #### Definition of a Complete Task (DoCT) — the quality bar
408
+ A task is "complete enough to implement without guessing" only when ALL hold. Each item is enforced by a named mechanism, not goodwill:
409
+
410
+ | DoCT element | Enforced by |
411
+ |---|---|
412
+ | **Related Files** name exact real paths (Create/Modify/Delete) | Layer 2 grounding (`spec-ground.cjs`) — phantom path fails |
413
+ | **Contract** (API/DB/event shape) stated concretely | Layer 1 contract-drift check (`<!-- contract:NAME -->`) |
414
+ | **Acceptance** measurable (no "fast/nice/safe" without a threshold) | EARS rule + reviewer judgment |
415
+ | **Evidence** uses commands that exist in the project (`package.json`) | Author + grounding spirit; never invent test commands |
416
+ | **Reachability** names a real entrypoint/caller | `Runtime reachability verification` (Layer 1 presence) + judgment |
417
+ | **Requirements mapping** present (`_Requirements: x.y_`) | Layer 1 coverage check |
418
+ | **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 <reference>` constraint | `tasks-generation.md` Frontend Fidelity Rule + reviewer/visual check |
419
+
420
+ 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.
421
+
389
422
  ### Step 8: Validation Review (Optional)
390
423
  Load: `references/review.md` + `rules/design-review.md`
391
424
  - 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`
398
431
  - **PROHIBITION:** The system MUST NOT skip Red Team because of a prior code-auditor review. Code review ≠ Spec review.
399
432
  - **PROHIBITION:** The system MUST NOT create `.ts`, `.js`, `.py` or any implementation files during validation. Spec-only outputs.
400
433
  - **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.
401
- - **Deterministic Gate:** Run `node .claude/scripts/validate-spec-output.cjs specs/<feature>` after all fixes and before final output. Script failure overrides any LLM checklist result and blocks `ready_for_implementation = true`.
434
+ - **Deterministic Gate (2 layers):** Run `node .claude/scripts/validate-spec-output.cjs specs/<feature>` (structural) AND `node .claude/scripts/spec-ground.cjs specs/<feature> [--root <work-context>]` (grounding — paths exist) after all fixes and before final output. Either script failing overrides any LLM checklist result and blocks `ready_for_implementation = true`.
402
435
 
403
436
  ### Step 8.5: Finalization Audit (MANDATORY)
404
437
  - 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).
405
- - Run `node .claude/scripts/validate-spec-output.cjs specs/<feature>` and treat any non-zero exit as a blocking failure.
438
+ - **Layer 1 — Structural:** run `node .claude/scripts/validate-spec-output.cjs specs/<feature>` and treat any non-zero exit as a blocking failure.
439
+ - **Layer 2 — Grounding (MANDATORY):** run `node .claude/scripts/spec-ground.cjs specs/<feature> [--root <work-context>]` 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.
406
440
 
407
441
  **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}-<slug>.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.
408
442
 
443
+ **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.
444
+
409
445
  **Judgment-only audit (validator cannot see these — assert manually):**
410
446
  - FAIL if a UI/app/runtime spec has multiple user-facing task outputs but no final integration/reachability task or section.
411
447
  - 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).
@@ -121,8 +121,13 @@ Rules:
121
121
  3. Sort by severity: Critical → High → Medium
122
122
  4. Cap at 15 findings maximum
123
123
 
124
+ #### Step 5.5: Evidence Filter (MANDATORY — auto-reject before merit)
125
+ 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.
126
+
127
+ 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.
128
+
124
129
  #### Step 6: Adjudicate
125
- For each finding, evaluate and propose: **Accept** or **Reject** with rationale.
130
+ For each finding (that survived the Evidence Filter), evaluate and propose: **Accept** or **Reject** with rationale.
126
131
 
127
132
  #### Step 7: User Review
128
133
  Present via `AskUserQuestion`:
@@ -48,6 +48,17 @@ Assess how many other parts of the system are affected if this change breaks or
48
48
 
49
49
  > **Rule**: If Blast Radius = Critical Path, the spec MUST include rollback strategy and explicit test coverage requirements in the task files.
50
50
 
51
+ ### Complexity Smell Check (by numbers)
52
+
53
+ 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:
54
+
55
+ - **> 8 files touched** → challenge whether the same goal is reachable with fewer; flag accidental scope creep.
56
+ - **> 2 new classes / services / modules** → smell; justify each new abstraction (could an existing module absorb it?).
57
+ - **> 12 task files** → the spec is large; consider splitting by deliverable boundary into sibling specs (cross-spec `blockedBy`) rather than one mega-spec.
58
+ - **> 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.
59
+
60
+ 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.
61
+
51
62
  ## Level Selection
52
63
 
53
64
  After completing the 5-Dimension Assessment, present via `AskUserQuestion`:
@@ -77,6 +88,7 @@ Scope Inquiry:
77
88
  - Gap Size: [Small/Medium/Large] — [reason]
78
89
  - Risk Level: [Clear/Complicated/Complex/Chaotic] — [reason]
79
90
  - Blast Radius: [Isolated/Moderate/Critical Path] — [affected modules]
91
+ - Complexity smells: [none | >8 files | >2 new services | >12 tasks → consider split | >15 tasks → split]
80
92
  - Minimum Change: [Essential gap vs Niceties]
81
- - User chose: [Expand / Hold / Reduce]
93
+ - User chose: [Expand / Hold / Reduce / Split]
82
94
  ```
@@ -192,6 +192,21 @@ Choose verification by task risk and touched surface. Do not force every task to
192
192
 
193
193
  `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.
194
194
 
195
+ ### Frontend Fidelity Rule (when a visual reference is provided)
196
+
197
+ 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:
198
+
199
+ 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. `「投稿を削除しました」`).
200
+ 2. **State a `MUST: match <reference>` constraint** naming the exact reference (e.g. `image_4.png` / Figma node / `tokens.css`), so fidelity is a requirement, not a suggestion.
201
+ 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.
202
+
203
+ Reuse vs new component:
204
+ - **Reuse** an existing component → reference it by path; it carries the design system's style (no need to restate pixels).
205
+ - **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).
206
+ - **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.
207
+
208
+ > 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.
209
+
195
210
  ## Task Hierarchy Rules
196
211
 
197
212
  ### Maximum 2 Levels
@@ -7,23 +7,24 @@
7
7
 
8
8
  ### Requirement 1: {{REQUIREMENT_AREA_1}}
9
9
  <!-- Requirement headings MUST include a leading numeric ID only (for example: "Requirement 1: ...", "1. Overview", "2 Feature: ..."). Alphabetic IDs like "Requirement A" are not allowed. -->
10
+ <!-- Acceptance criteria use EXPLICIT literal IDs `R{N}.{M}` (R1.1, R1.2, ...), NOT a bare numbered list. This lets the validator verify per-criterion coverage (each R1.1 must be mapped by a task `_Requirements: 1.1_`). Do not switch to a bare `1. 2. 3.` list — that silently disables sub-criterion coverage. -->
10
11
  **Objective:** As a {{ROLE}}, I want {{CAPABILITY}}, so that {{BENEFIT}}
11
12
 
12
13
  #### Acceptance Criteria
13
- 1. When [event], the [system] shall [response/action]
14
- 2. If [trigger], then the [system] shall [response/action]
15
- 3. While [precondition], the [system] shall [response/action]
16
- 4. Where [feature is included], the [system] shall [response/action]
17
- 5. The [system] shall [response/action]
14
+ - **R1.1** When [event], the [system] shall [response/action]
15
+ - **R1.2** If [trigger], then the [system] shall [response/action]
16
+ - **R1.3** While [precondition], the [system] shall [response/action]
17
+ - **R1.4** Where [feature is included], the [system] shall [response/action]
18
+ - **R1.5** The [system] shall [response/action]
18
19
 
19
20
  ### Requirement 2: {{REQUIREMENT_AREA_2}}
20
21
  **Objective:** As a {{ROLE}}, I want {{CAPABILITY}}, so that {{BENEFIT}}
21
22
 
22
23
  #### Acceptance Criteria
23
- 1. When [event], the [system] shall [response/action]
24
- 2. When [event] and [condition], the [system] shall [response/action]
24
+ - **R2.1** When [event], the [system] shall [response/action]
25
+ - **R2.2** When [event] and [condition], the [system] shall [response/action]
25
26
 
26
- <!-- Additional requirements follow the same pattern -->
27
+ <!-- Additional requirements follow the same pattern, with literal R{N}.{M} IDs -->
27
28
 
28
29
  ## Non-Functional Requirements
29
30
 
@@ -33,19 +34,19 @@
33
34
  **Objective:** As a system owner, I want predictable performance characteristics, so that the feature remains usable under expected load.
34
35
 
35
36
  #### Acceptance Criteria
36
- {{NEXT_REQ_NUMBER}}.1 The [system] shall [measurable performance metric, e.g. "respond within 500ms"]
37
- {{NEXT_REQ_NUMBER}}.2 The [system] shall [measurable scale metric, e.g. "support 100 concurrent users"]
37
+ - **R{{NEXT_REQ_NUMBER}}.1** The [system] shall [measurable performance metric, e.g. "respond within 500ms"]
38
+ - **R{{NEXT_REQ_NUMBER}}.2** The [system] shall [measurable scale metric, e.g. "support 100 concurrent users"]
38
39
 
39
40
  ### Requirement {{NEXT_REQ_NUMBER_PLUS_ONE}}: Security & Privacy
40
41
  **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.
41
42
 
42
43
  #### Acceptance Criteria
43
- {{NEXT_REQ_NUMBER_PLUS_ONE}}.1 The [system] shall [measurable security behavior, e.g. "encrypt data at rest using AES-256"]
44
- {{NEXT_REQ_NUMBER_PLUS_ONE}}.2 If [unauthorized or invalid condition], the [system] shall [deny or recover with explicit behavior]
44
+ - **R{{NEXT_REQ_NUMBER_PLUS_ONE}}.1** The [system] shall [measurable security behavior, e.g. "encrypt data at rest using AES-256"]
45
+ - **R{{NEXT_REQ_NUMBER_PLUS_ONE}}.2** If [unauthorized or invalid condition], the [system] shall [deny or recover with explicit behavior]
45
46
 
46
47
  ### Requirement {{NEXT_REQ_NUMBER_PLUS_TWO}}: Reliability & Availability
47
48
  **Objective:** As an operator, I want predictable failure handling, so that incidents remain diagnosable and recoverable.
48
49
 
49
50
  #### Acceptance Criteria
50
- {{NEXT_REQ_NUMBER_PLUS_TWO}}.1 If [failure condition], the [system] shall [recovery behavior]
51
- {{NEXT_REQ_NUMBER_PLUS_TWO}}.2 The [system] shall [durability / retry / fallback expectation]
51
+ - **R{{NEXT_REQ_NUMBER_PLUS_TWO}}.1** If [failure condition], the [system] shall [recovery behavior]
52
+ - **R{{NEXT_REQ_NUMBER_PLUS_TWO}}.2** The [system] shall [durability / retry / fallback expectation]
@@ -60,6 +60,7 @@
60
60
  "design_context": {
61
61
  "discovery_mode": null,
62
62
  "discovery_reason": null,
63
+ "execution_tier": "standard",
63
64
  "validation_recommended": false
64
65
  },
65
66
  "validation": {