@sienklogic/plan-build-run 2.29.0 → 2.31.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dashboard/public/css/explorer.css +458 -0
  3. package/dashboard/public/css/timeline.css +240 -0
  4. package/dashboard/src/components/Layout.tsx +4 -0
  5. package/dashboard/src/components/explorer/ExplorerPage.tsx +57 -0
  6. package/dashboard/src/components/explorer/tabs/AuditsTab.tsx +29 -0
  7. package/dashboard/src/components/explorer/tabs/MilestonesTab.tsx +84 -0
  8. package/dashboard/src/components/explorer/tabs/NotesTab.tsx +32 -0
  9. package/dashboard/src/components/explorer/tabs/PhasesTab.tsx +204 -0
  10. package/dashboard/src/components/explorer/tabs/QuickTab.tsx +40 -0
  11. package/dashboard/src/components/explorer/tabs/RequirementsTab.tsx +45 -0
  12. package/dashboard/src/components/explorer/tabs/ResearchTab.tsx +47 -0
  13. package/dashboard/src/components/explorer/tabs/TodosTab.tsx +161 -0
  14. package/dashboard/src/components/settings/ConfigEditor.tsx +399 -0
  15. package/dashboard/src/components/settings/SettingsPage.tsx +44 -0
  16. package/dashboard/src/components/timeline/AnalyticsPanel.tsx +99 -0
  17. package/dashboard/src/components/timeline/DependencyGraph.tsx +23 -0
  18. package/dashboard/src/components/timeline/TimelinePage.tsx +124 -0
  19. package/dashboard/src/index.tsx +4 -0
  20. package/dashboard/src/routes/explorer.routes.tsx +226 -0
  21. package/dashboard/src/routes/timeline.routes.tsx +50 -0
  22. package/dashboard/src/services/analytics.service.d.ts +24 -0
  23. package/dashboard/src/services/audit.service.d.ts +14 -0
  24. package/dashboard/src/services/local-llm-metrics.service.d.ts +26 -0
  25. package/dashboard/src/services/milestone.service.d.ts +35 -0
  26. package/dashboard/src/services/notes.service.d.ts +14 -0
  27. package/dashboard/src/services/phase.service.d.ts +59 -0
  28. package/dashboard/src/services/quick.service.d.ts +15 -0
  29. package/dashboard/src/services/requirements.service.d.ts +19 -0
  30. package/dashboard/src/services/research.service.d.ts +27 -0
  31. package/dashboard/src/services/roadmap.service.d.ts +23 -0
  32. package/dashboard/src/services/timeline.service.d.ts +20 -0
  33. package/dashboard/src/services/timeline.service.js +174 -0
  34. package/package.json +1 -1
  35. package/plugins/copilot-pbr/plugin.json +1 -1
  36. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  37. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  38. package/plugins/pbr/scripts/config-schema.json +12 -0
  39. package/plugins/pbr/scripts/context-budget-check.js +4 -1
  40. package/plugins/pbr/scripts/enforce-pbr-workflow.js +218 -0
  41. package/plugins/pbr/scripts/pre-bash-dispatch.js +7 -0
  42. package/plugins/pbr/scripts/pre-write-dispatch.js +30 -18
  43. package/plugins/pbr/scripts/progress-tracker.js +1 -1
  44. package/plugins/pbr/scripts/validate-task.js +6 -1
@@ -0,0 +1,174 @@
1
+ import { execFile as execFileCb } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile, readdir } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { TTLCache } from '../utils/cache.js';
6
+
7
+ const execFile = promisify(execFileCb);
8
+
9
+ export const cache = new TTLCache(30_000); // 30s TTL
10
+
11
+ /**
12
+ * Run a git command in the given directory, returning stdout.
13
+ * Returns empty string on failure.
14
+ */
15
+ async function git(projectDir, args) {
16
+ try {
17
+ const { stdout } = await execFile('git', args, {
18
+ cwd: projectDir,
19
+ maxBuffer: 10 * 1024 * 1024
20
+ });
21
+ return stdout;
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Aggregate timeline events from git commits, todo completions, and STATE.md
29
+ * phase transitions into a unified chronological array.
30
+ *
31
+ * @param {string} projectDir - Absolute path to the project root
32
+ * @param {{ types?: string[], phase?: string, dateFrom?: string, dateTo?: string }} filters
33
+ * @returns {Promise<Array>}
34
+ */
35
+ export async function getTimelineEvents(projectDir, filters = {}) {
36
+ const cacheKey = `timeline:${projectDir}:${JSON.stringify(filters)}`;
37
+ const cached = cache.get(cacheKey);
38
+ if (cached) return cached;
39
+
40
+ const dateFrom = filters.dateFrom ? new Date(filters.dateFrom) : null;
41
+ const dateTo = filters.dateTo ? new Date(filters.dateTo + 'T23:59:59Z') : null;
42
+
43
+ // --- Git commits ---
44
+ let commitEvents = [];
45
+ const logOutput = await git(projectDir, [
46
+ 'log', '--all', '--format=%H|%aI|%s|%an'
47
+ ]);
48
+ if (logOutput.trim()) {
49
+ for (const line of logOutput.trim().split('\n')) {
50
+ const parts = line.split('|');
51
+ if (parts.length < 4) continue;
52
+ const [id, isoDate, subject, ...authorParts] = parts;
53
+ const author = authorParts.join('|');
54
+ const date = new Date(isoDate);
55
+ if (isNaN(date.getTime())) continue;
56
+
57
+ // Phase filter: keep only commits whose subject matches scope pattern for that phase
58
+ if (filters.phase) {
59
+ const phaseNum = String(filters.phase).padStart(2, '0');
60
+ const scopeRe = new RegExp(`\\(${phaseNum}-`);
61
+ if (!scopeRe.test(subject)) continue;
62
+ }
63
+
64
+ if (dateFrom && date < dateFrom) continue;
65
+ if (dateTo && date > dateTo) continue;
66
+
67
+ commitEvents.push({ type: 'commit', id, date, title: subject, author });
68
+ }
69
+ }
70
+
71
+ // --- Todo completions ---
72
+ let todoEvents = [];
73
+ try {
74
+ const doneDir = join(projectDir, '.planning', 'todos', 'done');
75
+ const entries = await readdir(doneDir, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
78
+ try {
79
+ const raw = await readFile(join(doneDir, entry.name), 'utf-8');
80
+ // Parse frontmatter manually
81
+ const lines = raw.split(/\r?\n/);
82
+ if (lines[0] !== '---') continue;
83
+ const endIdx = lines.indexOf('---', 1);
84
+ if (endIdx === -1) continue;
85
+ const fmLines = lines.slice(1, endIdx);
86
+ let title = '';
87
+ let completedAt = '';
88
+ for (const fmLine of fmLines) {
89
+ const titleMatch = fmLine.match(/^title:\s*['"]?(.+?)['"]?\s*$/);
90
+ if (titleMatch) title = titleMatch[1];
91
+ const completedMatch = fmLine.match(/^completed_at:\s*['"]?(.+?)['"]?\s*$/);
92
+ if (completedMatch) completedAt = completedMatch[1];
93
+ }
94
+ if (!completedAt) continue;
95
+ const date = new Date(completedAt);
96
+ if (isNaN(date.getTime())) continue;
97
+
98
+ if (dateFrom && date < dateFrom) continue;
99
+ if (dateTo && date > dateTo) continue;
100
+
101
+ todoEvents.push({
102
+ type: 'todo-completion',
103
+ id: entry.name,
104
+ date,
105
+ title: title || entry.name.replace(/^\d{3}-/, '').replace(/\.md$/, '').replace(/-/g, ' ')
106
+ });
107
+ } catch {
108
+ // skip unreadable files
109
+ }
110
+ }
111
+ } catch (err) {
112
+ if (err.code !== 'ENOENT') {
113
+ // Non-ENOENT errors: skip silently for robustness
114
+ }
115
+ }
116
+
117
+ // --- Phase transitions from STATE.md ---
118
+ let phaseEvents = [];
119
+ try {
120
+ const statePath = join(projectDir, '.planning', 'STATE.md');
121
+ const raw = await readFile(statePath, 'utf-8');
122
+ const lines = raw.split(/\r?\n/);
123
+
124
+ // Best-effort: extract current phase and last updated
125
+ let currentPhase = '';
126
+ let currentStatus = '';
127
+ let lastUpdated = '';
128
+
129
+ for (const line of lines) {
130
+ const phaseMatch = line.match(/\*\*Current phase:\*\*\s*(.+)/);
131
+ if (phaseMatch) currentPhase = phaseMatch[1].trim();
132
+
133
+ const statusMatch = line.match(/\*\*Status:\*\*\s*(.+)/);
134
+ if (statusMatch) currentStatus = statusMatch[1].trim();
135
+
136
+ const updatedMatch = line.match(/\*\*Last updated:\*\*\s*(.+)/);
137
+ if (updatedMatch) lastUpdated = updatedMatch[1].trim();
138
+ }
139
+
140
+ if (lastUpdated) {
141
+ const date = new Date(lastUpdated);
142
+ if (!isNaN(date.getTime())) {
143
+ const title = currentPhase
144
+ ? `Phase ${currentPhase} — ${currentStatus || 'active'}`
145
+ : `Status: ${currentStatus || 'active'}`;
146
+
147
+ if ((!dateFrom || date >= dateFrom) && (!dateTo || date <= dateTo)) {
148
+ phaseEvents.push({
149
+ type: 'phase-transition',
150
+ id: 'state-current',
151
+ date,
152
+ title
153
+ });
154
+ }
155
+ }
156
+ }
157
+ } catch (err) {
158
+ if (err.code !== 'ENOENT') {
159
+ // Non-ENOENT: skip silently
160
+ }
161
+ }
162
+
163
+ // Merge and sort descending (newest first)
164
+ let events = [...commitEvents, ...todoEvents, ...phaseEvents];
165
+ events.sort((a, b) => b.date - a.date);
166
+
167
+ // Apply types filter
168
+ if (filters.types && filters.types.length > 0) {
169
+ events = events.filter(e => filters.types.includes(e.type));
170
+ }
171
+
172
+ cache.set(cacheKey, events);
173
+ return events;
174
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sienklogic/plan-build-run",
3
- "version": "2.29.0",
3
+ "version": "2.31.0",
4
4
  "description": "Plan it, Build it, Run it — structured development workflow for Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pbr",
3
3
  "displayName": "Plan-Build-Run",
4
- "version": "2.29.0",
4
+ "version": "2.31.0",
5
5
  "description": "Plan-Build-Run — Structured development workflow for GitHub Copilot CLI. Solves context rot through disciplined agent delegation, structured planning, atomic execution, and goal-backward verification.",
6
6
  "author": {
7
7
  "name": "SienkLogic",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pbr",
3
3
  "displayName": "Plan-Build-Run",
4
- "version": "2.29.0",
4
+ "version": "2.31.0",
5
5
  "description": "Plan-Build-Run — Structured development workflow for Cursor. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
6
6
  "author": {
7
7
  "name": "SienkLogic",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pbr",
3
- "version": "2.29.0",
3
+ "version": "2.31.0",
4
4
  "description": "Plan-Build-Run — Structured development workflow for Claude Code. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
5
5
  "author": {
6
6
  "name": "SienkLogic",
@@ -254,6 +254,18 @@
254
254
  },
255
255
  "additionalProperties": false
256
256
  },
257
+ "workflow": {
258
+ "type": "object",
259
+ "properties": {
260
+ "enforce_pbr_skills": {
261
+ "type": "string",
262
+ "enum": ["advisory", "block", "off"],
263
+ "default": "advisory",
264
+ "description": "Enforcement level for PBR workflow compliance"
265
+ }
266
+ },
267
+ "additionalProperties": false
268
+ },
257
269
  "local_llm": {
258
270
  "type": "object",
259
271
  "properties": {
@@ -268,7 +268,10 @@ function readConfigHighlights(planningDir) {
268
268
  }
269
269
 
270
270
  function buildRecoveryContext(activeOp, roadmapSummary, currentPlan, configHighlights, recentErrors, recentAgents) {
271
- const parts = ['[Post-Compaction Recovery] Context was auto-compacted. Key state preserved:'];
271
+ const parts = [
272
+ '[Post-Compaction Recovery] Context was auto-compacted. Key state preserved:',
273
+ '[PBR WORKFLOW REQUIRED — Route all work through PBR commands]\n- Fix a bug or small task → /pbr:quick\n- Plan a feature → /pbr:plan N\n- Build from a plan → /pbr:build N\n- Explore or research → /pbr:explore\n- Freeform request → /pbr:do\n- Do NOT write source code or spawn generic agents without an active PBR skill.\n- Use PBR agents (pbr:researcher, pbr:executor, etc.) not Explore/general-purpose.'
274
+ ];
272
275
 
273
276
  if (activeOp) parts.push(`Active operation: ${activeOp}`);
274
277
  if (currentPlan) parts.push(`Current plan: ${currentPlan}`);
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PBR workflow enforcement checks.
5
+ *
6
+ * Provides advisory and blocking checks that ensure PBR workflow is
7
+ * followed even when the user doesn't explicitly invoke /pbr:* commands.
8
+ *
9
+ * Functions:
10
+ * loadEnforcementConfig(planningDir) — reads config.json for enforcement level
11
+ * checkUnmanagedSourceWrite(data) — PreToolUse Write|Edit: warns/blocks unmanaged source writes
12
+ * checkNonPbrAgent(data) — PreToolUse Task: advises using pbr:* agents
13
+ * checkUnmanagedCommit(data) — PreToolUse Bash: advises git commits to use /pbr:quick
14
+ *
15
+ * All functions return null for pass, or { exitCode, output } for action.
16
+ * checkNonPbrAgent always returns advisory (exitCode 0) — never blocks Task().
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { logHook } = require('./hook-logger');
24
+
25
+ /**
26
+ * Load the enforcement configuration from .planning/config.json.
27
+ * Also checks for .planning/.native-mode which bypasses all enforcement.
28
+ *
29
+ * @param {string} planningDir - absolute path to the .planning directory
30
+ * @returns {{ level: "advisory"|"block"|"off" }}
31
+ */
32
+ function loadEnforcementConfig(planningDir) {
33
+ // .native-mode bypass takes precedence over all config settings
34
+ const nativeModeFile = path.join(planningDir, '.native-mode');
35
+ if (fs.existsSync(nativeModeFile)) {
36
+ return { level: 'off' };
37
+ }
38
+
39
+ try {
40
+ const configPath = path.join(planningDir, 'config.json');
41
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
42
+ const level = parsed.workflow && parsed.workflow.enforce_pbr_skills;
43
+ if (level === 'advisory' || level === 'block' || level === 'off') {
44
+ return { level };
45
+ }
46
+ } catch (_e) {
47
+ // No config or parse error — use default
48
+ }
49
+
50
+ return { level: 'advisory' };
51
+ }
52
+
53
+ /**
54
+ * PreToolUse Write|Edit: warns or blocks source file writes that happen
55
+ * without an active PBR skill.
56
+ *
57
+ * Skip conditions:
58
+ * - No .planning/ directory (not a PBR project)
59
+ * - .planning/.active-skill exists (PBR skill is managing the session)
60
+ * - Target file is inside .planning/ (planning files are always OK)
61
+ * - Enforcement level is "off"
62
+ *
63
+ * @param {Object} data - parsed hook input from Claude Code
64
+ * @returns {null|{ exitCode: number, output: Object }}
65
+ */
66
+ function checkUnmanagedSourceWrite(data) {
67
+ const filePath = data.tool_input && (data.tool_input.file_path || data.tool_input.path);
68
+ if (!filePath) return null;
69
+
70
+ const cwd = process.cwd();
71
+ const planningDir = path.join(cwd, '.planning');
72
+
73
+ // Skip if not a PBR project
74
+ if (!fs.existsSync(planningDir)) return null;
75
+
76
+ // Skip if a PBR skill is active
77
+ const activeSkillFile = path.join(planningDir, '.active-skill');
78
+ if (fs.existsSync(activeSkillFile)) return null;
79
+
80
+ // Skip if writing inside .planning/
81
+ const normalizedFile = filePath.replace(/\\/g, '/');
82
+ const normalizedPlanning = planningDir.replace(/\\/g, '/');
83
+ if (normalizedFile.startsWith(normalizedPlanning)) return null;
84
+
85
+ // Also check with resolved symlinks (macOS /var → /private/var)
86
+ try {
87
+ const resolvedPlanning = fs.realpathSync(planningDir).replace(/\\/g, '/');
88
+ if (normalizedFile.startsWith(resolvedPlanning)) return null;
89
+ } catch (_e) { /* not resolvable */ }
90
+
91
+ const config = loadEnforcementConfig(planningDir);
92
+ if (config.level === 'off') return null;
93
+
94
+ const message =
95
+ 'PBR workflow required: You are editing source code without an active PBR skill. ' +
96
+ 'Route this work through a PBR command: /pbr:quick (small fix), /pbr:build (planned work), ' +
97
+ 'or /pbr:do (auto-route). Set workflow.enforce_pbr_skills: off in config to disable.';
98
+
99
+ if (config.level === 'block') {
100
+ logHook('enforce-pbr-workflow', 'PreToolUse', 'block', { file: path.basename(filePath), level: 'block' });
101
+ return {
102
+ exitCode: 2,
103
+ output: { decision: 'block', reason: message }
104
+ };
105
+ }
106
+
107
+ // advisory (default)
108
+ logHook('enforce-pbr-workflow', 'PreToolUse', 'advisory', { file: path.basename(filePath), level: 'advisory' });
109
+ return {
110
+ exitCode: 0,
111
+ output: { additionalContext: '[pbr] ' + message }
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Agent type → PBR agent mapping for advisory messages.
117
+ */
118
+ const AGENT_MAPPING = {
119
+ 'Explore': 'pbr:researcher or pbr:codebase-mapper',
120
+ 'general-purpose': 'pbr:general',
121
+ 'Plan': 'pbr:planner',
122
+ 'Bash': 'pbr:executor'
123
+ };
124
+
125
+ /**
126
+ * PreToolUse Task: advises using pbr:* agents instead of generic agents.
127
+ *
128
+ * Skip conditions:
129
+ * - No .planning/ directory (not a PBR project)
130
+ * - subagent_type starts with "pbr:" (already using PBR agent)
131
+ * - subagent_type is missing/empty (can't determine type)
132
+ * - Enforcement level is "off"
133
+ *
134
+ * NOTE: This function NEVER blocks Task() — blocking is too disruptive.
135
+ * It always returns an advisory (exitCode 0) or null.
136
+ *
137
+ * @param {Object} data - parsed hook input from Claude Code
138
+ * @returns {null|{ exitCode: 0, output: Object }}
139
+ */
140
+ function checkNonPbrAgent(data) {
141
+ const subagentType = data.tool_input && data.tool_input.subagent_type;
142
+ if (!subagentType || typeof subagentType !== 'string') return null;
143
+
144
+ // Already using a PBR agent
145
+ if (subagentType.startsWith('pbr:')) return null;
146
+
147
+ const cwd = process.cwd();
148
+ const planningDir = path.join(cwd, '.planning');
149
+
150
+ // Skip if not a PBR project
151
+ if (!fs.existsSync(planningDir)) return null;
152
+
153
+ const config = loadEnforcementConfig(planningDir);
154
+ if (config.level === 'off') return null;
155
+
156
+ const suggestion = AGENT_MAPPING[subagentType] || 'a pbr:* agent (e.g., pbr:researcher, pbr:general, pbr:executor)';
157
+ const message =
158
+ `PBR workflow advisory: spawning generic agent "${subagentType}" without PBR routing. ` +
159
+ `Use ${suggestion} instead to maintain audit logging and workflow context. ` +
160
+ 'PBR agents are auto-loaded via subagent_type — no extra setup needed.';
161
+
162
+ logHook('enforce-pbr-workflow', 'PreToolUse', 'advisory', { agentType: subagentType, suggestion });
163
+ return {
164
+ exitCode: 0,
165
+ output: { additionalContext: '[pbr] ' + message }
166
+ };
167
+ }
168
+
169
+ /**
170
+ * PreToolUse Bash: advises using /pbr:quick when git commit is run without
171
+ * an active PBR skill.
172
+ *
173
+ * Skip conditions:
174
+ * - No .planning/ directory (not a PBR project)
175
+ * - .planning/.active-skill exists (PBR skill is managing the session)
176
+ * - Command is not a git commit
177
+ * - Enforcement level is "off"
178
+ *
179
+ * @param {Object} data - parsed hook input from Claude Code
180
+ * @returns {null|{ exitCode: 0, output: Object }}
181
+ */
182
+ function checkUnmanagedCommit(data) {
183
+ const command = data.tool_input && data.tool_input.command;
184
+ if (!command || typeof command !== 'string') return null;
185
+
186
+ // Only check git commit commands
187
+ if (!/\bgit\s+commit\b/.test(command)) return null;
188
+
189
+ const cwd = process.cwd();
190
+ const planningDir = path.join(cwd, '.planning');
191
+
192
+ // Skip if not a PBR project
193
+ if (!fs.existsSync(planningDir)) return null;
194
+
195
+ // Skip if a PBR skill is active
196
+ const activeSkillFile = path.join(planningDir, '.active-skill');
197
+ if (fs.existsSync(activeSkillFile)) return null;
198
+
199
+ const config = loadEnforcementConfig(planningDir);
200
+ if (config.level === 'off') return null;
201
+
202
+ const message =
203
+ 'Committing without PBR tracking. Use /pbr:quick to track this work. ' +
204
+ 'PBR quick tasks create atomic commits with proper scope and audit trail.';
205
+
206
+ logHook('enforce-pbr-workflow', 'PreToolUse', 'advisory', { check: 'unmanaged-commit' });
207
+ return {
208
+ exitCode: 0,
209
+ output: { additionalContext: '[pbr] ' + message }
210
+ };
211
+ }
212
+
213
+ module.exports = {
214
+ loadEnforcementConfig,
215
+ checkUnmanagedSourceWrite,
216
+ checkNonPbrAgent,
217
+ checkUnmanagedCommit
218
+ };
@@ -49,6 +49,7 @@
49
49
  const { logHook } = require('./hook-logger');
50
50
  const { checkDangerous } = require('./check-dangerous-commands');
51
51
  const { checkCommit, enrichCommitLlm } = require('./validate-commit');
52
+ const { checkUnmanagedCommit } = require('./enforce-pbr-workflow');
52
53
 
53
54
  function main() {
54
55
  let input = '';
@@ -97,6 +98,12 @@ function main() {
97
98
  }
98
99
  }
99
100
 
101
+ // Unmanaged commit advisory — warn when git commit runs without PBR skill
102
+ const unmanagedCommitResult = checkUnmanagedCommit(data);
103
+ if (unmanagedCommitResult) {
104
+ warnings.push(unmanagedCommitResult.output.additionalContext);
105
+ }
106
+
100
107
  // LLM commit semantic classification — advisory only
101
108
  const llmAdvisory = await enrichCommitLlm(data);
102
109
  if (llmAdvisory) {
@@ -3,33 +3,35 @@
3
3
  /**
4
4
  * PreToolUse dispatcher for Write|Edit hooks.
5
5
  *
6
- * Consolidates check-skill-workflow.js, check-phase-boundary.js,
7
- * and check-doc-sprawl.js into a single process, reading stdin once
8
- * and running all checks sequentially.
6
+ * Consolidates all PreToolUse Write|Edit checks into a single process,
7
+ * reading stdin once and running all checks sequentially.
9
8
  *
10
9
  * ── Dispatch Order & Rationale ──────────────────────────────────
11
10
  *
12
- * 1. check-skill-workflow — Enforces planning-phase rules (e.g. no
13
- * code writes during the plan phase). Runs first because workflow
14
- * violations are the most fundamental: if the write shouldn't
15
- * happen at all in the current workflow state, there's no point
16
- * evaluating boundary or sprawl rules. Can block (exit 2).
11
+ * 1. enforce-pbr-workflow — Warns or blocks source code writes that
12
+ * happen without an active PBR skill. Runs first because this is
13
+ * the most fundamental enforcement: if no PBR skill is managing
14
+ * the session, all other skill-specific checks are moot.
15
+ * Can block (exit 2) or advise (exit 0). Config-driven.
17
16
  *
18
- * 2. check-summary-gate — Blocks STATE.md status advancement
17
+ * 2. check-agent-state-write — Blocks subagents from writing STATE.md
18
+ * directly (except pbr:general). Runs before skill-specific checks
19
+ * because agent isolation is a hard invariant. Can block (exit 2).
20
+ *
21
+ * 3. check-skill-workflow — Enforces planning-phase rules (e.g. no
22
+ * code writes during the plan phase). Can block (exit 2).
23
+ *
24
+ * 4. check-summary-gate — Blocks STATE.md status advancement
19
25
  * (to built/verified/complete) unless a SUMMARY file exists for
20
- * the current phase. Prevents inconsistent state where a phase
21
- * appears complete but has no build receipt. Can block (exit 2).
26
+ * the current phase. Can block (exit 2).
22
27
  *
23
- * 3. check-phase-boundary — Guards against writes that target files
24
- * outside the current phase directory. Runs second because once
25
- * we know the write is allowed by workflow rules, we need to
26
- * verify it's scoped to the correct phase. Can block (exit 2)
28
+ * 5. check-phase-boundary — Guards against writes that target files
29
+ * outside the current phase directory. Can block (exit 2)
27
30
  * or warn (exit 0 with message).
28
31
  *
29
- * 4. check-doc-sprawl — Prevents creation of new .md/.txt files
32
+ * 6. check-doc-sprawl — Prevents creation of new .md/.txt files
30
33
  * outside a known allowlist (when enabled in config). Runs last
31
- * because it's the most granular check only relevant for new
32
- * documentation files, not all writes. Can block (exit 2).
34
+ * because it's the most granular check. Can block (exit 2).
33
35
  *
34
36
  * ── Short-Circuit Behavior ──────────────────────────────────────
35
37
  *
@@ -62,6 +64,7 @@ const { checkWorkflow } = require('./check-skill-workflow');
62
64
  const { checkSummaryGate } = require('./check-summary-gate');
63
65
  const { checkBoundary } = require('./check-phase-boundary');
64
66
  const { checkDocSprawl } = require('./check-doc-sprawl');
67
+ const { checkUnmanagedSourceWrite } = require('./enforce-pbr-workflow');
65
68
 
66
69
  function main() {
67
70
  let input = '';
@@ -72,6 +75,15 @@ function main() {
72
75
  try {
73
76
  const data = JSON.parse(input);
74
77
 
78
+ // Unmanaged source write check — runs first as the most fundamental
79
+ // workflow enforcement: warn/block when source code is edited without
80
+ // an active PBR skill managing the session.
81
+ const unmanagedResult = checkUnmanagedSourceWrite(data);
82
+ if (unmanagedResult) {
83
+ process.stdout.write(JSON.stringify(unmanagedResult.output));
84
+ process.exit(unmanagedResult.exitCode || 0);
85
+ }
86
+
75
87
  // Agent STATE.md write blocker — most fundamental check
76
88
  const agentResult = checkAgentStateWrite(data);
77
89
  if (agentResult) {
@@ -262,7 +262,7 @@ function buildContext(planningDir, stateFile) {
262
262
  parts.push(`\n${hookHealth}`);
263
263
  }
264
264
 
265
- parts.push('\nAvailable commands: /pbr:status, /pbr:plan, /pbr:build, /pbr:review, /pbr:help');
265
+ parts.push('\n[PBR WORKFLOW REQUIRED — Route all work through PBR commands]\n- Fix a bug or small task → /pbr:quick\n- Plan a feature → /pbr:plan N\n- Build from a plan → /pbr:build N\n- Explore or research → /pbr:explore\n- Freeform request → /pbr:do\n- Do NOT write source code or spawn generic agents without an active PBR skill.\n- Use PBR agents (pbr:researcher, pbr:executor, etc.) not Explore/general-purpose.');
266
266
 
267
267
  return parts.join('\n');
268
268
  }
@@ -22,6 +22,7 @@ const path = require('path');
22
22
  const { logHook } = require('./hook-logger');
23
23
  const { resolveConfig } = require('./local-llm/health');
24
24
  const { validateTask: llmValidateTask } = require('./local-llm/operations/validate-task');
25
+ const { checkNonPbrAgent } = require('./enforce-pbr-workflow');
25
26
 
26
27
  /**
27
28
  * Load and resolve the local_llm config block from .planning/config.json.
@@ -48,7 +49,9 @@ const KNOWN_AGENTS = [
48
49
  'debugger',
49
50
  'codebase-mapper',
50
51
  'synthesizer',
51
- 'general'
52
+ 'general',
53
+ 'audit',
54
+ 'dev-sync'
52
55
  ];
53
56
 
54
57
  const MAX_DESCRIPTION_LENGTH = 100;
@@ -800,6 +803,8 @@ function main() {
800
803
  if (debuggerWarning) warnings.push(debuggerWarning);
801
804
  const activeSkillWarning = checkActiveSkillIntegrity(data);
802
805
  if (activeSkillWarning) warnings.push(activeSkillWarning);
806
+ const nonPbrAgentResult = checkNonPbrAgent(data);
807
+ if (nonPbrAgentResult) warnings.push(nonPbrAgentResult.output.additionalContext);
803
808
 
804
809
  // LLM task coherence check — advisory only
805
810
  try {