@sienklogic/plan-build-run 2.27.0 → 2.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dashboard/public/css/layout.css +7 -283
- package/dashboard/public/css/status-colors.css +7 -0
- package/dashboard/public/css/tokens.css +3 -3
- package/dashboard/public/js/sidebar-toggle.js +9 -31
- package/dashboard/public/js/theme-toggle.js +4 -4
- package/dashboard/src/views/partials/activity-feed.ejs +17 -9
- package/dashboard/src/views/partials/analytics-content.ejs +178 -88
- package/dashboard/src/views/partials/audit-detail-content.ejs +6 -4
- package/dashboard/src/views/partials/audits-content.ejs +28 -26
- package/dashboard/src/views/partials/breadcrumbs.ejs +8 -4
- package/dashboard/src/views/partials/config-content.ejs +98 -95
- package/dashboard/src/views/partials/dashboard-content.ejs +69 -60
- package/dashboard/src/views/partials/dependencies-content.ejs +5 -3
- package/dashboard/src/views/partials/empty-state.ejs +10 -5
- package/dashboard/src/views/partials/footer.ejs +8 -2
- package/dashboard/src/views/partials/head.ejs +2 -1
- package/dashboard/src/views/partials/header.ejs +16 -19
- package/dashboard/src/views/partials/layout-bottom.ejs +5 -40
- package/dashboard/src/views/partials/layout-top.ejs +6 -5
- package/dashboard/src/views/partials/logs-content.ejs +26 -29
- package/dashboard/src/views/partials/milestone-detail-content.ejs +5 -5
- package/dashboard/src/views/partials/milestones-content.ejs +40 -31
- package/dashboard/src/views/partials/note-detail-content.ejs +7 -5
- package/dashboard/src/views/partials/notes-content.ejs +4 -4
- package/dashboard/src/views/partials/phase-content.ejs +6 -8
- package/dashboard/src/views/partials/phase-doc-content.ejs +13 -15
- package/dashboard/src/views/partials/phase-timeline.ejs +22 -15
- package/dashboard/src/views/partials/phases-content.ejs +98 -84
- package/dashboard/src/views/partials/quick-content.ejs +34 -32
- package/dashboard/src/views/partials/quick-detail-content.ejs +20 -19
- package/dashboard/src/views/partials/requirements-content.ejs +6 -6
- package/dashboard/src/views/partials/research-content.ejs +14 -14
- package/dashboard/src/views/partials/research-detail-content.ejs +13 -11
- package/dashboard/src/views/partials/roadmap-content.ejs +145 -128
- package/dashboard/src/views/partials/sidebar.ejs +86 -140
- package/dashboard/src/views/partials/todo-create-content.ejs +51 -46
- package/dashboard/src/views/partials/todo-detail-content.ejs +26 -25
- package/dashboard/src/views/partials/todos-content.ejs +65 -62
- package/dashboard/src/views/partials/todos-done-content.ejs +33 -31
- 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/local-llm/metrics.js +121 -33
|
@@ -1,54 +1,59 @@
|
|
|
1
1
|
<%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
|
|
2
2
|
<h1>Create Todo</h1>
|
|
3
3
|
|
|
4
|
-
<p><a href="/todos">← Back to Todos</a></p>
|
|
4
|
+
<p><a href="/todos" class="btn btn-outline-secondary">← Back to Todos</a></p>
|
|
5
5
|
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<form method="POST" action="/todos"
|
|
9
|
+
hx-post="/todos"
|
|
10
|
+
hx-target="#main-content">
|
|
11
|
+
<div class="mb-3">
|
|
12
|
+
<label class="form-label">Title</label>
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
name="title"
|
|
16
|
+
placeholder="Todo title"
|
|
17
|
+
required
|
|
18
|
+
maxlength="200"
|
|
19
|
+
class="form-control"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
<div class="mb-3">
|
|
24
|
+
<label class="form-label">Priority</label>
|
|
25
|
+
<select name="priority" required class="form-select">
|
|
26
|
+
<option value="">Select priority...</option>
|
|
27
|
+
<option value="P0">P0 - Critical</option>
|
|
28
|
+
<option value="P1">P1 - High</option>
|
|
29
|
+
<option value="P2">P2 - Medium</option>
|
|
30
|
+
<option value="PX">PX - Low</option>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
<div class="mb-3">
|
|
35
|
+
<label class="form-label">Phase (optional)</label>
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
name="phase"
|
|
39
|
+
placeholder="e.g., 09-todo-write-operations"
|
|
40
|
+
class="form-control"
|
|
41
|
+
/>
|
|
42
|
+
<small class="form-text text-muted">Format: 01-phase-name</small>
|
|
43
|
+
</div>
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
<div class="mb-3">
|
|
46
|
+
<label class="form-label">Description</label>
|
|
47
|
+
<textarea
|
|
48
|
+
name="description"
|
|
49
|
+
rows="10"
|
|
50
|
+
required
|
|
51
|
+
placeholder="Markdown content..."
|
|
52
|
+
class="form-control"
|
|
53
|
+
></textarea>
|
|
54
|
+
</div>
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</
|
|
56
|
+
<button type="submit" class="btn btn-primary">Create Todo</button>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
<p><a href="/todos">← Back to Todos</a></p>
|
|
5
5
|
|
|
6
|
-
<
|
|
7
|
-
<header>
|
|
6
|
+
<div class="card mb-3">
|
|
7
|
+
<div class="card-header">
|
|
8
8
|
<strong>Todo <%= id %></strong>
|
|
9
9
|
|
|
10
10
|
<span class="status-badge" data-priority="<%= priority %>">
|
|
@@ -14,29 +14,30 @@
|
|
|
14
14
|
<span class="status-badge" data-status="<%= status %>">
|
|
15
15
|
<%= status %>
|
|
16
16
|
</span>
|
|
17
|
-
</
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<% if (phase) { %>
|
|
20
|
+
<p><strong>Phase:</strong> <%= phase %></p>
|
|
21
|
+
<% } %>
|
|
22
|
+
<% if (created) { %>
|
|
23
|
+
<p><strong>Created:</strong> <%= created %></p>
|
|
24
|
+
<% } %>
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
<p><strong>Phase:</strong> <%= phase %></p>
|
|
21
|
-
<% } %>
|
|
22
|
-
<% if (created) { %>
|
|
23
|
-
<p><strong>Created:</strong> <%= created %></p>
|
|
24
|
-
<% } %>
|
|
26
|
+
<hr>
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
<%- html %>
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</
|
|
41
|
-
|
|
42
|
-
</article>
|
|
30
|
+
<% if (status === 'pending') { %>
|
|
31
|
+
<hr>
|
|
32
|
+
<form method="POST" action="/todos/<%= id %>/done"
|
|
33
|
+
hx-post="/todos/<%= id %>/done"
|
|
34
|
+
hx-target="closest .card"
|
|
35
|
+
hx-swap="outerHTML"
|
|
36
|
+
hx-indicator="#done-spinner"
|
|
37
|
+
style="display:inline;">
|
|
38
|
+
<button type="submit" class="btn btn-outline-secondary">Mark as Done</button>
|
|
39
|
+
<span id="done-spinner" class="spinner-border spinner-border-sm htmx-indicator" role="status"></span>
|
|
40
|
+
</form>
|
|
41
|
+
<% } %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<h1>Todos</h1>
|
|
3
3
|
|
|
4
4
|
<p>
|
|
5
|
-
<a href="/todos/new"
|
|
5
|
+
<a href="/todos/new" class="btn btn-primary"
|
|
6
6
|
hx-get="/todos/new"
|
|
7
7
|
hx-target="#main-content"
|
|
8
8
|
hx-push-url="true">Create Todo</a>
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<% const f = typeof filters !== 'undefined' ? filters : { priority: '', status: '', q: '' }; %>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
<div class="
|
|
20
|
-
<
|
|
21
|
-
Priority
|
|
22
|
-
<select name="priority" onchange="this.form.requestSubmit()">
|
|
17
|
+
<form hx-get="/todos" hx-target="#main-content" hx-push-url="true" class="card mb-3">
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<div class="row g-2">
|
|
20
|
+
<div class="col-auto">
|
|
21
|
+
<label class="form-label">Priority</label>
|
|
22
|
+
<select name="priority" class="form-select" onchange="this.form.requestSubmit()">
|
|
23
23
|
<option value="">All</option>
|
|
24
24
|
<option value="high"<%= f.priority === 'high' ? ' selected' : '' %>>High</option>
|
|
25
25
|
<option value="medium"<%= f.priority === 'medium' ? ' selected' : '' %>>Medium</option>
|
|
@@ -29,27 +29,28 @@
|
|
|
29
29
|
<option value="P2"<%= f.priority === 'P2' ? ' selected' : '' %>>P2</option>
|
|
30
30
|
<option value="PX"<%= f.priority === 'PX' ? ' selected' : '' %>>PX</option>
|
|
31
31
|
</select>
|
|
32
|
-
</
|
|
33
|
-
<
|
|
34
|
-
Status
|
|
35
|
-
<select name="status" onchange="this.form.requestSubmit()">
|
|
32
|
+
</div>
|
|
33
|
+
<div class="col-auto">
|
|
34
|
+
<label class="form-label">Status</label>
|
|
35
|
+
<select name="status" class="form-select" onchange="this.form.requestSubmit()">
|
|
36
36
|
<option value="">All</option>
|
|
37
37
|
<option value="pending"<%= f.status === 'pending' ? ' selected' : '' %>>pending</option>
|
|
38
38
|
</select>
|
|
39
|
-
</
|
|
40
|
-
<
|
|
41
|
-
Search
|
|
39
|
+
</div>
|
|
40
|
+
<div class="col-auto">
|
|
41
|
+
<label class="form-label">Search</label>
|
|
42
42
|
<input type="search" name="q" value="<%= f.q %>" placeholder="Search titles..."
|
|
43
|
+
class="form-control"
|
|
43
44
|
hx-get="/todos" hx-target="#main-content" hx-push-url="true"
|
|
44
45
|
hx-trigger="keyup changed delay:300ms, search" hx-include="closest form">
|
|
45
|
-
</
|
|
46
|
+
</div>
|
|
46
47
|
</div>
|
|
47
|
-
</
|
|
48
|
-
</
|
|
48
|
+
</div>
|
|
49
|
+
</form>
|
|
49
50
|
|
|
50
51
|
<% if (todos.length > 0) { %>
|
|
51
52
|
<p>
|
|
52
|
-
<button class="secondary"
|
|
53
|
+
<button class="btn btn-outline-secondary"
|
|
53
54
|
hx-post="/todos/bulk-complete?priority=<%= encodeURIComponent(f.priority) %>&status=<%= encodeURIComponent(f.status) %>&q=<%= encodeURIComponent(f.q) %>"
|
|
54
55
|
hx-target="#main-content"
|
|
55
56
|
hx-swap="outerHTML"
|
|
@@ -57,51 +58,53 @@
|
|
|
57
58
|
hx-confirm="Complete all <%= todos.length %> visible todo(s)?">
|
|
58
59
|
Complete All Visible (<%= todos.length %>)
|
|
59
60
|
</button>
|
|
60
|
-
<span id="bulk-spinner" class="htmx-indicator"
|
|
61
|
+
<span id="bulk-spinner" class="spinner-border spinner-border-sm htmx-indicator" role="status"></span>
|
|
61
62
|
</p>
|
|
62
|
-
<
|
|
63
|
-
<div class="
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
<%= todo.priority %>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
<%= todo.status %>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
63
|
+
<div class="card">
|
|
64
|
+
<div class="card-body p-0">
|
|
65
|
+
<div class="table-responsive">
|
|
66
|
+
<table class="table table-vcenter table-hover card-table">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th scope="col">ID</th>
|
|
70
|
+
<th scope="col">Title</th>
|
|
71
|
+
<th scope="col">Priority</th>
|
|
72
|
+
<th scope="col">Phase</th>
|
|
73
|
+
<th scope="col">Status</th>
|
|
74
|
+
<th scope="col">Created</th>
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
<% todos.forEach(function(todo) { %>
|
|
79
|
+
<tr>
|
|
80
|
+
<td><%= todo.id %></td>
|
|
81
|
+
<td>
|
|
82
|
+
<a href="/todos/<%= todo.id %>"
|
|
83
|
+
hx-get="/todos/<%= todo.id %>"
|
|
84
|
+
hx-target="#main-content"
|
|
85
|
+
hx-push-url="true">
|
|
86
|
+
<%= todo.title %>
|
|
87
|
+
</a>
|
|
88
|
+
</td>
|
|
89
|
+
<td>
|
|
90
|
+
<span class="status-badge" data-priority="<%= todo.priority %>">
|
|
91
|
+
<%= todo.priority %>
|
|
92
|
+
</span>
|
|
93
|
+
</td>
|
|
94
|
+
<td><%= todo.phase || '—' %></td>
|
|
95
|
+
<td>
|
|
96
|
+
<span class="status-badge" data-status="<%= todo.status %>">
|
|
97
|
+
<%= todo.status %>
|
|
98
|
+
</span>
|
|
99
|
+
</td>
|
|
100
|
+
<td><%= todo.created ? new Date(todo.created).toISOString().slice(0, 10) : '—' %></td>
|
|
101
|
+
</tr>
|
|
102
|
+
<% }); %>
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
103
106
|
</div>
|
|
104
|
-
</
|
|
107
|
+
</div>
|
|
105
108
|
<% } else { %>
|
|
106
|
-
<%- include('empty-state', { icon: '📋', title: 'No todos found', action: '<p><a href="/todos/new"
|
|
109
|
+
<%- include('empty-state', { icon: '📋', title: 'No todos found', action: '<p><a href="/todos/new" class="btn btn-primary" hx-get="/todos/new" hx-target="#main-content" hx-push-url="true">Create Todo</a></p>' }) %>
|
|
107
110
|
<% } %>
|
|
@@ -7,38 +7,40 @@
|
|
|
7
7
|
hx-push-url="true">Back to Pending Todos</a></p>
|
|
8
8
|
|
|
9
9
|
<% if (todos.length > 0) { %>
|
|
10
|
-
<
|
|
11
|
-
<div class="
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<%= todo.priority %>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
10
|
+
<div class="card">
|
|
11
|
+
<div class="card-body p-0">
|
|
12
|
+
<div class="table-responsive">
|
|
13
|
+
<table class="table table-vcenter card-table">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th scope="col">ID</th>
|
|
17
|
+
<th scope="col">Title</th>
|
|
18
|
+
<th scope="col">Priority</th>
|
|
19
|
+
<th scope="col">Phase</th>
|
|
20
|
+
<th scope="col">Completed</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<% todos.forEach(function(todo) { %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><%= todo.id %></td>
|
|
27
|
+
<td><%= todo.title %></td>
|
|
28
|
+
<td>
|
|
29
|
+
<% if (todo.priority) { %>
|
|
30
|
+
<span class="status-badge" data-priority="<%= todo.priority %>">
|
|
31
|
+
<%= todo.priority %>
|
|
32
|
+
</span>
|
|
33
|
+
<% } %>
|
|
34
|
+
</td>
|
|
35
|
+
<td><%= todo.phase %></td>
|
|
36
|
+
<td><%= todo.completedAt || '' %></td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% }); %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
40
42
|
</div>
|
|
41
|
-
</
|
|
43
|
+
</div>
|
|
42
44
|
<% } else { %>
|
|
43
45
|
<%- include('empty-state', { icon: '✓', title: 'No completed todos', action: '<p><a href="/todos" hx-get="/todos" hx-target="#main-content" hx-push-url="true">View pending todos</a></p>' }) %>
|
|
44
46
|
<% } %>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
3
|
"displayName": "Plan-Build-Run",
|
|
4
|
-
"version": "2.27.
|
|
4
|
+
"version": "2.27.1",
|
|
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.27.
|
|
4
|
+
"version": "2.27.1",
|
|
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.27.
|
|
3
|
+
"version": "2.27.1",
|
|
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",
|
|
@@ -29,6 +29,15 @@ function logMetric(planningDir, entry) {
|
|
|
29
29
|
const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
|
|
30
30
|
|
|
31
31
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Update lifetime totals BEFORE appending to JSONL so seeding
|
|
34
|
+
// (which reads the JSONL) doesn't double-count this entry
|
|
35
|
+
try {
|
|
36
|
+
updateLifetimeTotals(logsDir, entry);
|
|
37
|
+
} catch (_) {
|
|
38
|
+
// Totals update failure is non-fatal
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n', 'utf8');
|
|
33
42
|
|
|
34
43
|
// Rotate if over MAX_ENTRIES
|
|
@@ -47,6 +56,63 @@ function logMetric(planningDir, entry) {
|
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Atomically increments the lifetime-totals.json running counters.
|
|
61
|
+
* This file persists across JSONL log rotations so lifetime metrics never plateau.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} logsDir - path to the .planning/logs directory
|
|
64
|
+
* @param {object} entry - the metric entry being logged
|
|
65
|
+
*/
|
|
66
|
+
function updateLifetimeTotals(logsDir, entry) {
|
|
67
|
+
const totalsFile = path.join(logsDir, 'lifetime-totals.json');
|
|
68
|
+
let totals = null;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
totals = JSON.parse(fs.readFileSync(totalsFile, 'utf8'));
|
|
72
|
+
} catch (_) {
|
|
73
|
+
// File doesn't exist yet or is corrupt — seed from existing JSONL
|
|
74
|
+
totals = seedTotalsFromJsonl(logsDir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
totals.total_calls = (totals.total_calls || 0) + 1;
|
|
78
|
+
totals.fallback_count = (totals.fallback_count || 0) + (entry.fallback_used ? 1 : 0);
|
|
79
|
+
totals.tokens_saved = (totals.tokens_saved || 0) + (entry.tokens_saved_frontier || 0);
|
|
80
|
+
totals.total_latency_ms = (totals.total_latency_ms || 0) + (entry.latency_ms || 0);
|
|
81
|
+
|
|
82
|
+
fs.writeFileSync(totalsFile, JSON.stringify(totals) + '\n', 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Seeds lifetime totals by scanning the existing JSONL log.
|
|
87
|
+
* Called once when lifetime-totals.json doesn't exist yet (migration from pre-totals installs).
|
|
88
|
+
* Returns the accumulated totals from whatever entries remain in the JSONL.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} logsDir - path to the .planning/logs directory
|
|
91
|
+
* @returns {{ total_calls: number, fallback_count: number, tokens_saved: number, total_latency_ms: number }}
|
|
92
|
+
*/
|
|
93
|
+
function seedTotalsFromJsonl(logsDir) {
|
|
94
|
+
const seed = { total_calls: 0, fallback_count: 0, tokens_saved: 0, total_latency_ms: 0 };
|
|
95
|
+
try {
|
|
96
|
+
const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
|
|
97
|
+
const contents = fs.readFileSync(logFile, 'utf8');
|
|
98
|
+
const lines = contents.split(/\r?\n/).filter((l) => l.trim() !== '');
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
try {
|
|
101
|
+
const e = JSON.parse(line);
|
|
102
|
+
seed.total_calls += 1;
|
|
103
|
+
seed.fallback_count += e.fallback_used ? 1 : 0;
|
|
104
|
+
seed.tokens_saved += e.tokens_saved_frontier || 0;
|
|
105
|
+
seed.total_latency_ms += e.latency_ms || 0;
|
|
106
|
+
} catch (_) {
|
|
107
|
+
// Skip malformed lines
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (_) {
|
|
111
|
+
// No JSONL file — start at zero
|
|
112
|
+
}
|
|
113
|
+
return seed;
|
|
114
|
+
}
|
|
115
|
+
|
|
50
116
|
/**
|
|
51
117
|
* Reads metric entries from the JSONL log that occurred at or after sessionStartTime.
|
|
52
118
|
*
|
|
@@ -131,47 +197,69 @@ function computeLifetimeMetrics(planningDir, frontierTokenRate) {
|
|
|
131
197
|
};
|
|
132
198
|
|
|
133
199
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
200
|
+
const rate = frontierTokenRate != null ? frontierTokenRate : 3.0;
|
|
201
|
+
const logsDir = path.join(planningDir, 'logs');
|
|
202
|
+
const totalsFile = path.join(logsDir, 'lifetime-totals.json');
|
|
203
|
+
|
|
204
|
+
// Primary path: read from lifetime-totals.json (survives log rotation)
|
|
205
|
+
let totals = null;
|
|
136
206
|
try {
|
|
137
|
-
|
|
207
|
+
totals = JSON.parse(fs.readFileSync(totalsFile, 'utf8'));
|
|
138
208
|
} catch (_) {
|
|
139
|
-
|
|
209
|
+
// No totals file — fall back to JSONL scan (migration path for existing installs)
|
|
140
210
|
}
|
|
141
211
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
212
|
+
// Build by_operation from the JSONL (only covers recent entries, but still useful)
|
|
213
|
+
const by_operation = {};
|
|
214
|
+
const logFile = path.join(logsDir, 'local-llm-metrics.jsonl');
|
|
215
|
+
try {
|
|
216
|
+
const contents = fs.readFileSync(logFile, 'utf8');
|
|
217
|
+
const entries = contents
|
|
218
|
+
.split(/\r?\n/)
|
|
219
|
+
.filter((l) => l.trim() !== '')
|
|
220
|
+
.map((l) => {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(l);
|
|
223
|
+
} catch (_) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.filter((e) => e !== null);
|
|
155
228
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
229
|
+
for (const e of entries) {
|
|
230
|
+
const op = e.operation || 'unknown';
|
|
231
|
+
if (!by_operation[op]) {
|
|
232
|
+
by_operation[op] = { calls: 0, fallbacks: 0, tokens_saved: 0 };
|
|
233
|
+
}
|
|
234
|
+
by_operation[op].calls += 1;
|
|
235
|
+
if (e.fallback_used) by_operation[op].fallbacks += 1;
|
|
236
|
+
by_operation[op].tokens_saved += e.tokens_saved_frontier || 0;
|
|
237
|
+
}
|
|
163
238
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
239
|
+
// If no totals file, compute from JSONL (legacy/migration path)
|
|
240
|
+
if (!totals) {
|
|
241
|
+
if (entries.length === 0) return zero;
|
|
242
|
+
const total_calls = entries.length;
|
|
243
|
+
const fallback_count = entries.filter((e) => e.fallback_used).length;
|
|
244
|
+
const totalLatency = entries.reduce((sum, e) => sum + (e.latency_ms || 0), 0);
|
|
245
|
+
const avg_latency_ms = total_calls > 0 ? totalLatency / total_calls : 0;
|
|
246
|
+
const tokens_saved = entries.reduce((sum, e) => sum + (e.tokens_saved_frontier || 0), 0);
|
|
247
|
+
const cost_saved_usd = tokens_saved * (rate / 1_000_000);
|
|
248
|
+
return { total_calls, fallback_count, avg_latency_ms, tokens_saved, cost_saved_usd, by_operation };
|
|
169
249
|
}
|
|
170
|
-
|
|
171
|
-
if
|
|
172
|
-
|
|
250
|
+
} catch (_) {
|
|
251
|
+
// No JSONL file — if we have totals, continue; otherwise return zero
|
|
252
|
+
if (!totals) return zero;
|
|
173
253
|
}
|
|
174
254
|
|
|
255
|
+
// Use lifetime totals for the headline numbers
|
|
256
|
+
const total_calls = totals.total_calls || 0;
|
|
257
|
+
const fallback_count = totals.fallback_count || 0;
|
|
258
|
+
const tokens_saved = totals.tokens_saved || 0;
|
|
259
|
+
const total_latency_ms = totals.total_latency_ms || 0;
|
|
260
|
+
const avg_latency_ms = total_calls > 0 ? total_latency_ms / total_calls : 0;
|
|
261
|
+
const cost_saved_usd = tokens_saved * (rate / 1_000_000);
|
|
262
|
+
|
|
175
263
|
return { total_calls, fallback_count, avg_latency_ms, tokens_saved, cost_saved_usd, by_operation };
|
|
176
264
|
} catch (_) {
|
|
177
265
|
return zero;
|
|
@@ -249,4 +337,4 @@ function logAgreement(planningDir, entry) {
|
|
|
249
337
|
}
|
|
250
338
|
}
|
|
251
339
|
|
|
252
|
-
module.exports = { logMetric, readSessionMetrics, summarizeMetrics, computeLifetimeMetrics, formatSessionSummary, logAgreement };
|
|
340
|
+
module.exports = { logMetric, readSessionMetrics, summarizeMetrics, computeLifetimeMetrics, formatSessionSummary, logAgreement, updateLifetimeTotals, seedTotalsFromJsonl };
|