@haposoft/cafekit 0.13.1 → 0.13.3

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.1",
3
+ "version": "0.13.3",
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",
@@ -132,6 +132,28 @@ function validateTaskSections(taskPath, content, errors) {
132
132
  }
133
133
  }
134
134
 
135
+ /**
136
+ * Task files are created from the scaffold template (the scaffold-guard hook
137
+ * forces creation through it), so every task starts as a stub full of `{{...}}`
138
+ * placeholders. The hook guarantees the stub is CREATED via scaffold, but
139
+ * nothing guaranteed the model FILLED it. An unfilled `{{...}}` is an
140
+ * incomplete task — SKILL.md: "Leave NO {{...}} placeholder ... fails DoCT" —
141
+ * so it is a hard error here. A `.../` path fragment is a not-yet-resolved path
142
+ * placeholder; it is only a warning, because it usually survives in prose Steps
143
+ * while the Related Files table (which spec-ground.cjs does verify) is already
144
+ * concrete. Matching `\.\.\.\/` (three dots + slash) avoids flagging a relative
145
+ * `../` path or a prose ellipsis.
146
+ */
147
+ function validateTaskPlaceholders(taskPath, content, errors, warnings) {
148
+ const stub = content.match(/\{\{[^}\n]+\}\}/);
149
+ if (stub) {
150
+ errors.push(`${taskPath}: unfilled scaffold placeholder ${stub[0]} — task stub was not completed`);
151
+ }
152
+ if (/\.\.\.\//.test(content)) {
153
+ warnings.push(`${taskPath}: contains a '.../' path placeholder — replace with a concrete path`);
154
+ }
155
+ }
156
+
135
157
  /**
136
158
  * Each phase completion must carry its own timestamp. Reusing `timestamps.init`
137
159
  * for a later phase is forbidden (SKILL.md spec.json Update Rules). This used to
@@ -344,6 +366,7 @@ function validateSpec(specDir) {
344
366
  const fullPath = path.join(specDir, taskFile);
345
367
  const content = fs.readFileSync(fullPath, 'utf8');
346
368
  validateTaskSections(taskFile, content, errors);
369
+ validateTaskPlaceholders(taskFile, content, errors, warnings);
347
370
 
348
371
  const idRe = /\b((?:REQ-\d+)|(?:R\d+))\b/gi;
349
372
  let match;
@@ -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": [
@@ -333,9 +333,9 @@ Rules:
333
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.
334
334
  - Load `rules/task-scoring-rubric.md` for every candidate task to decide priority, split/merge, spike needs, dependencies, parallel eligibility, and evidence depth.
335
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.
336
- - **Scaffold instead of hand-writing (output-cost reducer).** Once the task list is decided, generate the stubs with the CLI rather than `Write`-ing each file by hand:
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
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. Hand-writing whole task files is the single biggest output-token cost — scaffold + fill cuts it.
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). (The guard fails open if the scaffold script is absent, and can be disabled via `"spec": { "scaffold_guard": false }` in `.claude/runtime.json`.)
339
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
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.
341
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`.