@sienklogic/plan-build-run 2.0.0 → 2.0.1
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/CHANGELOG.md +56 -56
- package/CLAUDE.md +149 -149
- package/LICENSE +21 -21
- package/README.md +247 -247
- package/dashboard/bin/cli.js +25 -25
- package/dashboard/package.json +34 -34
- package/dashboard/public/css/layout.css +406 -406
- package/dashboard/public/css/status-colors.css +98 -98
- package/dashboard/public/js/htmx-title.js +5 -5
- package/dashboard/public/js/sidebar-toggle.js +20 -20
- package/dashboard/src/app.js +78 -78
- package/dashboard/src/middleware/errorHandler.js +52 -52
- package/dashboard/src/middleware/notFoundHandler.js +9 -9
- package/dashboard/src/repositories/planning.repository.js +128 -128
- package/dashboard/src/routes/events.routes.js +40 -40
- package/dashboard/src/routes/index.routes.js +31 -31
- package/dashboard/src/routes/pages.routes.js +245 -195
- package/dashboard/src/server.js +42 -42
- package/dashboard/src/services/dashboard.service.js +222 -222
- package/dashboard/src/services/phase.service.js +220 -167
- package/dashboard/src/services/project.service.js +57 -57
- package/dashboard/src/services/roadmap.service.js +171 -171
- package/dashboard/src/services/sse.service.js +58 -58
- package/dashboard/src/services/todo.service.js +254 -254
- package/dashboard/src/services/watcher.service.js +48 -48
- package/dashboard/src/views/coming-soon.ejs +11 -11
- package/dashboard/src/views/error.ejs +13 -13
- package/dashboard/src/views/index.ejs +5 -5
- package/dashboard/src/views/layout.ejs +1 -1
- package/dashboard/src/views/partials/dashboard-content.ejs +77 -77
- package/dashboard/src/views/partials/footer.ejs +3 -3
- package/dashboard/src/views/partials/head.ejs +21 -21
- package/dashboard/src/views/partials/header.ejs +12 -12
- package/dashboard/src/views/partials/layout-bottom.ejs +15 -15
- package/dashboard/src/views/partials/layout-top.ejs +8 -8
- package/dashboard/src/views/partials/phase-content.ejs +188 -181
- package/dashboard/src/views/partials/phase-doc-content.ejs +38 -0
- package/dashboard/src/views/partials/phases-content.ejs +117 -117
- package/dashboard/src/views/partials/roadmap-content.ejs +142 -142
- package/dashboard/src/views/partials/sidebar.ejs +38 -38
- package/dashboard/src/views/partials/todo-create-content.ejs +53 -53
- package/dashboard/src/views/partials/todo-detail-content.ejs +38 -38
- package/dashboard/src/views/partials/todos-content.ejs +53 -53
- package/dashboard/src/views/phase-detail.ejs +5 -5
- package/dashboard/src/views/phase-doc.ejs +5 -0
- package/dashboard/src/views/phases.ejs +5 -5
- package/dashboard/src/views/roadmap.ejs +5 -5
- package/dashboard/src/views/todo-create.ejs +5 -5
- package/dashboard/src/views/todo-detail.ejs +5 -5
- package/dashboard/src/views/todos.ejs +5 -5
- package/package.json +57 -57
- package/plugins/pbr/.claude-plugin/plugin.json +13 -13
- package/plugins/pbr/UI-CONSISTENCY-GAPS.md +61 -61
- package/plugins/pbr/agents/codebase-mapper.md +279 -271
- package/plugins/pbr/agents/debugger.md +281 -281
- package/plugins/pbr/agents/executor.md +428 -407
- package/plugins/pbr/agents/general.md +164 -164
- package/plugins/pbr/agents/integration-checker.md +169 -141
- package/plugins/pbr/agents/plan-checker.md +296 -280
- package/plugins/pbr/agents/planner.md +358 -358
- package/plugins/pbr/agents/researcher.md +363 -363
- package/plugins/pbr/agents/synthesizer.md +230 -230
- package/plugins/pbr/agents/verifier.md +489 -454
- package/plugins/pbr/commands/begin.md +5 -5
- package/plugins/pbr/commands/build.md +5 -5
- package/plugins/pbr/commands/config.md +5 -5
- package/plugins/pbr/commands/continue.md +5 -5
- package/plugins/pbr/commands/debug.md +5 -5
- package/plugins/pbr/commands/discuss.md +5 -5
- package/plugins/pbr/commands/explore.md +5 -5
- package/plugins/pbr/commands/health.md +5 -5
- package/plugins/pbr/commands/help.md +5 -5
- package/plugins/pbr/commands/import.md +5 -5
- package/plugins/pbr/commands/milestone.md +5 -5
- package/plugins/pbr/commands/note.md +5 -5
- package/plugins/pbr/commands/pause.md +5 -5
- package/plugins/pbr/commands/plan.md +5 -5
- package/plugins/pbr/commands/quick.md +5 -5
- package/plugins/pbr/commands/resume.md +5 -5
- package/plugins/pbr/commands/review.md +5 -5
- package/plugins/pbr/commands/scan.md +5 -5
- package/plugins/pbr/commands/setup.md +5 -5
- package/plugins/pbr/commands/status.md +5 -5
- package/plugins/pbr/commands/todo.md +5 -5
- package/plugins/pbr/contexts/dev.md +27 -27
- package/plugins/pbr/contexts/research.md +28 -28
- package/plugins/pbr/contexts/review.md +36 -36
- package/plugins/pbr/hooks/hooks.json +183 -183
- package/plugins/pbr/references/agent-anti-patterns.md +24 -24
- package/plugins/pbr/references/agent-interactions.md +134 -134
- package/plugins/pbr/references/agent-teams.md +54 -54
- package/plugins/pbr/references/checkpoints.md +157 -157
- package/plugins/pbr/references/common-bug-patterns.md +13 -13
- package/plugins/pbr/references/config-reference.md +441 -0
- package/plugins/pbr/references/continuation-format.md +212 -212
- package/plugins/pbr/references/deviation-rules.md +112 -112
- package/plugins/pbr/references/git-integration.md +226 -226
- package/plugins/pbr/references/integration-patterns.md +117 -117
- package/plugins/pbr/references/model-profiles.md +99 -99
- package/plugins/pbr/references/model-selection.md +31 -31
- package/plugins/pbr/references/pbr-rules.md +193 -193
- package/plugins/pbr/references/plan-authoring.md +181 -181
- package/plugins/pbr/references/plan-format.md +287 -283
- package/plugins/pbr/references/planning-config.md +213 -213
- package/plugins/pbr/references/questioning.md +214 -214
- package/plugins/pbr/references/reading-verification.md +127 -127
- package/plugins/pbr/references/stub-patterns.md +160 -160
- package/plugins/pbr/references/subagent-coordination.md +119 -119
- package/plugins/pbr/references/ui-formatting.md +461 -399
- package/plugins/pbr/references/verification-patterns.md +198 -198
- package/plugins/pbr/references/wave-execution.md +95 -95
- package/plugins/pbr/scripts/auto-continue.js +80 -80
- package/plugins/pbr/scripts/check-dangerous-commands.js +136 -136
- package/plugins/pbr/scripts/check-doc-sprawl.js +102 -102
- package/plugins/pbr/scripts/check-phase-boundary.js +196 -196
- package/plugins/pbr/scripts/check-plan-format.js +270 -270
- package/plugins/pbr/scripts/check-roadmap-sync.js +322 -252
- package/plugins/pbr/scripts/check-skill-workflow.js +262 -262
- package/plugins/pbr/scripts/check-state-sync.js +476 -476
- package/plugins/pbr/scripts/check-subagent-output.js +144 -144
- package/plugins/pbr/scripts/config-schema.json +251 -251
- package/plugins/pbr/scripts/context-budget-check.js +287 -287
- package/plugins/pbr/scripts/event-handler.js +151 -151
- package/plugins/pbr/scripts/event-logger.js +92 -92
- package/plugins/pbr/scripts/hook-logger.js +80 -76
- package/plugins/pbr/scripts/hooks-schema.json +79 -79
- package/plugins/pbr/scripts/log-subagent.js +164 -152
- package/plugins/pbr/scripts/log-tool-failure.js +88 -88
- package/plugins/pbr/scripts/pbr-tools.js +1378 -1301
- package/plugins/pbr/scripts/post-write-dispatch.js +66 -66
- package/plugins/pbr/scripts/post-write-quality.js +207 -207
- package/plugins/pbr/scripts/pre-bash-dispatch.js +86 -56
- package/plugins/pbr/scripts/pre-write-dispatch.js +97 -62
- package/plugins/pbr/scripts/progress-tracker.js +281 -228
- package/plugins/pbr/scripts/run-hook.js +92 -0
- package/plugins/pbr/scripts/session-cleanup.js +254 -254
- package/plugins/pbr/scripts/status-line.js +288 -285
- package/plugins/pbr/scripts/suggest-compact.js +119 -119
- package/plugins/pbr/scripts/task-completed.js +45 -45
- package/plugins/pbr/scripts/track-context-budget.js +149 -119
- package/plugins/pbr/scripts/validate-commit.js +200 -200
- package/plugins/pbr/scripts/validate-plugin-structure.js +183 -172
- package/plugins/pbr/scripts/validate-task.js +106 -0
- package/plugins/pbr/skills/begin/SKILL.md +594 -545
- package/plugins/pbr/skills/begin/templates/PROJECT.md.tmpl +33 -33
- package/plugins/pbr/skills/begin/templates/REQUIREMENTS.md.tmpl +18 -18
- package/plugins/pbr/skills/begin/templates/STATE.md.tmpl +49 -49
- package/plugins/pbr/skills/begin/templates/config.json.tmpl +64 -63
- package/plugins/pbr/skills/begin/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/begin/templates/roadmap-prompt.md.tmpl +30 -30
- package/plugins/pbr/skills/begin/templates/synthesis-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/build/SKILL.md +943 -962
- package/plugins/pbr/skills/config/SKILL.md +256 -241
- package/plugins/pbr/skills/continue/SKILL.md +164 -127
- package/plugins/pbr/skills/debug/SKILL.md +515 -489
- package/plugins/pbr/skills/debug/templates/continuation-prompt.md.tmpl +16 -16
- package/plugins/pbr/skills/debug/templates/initial-investigation-prompt.md.tmpl +27 -27
- package/plugins/pbr/skills/discuss/SKILL.md +347 -338
- package/plugins/pbr/skills/discuss/templates/CONTEXT.md.tmpl +61 -61
- package/plugins/pbr/skills/discuss/templates/decision-categories.md +9 -9
- package/plugins/pbr/skills/explore/SKILL.md +378 -362
- package/plugins/pbr/skills/health/SKILL.md +221 -186
- package/plugins/pbr/skills/health/templates/check-pattern.md.tmpl +30 -30
- package/plugins/pbr/skills/health/templates/output-format.md.tmpl +63 -63
- package/plugins/pbr/skills/help/SKILL.md +155 -140
- package/plugins/pbr/skills/import/SKILL.md +504 -490
- package/plugins/pbr/skills/milestone/SKILL.md +704 -673
- package/plugins/pbr/skills/milestone/templates/audit-report.md.tmpl +48 -48
- package/plugins/pbr/skills/milestone/templates/stats-file.md.tmpl +30 -30
- package/plugins/pbr/skills/note/SKILL.md +231 -212
- package/plugins/pbr/skills/pause/SKILL.md +249 -235
- package/plugins/pbr/skills/pause/templates/continue-here.md.tmpl +71 -71
- package/plugins/pbr/skills/plan/SKILL.md +685 -628
- package/plugins/pbr/skills/plan/decimal-phase-calc.md +98 -98
- package/plugins/pbr/skills/plan/templates/checker-prompt.md.tmpl +21 -21
- package/plugins/pbr/skills/plan/templates/gap-closure-prompt.md.tmpl +32 -32
- package/plugins/pbr/skills/plan/templates/planner-prompt.md.tmpl +38 -38
- package/plugins/pbr/skills/plan/templates/researcher-prompt.md.tmpl +19 -19
- package/plugins/pbr/skills/plan/templates/revision-prompt.md.tmpl +23 -23
- package/plugins/pbr/skills/quick/SKILL.md +354 -335
- package/plugins/pbr/skills/resume/SKILL.md +402 -388
- package/plugins/pbr/skills/review/SKILL.md +686 -652
- package/plugins/pbr/skills/review/templates/debugger-prompt.md.tmpl +60 -60
- package/plugins/pbr/skills/review/templates/gap-planner-prompt.md.tmpl +40 -40
- package/plugins/pbr/skills/review/templates/verifier-prompt.md.tmpl +115 -115
- package/plugins/pbr/skills/scan/SKILL.md +304 -269
- package/plugins/pbr/skills/scan/templates/mapper-prompt.md.tmpl +201 -201
- package/plugins/pbr/skills/setup/SKILL.md +253 -227
- package/plugins/pbr/skills/shared/commit-planning-docs.md +35 -35
- package/plugins/pbr/skills/shared/config-loading.md +102 -102
- package/plugins/pbr/skills/shared/context-budget.md +40 -40
- package/plugins/pbr/skills/shared/context-loader-task.md +86 -86
- package/plugins/pbr/skills/shared/digest-select.md +79 -79
- package/plugins/pbr/skills/shared/domain-probes.md +125 -125
- package/plugins/pbr/skills/shared/error-reporting.md +79 -79
- package/plugins/pbr/skills/shared/gate-prompts.md +388 -388
- package/plugins/pbr/skills/shared/phase-argument-parsing.md +45 -45
- package/plugins/pbr/skills/shared/progress-display.md +53 -53
- package/plugins/pbr/skills/shared/revision-loop.md +81 -81
- package/plugins/pbr/skills/shared/state-loading.md +62 -62
- package/plugins/pbr/skills/shared/state-update.md +161 -161
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +33 -33
- package/plugins/pbr/skills/status/SKILL.md +367 -353
- package/plugins/pbr/skills/todo/SKILL.md +198 -181
- package/plugins/pbr/templates/CONTEXT.md.tmpl +52 -52
- package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +151 -151
- package/plugins/pbr/templates/RESEARCH-SUMMARY.md.tmpl +97 -97
- package/plugins/pbr/templates/ROADMAP.md.tmpl +40 -40
- package/plugins/pbr/templates/SUMMARY.md.tmpl +81 -81
- package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +116 -116
- package/plugins/pbr/templates/codebase/ARCHITECTURE.md.tmpl +98 -98
- package/plugins/pbr/templates/codebase/CONCERNS.md.tmpl +93 -93
- package/plugins/pbr/templates/codebase/CONVENTIONS.md.tmpl +104 -104
- package/plugins/pbr/templates/codebase/INTEGRATIONS.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STACK.md.tmpl +78 -78
- package/plugins/pbr/templates/codebase/STRUCTURE.md.tmpl +80 -80
- package/plugins/pbr/templates/codebase/TESTING.md.tmpl +107 -107
- package/plugins/pbr/templates/continue-here.md.tmpl +73 -73
- package/plugins/pbr/templates/prompt-partials/phase-project-context.md.tmpl +37 -37
- package/plugins/pbr/templates/research/ARCHITECTURE.md.tmpl +124 -124
- package/plugins/pbr/templates/research/STACK.md.tmpl +71 -71
- package/plugins/pbr/templates/research/SUMMARY.md.tmpl +112 -112
- package/plugins/pbr/templates/research-outputs/phase-research.md.tmpl +81 -81
- package/plugins/pbr/templates/research-outputs/project-research.md.tmpl +99 -99
- package/plugins/pbr/templates/research-outputs/synthesis.md.tmpl +36 -36
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// PreToolUse hook (Write|Edit): Warns or blocks when editing files
|
|
4
|
-
// outside the current active phase.
|
|
5
|
-
//
|
|
6
|
-
// Reads STATE.md for current phase number. If the file being written
|
|
7
|
-
// is under .planning/phases/NN-<slug>/ and NN does not match the
|
|
8
|
-
// current phase, issues a warning or blocks depending on config.
|
|
9
|
-
//
|
|
10
|
-
// Config: safety.enforce_phase_boundaries
|
|
11
|
-
// - true = block cross-phase writes (exit 2)
|
|
12
|
-
// - false = warn only (default)
|
|
13
|
-
// - absent = warn only
|
|
14
|
-
//
|
|
15
|
-
// Files outside .planning/phases/ are always allowed (source code,
|
|
16
|
-
// config files, etc.).
|
|
17
|
-
//
|
|
18
|
-
// Exit codes:
|
|
19
|
-
// 0 = allowed or not applicable
|
|
20
|
-
// 2 = blocked (only when enforce_phase_boundaries is true)
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const { logHook } = require('./hook-logger');
|
|
25
|
-
const { logEvent } = require('./event-logger');
|
|
26
|
-
|
|
27
|
-
function main() {
|
|
28
|
-
let input = '';
|
|
29
|
-
|
|
30
|
-
process.stdin.setEncoding('utf8');
|
|
31
|
-
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
32
|
-
process.stdin.on('end', () => {
|
|
33
|
-
try {
|
|
34
|
-
const data = JSON.parse(input);
|
|
35
|
-
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
36
|
-
|
|
37
|
-
if (!filePath) {
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const cwd = process.cwd();
|
|
42
|
-
const planningDir = path.join(cwd, '.planning');
|
|
43
|
-
|
|
44
|
-
// Check if the file is under .planning/phases/ using marker matching
|
|
45
|
-
// instead of absolute path comparison (avoids macOS /var -> /private/var symlink issues)
|
|
46
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
47
|
-
const phasesMarker = '.planning/phases/';
|
|
48
|
-
const markerIdx = normalizedPath.indexOf(phasesMarker);
|
|
49
|
-
|
|
50
|
-
if (markerIdx === -1) {
|
|
51
|
-
process.exit(0);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Extract phase number from path after the marker
|
|
55
|
-
// Path pattern: .planning/phases/NN-slug/...
|
|
56
|
-
const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
|
|
57
|
-
const phaseMatch = afterMarker.match(/^(\d+)-/);
|
|
58
|
-
if (!phaseMatch) {
|
|
59
|
-
process.exit(0);
|
|
60
|
-
}
|
|
61
|
-
const filePhase = parseInt(phaseMatch[1], 10);
|
|
62
|
-
|
|
63
|
-
// Get current phase from STATE.md
|
|
64
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
65
|
-
if (!fs.existsSync(stateFile)) {
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
70
|
-
const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
71
|
-
if (!currentPhaseMatch) {
|
|
72
|
-
process.exit(0);
|
|
73
|
-
}
|
|
74
|
-
const currentPhase = parseInt(currentPhaseMatch[1], 10);
|
|
75
|
-
|
|
76
|
-
// Same phase — always allowed
|
|
77
|
-
if (filePhase === currentPhase) {
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Cross-phase write detected — check config
|
|
82
|
-
const enforce = getEnforceSetting(planningDir);
|
|
83
|
-
|
|
84
|
-
logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
|
|
85
|
-
filePhase,
|
|
86
|
-
currentPhase,
|
|
87
|
-
file: path.basename(filePath)
|
|
88
|
-
});
|
|
89
|
-
logEvent('workflow', 'phase-boundary', {
|
|
90
|
-
filePhase,
|
|
91
|
-
currentPhase,
|
|
92
|
-
file: path.basename(filePath),
|
|
93
|
-
action: enforce ? 'block' : 'warn'
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (enforce) {
|
|
97
|
-
const output = {
|
|
98
|
-
decision: 'block',
|
|
99
|
-
reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
|
|
100
|
-
};
|
|
101
|
-
process.stdout.write(JSON.stringify(output));
|
|
102
|
-
process.exit(2);
|
|
103
|
-
} else {
|
|
104
|
-
const output = {
|
|
105
|
-
hookSpecificOutput: {
|
|
106
|
-
hookEventName: 'PreToolUse',
|
|
107
|
-
additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
process.stdout.write(JSON.stringify(output));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
process.exit(0);
|
|
114
|
-
} catch (_e) {
|
|
115
|
-
// Don't block on errors
|
|
116
|
-
process.exit(0);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function getEnforceSetting(planningDir) {
|
|
122
|
-
const configFile = path.join(planningDir, 'config.json');
|
|
123
|
-
if (!fs.existsSync(configFile)) return false;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
127
|
-
return !!(config.safety && config.safety.enforce_phase_boundaries);
|
|
128
|
-
} catch (_e) {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Core boundary check logic for use by dispatchers.
|
|
135
|
-
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
136
|
-
* @returns {null|{exitCode: number, output: Object}} null if pass, result otherwise
|
|
137
|
-
*/
|
|
138
|
-
function checkBoundary(data) {
|
|
139
|
-
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
140
|
-
if (!filePath) return null;
|
|
141
|
-
|
|
142
|
-
const cwd = process.cwd();
|
|
143
|
-
const planningDir = path.join(cwd, '.planning');
|
|
144
|
-
|
|
145
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
146
|
-
const phasesMarker = '.planning/phases/';
|
|
147
|
-
const markerIdx = normalizedPath.indexOf(phasesMarker);
|
|
148
|
-
if (markerIdx === -1) return null;
|
|
149
|
-
|
|
150
|
-
const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
|
|
151
|
-
const phaseMatch = afterMarker.match(/^(\d+)-/);
|
|
152
|
-
if (!phaseMatch) return null;
|
|
153
|
-
const filePhase = parseInt(phaseMatch[1], 10);
|
|
154
|
-
|
|
155
|
-
const stateFile = path.join(planningDir, 'STATE.md');
|
|
156
|
-
if (!fs.existsSync(stateFile)) return null;
|
|
157
|
-
|
|
158
|
-
const state = fs.readFileSync(stateFile, 'utf8');
|
|
159
|
-
const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
160
|
-
if (!currentPhaseMatch) return null;
|
|
161
|
-
const currentPhase = parseInt(currentPhaseMatch[1], 10);
|
|
162
|
-
|
|
163
|
-
if (filePhase === currentPhase) return null;
|
|
164
|
-
|
|
165
|
-
const enforce = getEnforceSetting(planningDir);
|
|
166
|
-
|
|
167
|
-
logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
|
|
168
|
-
filePhase, currentPhase, file: path.basename(filePath)
|
|
169
|
-
});
|
|
170
|
-
logEvent('workflow', 'phase-boundary', {
|
|
171
|
-
filePhase, currentPhase, file: path.basename(filePath), action: enforce ? 'block' : 'warn'
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (enforce) {
|
|
175
|
-
return {
|
|
176
|
-
exitCode: 2,
|
|
177
|
-
output: {
|
|
178
|
-
decision: 'block',
|
|
179
|
-
reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
exitCode: 0,
|
|
186
|
-
output: {
|
|
187
|
-
hookSpecificOutput: {
|
|
188
|
-
hookEventName: 'PreToolUse',
|
|
189
|
-
additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
module.exports = { getEnforceSetting, checkBoundary };
|
|
196
|
-
if (require.main === module) { main(); }
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// PreToolUse hook (Write|Edit): Warns or blocks when editing files
|
|
4
|
+
// outside the current active phase.
|
|
5
|
+
//
|
|
6
|
+
// Reads STATE.md for current phase number. If the file being written
|
|
7
|
+
// is under .planning/phases/NN-<slug>/ and NN does not match the
|
|
8
|
+
// current phase, issues a warning or blocks depending on config.
|
|
9
|
+
//
|
|
10
|
+
// Config: safety.enforce_phase_boundaries
|
|
11
|
+
// - true = block cross-phase writes (exit 2)
|
|
12
|
+
// - false = warn only (default)
|
|
13
|
+
// - absent = warn only
|
|
14
|
+
//
|
|
15
|
+
// Files outside .planning/phases/ are always allowed (source code,
|
|
16
|
+
// config files, etc.).
|
|
17
|
+
//
|
|
18
|
+
// Exit codes:
|
|
19
|
+
// 0 = allowed or not applicable
|
|
20
|
+
// 2 = blocked (only when enforce_phase_boundaries is true)
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { logHook } = require('./hook-logger');
|
|
25
|
+
const { logEvent } = require('./event-logger');
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
let input = '';
|
|
29
|
+
|
|
30
|
+
process.stdin.setEncoding('utf8');
|
|
31
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
32
|
+
process.stdin.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(input);
|
|
35
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
36
|
+
|
|
37
|
+
if (!filePath) {
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
const planningDir = path.join(cwd, '.planning');
|
|
43
|
+
|
|
44
|
+
// Check if the file is under .planning/phases/ using marker matching
|
|
45
|
+
// instead of absolute path comparison (avoids macOS /var -> /private/var symlink issues)
|
|
46
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
47
|
+
const phasesMarker = '.planning/phases/';
|
|
48
|
+
const markerIdx = normalizedPath.indexOf(phasesMarker);
|
|
49
|
+
|
|
50
|
+
if (markerIdx === -1) {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract phase number from path after the marker
|
|
55
|
+
// Path pattern: .planning/phases/NN-slug/...
|
|
56
|
+
const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
|
|
57
|
+
const phaseMatch = afterMarker.match(/^(\d+)-/);
|
|
58
|
+
if (!phaseMatch) {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
const filePhase = parseInt(phaseMatch[1], 10);
|
|
62
|
+
|
|
63
|
+
// Get current phase from STATE.md
|
|
64
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
65
|
+
if (!fs.existsSync(stateFile)) {
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
70
|
+
const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
71
|
+
if (!currentPhaseMatch) {
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
const currentPhase = parseInt(currentPhaseMatch[1], 10);
|
|
75
|
+
|
|
76
|
+
// Same phase — always allowed
|
|
77
|
+
if (filePhase === currentPhase) {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cross-phase write detected — check config
|
|
82
|
+
const enforce = getEnforceSetting(planningDir);
|
|
83
|
+
|
|
84
|
+
logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
|
|
85
|
+
filePhase,
|
|
86
|
+
currentPhase,
|
|
87
|
+
file: path.basename(filePath)
|
|
88
|
+
});
|
|
89
|
+
logEvent('workflow', 'phase-boundary', {
|
|
90
|
+
filePhase,
|
|
91
|
+
currentPhase,
|
|
92
|
+
file: path.basename(filePath),
|
|
93
|
+
action: enforce ? 'block' : 'warn'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (enforce) {
|
|
97
|
+
const output = {
|
|
98
|
+
decision: 'block',
|
|
99
|
+
reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
|
|
100
|
+
};
|
|
101
|
+
process.stdout.write(JSON.stringify(output));
|
|
102
|
+
process.exit(2);
|
|
103
|
+
} else {
|
|
104
|
+
const output = {
|
|
105
|
+
hookSpecificOutput: {
|
|
106
|
+
hookEventName: 'PreToolUse',
|
|
107
|
+
additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
process.stdout.write(JSON.stringify(output));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} catch (_e) {
|
|
115
|
+
// Don't block on errors
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getEnforceSetting(planningDir) {
|
|
122
|
+
const configFile = path.join(planningDir, 'config.json');
|
|
123
|
+
if (!fs.existsSync(configFile)) return false;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
127
|
+
return !!(config.safety && config.safety.enforce_phase_boundaries);
|
|
128
|
+
} catch (_e) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Core boundary check logic for use by dispatchers.
|
|
135
|
+
* @param {Object} data - Parsed hook input (tool_input, etc.)
|
|
136
|
+
* @returns {null|{exitCode: number, output: Object}} null if pass, result otherwise
|
|
137
|
+
*/
|
|
138
|
+
function checkBoundary(data) {
|
|
139
|
+
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
|
140
|
+
if (!filePath) return null;
|
|
141
|
+
|
|
142
|
+
const cwd = process.cwd();
|
|
143
|
+
const planningDir = path.join(cwd, '.planning');
|
|
144
|
+
|
|
145
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
146
|
+
const phasesMarker = '.planning/phases/';
|
|
147
|
+
const markerIdx = normalizedPath.indexOf(phasesMarker);
|
|
148
|
+
if (markerIdx === -1) return null;
|
|
149
|
+
|
|
150
|
+
const afterMarker = normalizedPath.substring(markerIdx + phasesMarker.length);
|
|
151
|
+
const phaseMatch = afterMarker.match(/^(\d+)-/);
|
|
152
|
+
if (!phaseMatch) return null;
|
|
153
|
+
const filePhase = parseInt(phaseMatch[1], 10);
|
|
154
|
+
|
|
155
|
+
const stateFile = path.join(planningDir, 'STATE.md');
|
|
156
|
+
if (!fs.existsSync(stateFile)) return null;
|
|
157
|
+
|
|
158
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
159
|
+
const currentPhaseMatch = state.match(/Phase:\s*(\d+)\s+of\s+\d+/);
|
|
160
|
+
if (!currentPhaseMatch) return null;
|
|
161
|
+
const currentPhase = parseInt(currentPhaseMatch[1], 10);
|
|
162
|
+
|
|
163
|
+
if (filePhase === currentPhase) return null;
|
|
164
|
+
|
|
165
|
+
const enforce = getEnforceSetting(planningDir);
|
|
166
|
+
|
|
167
|
+
logHook('check-phase-boundary', 'PreToolUse', enforce ? 'block' : 'warn', {
|
|
168
|
+
filePhase, currentPhase, file: path.basename(filePath)
|
|
169
|
+
});
|
|
170
|
+
logEvent('workflow', 'phase-boundary', {
|
|
171
|
+
filePhase, currentPhase, file: path.basename(filePath), action: enforce ? 'block' : 'warn'
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (enforce) {
|
|
175
|
+
return {
|
|
176
|
+
exitCode: 2,
|
|
177
|
+
output: {
|
|
178
|
+
decision: 'block',
|
|
179
|
+
reason: `Cross-phase write blocked: editing phase ${filePhase} file but current phase is ${currentPhase}.\n\nFile: ${filePath}\n\nIf this is intentional, either:\n 1. Update STATE.md to reflect the correct phase\n 2. Set safety.enforce_phase_boundaries: false in config.json`
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
exitCode: 0,
|
|
186
|
+
output: {
|
|
187
|
+
hookSpecificOutput: {
|
|
188
|
+
hookEventName: 'PreToolUse',
|
|
189
|
+
additionalContext: `Warning: editing phase ${filePhase} file but current phase is ${currentPhase}. Ensure this cross-phase edit is intentional.`
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { getEnforceSetting, checkBoundary };
|
|
196
|
+
if (require.main === module) { main(); }
|