@haposoft/cafekit 0.13.1 → 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.
|
|
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
|
+
}
|
|
@@ -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
|
|
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). 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
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`.
|