5-phase-workflow 2.0.6 → 3.0.0-beta-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.
@@ -1,269 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Plan Guard - PreToolUse Hook
4
- // Prevents LLM breakout from planning phases by blocking:
5
- // - Agent/Task agents other than Explore (when not in implementation mode)
6
- // - Write/Edit operations outside .5/ (when not in implementation mode)
7
- //
8
- // Planning mode is detected per-feature by checking if that specific feature's
9
- // state.json exists. Only the feature in implementation mode gets unrestricted
10
- // tool access. Other features remain in planning mode.
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- let input = '';
16
- process.stdin.setEncoding('utf8');
17
- process.stdin.on('data', chunk => input += chunk);
18
- process.stdin.on('end', () => {
19
- try {
20
- const data = JSON.parse(input);
21
- const toolName = data.tool_name || '';
22
-
23
- // Short-circuit: only check Agent, Task, Write, Edit, EnterPlanMode, and Bash tools
24
- if (toolName !== 'Agent' && toolName !== 'Task' && toolName !== 'Write' && toolName !== 'Edit' && toolName !== 'EnterPlanMode' && toolName !== 'Bash') {
25
- process.exit(0);
26
- }
27
-
28
- const workspaceDir = data.cwd || data.workspace?.current_dir || process.cwd();
29
-
30
- // If no planning phase is active, allow all tools
31
- if (!isPlanningActive(workspaceDir)) {
32
- process.exit(0);
33
- }
34
-
35
- const toolInput = data.tool_input || {};
36
-
37
- // Determine which feature is being targeted and check its state
38
- const targetFeature = getTargetFeature(toolName, toolInput, workspaceDir);
39
- if (targetFeature && isFeatureInImplementationMode(workspaceDir, targetFeature)) {
40
- process.exit(0); // Tools allowed for features in implementation mode
41
- }
42
-
43
- // Planning mode enforcement
44
- if (toolName === 'EnterPlanMode') {
45
- const blockCount = incrementBlockCount(workspaceDir);
46
- const phase = getPlanningPhase(workspaceDir);
47
- const escalation = blockCount >= 3
48
- ? ` CRITICAL: Block #${blockCount}. You have attempted to break out of planning ${blockCount} times. You MUST return to your current step in the Progress Checklist, complete your planning artifact, output the completion message, and STOP. Do NOT attempt any other action.`
49
- : '';
50
- process.stderr.write(
51
- `BLOCKED: EnterPlanMode is not allowed during workflow planning. ` +
52
- `dev-workflow has its own planning process. ` +
53
- `REDIRECT: You are in ${phase || 'a planning phase'}. Return to your Progress Checklist. ` +
54
- `Find the last "✓ Step N complete" you output, then continue with Step N+1. ` +
55
- `Write your output to .5/features/{name}/ and output the completion message when done.${escalation}`
56
- );
57
- process.exit(2);
58
- }
59
-
60
- if (toolName === 'Agent' || toolName === 'Task') {
61
- const agentType = toolInput.subagent_type || '';
62
- if (agentType && agentType !== 'Explore') {
63
- const blockCount = incrementBlockCount(workspaceDir);
64
- const phase = getPlanningPhase(workspaceDir);
65
- const escalation = blockCount >= 3
66
- ? ` CRITICAL: Block #${blockCount}. You are a planner, NOT an implementer. You have attempted to spawn non-Explore agents ${blockCount} times. Implementation happens in /5:implement. Return to your Progress Checklist immediately, complete your planning artifact, and STOP.`
67
- : '';
68
- process.stderr.write(
69
- `BLOCKED: Only Explore agents are allowed during planning. ` +
70
- `Attempted: subagent_type="${agentType}". ` +
71
- `You are in ${phase || 'the planning phase'}. Implementation agents are /5:implement only. ` +
72
- `REDIRECT: Return to your Progress Checklist. Find your last "✓ Step N complete" and continue with Step N+1. ` +
73
- `If you need codebase information, use subagent_type=Explore. ` +
74
- `If you are done planning, output the completion message and STOP.${escalation}`
75
- );
76
- process.exit(2);
77
- }
78
- }
79
-
80
- if (toolName === 'Bash') {
81
- const command = toolInput.command || '';
82
- // Detect file-writing shell commands that bypass Write/Edit guards
83
- const writePatterns = /\b(cat\s*>|cat\s*<<|echo\s.*>|tee\s|printf\s.*>|cp\s|mv\s|mkdir\s.*&&.*>|sed\s+-i|awk\s.*>|touch\s|install\s)/;
84
- if (writePatterns.test(command)) {
85
- const blockCount = incrementBlockCount(workspaceDir);
86
- const phase = getPlanningPhase(workspaceDir);
87
- const escalation = blockCount >= 3
88
- ? ` CRITICAL: Block #${blockCount}. You are repeatedly attempting to write files via Bash to bypass planning guards. STOP immediately.`
89
- : '';
90
- process.stderr.write(
91
- `BLOCKED: File-writing Bash commands are not allowed during planning. ` +
92
- `You are in ${phase || 'the planning phase'}. ` +
93
- `REDIRECT: Return to your Progress Checklist. Planners do not create or modify source files. ` +
94
- `Complete your planning artifact and output the completion message.${escalation}`
95
- );
96
- process.exit(2);
97
- }
98
- }
99
-
100
- if (toolName === 'Write' || toolName === 'Edit') {
101
- const filePath = toolInput.file_path || '';
102
- if (!filePath) {
103
- process.exit(0);
104
- }
105
-
106
- const phase = getPlanningPhase(workspaceDir);
107
-
108
- // First check: block anything outside .5/
109
- if (!isInsideDotFive(filePath, workspaceDir)) {
110
- const blockCount = incrementBlockCount(workspaceDir);
111
- const isSourceFile = !filePath.includes('.5/') && !filePath.includes('.claude/');
112
- const escalation = blockCount >= 3
113
- ? ` CRITICAL: Block #${blockCount}. You have attempted to write source files ${blockCount} times. You are a PLANNER, not an implementer. Writing source code is /5:implement's job. Return to your Progress Checklist, finish your planning artifact, and STOP.`
114
- : '';
115
- const redirectMsg = isSourceFile
116
- ? `REDIRECT: You are in ${phase || 'the planning phase'}. You may ONLY write to .5/features/. ` +
117
- `Source file creation happens in /5:implement. ` +
118
- `Return to your Progress Checklist — find your last "✓ Step N complete" and continue with Step N+1.`
119
- : `REDIRECT: The path "${filePath}" is outside the allowed .5/ directory. ` +
120
- `Check your file path — you should be writing to .5/features/{name}/.`;
121
- process.stderr.write(
122
- `BLOCKED: ${toolName} outside .5/ is not allowed during planning. ` +
123
- `Attempted: "${filePath}". ${redirectMsg}${escalation}`
124
- );
125
- process.exit(2);
126
- }
127
-
128
- // Second check: during plan, only allow specific files
129
- if (normalizePlanningPhase(phase) === 'plan' && !isAllowedPlanFile(filePath, workspaceDir)) {
130
- const blockCount = incrementBlockCount(workspaceDir);
131
- const escalation = blockCount >= 3
132
- ? ` CRITICAL: Block #${blockCount}. You are attempting to create implementation artifacts during planning. Planning ONLY produces plan.md and codebase-scan.md. STOP and output your completion message.`
133
- : '';
134
- process.stderr.write(
135
- `BLOCKED: During plan, you may only write to .planning-active, codebase-scan.md, and plan.md. ` +
136
- `Attempted: "${filePath}". ` +
137
- `REDIRECT: If you have already written plan.md, output the completion message and STOP. ` +
138
- `Do NOT create source files or state.json during planning.${escalation}`
139
- );
140
- process.exit(2);
141
- }
142
- }
143
-
144
- process.exit(0);
145
- } catch (e) {
146
- // Silent failure - don't block on parse errors
147
- process.exit(0);
148
- }
149
- });
150
-
151
- function isAllowedPlanFile(filePath, workspaceDir) {
152
- const resolved = path.resolve(workspaceDir, filePath);
153
- const dotFiveDir = path.join(workspaceDir, '.5');
154
-
155
- // Allow .5/.planning-active
156
- if (resolved === path.join(dotFiveDir, '.planning-active')) return true;
157
-
158
- // Allow .5/features/{name}/plan.md and .5/features/{name}/codebase-scan.md
159
- const featuresDir = path.join(dotFiveDir, 'features');
160
- if (resolved.startsWith(featuresDir + path.sep)) {
161
- const basename = path.basename(resolved);
162
- if (basename === 'plan.md' || basename === 'codebase-scan.md') return true;
163
- }
164
-
165
- return false;
166
- }
167
-
168
- function isInsideDotFive(filePath, workspaceDir) {
169
- const resolved = path.resolve(workspaceDir, filePath);
170
- const dotFiveDir = path.join(workspaceDir, '.5');
171
- return resolved.startsWith(dotFiveDir + path.sep) ||
172
- resolved === dotFiveDir;
173
- }
174
-
175
- function getTargetFeature(toolName, toolInput, workspaceDir) {
176
- // Extract the feature name from the tool input context
177
- const featuresDir = path.join(workspaceDir, '.5', 'features');
178
-
179
- if (toolName === 'Write' || toolName === 'Edit') {
180
- // Check if the file path is inside a feature directory
181
- const filePath = toolInput.file_path || '';
182
- const resolved = path.resolve(workspaceDir, filePath);
183
- if (resolved.startsWith(featuresDir + path.sep)) {
184
- // Extract feature name: .5/features/{feature-name}/...
185
- const relative = resolved.slice(featuresDir.length + 1);
186
- const featureName = relative.split(path.sep)[0];
187
- if (featureName) return featureName;
188
- }
189
- }
190
-
191
- if (toolName === 'Agent' || toolName === 'Task') {
192
- // Check the task prompt for feature name references
193
- const prompt = toolInput.prompt || '';
194
- const desc = toolInput.description || '';
195
- const combined = prompt + ' ' + desc;
196
-
197
- // Try to match a feature directory that exists
198
- try {
199
- if (fs.existsSync(featuresDir)) {
200
- const features = fs.readdirSync(featuresDir, { withFileTypes: true });
201
- for (const entry of features) {
202
- if (!entry.isDirectory()) continue;
203
- if (combined.includes(entry.name)) {
204
- return entry.name;
205
- }
206
- }
207
- }
208
- } catch (e) {
209
- // Ignore read errors
210
- }
211
- }
212
-
213
- return null;
214
- }
215
-
216
- function isPlanningActive(workspaceDir) {
217
- const markerPath = path.join(workspaceDir, '.5', '.planning-active');
218
- if (!fs.existsSync(markerPath)) return false;
219
- try {
220
- const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
221
- if (marker.startedAt) {
222
- const elapsed = Date.now() - new Date(marker.startedAt).getTime();
223
- if (elapsed > 4 * 60 * 60 * 1000) {
224
- try { fs.unlinkSync(markerPath); } catch (e) {}
225
- return false;
226
- }
227
- }
228
- return true;
229
- } catch (e) {
230
- return true; // Unreadable marker → fail-safe, assume active
231
- }
232
- }
233
-
234
- function incrementBlockCount(workspaceDir) {
235
- const markerPath = path.join(workspaceDir, '.5', '.planning-active');
236
- try {
237
- const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
238
- marker.blockCount = (marker.blockCount || 0) + 1;
239
- fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2));
240
- return marker.blockCount;
241
- } catch (e) {
242
- return 1;
243
- }
244
- }
245
-
246
- function getPlanningPhase(workspaceDir) {
247
- const markerPath = path.join(workspaceDir, '.5', '.planning-active');
248
- try {
249
- const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
250
- return marker.phase || null;
251
- } catch (e) {
252
- return null;
253
- }
254
- }
255
-
256
- function normalizePlanningPhase(phase) {
257
- if (!phase) return 'plan';
258
- if (phase === 'plan-feature' || phase === 'plan-implementation') return 'plan';
259
- if (phase === 'plan') return 'plan';
260
- return 'plan';
261
- }
262
-
263
- function isFeatureInImplementationMode(workspaceDir, featureName) {
264
- // Check if this specific feature has a state.json (created by /5:implement)
265
- const stateFile = path.join(
266
- workspaceDir, '.5', 'features', featureName, 'state.json'
267
- );
268
- return fs.existsSync(stateFile);
269
- }
@@ -1,43 +0,0 @@
1
- {
2
- "ticket": "{TICKET-ID}",
3
- "feature": "{feature-name}",
4
- "status": "in-progress",
5
- "currentStep": 1,
6
- "totalSteps": 0,
7
- "steps": [
8
- {
9
- "number": 1,
10
- "name": "{step-name}",
11
- "mode": "parallel",
12
- "model": "haiku",
13
- "components": []
14
- }
15
- ],
16
- "pendingComponents": [
17
- {
18
- "name": "{component-name}",
19
- "action": "create",
20
- "step": 1,
21
- "mode": "parallel",
22
- "model": "haiku",
23
- "file": "{target-path}",
24
- "sourceFile": null,
25
- "description": "{one-sentence intent}",
26
- "dependsOn": [],
27
- "patternRefs": [],
28
- "verifyCommands": [],
29
- "notes": []
30
- }
31
- ],
32
- "completedComponents": [],
33
- "recentFailures": [],
34
- "baseline": {},
35
- "latestCommandResults": [],
36
- "verificationStatus": "pending",
37
- "verificationResults": {},
38
- "verifiedAt": null,
39
- "latestCommitResults": [],
40
- "eventLog": ".5/features/{feature-name}/state-events.jsonl",
41
- "startedAt": "{ISO-timestamp}",
42
- "lastUpdated": "{ISO-timestamp}"
43
- }