@haposoft/cafekit 0.7.24 → 0.7.26
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 +1 -1
- package/src/claude/agents/code-auditor.md +8 -3
- package/src/claude/agents/docs-keeper.md +12 -1
- package/src/claude/agents/god-developer.md +3 -3
- package/src/claude/agents/spec-maker.md +14 -7
- package/src/claude/agents/test-runner.md +14 -0
- package/src/claude/hooks/privacy-block.cjs +133 -80
- package/src/claude/hooks/spec-state.cjs +21 -2
- package/src/claude/hooks/state.cjs +164 -122
- package/src/claude/rules/manage-docs.md +6 -5
- package/src/claude/rules/state-sync.md +7 -6
- package/src/claude/settings/settings.json +10 -1
- package/src/claude/skills/develop/SKILL.md +51 -9
- package/src/claude/skills/develop/references/quality-gate.md +13 -4
- package/src/claude/skills/specs/SKILL.md +33 -6
- package/src/claude/skills/specs/references/review.md +32 -3
- package/src/claude/skills/specs/references/task-hydration.md +18 -16
- package/src/claude/skills/specs/rules/tasks-generation.md +4 -0
- package/src/claude/skills/specs/templates/design.md +2 -1
- package/src/claude/skills/specs/templates/init.json +3 -1
- package/src/claude/skills/sync/SKILL.md +13 -10
- package/src/claude/skills/sync/references/sync-protocols.md +39 -13
- package/src/claude/skills/test/SKILL.md +2 -2
- package/src/claude/skills/test/references/execution-strategy.md +2 -3
|
@@ -3,32 +3,30 @@
|
|
|
3
3
|
* Copyright (c) 2026 Haposoft. MIT License.
|
|
4
4
|
*
|
|
5
5
|
* Multi-event Hook — state.cjs
|
|
6
|
-
* Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
7
6
|
*
|
|
8
7
|
* Persists and restores session progress across Claude Code sessions.
|
|
9
8
|
*
|
|
10
9
|
* Events:
|
|
11
10
|
* SessionStart → load previous state and print to context
|
|
12
|
-
*
|
|
11
|
+
* PostToolUse → refresh state after Task/TaskCreate/TaskUpdate/TodoWrite
|
|
12
|
+
* Stop → persist full session state and archive
|
|
13
13
|
* SubagentStop → append agent completion note to current state
|
|
14
14
|
*
|
|
15
15
|
* Storage: .claude/session-state/latest.md (+ archive/)
|
|
16
|
-
* Safety: atomic writes, 7-day expiry, max 5 archives
|
|
17
|
-
*
|
|
18
16
|
* Exit: 0 always (fail-open)
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
try {
|
|
22
|
-
const fs
|
|
23
|
-
const path
|
|
24
|
-
const os
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
25
23
|
const crypto = require('crypto');
|
|
26
24
|
const { execSync } = require('child_process');
|
|
25
|
+
const { parseTranscript } = require('./lib/parser.cjs');
|
|
27
26
|
|
|
28
|
-
const EXPIRY_DAYS
|
|
27
|
+
const EXPIRY_DAYS = 7;
|
|
29
28
|
const MAX_ARCHIVES = 5;
|
|
30
|
-
|
|
31
|
-
// ── Storage ───────────────────────────────────────────────────────────────
|
|
29
|
+
const TRACKED_POST_TOOL_EVENTS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
|
|
32
30
|
|
|
33
31
|
function stateDir(cwd) {
|
|
34
32
|
try {
|
|
@@ -37,56 +35,73 @@ try {
|
|
|
37
35
|
if (!fs.existsSync(local)) fs.mkdirSync(local, { recursive: true });
|
|
38
36
|
return local;
|
|
39
37
|
}
|
|
40
|
-
|
|
38
|
+
|
|
39
|
+
const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
41
40
|
const global = path.join(os.homedir(), '.claude', 'session-states', hash);
|
|
42
41
|
if (!fs.existsSync(global)) fs.mkdirSync(global, { recursive: true });
|
|
43
42
|
return global;
|
|
44
|
-
} catch {
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function loadLatest(cwd) {
|
|
48
49
|
try {
|
|
49
|
-
const dir
|
|
50
|
+
const dir = stateDir(cwd);
|
|
50
51
|
if (!dir) return null;
|
|
51
|
-
|
|
52
|
+
|
|
53
|
+
const file = path.join(dir, 'latest.md');
|
|
52
54
|
if (!fs.existsSync(file)) return null;
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
55
|
+
|
|
56
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
57
|
+
const match = text.match(/<!-- Generated: (.+?) -->/);
|
|
58
|
+
if (match) {
|
|
59
|
+
const generatedAt = new Date(match[1]).getTime();
|
|
60
|
+
if (Number.isNaN(generatedAt)) return null;
|
|
61
|
+
if (Date.now() - generatedAt > EXPIRY_DAYS * 24 * 60 * 60 * 1000) return null;
|
|
59
62
|
}
|
|
63
|
+
|
|
60
64
|
return text;
|
|
61
|
-
} catch {
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
function writeAtomic(filePath, content) {
|
|
65
|
-
const
|
|
66
|
-
fs.writeFileSync(
|
|
67
|
-
fs.renameSync(
|
|
71
|
+
const tempFile = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
72
|
+
fs.writeFileSync(tempFile, content);
|
|
73
|
+
fs.renameSync(tempFile, filePath);
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
function archive(dir) {
|
|
71
77
|
try {
|
|
72
|
-
const
|
|
73
|
-
if (!fs.existsSync(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const
|
|
78
|
+
const latestFile = path.join(dir, 'latest.md');
|
|
79
|
+
if (!fs.existsSync(latestFile)) return;
|
|
80
|
+
|
|
81
|
+
const archiveDir = path.join(dir, 'archive');
|
|
82
|
+
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir);
|
|
83
|
+
|
|
84
|
+
const now = new Date();
|
|
85
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
86
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
87
|
+
|
|
88
|
+
fs.copyFileSync(latestFile, path.join(archiveDir, `${stamp}.md`));
|
|
89
|
+
|
|
90
|
+
const files = fs.readdirSync(archiveDir).filter((file) => file.endsWith('.md')).sort();
|
|
81
91
|
while (files.length > MAX_ARCHIVES) {
|
|
82
|
-
|
|
92
|
+
const oldest = files.shift();
|
|
93
|
+
try {
|
|
94
|
+
fs.unlinkSync(path.join(archiveDir, oldest));
|
|
95
|
+
} catch {
|
|
96
|
+
// fail-open
|
|
97
|
+
}
|
|
83
98
|
}
|
|
84
|
-
} catch {
|
|
99
|
+
} catch {
|
|
100
|
+
// fail-open
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
function extractSessionData(stdinData) {
|
|
104
|
+
async function extractSessionData(stdinData) {
|
|
90
105
|
const data = {
|
|
91
106
|
timestamp: new Date().toISOString(),
|
|
92
107
|
branch: process.env.GIT_BRANCH || '',
|
|
@@ -96,132 +111,159 @@ try {
|
|
|
96
111
|
|
|
97
112
|
if (stdinData.transcript_path && fs.existsSync(stdinData.transcript_path)) {
|
|
98
113
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const blocks = entry.message?.content;
|
|
105
|
-
if (!Array.isArray(blocks)) continue;
|
|
106
|
-
for (const b of blocks) {
|
|
107
|
-
if (b.type === 'tool_use' && b.name === 'TodoWrite' && Array.isArray(b.input?.todos)) {
|
|
108
|
-
latest.length = 0;
|
|
109
|
-
latest.push(...b.input.todos);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch { /* skip bad lines */ }
|
|
113
|
-
}
|
|
114
|
-
data.todos = latest;
|
|
115
|
-
} catch { /* ignore */ }
|
|
114
|
+
const transcript = await parseTranscript(stdinData.transcript_path);
|
|
115
|
+
data.todos = transcript.todos;
|
|
116
|
+
} catch {
|
|
117
|
+
// fail-open
|
|
118
|
+
}
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
try {
|
|
119
|
-
const
|
|
120
|
-
encoding: 'utf8',
|
|
122
|
+
const diff = execSync('git diff --name-only HEAD', {
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
timeout: 3000,
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
121
126
|
}).trim();
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
|
|
128
|
+
if (diff) {
|
|
129
|
+
data.modifiedFiles = diff.split('\n').slice(0, 20);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// fail-open
|
|
133
|
+
}
|
|
124
134
|
|
|
125
135
|
return data;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
// ── Markdown builder ──────────────────────────────────────────────────────
|
|
129
|
-
|
|
130
138
|
function buildStateContent(data) {
|
|
131
|
-
const done
|
|
132
|
-
const pending = data.todos.filter(
|
|
139
|
+
const done = data.todos.filter((todo) => todo.status === 'completed' || todo.status === 'done');
|
|
140
|
+
const pending = data.todos.filter((todo) => !['completed', 'done'].includes(todo.status));
|
|
141
|
+
|
|
133
142
|
return [
|
|
134
143
|
'# Session State',
|
|
135
144
|
`<!-- Generated: ${data.timestamp} -->`,
|
|
136
145
|
`<!-- Branch: ${data.branch || 'unknown'} -->`,
|
|
137
146
|
'',
|
|
138
147
|
'## What Worked (Verified)',
|
|
139
|
-
...(done.length
|
|
148
|
+
...(done.length ? done.map((todo) => `- ${todo.content}`) : ['- (No completed tasks recorded)']),
|
|
140
149
|
'',
|
|
141
150
|
"## What's Left",
|
|
142
|
-
...(pending.length ? pending.map(
|
|
151
|
+
...(pending.length ? pending.map((todo) => `- [ ] ${todo.content}`) : ['- (All tasks completed)']),
|
|
143
152
|
'',
|
|
144
153
|
'## Key Files Modified',
|
|
145
|
-
...(data.modifiedFiles.length
|
|
154
|
+
...(data.modifiedFiles.length ? data.modifiedFiles.map((file) => `- ${file}`) : ['- (No file changes detected)']),
|
|
146
155
|
''
|
|
147
156
|
].join('\n');
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
function buildAgentSection(data) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
return `\n## Agent Result: ${
|
|
160
|
+
const agentType = data.agent_type || 'unknown';
|
|
161
|
+
const time = new Date().toISOString().slice(11, 19);
|
|
162
|
+
return `\n## Agent Result: ${agentType} (${time})\n- Completed at ${time}\n`;
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
|
|
165
|
+
function mergeAgentSections(existing, content) {
|
|
166
|
+
if (!existing) return content;
|
|
167
|
+
|
|
168
|
+
const agentSections = existing.match(/## Agent Result:.+?(?=\n## |$)/gs);
|
|
169
|
+
if (!agentSections) return content;
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
const marker = '\n## Key Files Modified';
|
|
172
|
+
if (content.includes(marker)) {
|
|
173
|
+
return content.replace(marker, `\n${agentSections.join('\n')}${marker}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return `${content.trimEnd()}\n\n${agentSections.join('\n')}\n`;
|
|
177
|
+
}
|
|
160
178
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const cwd = data.cwd || process.cwd();
|
|
164
|
-
const dir = stateDir(cwd);
|
|
179
|
+
function appendAgentSection(existing, agentSection) {
|
|
180
|
+
if (!existing) return agentSection.trimStart();
|
|
165
181
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (prev) {
|
|
170
|
-
console.log('\n=== Prior Execution Context ===');
|
|
171
|
-
console.log(prev.trim());
|
|
172
|
-
console.log('=== End of Prior Context ===\n');
|
|
182
|
+
const marker = '\n## Key Files Modified';
|
|
183
|
+
if (existing.includes(marker)) {
|
|
184
|
+
return existing.replace(marker, `\n${agentSection}${marker}`);
|
|
173
185
|
}
|
|
174
|
-
|
|
186
|
+
|
|
187
|
+
return `${existing.trimEnd()}\n${agentSection}`;
|
|
175
188
|
}
|
|
176
189
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const file = path.join(dir, 'latest.md');
|
|
180
|
-
const agentSection = buildAgentSection(data);
|
|
190
|
+
async function persistSnapshot(dir, data, options = {}) {
|
|
191
|
+
const file = path.join(dir, 'latest.md');
|
|
181
192
|
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (updated === existing) updated = existing.trimEnd() + '\n' + agentSection;
|
|
186
|
-
} else {
|
|
187
|
-
updated = buildStateContent(extractSessionData(data)) + '\n' + agentSection;
|
|
188
|
-
}
|
|
189
|
-
writeAtomic(file, updated);
|
|
190
|
-
process.exit(0);
|
|
193
|
+
const content = mergeAgentSections(existing, buildStateContent(data));
|
|
194
|
+
writeAtomic(file, content);
|
|
195
|
+
if (options.archive) archive(dir);
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
198
|
+
async function main() {
|
|
199
|
+
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
200
|
+
if (!stdin) process.exit(0);
|
|
201
|
+
|
|
202
|
+
const data = JSON.parse(stdin);
|
|
203
|
+
const event = data.hook_event_name || '';
|
|
204
|
+
const cwd = data.cwd || process.cwd();
|
|
205
|
+
const dir = stateDir(cwd);
|
|
206
|
+
|
|
207
|
+
if (event === 'SessionStart') {
|
|
208
|
+
const previous = loadLatest(cwd);
|
|
209
|
+
if (previous) {
|
|
210
|
+
console.log('\n=== Prior Execution Context ===');
|
|
211
|
+
console.log(previous.trim());
|
|
212
|
+
console.log('=== End of Prior Context ===\n');
|
|
208
213
|
}
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!dir) process.exit(0);
|
|
218
|
+
|
|
219
|
+
if (event === 'PostToolUse') {
|
|
220
|
+
const toolName = data.tool_name || '';
|
|
221
|
+
if (TRACKED_POST_TOOL_EVENTS.has(toolName)) {
|
|
222
|
+
const sessionData = await extractSessionData(data);
|
|
223
|
+
await persistSnapshot(dir, sessionData);
|
|
224
|
+
}
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (event === 'SubagentStop') {
|
|
229
|
+
const file = path.join(dir, 'latest.md');
|
|
230
|
+
const agentSection = buildAgentSection(data);
|
|
231
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
232
|
+
const updated = existing
|
|
233
|
+
? appendAgentSection(existing, agentSection)
|
|
234
|
+
: `${buildStateContent(await extractSessionData(data))}\n${agentSection}`;
|
|
235
|
+
|
|
236
|
+
writeAtomic(file, updated);
|
|
237
|
+
process.exit(0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (event === 'Stop') {
|
|
241
|
+
const sessionData = await extractSessionData(data);
|
|
242
|
+
await persistSnapshot(dir, sessionData, { archive: true });
|
|
243
|
+
process.exit(0);
|
|
209
244
|
}
|
|
210
245
|
|
|
211
|
-
writeAtomic(file, content);
|
|
212
|
-
archive(dir);
|
|
213
246
|
process.exit(0);
|
|
214
247
|
}
|
|
215
248
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
249
|
+
main().catch(() => {
|
|
250
|
+
process.exit(0);
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
219
253
|
try {
|
|
220
|
-
const fs = require('fs')
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
fs.
|
|
224
|
-
|
|
254
|
+
const fs = require('fs');
|
|
255
|
+
const path = require('path');
|
|
256
|
+
const logDir = path.join(__dirname, '.logs');
|
|
257
|
+
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
258
|
+
fs.appendFileSync(
|
|
259
|
+
path.join(logDir, 'hook-log.jsonl'),
|
|
260
|
+
JSON.stringify({
|
|
261
|
+
ts: new Date().toISOString(),
|
|
262
|
+
hook: 'state',
|
|
263
|
+
status: 'crash',
|
|
264
|
+
error: error.message
|
|
265
|
+
}) + '\n'
|
|
266
|
+
);
|
|
225
267
|
} catch (_) {}
|
|
226
268
|
process.exit(0);
|
|
227
269
|
}
|
|
@@ -22,6 +22,7 @@ The project maintains these core documents in `./docs`:
|
|
|
22
22
|
The `hapo:docs-keeper` agent is responsible for keeping these documents current. Trigger an update whenever:
|
|
23
23
|
|
|
24
24
|
- A development phase transitions (e.g., "In Progress" → "Complete")
|
|
25
|
+
- A verified task completion changes user-facing behavior, architecture, API contracts, operational flow, or project status enough that docs should be refreshed
|
|
25
26
|
- A significant feature ships or a critical bug is resolved
|
|
26
27
|
- Security patches are applied or dependencies change
|
|
27
28
|
- Project scope or timeline shifts
|
|
@@ -67,8 +68,8 @@ specs/
|
|
|
67
68
|
├── spec.json # System state machine & global status
|
|
68
69
|
├── design.md # Architecture, requirements, and data flows
|
|
69
70
|
└── tasks/
|
|
70
|
-
├── task-01-setup.md
|
|
71
|
-
└── task-
|
|
71
|
+
├── task-R0-01-setup.md # Actionable granular steps for development
|
|
72
|
+
└── task-R1-01-api.md # Next requirement-driven task
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
### The State Machine (`spec.json`)
|
|
@@ -81,11 +82,11 @@ This blueprint covers:
|
|
|
81
82
|
- **Data Flow:** Mandatory Mermaid Data Flow Diagram detailing state transitions, DB interactions, and API payloads.
|
|
82
83
|
- **Risk Assessment:** Pre-identified failure points and mitigations.
|
|
83
84
|
|
|
84
|
-
### Execution Checklists (`tasks/task-
|
|
85
|
-
Work is decomposed into
|
|
85
|
+
### Execution Checklists (`tasks/task-R*.md`)
|
|
86
|
+
Work is decomposed into requirement-driven markdown task files.
|
|
86
87
|
Each task file contains:
|
|
87
88
|
- **Prerequisites:** Blockers that must clear before this stage begins. (Task N+1 cannot start without Task N defining its payload).
|
|
88
89
|
- **Execution Checklist:** Granular `[ ]` markdown items for agents to toggle `[x]` as they implement code.
|
|
89
90
|
- **Success Criteria:** Strict definition of "Done".
|
|
90
91
|
|
|
91
|
-
Comply with the overarching rules in `./rules/ai-dev-rules.md`.
|
|
92
|
+
Comply with the overarching rules in `./rules/ai-dev-rules.md`.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Single Source of Truth
|
|
4
4
|
|
|
5
5
|
In any Spec-driven workflow (`hapo:specs`), the state of the project is physically persisted in **two layers**:
|
|
6
|
-
1. **Machine Layer (`spec.json`)**: Tracks phase, status, and
|
|
6
|
+
1. **Machine Layer (`spec.json`)**: Tracks phase, status, overall completion, and per-task machine state via `task_registry`.
|
|
7
7
|
2. **Human Layer (`tasks/task-*.md`)**: Checkboxes indicating granular execution progress.
|
|
8
8
|
|
|
9
9
|
## The Sync-back Rule (Mandatory)
|
|
@@ -12,15 +12,16 @@ Whenever an agent finishes a task or blocks due to an issue, it **MUST NOT** sim
|
|
|
12
12
|
Before returning control to the user or orchestrator, the agent **MUST**:
|
|
13
13
|
|
|
14
14
|
### On Success:
|
|
15
|
-
1. Update `spec.json`: Modify `current_phase` if moving forward, ensure `status` accurately reflects progress,
|
|
16
|
-
2. Edit `task-
|
|
15
|
+
1. Update `spec.json`: Modify `current_phase` if moving forward, ensure `status` accurately reflects progress, keep `task_files` synchronized with the real files on disk, and update the corresponding `task_registry` entry (`status`, `blocker`, `started_at`, `completed_at`, `last_updated_at`).
|
|
16
|
+
2. Edit `task-R*.md`: Change `Status` only after real verification has passed (build/test/runtime/artifact). Then check `[x]` the sub-task boxes and relevant completion criteria.
|
|
17
17
|
3. Call `TaskUpdate` if Claude Tasks are active, setting the status to "completed" only after the physical files were updated.
|
|
18
18
|
|
|
19
19
|
### On Block/Failure (>3 retries):
|
|
20
20
|
1. Update `spec.json`: Set `"status": "blocked"` and fill out the `"blocker"` string with the root cause.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
21
|
+
2. Update the corresponding `task_registry` entry to `blocked`, persist the blocker reason, and stamp `last_updated_at`.
|
|
22
|
+
3. Edit `task-R*.md`: Change `Status: pending` (or `in_progress`) to `Status: blocked` with a note.
|
|
23
|
+
4. Alert the orchestrator or user via `AskUserQuestion` or explicit warning.
|
|
23
24
|
|
|
24
25
|
**Canonical state values:** New specs MUST use `status: "in_progress"` for active work. Legacy `in-progress` may be read for compatibility, but must not be emitted in new files.
|
|
25
26
|
|
|
26
|
-
**Golden Rule:** If the current phase changes, or a task completes, the agent must update the physical files. Never mark a task completed before there is execution proof. The context is intentionally NOT persisted in the chat to save tokens. An injected Hook (`spec-state.cjs`) constantly enforces and validates this state.
|
|
27
|
+
**Golden Rule:** If the current phase changes, or a task completes, the agent must update the physical files. Never mark a task completed before there is execution proof, and never let `task_registry` disagree with the matching markdown task file. The context is intentionally NOT persisted in the chat to save tokens. An injected Hook (`spec-state.cjs`) constantly enforces and validates this state.
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
],
|
|
57
57
|
"PreToolUse": [
|
|
58
58
|
{
|
|
59
|
-
"matcher": "Read|Write|Edit|MultiEdit|Bash|Glob",
|
|
59
|
+
"matcher": "Read|Write|Edit|MultiEdit|Bash|Glob|Grep",
|
|
60
60
|
"hooks": [
|
|
61
61
|
{
|
|
62
62
|
"type": "command",
|
|
@@ -70,6 +70,15 @@
|
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
72
|
"PostToolUse": [
|
|
73
|
+
{
|
|
74
|
+
"matcher": "Task|TaskCreate|TaskUpdate|TodoWrite",
|
|
75
|
+
"hooks": [
|
|
76
|
+
{
|
|
77
|
+
"type": "command",
|
|
78
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/state.cjs\""
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
73
82
|
{
|
|
74
83
|
"matcher": "Edit|Write|MultiEdit",
|
|
75
84
|
"hooks": [
|
|
@@ -4,9 +4,9 @@ description: "Code execution engine: Reads specs and implements code end-to-end
|
|
|
4
4
|
argument-hint: "[feature-name|specs-directory-path]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Develop — Feature Implementation (
|
|
7
|
+
# Develop — Feature Implementation (Task-Orchestrated Build)
|
|
8
8
|
|
|
9
|
-
Reads the
|
|
9
|
+
Reads the project specification (`hapo:specs`) and implements code through a disciplined task loop. In specific-task mode it behaves like a surgical executor. In full-spec mode it behaves like a sequential orchestrator, processing one unblocked task at a time and syncing state after every verified task.
|
|
10
10
|
|
|
11
11
|
**Principles:** YAGNI, KISS, DRY | Continuous execution | Smart self-healing
|
|
12
12
|
|
|
@@ -18,6 +18,26 @@ Reads the full project specification (`hapo:specs`) and relentlessly implements
|
|
|
18
18
|
/hapo:develop <feature name> <specific-task-file.md>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Execution Modes
|
|
22
|
+
|
|
23
|
+
### 1. Specific-Task Mode
|
|
24
|
+
Triggered by `/hapo:develop <feature> <task-file>`.
|
|
25
|
+
|
|
26
|
+
- Load exactly one task file.
|
|
27
|
+
- Implement only that task packet.
|
|
28
|
+
- STOP immediately after the task is verified and synchronized.
|
|
29
|
+
- Never auto-chain into the next task.
|
|
30
|
+
|
|
31
|
+
### 2. Full-Spec Mode
|
|
32
|
+
Triggered by `/hapo:develop <feature>` or `/hapo:develop specs/<feature>`.
|
|
33
|
+
|
|
34
|
+
- Build a queue from `spec.json.task_registry`.
|
|
35
|
+
- Select the next `pending` + unblocked task only.
|
|
36
|
+
- Run the full implementation cycle for that single task.
|
|
37
|
+
- Sync state.
|
|
38
|
+
- Recompute the queue and continue.
|
|
39
|
+
- STOP the overall run on the first blocked task, unresolved gate failure, or missing proof.
|
|
40
|
+
|
|
21
41
|
<HARD-GATE>
|
|
22
42
|
DO NOT write implementation code until an approved spec exists.
|
|
23
43
|
- If the directory `specs/<feature-name>` DOES NOT EXIST or `spec.json` is not ready, automatically trigger `/hapo:specs <feature-name>` first to create the specification. Do not improvise.
|
|
@@ -52,17 +72,20 @@ flowchart TD
|
|
|
52
72
|
### Step 1: Initialize & Load Spec
|
|
53
73
|
- Identify input: Open `specs/<feature-name>/spec.json`.
|
|
54
74
|
- Check `ready_for_implementation` status. If not ready, notify user.
|
|
75
|
+
- Load `task_registry` and verify it matches the requested task file(s). If registry is missing or stale, route to `/hapo:sync audit <feature>` before coding.
|
|
55
76
|
- **Task Scoping (CRITICAL):**
|
|
56
77
|
- If the user specifies a particular task file (e.g., `task-R0-02...md`), load **ONLY** that specific file into working memory.
|
|
57
|
-
- If no specific task is mentioned,
|
|
78
|
+
- If no specific task is mentioned, DO NOT load all tasks into working memory. Resolve the next single unblocked `pending` task from `task_registry` and load only that task packet.
|
|
58
79
|
- **Task Packet Extraction (MANDATORY):** Before coding, extract from the active task file(s):
|
|
59
80
|
- Objective + Constraints
|
|
60
81
|
- Related Files
|
|
61
82
|
- Completion Criteria
|
|
62
83
|
- Verification & Evidence
|
|
84
|
+
- Exact executable verification commands named in the task
|
|
63
85
|
- Requirement IDs referenced by the task
|
|
64
86
|
- Relevant `Canonical Contracts & Invariants` from `design.md`
|
|
65
87
|
- If the task file is missing actionable completion or verification detail, STOP and route back to spec correction. Do not guess.
|
|
88
|
+
- Before coding, set the active task(s) to `in_progress` in both markdown and `spec.json.task_registry`, or route through `/hapo:sync` if the runtime expects the sync protocol.
|
|
66
89
|
|
|
67
90
|
### Step 2: Scout (Codebase Inspection)
|
|
68
91
|
- **Mandatory:** Call agent `Task(subagent_type="inspect", ...)` to scan the overall codebase structure (e.g., where components live, where utils are). Avoid wandering into forbidden zones.
|
|
@@ -71,7 +94,13 @@ flowchart TD
|
|
|
71
94
|
- Act as `god-developer` OR directly write code, executing tasks specified in the loaded Markdown file(s) sequentially.
|
|
72
95
|
- **Important:** You may create and modify files directly, but must faithfully follow the design from the Spec.
|
|
73
96
|
- Progress tracking: Temporarily change `[ ]` to `[/]` in Spec files while coding is in progress. Do NOT mark `[x]` before Step 4 passes.
|
|
97
|
+
- **Task Boundary Protocol (CRITICAL):**
|
|
98
|
+
- Default editable scope is `Related Files` from the task packet.
|
|
99
|
+
- You may additionally touch direct test files plus minimal support files required to make the current task executable (shared types, exports, config glue, generated migration wiring).
|
|
100
|
+
- If you must edit a file outside this scope, explicitly treat it as a `scope escape` and justify why it is required for the current task.
|
|
101
|
+
- If the out-of-scope change would deliver functionality clearly assigned to a later task, STOP instead of implementing it early.
|
|
74
102
|
- **Hard Stop Protocol:** If you were asked to implement a specific task file, you MUST STOP completely after that task is verified. DO NOT auto-chain or jump to "Next Task" simply because you see it in the spec. Wait for the user's next command.
|
|
103
|
+
- **Full-Spec Loop Protocol:** If you were asked to implement the whole feature, you MUST still work one task at a time. Finish Step 4 and Step 5 for the current task before selecting the next unblocked task from `task_registry`.
|
|
75
104
|
- **Test Integrity Protocol:** You MUST NOT delete, replace, or reduce the scope of existing test cases to make tests pass. If a test fails, you must fix the **implementation code** or fix the **test setup/mock**, NOT remove the assertion. Reducing test count or weakening assertions (e.g., removing `toHaveBeenCalledWith` and replacing with `toEqual(expect.any(...))`) is a Critical violation.
|
|
76
105
|
- **Contract Integrity Protocol:** If implementation appears to require changing auth/session, transport, persistence, entrypoint wiring, or generated artifact behavior beyond what `design.md` states, STOP and route back to spec correction instead of inventing a new contract in code.
|
|
77
106
|
|
|
@@ -80,19 +109,32 @@ The moment you finish coding, DO NOT proceed further. Switch to `references/qual
|
|
|
80
109
|
**Mantra:** All feedback from code-auditor must be addressed thoroughly: Score >= 9.5 & Zero Critical issues.
|
|
81
110
|
|
|
82
111
|
- Passing Step 4 requires ALL of the following:
|
|
83
|
-
1. Automated verification passes
|
|
112
|
+
1. Automated verification passes, including every exact command named in the task's `Verification & Evidence` section
|
|
84
113
|
2. Code review passes
|
|
85
114
|
3. Task evidence passes (artifacts/runtime surfaces/negative-path checks from the task file are proven)
|
|
115
|
+
- `NO_TESTS` is NOT equivalent to PASS. If the task explicitly requires a test command or automated test proof, `NO_TESTS` is a FAIL or BLOCKED outcome until the requirement is satisfied or the spec is corrected.
|
|
86
116
|
- If build/test passes but task evidence is missing, the task is still FAIL.
|
|
87
117
|
- Only escalate to the user after 3 consecutive failed review rounds.
|
|
88
118
|
|
|
89
|
-
### Step 5: State Sync +
|
|
90
|
-
- Only after Step 4 passes may you mark task checkboxes completed and sync `spec.json` progress/timestamps.
|
|
119
|
+
### Step 5: State Sync + Task-Level Docs Sync
|
|
120
|
+
- Only after Step 4 passes may you mark task checkboxes completed and sync `spec.json` progress/timestamps/task_registry.
|
|
91
121
|
- If verification is partial or blocked by environment, keep the task in `pending` or `in_progress` and record the blocker instead of pretending completion.
|
|
92
|
-
-
|
|
93
|
-
-
|
|
122
|
+
- A completed task MUST leave behind:
|
|
123
|
+
- markdown `**Status:** done`
|
|
124
|
+
- `spec.json.task_registry[path].status = "done"`
|
|
125
|
+
- `completed_at` + `last_updated_at`
|
|
126
|
+
- synchronized top-level `updated_at`
|
|
127
|
+
- a human-readable verification receipt inside the task's `Verification & Evidence` section showing which commands ran and what proof was observed
|
|
128
|
+
- After syncing the active task, run a **Task Closeout Docs Checkpoint**
|
|
129
|
+
- Task Closeout Docs Checkpoint:
|
|
130
|
+
- Evaluate `Docs impact: none | minor | major` based on real behavior changes from the just-completed task
|
|
131
|
+
- If `none`: record that explicitly in the completion report and stop
|
|
132
|
+
- If `minor` or `major`: trigger `docs-keeper` to surgically update affected existing docs under `./docs`
|
|
133
|
+
- Default to **lightweight docs sync**: update only the docs touched by this task and its verified behavior; do NOT run `repomix` unless `docs-keeper` truly cannot verify the required architecture/context from the code, spec, and current docs
|
|
94
134
|
- **CWD Protocol (CRITICAL):** When spawning `docs-keeper`, you MUST ensure the agent's Current Working Directory (CWD context) is explicitly set to the **Workspace Root**, NOT the inner package directory you were just coding in. Otherwise, `docs-keeper` will search for the root `docs/` folder in the wrong place and crash.
|
|
95
|
-
-
|
|
135
|
+
- Task-level docs sync happens after every verified completed task, but actual edits still depend on `Docs impact`.
|
|
136
|
+
- In **Specific-Task Mode**, STOP after sync and report the result.
|
|
137
|
+
- In **Full-Spec Mode**, only after sync may you re-read `task_registry`, pick the next unblocked pending task, and repeat from Step 1 for that task.
|
|
96
138
|
|
|
97
139
|
---
|
|
98
140
|
## Attached References
|