@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.
@@ -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
- * Stop extract todos + git changes, save to latest.md
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 = require('fs');
23
- const path = require('path');
24
- const os = require('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 = 7;
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
- const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
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 { return null; }
43
+ } catch {
44
+ return null;
45
+ }
45
46
  }
46
47
 
47
48
  function loadLatest(cwd) {
48
49
  try {
49
- const dir = stateDir(cwd);
50
+ const dir = stateDir(cwd);
50
51
  if (!dir) return null;
51
- const file = path.join(dir, 'latest.md');
52
+
53
+ const file = path.join(dir, 'latest.md');
52
54
  if (!fs.existsSync(file)) return null;
53
- const text = fs.readFileSync(file, 'utf8');
54
- const tsMatch = text.match(/<!-- Generated: (.+?) -->/);
55
- if (tsMatch) {
56
- const parsed = new Date(tsMatch[1]).getTime();
57
- if (isNaN(parsed)) return null;
58
- if (Date.now() - parsed > EXPIRY_DAYS * 24 * 60 * 60 * 1000) return null;
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 { return null; }
65
+ } catch {
66
+ return null;
67
+ }
62
68
  }
63
69
 
64
70
  function writeAtomic(filePath, content) {
65
- const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
66
- fs.writeFileSync(tmp, content);
67
- fs.renameSync(tmp, filePath);
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 src = path.join(dir, 'latest.md');
73
- if (!fs.existsSync(src)) return;
74
- const aDir = path.join(dir, 'archive');
75
- if (!fs.existsSync(aDir)) fs.mkdirSync(aDir);
76
- const now = new Date();
77
- const pad = n => String(n).padStart(2, '0');
78
- const ts = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
79
- fs.copyFileSync(src, path.join(aDir, `${ts}.md`));
80
- const files = fs.readdirSync(aDir).filter(f => f.endsWith('.md')).sort();
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
- try { fs.unlinkSync(path.join(aDir, files.shift())); } catch { /* ignore */ }
92
+ const oldest = files.shift();
93
+ try {
94
+ fs.unlinkSync(path.join(archiveDir, oldest));
95
+ } catch {
96
+ // fail-open
97
+ }
83
98
  }
84
- } catch { /* fail-open */ }
99
+ } catch {
100
+ // fail-open
101
+ }
85
102
  }
86
103
 
87
- // ── Data extraction ───────────────────────────────────────────────────────
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 latest = [];
100
- const lines = fs.readFileSync(stdinData.transcript_path, 'utf8').split('\n').filter(Boolean);
101
- for (const line of lines) {
102
- try {
103
- const entry = JSON.parse(line);
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 out = execSync('git diff --name-only HEAD', {
120
- encoding: 'utf8', timeout: 3000, stdio: ['pipe','pipe','pipe']
122
+ const diff = execSync('git diff --name-only HEAD', {
123
+ encoding: 'utf8',
124
+ timeout: 3000,
125
+ stdio: ['pipe', 'pipe', 'pipe']
121
126
  }).trim();
122
- if (out) data.modifiedFiles = out.split('\n').slice(0, 20);
123
- } catch { /* ignore */ }
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 = data.todos.filter(t => t.status === 'completed');
132
- const pending = data.todos.filter(t => t.status !== 'completed');
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 ? done.map(t => `- ${t.content}`) : ['- (No completed tasks recorded)']),
148
+ ...(done.length ? done.map((todo) => `- ${todo.content}`) : ['- (No completed tasks recorded)']),
140
149
  '',
141
150
  "## What's Left",
142
- ...(pending.length ? pending.map(t => `- [ ] ${t.content}`) : ['- (All tasks completed)']),
151
+ ...(pending.length ? pending.map((todo) => `- [ ] ${todo.content}`) : ['- (All tasks completed)']),
143
152
  '',
144
153
  '## Key Files Modified',
145
- ...(data.modifiedFiles.length ? data.modifiedFiles.map(f => `- ${f}`) : ['- (No file changes detected)']),
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 type = data.agent_type || 'unknown';
152
- const ts = new Date().toISOString().slice(11, 19);
153
- return `\n## Agent Result: ${type} (${ts})\n- Completed at ${ts}\n`;
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
- // ── Main ──────────────────────────────────────────────────────────────────
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
- const stdin = fs.readFileSync(0, 'utf8').trim();
159
- if (!stdin) process.exit(0);
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
- const data = JSON.parse(stdin);
162
- const event = data.hook_event_name || '';
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
- // SessionStart: restore previous state
167
- if (event === 'SessionStart') {
168
- const prev = loadLatest(cwd);
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
- process.exit(0);
186
+
187
+ return `${existing.trimEnd()}\n${agentSection}`;
175
188
  }
176
189
 
177
- // SubagentStop: append completion note
178
- if (event === 'SubagentStop' && dir) {
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
- let updated;
183
- if (existing) {
184
- updated = existing.replace(/(\n## Key Files Modified)/, `\n${agentSection}$1`);
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
- // Stop: persist full state
194
- if (event === 'Stop' && dir) {
195
- const file = path.join(dir, 'latest.md');
196
- const sessionData = extractSessionData(data);
197
- let content = buildStateContent(sessionData);
198
-
199
- // Preserve agent sections from SubagentStop
200
- if (fs.existsSync(file)) {
201
- const existing = fs.readFileSync(file, 'utf8');
202
- const agentMatches = existing.match(/## Agent Result:.+?(?=\n## |$)/gs);
203
- if (agentMatches) {
204
- content = content.replace(
205
- /(\n## Key Files Modified)/,
206
- `\n${agentMatches.join('\n')}$1`
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
- process.exit(0);
217
-
218
- } catch (e) {
249
+ main().catch(() => {
250
+ process.exit(0);
251
+ });
252
+ } catch (error) {
219
253
  try {
220
- const fs = require('fs'), p = require('path');
221
- const d = p.join(__dirname, '.logs');
222
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
223
- fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
224
- JSON.stringify({ ts: new Date().toISOString(), hook: 'state', status: 'crash', error: e.message }) + '\n');
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 # Actionable granular steps for development
71
- └── task-02-api.md # Next sequential 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-0*.md`)
85
- Work is decomposed into linear markdown task files.
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 overall completion.
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, and keep `task_files` synchronized with the real files on disk.
16
- 2. Edit `task-XX.md`: Change `Status` only after real verification has passed (build/test/runtime/artifact). Then check `[x]` the sub-task boxes and relevant completion criteria.
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. Edit `task-XX.md`: Change `Trạng thái: pending` (or `in_progress`) to `Trạng thái: blocked` with a note.
22
- 3. Alert the orchestrator or user via `AskUserQuestion` or explicit warning.
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 (Full Build)
7
+ # Develop — Feature Implementation (Task-Orchestrated Build)
8
8
 
9
- Reads the full project specification (`hapo:specs`) and relentlessly implements code from A to Z in a disciplined, single-track workflow. Automatically overcomes obstacles and only escalates to the user when facing persistent critical failures.
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, list and load all Markdown files in `specs/<feature-name>/tasks/*.md`.
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 (typecheck/test/build as applicable)
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 + Incremental Docs 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
- - After passing the Quality Gate, evaluate if any actual codebase modifications occurred (e.g., check pending files via git status).
93
- - If files were created or modified: Trigger `docs-keeper` automatically to execute `repomix` and update the global `/docs/` and project logs.
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
- - Do NOT skip this step! The user explicitly requires documentation to be synced immediately after every `/hapo:develop` action, overriding the default Phase 3-only rule.
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