@sienklogic/plan-build-run 2.30.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.
@@ -0,0 +1,44 @@
1
+ import { ConfigEditor } from './ConfigEditor';
2
+
3
+ interface SettingsPageProps {
4
+ config: object;
5
+ activeTab?: 'config' | 'logs';
6
+ }
7
+
8
+ export function SettingsPage({ config, activeTab }: SettingsPageProps) {
9
+ const initialTab = activeTab || 'config';
10
+ return (
11
+ <div class="settings" x-data={`{ tab: '${initialTab}' }`}>
12
+ <h1 class="page-title">Settings</h1>
13
+
14
+ <div class="settings-tabs" role="tablist">
15
+ <button
16
+ role="tab"
17
+ class="tab-btn"
18
+ x-on:click="tab = 'config'"
19
+ x-bind:class="{ active: tab === 'config' }"
20
+ x-bind:aria-selected="tab === 'config'"
21
+ >
22
+ Config
23
+ </button>
24
+ <button
25
+ role="tab"
26
+ class="tab-btn"
27
+ x-on:click="tab = 'logs'"
28
+ x-bind:class="{ active: tab === 'logs' }"
29
+ x-bind:aria-selected="tab === 'logs'"
30
+ >
31
+ Logs
32
+ </button>
33
+ </div>
34
+
35
+ <div x-show="tab === 'config'">
36
+ <ConfigEditor config={config} />
37
+ </div>
38
+
39
+ <div x-show="tab === 'logs'" x-cloak>
40
+ <p>Log Viewer — <a href="/settings/logs">Open Log Viewer</a></p>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,99 @@
1
+ interface AnalyticsProps {
2
+ analytics: any;
3
+ llmMetrics: any | null;
4
+ }
5
+
6
+ export function AnalyticsPanelFragment({ analytics, llmMetrics }: AnalyticsProps) {
7
+ const summary = analytics?.summary ?? {};
8
+ const phases: any[] = analytics?.phases ?? [];
9
+
10
+ return (
11
+ <div class="analytics-panel">
12
+ {/* Git & Phase stats */}
13
+ <section class="analytics-panel__section">
14
+ <h2>Git &amp; Phase Stats</h2>
15
+ <div class="analytics-panel__stats">
16
+ <div class="analytics-panel__stat">
17
+ <span class="analytics-panel__stat-value">{summary.totalCommits ?? 0}</span>
18
+ <span class="analytics-panel__stat-label">Total Commits</span>
19
+ </div>
20
+ <div class="analytics-panel__stat">
21
+ <span class="analytics-panel__stat-value">{summary.totalLinesChanged ?? 0}</span>
22
+ <span class="analytics-panel__stat-label">Lines Changed</span>
23
+ </div>
24
+ <div class="analytics-panel__stat">
25
+ <span class="analytics-panel__stat-value">{summary.avgDuration ?? 'N/A'}</span>
26
+ <span class="analytics-panel__stat-label">Avg Phase Duration</span>
27
+ </div>
28
+ <div class="analytics-panel__stat">
29
+ <span class="analytics-panel__stat-value">{summary.totalPhases ?? 0}</span>
30
+ <span class="analytics-panel__stat-label">Phases</span>
31
+ </div>
32
+ </div>
33
+ {phases.length > 0 ? (
34
+ <table class="analytics-panel__table">
35
+ <thead>
36
+ <tr>
37
+ <th>Phase</th>
38
+ <th>Name</th>
39
+ <th>Commits</th>
40
+ <th>Lines Changed</th>
41
+ <th>Duration</th>
42
+ <th>Plans</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody>
46
+ {phases.map((p: any) => (
47
+ <tr key={p.phaseId}>
48
+ <td>{p.phaseId}</td>
49
+ <td>{p.phaseName}</td>
50
+ <td>{p.commitCount}</td>
51
+ <td>{p.linesChanged}</td>
52
+ <td>{p.duration ?? '—'}</td>
53
+ <td>{p.planCount}</td>
54
+ </tr>
55
+ ))}
56
+ </tbody>
57
+ </table>
58
+ ) : (
59
+ <p class="analytics-panel__empty">No phase data available.</p>
60
+ )}
61
+ </section>
62
+
63
+ {/* LLM Offload stats */}
64
+ <section class="analytics-panel__section">
65
+ <h2>LLM Offload Metrics</h2>
66
+ {llmMetrics === null ? (
67
+ <p class="analytics-panel__empty">No LLM metrics data — run local model integrations to populate.</p>
68
+ ) : (
69
+ <div class="analytics-panel__stats">
70
+ <div class="analytics-panel__stat">
71
+ <span class="analytics-panel__stat-value">{llmMetrics.summary.total_calls}</span>
72
+ <span class="analytics-panel__stat-label">Total Calls</span>
73
+ </div>
74
+ <div class="analytics-panel__stat">
75
+ <span class="analytics-panel__stat-value">{llmMetrics.summary.fallback_rate_pct}%</span>
76
+ <span class="analytics-panel__stat-label">Fallback Rate</span>
77
+ </div>
78
+ <div class="analytics-panel__stat">
79
+ <span class="analytics-panel__stat-value">{llmMetrics.summary.tokens_saved}</span>
80
+ <span class="analytics-panel__stat-label">Tokens Saved</span>
81
+ </div>
82
+ <div class="analytics-panel__stat">
83
+ <span class="analytics-panel__stat-value">${llmMetrics.summary.cost_saved_usd}</span>
84
+ <span class="analytics-panel__stat-label">Cost Saved</span>
85
+ </div>
86
+ </div>
87
+ )}
88
+ </section>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ export function AnalyticsPanel() {
94
+ return (
95
+ <div id="analytics-panel" hx-get="/api/timeline/analytics" hx-trigger="intersect once" hx-swap="innerHTML">
96
+ <div class="analytics-panel__loading">Loading analytics...</div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,23 @@
1
+ export function DependencyGraphFragment({ mermaidDef }: { mermaidDef: string }) {
2
+ return (
3
+ <div class="dep-graph">
4
+ <div
5
+ class="dep-graph__container"
6
+ id="dep-graph-render"
7
+ x-data="{ rendered: false }"
8
+ x-init={`$nextTick(() => { mermaid.render('dep-graph-svg', $el.dataset.def).then(r => { $el.innerHTML = r.svg; rendered = true; }) })`}
9
+ data-def={mermaidDef}
10
+ >
11
+ <div class="dep-graph__loading" x-show="!rendered">Rendering graph...</div>
12
+ </div>
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export function DependencyGraph() {
18
+ return (
19
+ <div id="dep-graph-panel" hx-get="/api/timeline/dependency-graph" hx-trigger="intersect once" hx-swap="innerHTML">
20
+ <div class="dep-graph__loading">Loading dependency graph...</div>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,124 @@
1
+ import { AnalyticsPanel } from './AnalyticsPanel';
2
+ import { DependencyGraph } from './DependencyGraph';
3
+
4
+ export function TimelinePage() {
5
+ return (
6
+ <div class="timeline" x-data="{ activeSection: 'events' }">
7
+ <h1 class="page-title">Timeline</h1>
8
+ <div class="timeline__section-tabs" role="tablist">
9
+ <button
10
+ role="tab"
11
+ x-bind:aria-selected="activeSection === 'events'"
12
+ x-on:click="activeSection = 'events'"
13
+ class="timeline__section-tab"
14
+ >
15
+ Event Stream
16
+ </button>
17
+ <button
18
+ role="tab"
19
+ x-bind:aria-selected="activeSection === 'analytics'"
20
+ x-on:click="activeSection = 'analytics'"
21
+ class="timeline__section-tab"
22
+ >
23
+ Analytics
24
+ </button>
25
+ <button
26
+ role="tab"
27
+ x-bind:aria-selected="activeSection === 'graph'"
28
+ x-on:click="activeSection = 'graph'"
29
+ class="timeline__section-tab"
30
+ >
31
+ Dependency Graph
32
+ </button>
33
+ </div>
34
+
35
+ <div x-show="activeSection === 'events'">
36
+ <div class="timeline__filters">
37
+ <label>
38
+ <input type="checkbox" name="types" value="commit" />
39
+ Commits
40
+ </label>
41
+ <label>
42
+ <input type="checkbox" name="types" value="phase-transition" />
43
+ Phase Transitions
44
+ </label>
45
+ <label>
46
+ <input type="checkbox" name="types" value="todo-completion" />
47
+ Todo Completions
48
+ </label>
49
+
50
+ <select name="phase">
51
+ <option value="">All phases</option>
52
+ </select>
53
+
54
+ <input type="date" name="dateFrom" aria-label="From date" />
55
+ <input type="date" name="dateTo" aria-label="To date" />
56
+
57
+ <button
58
+ type="button"
59
+ hx-get="/api/timeline/events"
60
+ hx-include="closest .timeline__filters"
61
+ hx-target="#timeline-stream"
62
+ hx-swap="innerHTML"
63
+ hx-indicator="#timeline-loading"
64
+ >
65
+ Apply Filters
66
+ </button>
67
+ </div>
68
+
69
+ <div id="timeline-loading" class="timeline__loading htmx-indicator">Refreshing...</div>
70
+
71
+ <div
72
+ id="timeline-stream"
73
+ class="timeline__stream"
74
+ hx-get="/api/timeline/events"
75
+ hx-trigger="load"
76
+ hx-swap="innerHTML"
77
+ >
78
+ <div class="timeline__loading">Loading events...</div>
79
+ </div>
80
+ </div>
81
+
82
+ <div x-show="activeSection === 'analytics'">
83
+ <AnalyticsPanel />
84
+ </div>
85
+
86
+ <div x-show="activeSection === 'graph'">
87
+ <DependencyGraph />
88
+ </div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function formatDate(date: Date): string {
94
+ const y = date.getUTCFullYear();
95
+ const mo = String(date.getUTCMonth() + 1).padStart(2, '0');
96
+ const d = String(date.getUTCDate()).padStart(2, '0');
97
+ const h = String(date.getUTCHours()).padStart(2, '0');
98
+ const mi = String(date.getUTCMinutes()).padStart(2, '0');
99
+ return `${y}-${mo}-${d} ${h}:${mi}`;
100
+ }
101
+
102
+ export function EventStreamFragment({ events }: { events: any[] }) {
103
+ if (events.length === 0) {
104
+ return <p class="timeline__empty">No events match the current filters.</p>;
105
+ }
106
+
107
+ return (
108
+ <ol class="timeline__list">
109
+ {events.map((event) => (
110
+ <li key={event.id} class={`timeline__event timeline__event--${event.type}`}>
111
+ <span class="timeline__event-dot" aria-hidden="true"></span>
112
+ <time class="timeline__event-time" datetime={event.date.toISOString()}>
113
+ {formatDate(event.date)}
114
+ </time>
115
+ <span class="timeline__event-type">{event.type}</span>
116
+ <span class="timeline__event-title">{event.title}</span>
117
+ {event.author && (
118
+ <span class="timeline__event-author">{event.author}</span>
119
+ )}
120
+ </li>
121
+ ))}
122
+ </ol>
123
+ );
124
+ }
@@ -8,6 +8,7 @@ import { Layout } from './components/Layout';
8
8
  import { indexRouter } from './routes/index.routes';
9
9
  import { commandCenterRouter } from './routes/command-center.routes';
10
10
  import { explorerRouter } from './routes/explorer.routes';
11
+ import { timelineRouter } from './routes/timeline.routes';
11
12
  import { sseHandler } from './sse-handler';
12
13
  import { startWatcher } from './watcher-setup';
13
14
  import { currentPhaseMiddleware } from './middleware/current-phase';
@@ -70,6 +71,7 @@ function createApp(config: ServerConfig) {
70
71
  app.route('/', indexRouter);
71
72
  app.route('/api/command-center', commandCenterRouter);
72
73
  app.route('/', explorerRouter);
74
+ app.route('/', timelineRouter);
73
75
 
74
76
  // SSE endpoint — real streamSSE handler with multi-client broadcast
75
77
  app.get('/api/events/stream', sseHandler);
@@ -0,0 +1,50 @@
1
+ import { Hono } from 'hono';
2
+ import { Layout } from '../components/Layout';
3
+ import { TimelinePage, EventStreamFragment } from '../components/timeline/TimelinePage';
4
+ import { AnalyticsPanelFragment } from '../components/timeline/AnalyticsPanel';
5
+ import { DependencyGraphFragment } from '../components/timeline/DependencyGraph';
6
+ import { getTimelineEvents } from '../services/timeline.service.js';
7
+ import { getProjectAnalytics } from '../services/analytics.service.js';
8
+ import { getLlmMetrics } from '../services/local-llm-metrics.service.js';
9
+ import { generateDependencyMermaid } from '../services/roadmap.service.js';
10
+
11
+ type Env = { Variables: { projectDir: string } };
12
+
13
+ const router = new Hono<Env>();
14
+
15
+ router.get('/timeline', async (c) => {
16
+ const isHtmx = c.req.header('HX-Request');
17
+ const content = <TimelinePage />;
18
+ if (isHtmx) return c.html(content);
19
+ return c.html(<Layout title="Timeline" currentView="timeline">{content}</Layout>);
20
+ });
21
+
22
+ router.get('/api/timeline/events', async (c) => {
23
+ const projectDir = c.get('projectDir');
24
+ const { types, phase, dateFrom, dateTo } = c.req.query();
25
+ const filters = {
26
+ types: types ? (Array.isArray(types) ? types : [types]) : [],
27
+ phase: phase || '',
28
+ dateFrom: dateFrom || '',
29
+ dateTo: dateTo || ''
30
+ };
31
+ const events = await getTimelineEvents(projectDir, filters).catch(() => []);
32
+ return c.html(<EventStreamFragment events={events} />);
33
+ });
34
+
35
+ router.get('/api/timeline/analytics', async (c) => {
36
+ const projectDir = c.get('projectDir');
37
+ const [analytics, llmMetrics] = await Promise.all([
38
+ getProjectAnalytics(projectDir).catch(() => ({ phases: [], summary: {} })),
39
+ getLlmMetrics(projectDir).catch(() => null)
40
+ ]);
41
+ return c.html(<AnalyticsPanelFragment analytics={analytics} llmMetrics={llmMetrics} />);
42
+ });
43
+
44
+ router.get('/api/timeline/dependency-graph', async (c) => {
45
+ const projectDir = c.get('projectDir');
46
+ const mermaidDef = await generateDependencyMermaid(projectDir).catch(() => 'graph TD\n err["Could not load graph"]');
47
+ return c.html(<DependencyGraphFragment mermaidDef={mermaidDef} />);
48
+ });
49
+
50
+ export { router as timelineRouter };
@@ -0,0 +1,24 @@
1
+ export interface PhaseAnalytics {
2
+ phaseId: string;
3
+ phaseName: string;
4
+ commitCount: number;
5
+ duration: string | null;
6
+ planCount: number;
7
+ linesChanged: number;
8
+ }
9
+
10
+ export interface AnalyticsSummary {
11
+ totalCommits: number;
12
+ totalPhases: number;
13
+ avgDuration: string;
14
+ totalLinesChanged: number;
15
+ }
16
+
17
+ export interface ProjectAnalytics {
18
+ phases: PhaseAnalytics[];
19
+ summary: AnalyticsSummary;
20
+ warning?: string;
21
+ }
22
+
23
+ export declare const cache: any;
24
+ export declare function getProjectAnalytics(projectDir: string): Promise<ProjectAnalytics>;
@@ -0,0 +1,26 @@
1
+ export interface LlmMetricsSummary {
2
+ total_calls: number;
3
+ fallback_count: number;
4
+ fallback_rate_pct: number;
5
+ avg_latency_ms: number;
6
+ tokens_saved: number;
7
+ cost_saved_usd: number;
8
+ }
9
+
10
+ export interface LlmOperationMetrics {
11
+ operation: string;
12
+ calls: number;
13
+ fallbacks: number;
14
+ tokens_saved: number;
15
+ }
16
+
17
+ export interface LlmMetrics {
18
+ summary: LlmMetricsSummary;
19
+ byOperation: LlmOperationMetrics[];
20
+ baseline: {
21
+ hook_invocations: number;
22
+ estimated_frontier_tokens_without_local: number;
23
+ };
24
+ }
25
+
26
+ export declare function getLlmMetrics(projectDir: string): Promise<LlmMetrics | null>;
@@ -0,0 +1,20 @@
1
+ import { TTLCache } from '../utils/cache.js';
2
+
3
+ export interface TimelineEvent {
4
+ type: 'commit' | 'phase-transition' | 'todo-completion';
5
+ id: string;
6
+ date: Date;
7
+ title: string;
8
+ author?: string;
9
+ }
10
+
11
+ export interface TimelineFilters {
12
+ types?: string[];
13
+ phase?: string;
14
+ dateFrom?: string;
15
+ dateTo?: string;
16
+ }
17
+
18
+ export const cache: TTLCache;
19
+
20
+ export function getTimelineEvents(projectDir: string, filters?: TimelineFilters): Promise<TimelineEvent[]>;
@@ -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.30.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.30.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.30.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.30.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": {