@sienklogic/plan-build-run 2.33.1 → 2.34.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.
- package/CHANGELOG.md +15 -0
- package/dashboard/public/css/explorer.css +48 -0
- package/dashboard/public/css/layout.css +44 -1
- package/dashboard/public/css/settings.css +108 -110
- package/dashboard/public/css/timeline.css +2 -1
- package/dashboard/public/js/sidebar-toggle.js +21 -7
- package/dashboard/src/components/Layout.tsx +19 -1
- package/dashboard/src/components/explorer/tabs/MilestonesTab.tsx +18 -2
- package/dashboard/src/components/explorer/tabs/TodosTab.tsx +7 -4
- package/dashboard/src/components/partials/CurrentPhaseCard.tsx +1 -1
- package/dashboard/src/components/partials/StatusHeader.tsx +1 -1
- package/dashboard/src/components/settings/LogEntryList.tsx +43 -5
- package/package.json +1 -1
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/scripts/check-plan-format.js +2 -2
- package/plugins/pbr/scripts/check-state-sync.js +6 -6
- package/plugins/pbr/scripts/context-budget-check.js +2 -2
- package/plugins/pbr/scripts/pbr-tools.js +76 -1
- package/plugins/pbr/scripts/post-write-quality.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ All notable changes to Plan-Build-Run will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.34.0](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.33.1...plan-build-run-v2.34.0) (2026-02-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **quick-011:** add mobile responsive sidebar with hamburger toggle ([2958c60](https://github.com/SienkLogic/plan-build-run/commit/2958c605a2d9c876d297872d8b823e560679e3da))
|
|
14
|
+
* **quick-011:** fix status badge data-status attrs and mermaid dark mode ([8359c8c](https://github.com/SienkLogic/plan-build-run/commit/8359c8c001e676d8f776ad274b52b4b251cefe5a))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **quick-009:** fix dashboard UX across milestones, timeline, logs, todos, and layout ([3529d9e](https://github.com/SienkLogic/plan-build-run/commit/3529d9ebad8bac7f58b0780107a36eadbd5f66e7))
|
|
20
|
+
* **quick-010:** use lockedFileUpdate for atomic writes, fix shell injection, add writeActiveSkill ([1cefb13](https://github.com/SienkLogic/plan-build-run/commit/1cefb132ff1aa1ab07be252695dec43f1397f689))
|
|
21
|
+
* **quick-011:** move hamburger button outside sidebar for correct fixed positioning ([65054c3](https://github.com/SienkLogic/plan-build-run/commit/65054c337c8adc812669976ae5c14d3c150c452f))
|
|
22
|
+
|
|
8
23
|
## [2.33.1](https://github.com/SienkLogic/plan-build-run/compare/plan-build-run-v2.33.0...plan-build-run-v2.33.1) (2026-02-25)
|
|
9
24
|
|
|
10
25
|
|
|
@@ -418,6 +418,54 @@
|
|
|
418
418
|
gap: var(--space-md);
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
/* ---- Todo cards ---- */
|
|
422
|
+
|
|
423
|
+
.todo-card {
|
|
424
|
+
display: flex;
|
|
425
|
+
flex-direction: column;
|
|
426
|
+
gap: var(--space-xs);
|
|
427
|
+
background: var(--color-surface-raised);
|
|
428
|
+
border: 1px solid var(--color-border);
|
|
429
|
+
border-radius: var(--radius-md);
|
|
430
|
+
border-left-width: 4px;
|
|
431
|
+
padding: var(--space-sm) var(--space-md);
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.todo-card--high {
|
|
436
|
+
border-left-color: var(--color-error, #ef4444);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.todo-card--medium {
|
|
440
|
+
border-left-color: var(--color-warning, #f59e0b);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.todo-card--low {
|
|
444
|
+
border-left-color: var(--color-border);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.todo-card__main {
|
|
448
|
+
display: flex;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: var(--space-sm);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.todo-card__main .explorer-item__title {
|
|
454
|
+
flex: 1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.todo-card__meta {
|
|
458
|
+
display: flex;
|
|
459
|
+
gap: var(--space-md);
|
|
460
|
+
font-size: 0.8rem;
|
|
461
|
+
color: var(--color-text-dim);
|
|
462
|
+
padding-left: calc(var(--space-sm) + 60px);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.todo-card__meta:empty {
|
|
466
|
+
display: none;
|
|
467
|
+
}
|
|
468
|
+
|
|
421
469
|
/* ---- Requirements traceability table ---- */
|
|
422
470
|
|
|
423
471
|
.explorer-req-section {
|
|
@@ -103,13 +103,39 @@ ul, ol {
|
|
|
103
103
|
z-index: 10;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
.sidebar__toggle {
|
|
107
|
+
display: none;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
position: fixed;
|
|
111
|
+
top: var(--space-sm);
|
|
112
|
+
left: var(--space-sm);
|
|
113
|
+
z-index: 20;
|
|
114
|
+
width: 2.5rem;
|
|
115
|
+
height: 2.5rem;
|
|
116
|
+
border: 1px solid var(--color-border);
|
|
117
|
+
border-radius: var(--radius-sm);
|
|
118
|
+
background: var(--color-surface-raised);
|
|
119
|
+
color: var(--color-text);
|
|
120
|
+
font-size: 1.25rem;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
body.sidebar-visible .main-content::before {
|
|
125
|
+
content: '';
|
|
126
|
+
position: fixed;
|
|
127
|
+
inset: 0;
|
|
128
|
+
background: rgba(0,0,0,0.4);
|
|
129
|
+
z-index: 5;
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
.main-content {
|
|
107
133
|
margin-left: var(--size-sidebar);
|
|
108
134
|
flex: 1;
|
|
109
135
|
padding: var(--space-lg) var(--space-xl);
|
|
110
136
|
min-height: 100vh;
|
|
111
137
|
background: var(--color-surface);
|
|
112
|
-
max-width:
|
|
138
|
+
max-width: 1400px;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
/* --- Sidebar Brand --- */
|
|
@@ -843,6 +869,23 @@ details li {
|
|
|
843
869
|
min-width: 100px;
|
|
844
870
|
font-size: 0.75rem;
|
|
845
871
|
}
|
|
872
|
+
|
|
873
|
+
.sidebar {
|
|
874
|
+
transform: translateX(-100%);
|
|
875
|
+
transition: transform 0.2s ease;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.sidebar--open {
|
|
879
|
+
transform: translateX(0);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.sidebar__toggle {
|
|
883
|
+
display: flex;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.main-content {
|
|
887
|
+
margin-left: 0;
|
|
888
|
+
}
|
|
846
889
|
}
|
|
847
890
|
|
|
848
891
|
@media (max-width: 480px) {
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
/* Tab bar */
|
|
4
4
|
.settings-tabs {
|
|
5
5
|
display: flex;
|
|
6
|
-
gap: var(--
|
|
7
|
-
border-bottom: 2px solid var(--
|
|
8
|
-
margin-bottom: var(--
|
|
6
|
+
gap: var(--space-sm);
|
|
7
|
+
border-bottom: 2px solid var(--color-border);
|
|
8
|
+
margin-bottom: var(--space-md);
|
|
9
9
|
padding-bottom: 0;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/* Mode toggle (Form / Raw JSON) */
|
|
13
13
|
.config-mode-toggle {
|
|
14
14
|
display: flex;
|
|
15
|
-
gap: var(--
|
|
16
|
-
margin-bottom: var(--
|
|
15
|
+
gap: var(--space-sm);
|
|
16
|
+
margin-bottom: var(--space-md);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/* Tab buttons — shared by settings-tabs and config-mode-toggle */
|
|
@@ -21,44 +21,44 @@
|
|
|
21
21
|
background: none;
|
|
22
22
|
border: none;
|
|
23
23
|
border-bottom: 2px solid transparent;
|
|
24
|
-
padding: var(--
|
|
25
|
-
font-size:
|
|
24
|
+
padding: var(--space-sm) var(--space-md);
|
|
25
|
+
font-size: 0.875rem;
|
|
26
26
|
font-weight: 500;
|
|
27
|
-
color: var(--text-
|
|
27
|
+
color: var(--color-text-dim);
|
|
28
28
|
cursor: pointer;
|
|
29
29
|
transition: color 0.15s ease, border-color 0.15s ease;
|
|
30
30
|
margin-bottom: -2px;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
.tab-btn:hover {
|
|
34
|
-
color: var(--text
|
|
34
|
+
color: var(--color-text);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.tab-btn.active {
|
|
38
|
-
color: var(--
|
|
39
|
-
border-bottom-color: var(--
|
|
38
|
+
color: var(--color-accent);
|
|
39
|
+
border-bottom-color: var(--color-accent);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/* Config section card */
|
|
43
43
|
.config-section {
|
|
44
|
-
border: 1px solid var(--
|
|
45
|
-
border-radius: var(--radius-
|
|
46
|
-
padding: var(--
|
|
47
|
-
margin-bottom: var(--
|
|
48
|
-
background: var(--surface
|
|
44
|
+
border: 1px solid var(--color-border);
|
|
45
|
+
border-radius: var(--radius-sm);
|
|
46
|
+
padding: var(--space-md);
|
|
47
|
+
margin-bottom: var(--space-md);
|
|
48
|
+
background: var(--color-surface);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
.config-section--nested {
|
|
52
|
-
margin-top: var(--
|
|
53
|
-
background: var(--surface-
|
|
52
|
+
margin-top: var(--space-md);
|
|
53
|
+
background: var(--color-surface-raised);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/* Section heading */
|
|
57
57
|
.config-section__title {
|
|
58
|
-
font-size:
|
|
58
|
+
font-size: 0.875rem;
|
|
59
59
|
font-weight: 600;
|
|
60
|
-
color: var(--text
|
|
61
|
-
margin: 0 0 var(--
|
|
60
|
+
color: var(--color-text);
|
|
61
|
+
margin: 0 0 var(--space-sm) 0;
|
|
62
62
|
text-transform: uppercase;
|
|
63
63
|
letter-spacing: 0.05em;
|
|
64
64
|
}
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
.config-field {
|
|
68
68
|
display: flex;
|
|
69
69
|
align-items: center;
|
|
70
|
-
gap: var(--
|
|
71
|
-
padding: var(--
|
|
72
|
-
font-size:
|
|
73
|
-
color: var(--text
|
|
70
|
+
gap: var(--space-sm);
|
|
71
|
+
padding: var(--space-xs) 0;
|
|
72
|
+
font-size: 0.875rem;
|
|
73
|
+
color: var(--color-text);
|
|
74
74
|
cursor: pointer;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -80,20 +80,20 @@
|
|
|
80
80
|
|
|
81
81
|
.config-field__label {
|
|
82
82
|
min-width: 12rem;
|
|
83
|
-
color: var(--text-
|
|
84
|
-
font-size:
|
|
83
|
+
color: var(--color-text-dim);
|
|
84
|
+
font-size: 0.8125rem;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
.config-field input[type='text'],
|
|
88
88
|
.config-field input[type='number'],
|
|
89
89
|
.config-field select {
|
|
90
90
|
flex: 1;
|
|
91
|
-
padding: var(--
|
|
92
|
-
border: 1px solid var(--
|
|
93
|
-
border-radius: var(--radius-
|
|
94
|
-
font-size:
|
|
95
|
-
background: var(--surface
|
|
96
|
-
color: var(--text
|
|
91
|
+
padding: var(--space-xs) var(--space-sm);
|
|
92
|
+
border: 1px solid var(--color-border);
|
|
93
|
+
border-radius: var(--radius-sm);
|
|
94
|
+
font-size: 0.8125rem;
|
|
95
|
+
background: var(--color-surface);
|
|
96
|
+
color: var(--color-text);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
.config-field input[readonly] {
|
|
@@ -104,20 +104,20 @@
|
|
|
104
104
|
.config-field input[type='checkbox'] {
|
|
105
105
|
width: 1rem;
|
|
106
106
|
height: 1rem;
|
|
107
|
-
accent-color: var(--
|
|
107
|
+
accent-color: var(--color-accent);
|
|
108
108
|
cursor: pointer;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/* Raw JSON textarea */
|
|
112
112
|
.config-raw-json {
|
|
113
113
|
width: 100%;
|
|
114
|
-
font-family: var(--font-mono
|
|
115
|
-
font-size:
|
|
116
|
-
padding: var(--
|
|
117
|
-
border: 1px solid var(--
|
|
118
|
-
border-radius: var(--radius-
|
|
119
|
-
background: var(--surface
|
|
120
|
-
color: var(--text
|
|
114
|
+
font-family: var(--font-mono);
|
|
115
|
+
font-size: 0.8125rem;
|
|
116
|
+
padding: var(--space-sm);
|
|
117
|
+
border: 1px solid var(--color-border);
|
|
118
|
+
border-radius: var(--radius-sm);
|
|
119
|
+
background: var(--color-surface);
|
|
120
|
+
color: var(--color-text);
|
|
121
121
|
resize: vertical;
|
|
122
122
|
box-sizing: border-box;
|
|
123
123
|
}
|
|
@@ -126,30 +126,30 @@
|
|
|
126
126
|
.config-actions {
|
|
127
127
|
display: flex;
|
|
128
128
|
justify-content: flex-end;
|
|
129
|
-
margin-bottom: var(--
|
|
129
|
+
margin-bottom: var(--space-sm);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/* Inline feedback */
|
|
133
133
|
.config-feedback {
|
|
134
134
|
min-height: 1.5rem;
|
|
135
|
-
margin-top: var(--
|
|
136
|
-
font-size:
|
|
135
|
+
margin-top: var(--space-sm);
|
|
136
|
+
font-size: 0.8125rem;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
.feedback--success {
|
|
140
|
-
color: var(--
|
|
140
|
+
color: var(--status-complete);
|
|
141
141
|
font-weight: 500;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
.feedback--error {
|
|
145
|
-
color: var(--
|
|
145
|
+
color: var(--status-blocked);
|
|
146
146
|
font-weight: 500;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/* Empty state */
|
|
150
150
|
.config-empty {
|
|
151
|
-
font-size:
|
|
152
|
-
color: var(--text-
|
|
151
|
+
font-size: 0.8125rem;
|
|
152
|
+
color: var(--color-text-dim);
|
|
153
153
|
font-style: italic;
|
|
154
154
|
margin: 0;
|
|
155
155
|
}
|
|
@@ -158,52 +158,33 @@
|
|
|
158
158
|
.btn--primary {
|
|
159
159
|
display: inline-flex;
|
|
160
160
|
align-items: center;
|
|
161
|
-
gap: var(--
|
|
162
|
-
padding: var(--
|
|
163
|
-
background: var(--
|
|
161
|
+
gap: var(--space-sm);
|
|
162
|
+
padding: var(--space-sm) var(--space-md);
|
|
163
|
+
background: var(--color-accent);
|
|
164
164
|
color: #fff;
|
|
165
165
|
border: none;
|
|
166
|
-
border-radius: var(--radius-
|
|
167
|
-
font-size:
|
|
166
|
+
border-radius: var(--radius-sm);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
168
|
font-weight: 500;
|
|
169
169
|
cursor: pointer;
|
|
170
170
|
transition: background 0.15s ease;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
.btn--primary:hover {
|
|
174
|
-
background: var(--
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/* Dark theme overrides */
|
|
178
|
-
[data-theme='dark'] .config-section {
|
|
179
|
-
background: var(--surface-2, #1e2535);
|
|
180
|
-
border-color: var(--surface-3, #2d3748);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
[data-theme='dark'] .config-section--nested {
|
|
184
|
-
background: var(--surface-1, #161e2d);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
[data-theme='dark'] .config-raw-json,
|
|
188
|
-
[data-theme='dark'] .config-field input[type='text'],
|
|
189
|
-
[data-theme='dark'] .config-field input[type='number'],
|
|
190
|
-
[data-theme='dark'] .config-field select {
|
|
191
|
-
background: var(--surface-2, #1e2535);
|
|
192
|
-
border-color: var(--surface-3, #2d3748);
|
|
193
|
-
color: var(--text-1, #e2e8f0);
|
|
174
|
+
background: var(--color-accent-hover);
|
|
194
175
|
}
|
|
195
176
|
|
|
196
177
|
/* ── Log Viewer ─────────────────────────────────────── */
|
|
197
178
|
.log-viewer-layout {
|
|
198
179
|
display: grid;
|
|
199
180
|
grid-template-columns: 220px 1fr;
|
|
200
|
-
gap: var(--
|
|
201
|
-
height: calc(100vh - var(--
|
|
181
|
+
gap: var(--space-md);
|
|
182
|
+
height: calc(100vh - var(--space-2xl));
|
|
202
183
|
}
|
|
203
184
|
|
|
204
185
|
.log-file-sidebar {
|
|
205
|
-
border-right: 1px solid var(--
|
|
206
|
-
padding-right: var(--
|
|
186
|
+
border-right: 1px solid var(--color-border);
|
|
187
|
+
padding-right: var(--space-sm);
|
|
207
188
|
overflow-y: auto;
|
|
208
189
|
}
|
|
209
190
|
|
|
@@ -215,57 +196,74 @@
|
|
|
215
196
|
|
|
216
197
|
.log-file-item {
|
|
217
198
|
display: block;
|
|
218
|
-
padding: var(--
|
|
219
|
-
border-radius: var(--radius-
|
|
199
|
+
padding: var(--space-sm);
|
|
200
|
+
border-radius: var(--radius-sm);
|
|
220
201
|
text-decoration: none;
|
|
221
|
-
color: var(--text-
|
|
222
|
-
font-size:
|
|
202
|
+
color: var(--color-text-dim);
|
|
203
|
+
font-size: 0.8125rem;
|
|
223
204
|
margin-block: 2px;
|
|
224
205
|
}
|
|
225
206
|
|
|
226
|
-
.log-file-item:hover { background: var(--surface-
|
|
227
|
-
.log-file-item--active { background: var(--
|
|
207
|
+
.log-file-item:hover { background: var(--color-surface-raised); }
|
|
208
|
+
.log-file-item--active { background: var(--color-border); color: var(--color-text); font-weight: 500; }
|
|
228
209
|
|
|
229
210
|
.log-file-name { display: block; font-family: var(--font-mono); font-size: 0.75rem; }
|
|
230
|
-
.log-file-size { color: var(--text-
|
|
231
|
-
.log-file-date { color: var(--text-
|
|
211
|
+
.log-file-size { color: var(--color-text-dim); }
|
|
212
|
+
.log-file-date { color: var(--color-text-dim); }
|
|
232
213
|
|
|
233
|
-
.log-main { overflow-y: auto; padding-right: var(--
|
|
214
|
+
.log-main { overflow-y: auto; padding-right: var(--space-sm); }
|
|
234
215
|
|
|
235
216
|
.log-filters {
|
|
236
217
|
display: flex;
|
|
237
|
-
gap: var(--
|
|
218
|
+
gap: var(--space-sm);
|
|
238
219
|
align-items: center;
|
|
239
|
-
margin-bottom: var(--
|
|
240
|
-
padding: var(--
|
|
241
|
-
background: var(--surface-
|
|
242
|
-
border-radius: var(--radius-
|
|
220
|
+
margin-bottom: var(--space-sm);
|
|
221
|
+
padding: var(--space-sm);
|
|
222
|
+
background: var(--color-surface-raised);
|
|
223
|
+
border-radius: var(--radius-sm);
|
|
243
224
|
}
|
|
244
225
|
|
|
245
226
|
.log-table {
|
|
246
227
|
width: 100%;
|
|
247
228
|
border-collapse: collapse;
|
|
248
|
-
font-size:
|
|
229
|
+
font-size: 0.8125rem;
|
|
249
230
|
}
|
|
250
231
|
|
|
251
232
|
.log-table th,
|
|
252
233
|
.log-table td {
|
|
253
234
|
text-align: left;
|
|
254
|
-
padding: var(--
|
|
255
|
-
border-bottom: 1px solid var(--
|
|
235
|
+
padding: var(--space-xs) var(--space-sm);
|
|
236
|
+
border-bottom: 1px solid var(--color-border);
|
|
256
237
|
}
|
|
257
238
|
|
|
258
|
-
.log-table th { color: var(--text-
|
|
239
|
+
.log-table th { color: var(--color-text-dim); font-weight: 500; }
|
|
240
|
+
|
|
241
|
+
.log-table th:first-child,
|
|
242
|
+
.log-table td:first-child {
|
|
243
|
+
width: 80px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.log-table th:nth-child(2),
|
|
247
|
+
.log-table td:nth-child(2) {
|
|
248
|
+
width: 170px;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.log-table td:nth-child(3) {
|
|
253
|
+
font-family: var(--font-mono);
|
|
254
|
+
font-size: 0.8rem;
|
|
255
|
+
word-break: break-all;
|
|
256
|
+
}
|
|
259
257
|
|
|
260
258
|
.log-badge {
|
|
261
259
|
display: inline-block;
|
|
262
260
|
padding: 1px 6px;
|
|
263
|
-
border-radius:
|
|
261
|
+
border-radius: 999px;
|
|
264
262
|
font-size: 0.7rem;
|
|
265
263
|
font-weight: 600;
|
|
266
264
|
text-transform: uppercase;
|
|
267
|
-
background: var(--
|
|
268
|
-
color: var(--text-
|
|
265
|
+
background: var(--color-border);
|
|
266
|
+
color: var(--color-text-dim);
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
.log-badge--error { background: var(--red-2); color: var(--red-9); }
|
|
@@ -277,29 +275,29 @@
|
|
|
277
275
|
|
|
278
276
|
.log-pagination {
|
|
279
277
|
display: flex;
|
|
280
|
-
gap: var(--
|
|
278
|
+
gap: var(--space-sm);
|
|
281
279
|
align-items: center;
|
|
282
|
-
padding: var(--
|
|
280
|
+
padding: var(--space-sm) 0;
|
|
283
281
|
}
|
|
284
282
|
|
|
285
283
|
.log-tail-indicator {
|
|
286
284
|
display: flex;
|
|
287
285
|
align-items: center;
|
|
288
|
-
gap: var(--
|
|
289
|
-
font-size:
|
|
290
|
-
color: var(--text-
|
|
291
|
-
margin-bottom: var(--
|
|
286
|
+
gap: var(--space-sm);
|
|
287
|
+
font-size: 0.8125rem;
|
|
288
|
+
color: var(--color-text-dim);
|
|
289
|
+
margin-bottom: var(--space-sm);
|
|
292
290
|
}
|
|
293
291
|
|
|
294
292
|
.tail-dot {
|
|
295
293
|
width: 8px;
|
|
296
294
|
height: 8px;
|
|
297
295
|
border-radius: 50%;
|
|
298
|
-
background: var(--
|
|
296
|
+
background: var(--color-border);
|
|
299
297
|
}
|
|
300
298
|
|
|
301
299
|
.tail-dot--active {
|
|
302
|
-
background: var(--
|
|
300
|
+
background: var(--status-complete);
|
|
303
301
|
animation: pulse 1.5s ease-in-out infinite;
|
|
304
302
|
}
|
|
305
303
|
|
|
@@ -308,7 +306,7 @@
|
|
|
308
306
|
50% { opacity: 0.4; }
|
|
309
307
|
}
|
|
310
308
|
|
|
311
|
-
.log-tail-entries { font-family: var(--font-mono); font-size: 0.75rem; color: var(--text-
|
|
312
|
-
.log-tail-row { padding: 2px 0; border-bottom: 1px solid var(--surface-
|
|
313
|
-
.log-empty, .log-prompt { color: var(--text-
|
|
314
|
-
.log-summary { color: var(--text-
|
|
309
|
+
.log-tail-entries { font-family: var(--font-mono); font-size: 0.75rem; color: var(--color-text-dim); }
|
|
310
|
+
.log-tail-row { padding: 2px 0; border-bottom: 1px solid var(--color-surface-raised); }
|
|
311
|
+
.log-empty, .log-prompt { color: var(--color-text-dim); padding: var(--space-md); }
|
|
312
|
+
.log-summary { color: var(--color-text-dim); font-size: 0.8125rem; margin-bottom: var(--space-sm); }
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
.timeline__event {
|
|
48
48
|
display: grid;
|
|
49
|
-
grid-template-columns: 1rem
|
|
49
|
+
grid-template-columns: 1rem auto 5rem 1fr auto;
|
|
50
50
|
align-items: start;
|
|
51
51
|
gap: var(--space-sm);
|
|
52
52
|
padding: var(--space-sm) 0;
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
.timeline__event-title {
|
|
93
93
|
font-size: 0.9rem;
|
|
94
|
+
overflow-wrap: anywhere;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
.timeline__event-author {
|
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
// sidebar-toggle.js — Tabler vertical navbar toggle shim
|
|
2
|
-
// Tabler's Bootstrap JS handles collapse via data-bs-toggle="collapse".
|
|
3
|
-
// This file remains for the header button (#sidebar-toggle) which triggers
|
|
4
|
-
// the .navbar-vertical collapse on mobile.
|
|
5
1
|
document.addEventListener('DOMContentLoaded', function() {
|
|
6
2
|
var toggle = document.getElementById('sidebar-toggle');
|
|
7
|
-
var
|
|
8
|
-
if (!toggle || !
|
|
3
|
+
var sidebar = document.querySelector('.sidebar');
|
|
4
|
+
if (!toggle || !sidebar) return;
|
|
5
|
+
|
|
9
6
|
toggle.addEventListener('click', function() {
|
|
10
|
-
|
|
7
|
+
sidebar.classList.toggle('sidebar--open');
|
|
8
|
+
document.body.classList.toggle('sidebar-visible');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Close sidebar when a nav link is clicked (mobile)
|
|
12
|
+
sidebar.querySelectorAll('.sidebar__nav-link').forEach(function(link) {
|
|
13
|
+
link.addEventListener('click', function() {
|
|
14
|
+
sidebar.classList.remove('sidebar--open');
|
|
15
|
+
document.body.classList.remove('sidebar-visible');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Close sidebar when clicking the backdrop
|
|
20
|
+
document.addEventListener('click', function(e) {
|
|
21
|
+
if (!sidebar.classList.contains('sidebar--open')) return;
|
|
22
|
+
if (sidebar.contains(e.target) || toggle.contains(e.target)) return;
|
|
23
|
+
sidebar.classList.remove('sidebar--open');
|
|
24
|
+
document.body.classList.remove('sidebar-visible');
|
|
11
25
|
});
|
|
12
26
|
});
|
|
@@ -50,11 +50,27 @@ export function Layout({ title, children, currentView }: LayoutProps) {
|
|
|
50
50
|
})();
|
|
51
51
|
</script>`}
|
|
52
52
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js" defer></script>
|
|
53
|
-
{html`<script>
|
|
53
|
+
{html`<script>
|
|
54
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
55
|
+
function initMermaid() {
|
|
56
|
+
var isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
|
57
|
+
(!document.documentElement.getAttribute('data-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
58
|
+
mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'neutral' });
|
|
59
|
+
}
|
|
60
|
+
initMermaid();
|
|
61
|
+
new MutationObserver(function(mutations) {
|
|
62
|
+
mutations.forEach(function(m) { if (m.attributeName === 'data-theme') initMermaid(); });
|
|
63
|
+
}).observe(document.documentElement, { attributes: true });
|
|
64
|
+
});
|
|
65
|
+
</script>`}
|
|
54
66
|
</head>
|
|
55
67
|
<body>
|
|
56
68
|
<a href="#main-content" class="skip-link">Skip to content</a>
|
|
57
69
|
|
|
70
|
+
<button id="sidebar-toggle" class="sidebar__toggle" type="button" aria-label="Toggle navigation">
|
|
71
|
+
<span aria-hidden="true">☰</span>
|
|
72
|
+
</button>
|
|
73
|
+
|
|
58
74
|
<nav class="sidebar" aria-label="Main navigation">
|
|
59
75
|
<div class="sidebar__brand">
|
|
60
76
|
<span class="sidebar__brand-name">PBR</span>
|
|
@@ -88,6 +104,7 @@ export function Layout({ title, children, currentView }: LayoutProps) {
|
|
|
88
104
|
</nav>
|
|
89
105
|
|
|
90
106
|
<main id="main-content" class="main-content">
|
|
107
|
+
<div class="loading-bar" aria-hidden="true"></div>
|
|
91
108
|
{children}
|
|
92
109
|
</main>
|
|
93
110
|
|
|
@@ -98,6 +115,7 @@ export function Layout({ title, children, currentView }: LayoutProps) {
|
|
|
98
115
|
{/* Local scripts */}
|
|
99
116
|
<script src="/js/theme-toggle.js"></script>
|
|
100
117
|
<script src="/js/sse-client.js" defer></script>
|
|
118
|
+
<script src="/js/sidebar-toggle.js" defer></script>
|
|
101
119
|
</body>
|
|
102
120
|
</html>
|
|
103
121
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
interface Milestone {
|
|
2
2
|
version: string;
|
|
3
3
|
name?: string;
|
|
4
|
+
goal?: string;
|
|
5
|
+
startPhase?: string | number;
|
|
6
|
+
endPhase?: string | number;
|
|
7
|
+
completed?: boolean;
|
|
4
8
|
date?: string;
|
|
5
9
|
duration?: string;
|
|
6
10
|
}
|
|
@@ -27,9 +31,21 @@ export function MilestonesTab({ active, archived }: MilestonesTabProps) {
|
|
|
27
31
|
{active.length === 0 && <p class="explorer__loading">No active milestones.</p>}
|
|
28
32
|
<div class="explorer-list">
|
|
29
33
|
{active.map((ms) => (
|
|
30
|
-
<div class="explorer-item" key={ms.version}>
|
|
34
|
+
<div class="explorer-item" key={ms.name || ms.version}>
|
|
31
35
|
<div class="explorer-item__header">
|
|
32
|
-
<span class="explorer-item__title">
|
|
36
|
+
<span class="explorer-item__title">
|
|
37
|
+
{ms.name || `v${ms.version}`}
|
|
38
|
+
</span>
|
|
39
|
+
{ms.startPhase != null && ms.endPhase != null && (
|
|
40
|
+
<span class="explorer-item__meta">
|
|
41
|
+
Phase {ms.startPhase}–{ms.endPhase}
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
{ms.goal && (
|
|
45
|
+
<span class="explorer-item__meta" style="flex:1; text-align:right; white-space:normal;">
|
|
46
|
+
{ms.goal}
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
33
49
|
<span class="explorer-badge explorer-badge--building">active</span>
|
|
34
50
|
</div>
|
|
35
51
|
</div>
|
|
@@ -74,16 +74,15 @@ export function TodoListFragment({ todos }: TodoListFragmentProps) {
|
|
|
74
74
|
{todos.length === 0 && <li class="explorer__loading">No pending todos.</li>}
|
|
75
75
|
{todos.map((todo) => (
|
|
76
76
|
<li
|
|
77
|
-
class=
|
|
77
|
+
class={`todo-card todo-card--${todo.priority}`}
|
|
78
78
|
key={todo.id}
|
|
79
79
|
x-show={`!search || '${todo.title.toLowerCase()}'.includes(search.toLowerCase())`}
|
|
80
80
|
>
|
|
81
|
-
<div class="
|
|
81
|
+
<div class="todo-card__main">
|
|
82
82
|
<span class={`explorer-badge explorer-badge--${todo.priority}`}>{todo.priority}</span>
|
|
83
83
|
<span class="explorer-item__title">{todo.title}</span>
|
|
84
|
-
{todo.phase && <span class="explorer-item__meta">Phase {todo.phase}</span>}
|
|
85
84
|
<button
|
|
86
|
-
class="btn btn--
|
|
85
|
+
class="btn btn--ghost btn--sm"
|
|
87
86
|
hx-post={`/api/explorer/todos/${todo.id}/complete`}
|
|
88
87
|
hx-target="closest li"
|
|
89
88
|
hx-swap="outerHTML"
|
|
@@ -92,6 +91,10 @@ export function TodoListFragment({ todos }: TodoListFragmentProps) {
|
|
|
92
91
|
Done
|
|
93
92
|
</button>
|
|
94
93
|
</div>
|
|
94
|
+
<div class="todo-card__meta">
|
|
95
|
+
{todo.phase && <span>Phase {todo.phase}</span>}
|
|
96
|
+
{todo.created && <span>{new Date(todo.created).toLocaleDateString()}</span>}
|
|
97
|
+
</div>
|
|
95
98
|
</li>
|
|
96
99
|
))}
|
|
97
100
|
</ul>
|
|
@@ -25,7 +25,7 @@ export const CurrentPhaseCard: FC<CurrentPhaseCardProps> = ({
|
|
|
25
25
|
<h2 class="card__title">
|
|
26
26
|
Phase {currentPhase.id}: {currentPhase.name}
|
|
27
27
|
</h2>
|
|
28
|
-
<span class={`badge status-badge status-badge--${currentPhase.status}`}>
|
|
28
|
+
<span class={`badge status-badge status-badge--${currentPhase.status}`} data-status={currentPhase.status}>
|
|
29
29
|
{currentPhase.status}
|
|
30
30
|
</span>
|
|
31
31
|
<p class="card__meta">Plans: {currentPhase.planStatus}</p>
|
|
@@ -26,7 +26,7 @@ export const StatusHeader: FC<StatusHeaderProps> = ({
|
|
|
26
26
|
<span class="status-header__phase">
|
|
27
27
|
Phase {currentPhase.id}: {currentPhase.name}
|
|
28
28
|
</span>
|
|
29
|
-
<span class={`badge status-badge status-badge--${currentPhase.status}`}>
|
|
29
|
+
<span class={`badge status-badge status-badge--${currentPhase.status}`} data-status={currentPhase.status}>
|
|
30
30
|
{currentPhase.status}
|
|
31
31
|
</span>
|
|
32
32
|
<div class="milestone-bar">
|
|
@@ -14,7 +14,7 @@ function sanitizeType(t: unknown): string {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function getTimestamp(entry: Record<string, unknown>): string {
|
|
17
|
-
const raw = entry['timestamp'] ?? entry['ts'] ?? entry['time'];
|
|
17
|
+
const raw = entry['timestamp'] ?? entry['ts'] ?? entry['time'] ?? entry['start'] ?? entry['end'];
|
|
18
18
|
if (!raw) return '—';
|
|
19
19
|
try {
|
|
20
20
|
return new Date(raw as string).toLocaleString();
|
|
@@ -23,15 +23,53 @@ function getTimestamp(entry: Record<string, unknown>): string {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function getType(entry: Record<string, unknown>): string {
|
|
27
|
+
if (typeof entry['type'] === 'string' && entry['type']) return entry['type'];
|
|
28
|
+
if (typeof entry['event'] === 'string' && entry['event']) return entry['event'];
|
|
29
|
+
if (typeof entry['hook'] === 'string' && entry['hook']) return 'hook';
|
|
30
|
+
if (typeof entry['action'] === 'string' && entry['action']) return entry['action'];
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
function getSummary(entry: Record<string, unknown>): string {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
// Session log entry: has start/end/duration_minutes
|
|
36
|
+
if (entry['duration_minutes'] != null || (entry['start'] && entry['end'])) {
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
if (entry['duration_minutes'] != null) parts.push(`Duration: ${entry['duration_minutes']}m`);
|
|
39
|
+
if (entry['reason']) parts.push(`Reason: ${entry['reason']}`);
|
|
40
|
+
if (entry['agents'] != null) parts.push(`Agents: ${entry['agents']}`);
|
|
41
|
+
if (parts.length > 0) return parts.join(', ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Hook log entry: has hook/script fields
|
|
45
|
+
if (entry['hook'] || entry['script']) {
|
|
46
|
+
const parts: string[] = [];
|
|
47
|
+
if (entry['hook']) parts.push(`Hook: ${entry['hook']}`);
|
|
48
|
+
if (entry['script']) parts.push(`Script: ${entry['script']}`);
|
|
49
|
+
if (entry['result']) parts.push(`Result: ${entry['result']}`);
|
|
50
|
+
if (parts.length > 0) return parts.join(', ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Event/action entry
|
|
54
|
+
if (entry['event'] || entry['action']) {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
if (entry['event']) parts.push(`Event: ${entry['event']}`);
|
|
57
|
+
if (entry['action']) parts.push(`Action: ${entry['action']}`);
|
|
58
|
+
if (parts.length > 0) return parts.join(', ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: first 3 meaningful key-value pairs
|
|
62
|
+
const skip = new Set(['type', 'timestamp', 'ts', 'time', 'start', 'end']);
|
|
63
|
+
const pairs = Object.entries(entry)
|
|
64
|
+
.filter(([k, v]) => !skip.has(k) && v != null && typeof v !== 'object')
|
|
65
|
+
.slice(0, 3)
|
|
66
|
+
.map(([k, v]) => `${k}: ${v}`);
|
|
67
|
+
return pairs.length > 0 ? pairs.join(', ') : '—';
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
export function LogEntryRow({ entry }: { entry: object }) {
|
|
33
71
|
const e = entry as Record<string, unknown>;
|
|
34
|
-
const type =
|
|
72
|
+
const type = getType(e);
|
|
35
73
|
const badge = sanitizeType(type);
|
|
36
74
|
const timestamp = getTimestamp(e);
|
|
37
75
|
const summary = getSummary(e);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.34.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.
|
|
4
|
+
"version": "2.34.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.
|
|
3
|
+
"version": "2.34.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",
|
|
@@ -24,7 +24,7 @@ const fs = require('fs');
|
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { logHook } = require('./hook-logger');
|
|
26
26
|
const { logEvent } = require('./event-logger');
|
|
27
|
-
const {
|
|
27
|
+
const { lockedFileUpdate } = require('./pbr-tools');
|
|
28
28
|
const { resolveConfig } = require('./local-llm/health');
|
|
29
29
|
const { classifyArtifact } = require('./local-llm/operations/classify-artifact');
|
|
30
30
|
|
|
@@ -464,7 +464,7 @@ function syncStateBody(content, filePath) {
|
|
|
464
464
|
if (!needsFix) return null;
|
|
465
465
|
|
|
466
466
|
try {
|
|
467
|
-
|
|
467
|
+
lockedFileUpdate(filePath, () => updated);
|
|
468
468
|
logHook('check-plan-format', 'PostToolUse', 'body-sync', {
|
|
469
469
|
fromPhase: bodyPhaseMatch[1], toPhase: fmPhase
|
|
470
470
|
});
|
|
@@ -26,7 +26,7 @@ const fs = require('fs');
|
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const { logHook } = require('./hook-logger');
|
|
28
28
|
const { logEvent } = require('./event-logger');
|
|
29
|
-
const {
|
|
29
|
+
const { lockedFileUpdate } = require('./pbr-tools');
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Extract phase number from a phase directory name.
|
|
@@ -370,7 +370,7 @@ function checkStateSync(data) {
|
|
|
370
370
|
} else {
|
|
371
371
|
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, newStatus, completedDate);
|
|
372
372
|
if (updatedRoadmap !== roadmapContent) {
|
|
373
|
-
|
|
373
|
+
lockedFileUpdate(roadmapPath, () => updatedRoadmap);
|
|
374
374
|
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${plansComplete} plans, ${newStatus}`);
|
|
375
375
|
}
|
|
376
376
|
}
|
|
@@ -410,7 +410,7 @@ function checkStateSync(data) {
|
|
|
410
410
|
|
|
411
411
|
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
412
412
|
if (updatedState !== stateContent) {
|
|
413
|
-
|
|
413
|
+
lockedFileUpdate(statePath, () => updatedState);
|
|
414
414
|
messages.push(`STATE.md: ${artifacts.completeSummaries}/${artifacts.plans} plans, ${overallPct}%`);
|
|
415
415
|
}
|
|
416
416
|
} catch (e) {
|
|
@@ -455,7 +455,7 @@ function checkStateSync(data) {
|
|
|
455
455
|
} else {
|
|
456
456
|
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, roadmapStatus, completedDate);
|
|
457
457
|
if (updatedRoadmap !== roadmapContent) {
|
|
458
|
-
|
|
458
|
+
lockedFileUpdate(roadmapPath, () => updatedRoadmap);
|
|
459
459
|
messages.push(`ROADMAP.md: Phase ${phaseNum} → ${roadmapStatus}`);
|
|
460
460
|
}
|
|
461
461
|
}
|
|
@@ -493,7 +493,7 @@ function checkStateSync(data) {
|
|
|
493
493
|
|
|
494
494
|
const updatedState = updateStatePosition(stateContent, stateUpdates);
|
|
495
495
|
if (updatedState !== stateContent) {
|
|
496
|
-
|
|
496
|
+
lockedFileUpdate(statePath, () => updatedState);
|
|
497
497
|
messages.push(`STATE.md: ${stateStatus}, ${overallPct}%`);
|
|
498
498
|
}
|
|
499
499
|
} catch (e) {
|
|
@@ -543,7 +543,7 @@ function checkStateSync(data) {
|
|
|
543
543
|
const plansComplete = `${artifacts.completeSummaries}/${artifacts.plans}`;
|
|
544
544
|
const updatedRoadmap = updateProgressTable(roadmapContent, phaseNum, plansComplete, 'Planning', null);
|
|
545
545
|
if (updatedRoadmap !== roadmapContent) {
|
|
546
|
-
|
|
546
|
+
lockedFileUpdate(roadmapPath, () => updatedRoadmap);
|
|
547
547
|
messages.push(`ROADMAP.md: Phase ${phaseNum} → Planning`);
|
|
548
548
|
}
|
|
549
549
|
}
|
|
@@ -21,7 +21,7 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const { logHook } = require('./hook-logger');
|
|
23
23
|
const { logEvent } = require('./event-logger');
|
|
24
|
-
const {
|
|
24
|
+
const { configLoad, tailLines, lockedFileUpdate } = require('./pbr-tools');
|
|
25
25
|
|
|
26
26
|
function main() {
|
|
27
27
|
const cwd = process.cwd();
|
|
@@ -87,7 +87,7 @@ function main() {
|
|
|
87
87
|
content = content.trimEnd() + `\n\n${continuityHeader}\n${continuityContent}\n`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
lockedFileUpdate(stateFile, () => content);
|
|
91
91
|
|
|
92
92
|
// Output additionalContext for post-compaction recovery
|
|
93
93
|
const recoveryContext = buildRecoveryContext(activeOp, roadmapSummary, currentPlan, configHighlights, recentErrors, recentAgents);
|
|
@@ -1491,5 +1491,80 @@ function atomicWrite(filePath, content) {
|
|
|
1491
1491
|
}
|
|
1492
1492
|
}
|
|
1493
1493
|
|
|
1494
|
+
/**
|
|
1495
|
+
* Write .active-skill with OS-level mutual exclusion.
|
|
1496
|
+
* Uses a .active-skill.lock file with exclusive create (O_EXCL) to prevent
|
|
1497
|
+
* two sessions from racing on the same file.
|
|
1498
|
+
*
|
|
1499
|
+
* @param {string} planningDir - Path to .planning/ directory
|
|
1500
|
+
* @param {string} skillName - Skill name to write
|
|
1501
|
+
* @returns {{success: boolean, warning?: string}} Result
|
|
1502
|
+
*/
|
|
1503
|
+
function writeActiveSkill(planningDir, skillName) {
|
|
1504
|
+
const skillFile = path.join(planningDir, '.active-skill');
|
|
1505
|
+
const lockFile = skillFile + '.lock';
|
|
1506
|
+
const staleThresholdMs = 60 * 60 * 1000; // 60 minutes
|
|
1507
|
+
|
|
1508
|
+
let lockFd = null;
|
|
1509
|
+
try {
|
|
1510
|
+
// Try exclusive create of lock file
|
|
1511
|
+
lockFd = fs.openSync(lockFile, 'wx');
|
|
1512
|
+
fs.writeSync(lockFd, `${process.pid}`);
|
|
1513
|
+
fs.closeSync(lockFd);
|
|
1514
|
+
lockFd = null;
|
|
1515
|
+
|
|
1516
|
+
// Check for existing .active-skill from another session
|
|
1517
|
+
let warning = null;
|
|
1518
|
+
if (fs.existsSync(skillFile)) {
|
|
1519
|
+
try {
|
|
1520
|
+
const stats = fs.statSync(skillFile);
|
|
1521
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
1522
|
+
if (ageMs < staleThresholdMs) {
|
|
1523
|
+
const existing = fs.readFileSync(skillFile, 'utf8').trim();
|
|
1524
|
+
warning = `.active-skill already set to "${existing}" (${Math.round(ageMs / 60000)}min ago). Overwriting — possible concurrent session.`;
|
|
1525
|
+
}
|
|
1526
|
+
} catch (_e) {
|
|
1527
|
+
// File disappeared between exists and stat — fine
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// Write the skill name
|
|
1532
|
+
fs.writeFileSync(skillFile, skillName, 'utf8');
|
|
1533
|
+
|
|
1534
|
+
// Release lock
|
|
1535
|
+
try { fs.unlinkSync(lockFile); } catch (_e) { /* best effort */ }
|
|
1536
|
+
|
|
1537
|
+
return { success: true, warning };
|
|
1538
|
+
} catch (e) {
|
|
1539
|
+
// Close fd if still open
|
|
1540
|
+
try { if (lockFd !== null) fs.closeSync(lockFd); } catch (_e) { /* ignore */ }
|
|
1541
|
+
|
|
1542
|
+
if (e.code === 'EEXIST') {
|
|
1543
|
+
// Lock held by another process — check staleness
|
|
1544
|
+
try {
|
|
1545
|
+
const lockStats = fs.statSync(lockFile);
|
|
1546
|
+
const lockAgeMs = Date.now() - lockStats.mtimeMs;
|
|
1547
|
+
if (lockAgeMs > staleThresholdMs) {
|
|
1548
|
+
// Stale lock — force remove and retry once
|
|
1549
|
+
fs.unlinkSync(lockFile);
|
|
1550
|
+
return writeActiveSkill(planningDir, skillName);
|
|
1551
|
+
}
|
|
1552
|
+
} catch (_statErr) {
|
|
1553
|
+
// Lock disappeared — retry once
|
|
1554
|
+
return writeActiveSkill(planningDir, skillName);
|
|
1555
|
+
}
|
|
1556
|
+
return { success: false, warning: `.active-skill.lock held by another process. Another PBR session may be active.` };
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Other error — write without lock as fallback
|
|
1560
|
+
try {
|
|
1561
|
+
fs.writeFileSync(skillFile, skillName, 'utf8');
|
|
1562
|
+
return { success: true, warning: `Lock failed (${e.code}), wrote without lock` };
|
|
1563
|
+
} catch (writeErr) {
|
|
1564
|
+
return { success: false, warning: `Failed to write .active-skill: ${writeErr.message}` };
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1494
1569
|
if (require.main === module || process.argv[1] === __filename) { main().catch(err => { process.stderr.write(err.message + '\n'); process.exit(1); }); }
|
|
1495
|
-
module.exports = { parseStateMd, parseRoadmapMd, parseYamlFrontmatter, parseMustHaves, countMustHaves, stateLoad, stateCheckProgress, configLoad, configClearCache, configValidate, lockedFileUpdate, planIndex, determinePhaseStatus, findFiles, atomicWrite, tailLines, frontmatter, mustHavesCollect, phaseInfo, stateUpdate, roadmapUpdateStatus, roadmapUpdatePlans, updateLegacyStateField, updateFrontmatterField, updateTableRow, findRoadmapRow, resolveDepthProfile, DEPTH_PROFILE_DEFAULTS, historyAppend, historyLoad, VALID_STATUS_TRANSITIONS, validateStatusTransition };
|
|
1570
|
+
module.exports = { parseStateMd, parseRoadmapMd, parseYamlFrontmatter, parseMustHaves, countMustHaves, stateLoad, stateCheckProgress, configLoad, configClearCache, configValidate, lockedFileUpdate, planIndex, determinePhaseStatus, findFiles, atomicWrite, tailLines, frontmatter, mustHavesCollect, phaseInfo, stateUpdate, roadmapUpdateStatus, roadmapUpdatePlans, updateLegacyStateField, updateFrontmatterField, updateTableRow, findRoadmapRow, resolveDepthProfile, DEPTH_PROFILE_DEFAULTS, historyAppend, historyLoad, VALID_STATUS_TRANSITIONS, validateStatusTransition, writeActiveSkill };
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const {
|
|
23
|
+
const { execFileSync } = require('child_process');
|
|
24
24
|
const { logHook } = require('./hook-logger');
|
|
25
25
|
|
|
26
26
|
const JS_TS_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
@@ -132,7 +132,7 @@ function runPrettier(filePath, cwd) {
|
|
|
132
132
|
if (!bin) return null;
|
|
133
133
|
|
|
134
134
|
try {
|
|
135
|
-
|
|
135
|
+
execFileSync(bin, ['--write', filePath], {
|
|
136
136
|
cwd,
|
|
137
137
|
timeout: 15000,
|
|
138
138
|
stdio: 'pipe',
|
|
@@ -154,7 +154,7 @@ function runTypeCheck(filePath, cwd) {
|
|
|
154
154
|
if (!fs.existsSync(path.join(cwd, 'tsconfig.json'))) return null;
|
|
155
155
|
|
|
156
156
|
try {
|
|
157
|
-
|
|
157
|
+
execFileSync(bin, ['--noEmit'], {
|
|
158
158
|
cwd,
|
|
159
159
|
timeout: 30000,
|
|
160
160
|
stdio: 'pipe',
|