@sienklogic/plan-build-run 2.23.0 → 2.26.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 (90) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +62 -13
  3. package/dashboard/package.json +2 -2
  4. package/dashboard/public/css/layout.css +128 -21
  5. package/dashboard/public/css/status-colors.css +14 -2
  6. package/dashboard/public/css/tokens.css +36 -0
  7. package/dashboard/src/middleware/current-phase.js +2 -1
  8. package/dashboard/src/repositories/planning.repository.js +1 -11
  9. package/dashboard/src/routes/events.routes.js +49 -0
  10. package/dashboard/src/routes/pages.routes.js +367 -3
  11. package/dashboard/src/server.js +4 -0
  12. package/dashboard/src/services/audit.service.js +42 -0
  13. package/dashboard/src/services/config.service.js +140 -0
  14. package/dashboard/src/services/dashboard.service.js +153 -19
  15. package/dashboard/src/services/log.service.js +105 -0
  16. package/dashboard/src/services/notes.service.js +16 -0
  17. package/dashboard/src/services/phase.service.js +58 -9
  18. package/dashboard/src/services/requirements.service.js +130 -0
  19. package/dashboard/src/services/research.service.js +137 -0
  20. package/dashboard/src/services/roadmap.service.js +1 -11
  21. package/dashboard/src/services/todo.service.js +30 -0
  22. package/dashboard/src/utils/strip-bom.js +8 -0
  23. package/dashboard/src/views/audit-detail.ejs +5 -0
  24. package/dashboard/src/views/audits.ejs +5 -0
  25. package/dashboard/src/views/config.ejs +5 -0
  26. package/dashboard/src/views/logs.ejs +3 -0
  27. package/dashboard/src/views/note-detail.ejs +3 -0
  28. package/dashboard/src/views/partials/activity-feed.ejs +12 -0
  29. package/dashboard/src/views/partials/audit-detail-content.ejs +12 -0
  30. package/dashboard/src/views/partials/audits-content.ejs +34 -0
  31. package/dashboard/src/views/partials/config-content.ejs +196 -0
  32. package/dashboard/src/views/partials/dashboard-content.ejs +71 -46
  33. package/dashboard/src/views/partials/log-entries-content.ejs +17 -0
  34. package/dashboard/src/views/partials/logs-content.ejs +131 -0
  35. package/dashboard/src/views/partials/note-detail-content.ejs +22 -0
  36. package/dashboard/src/views/partials/notes-content.ejs +7 -1
  37. package/dashboard/src/views/partials/phase-content.ejs +181 -146
  38. package/dashboard/src/views/partials/phase-timeline.ejs +16 -0
  39. package/dashboard/src/views/partials/requirements-content.ejs +44 -0
  40. package/dashboard/src/views/partials/research-content.ejs +49 -0
  41. package/dashboard/src/views/partials/research-detail-content.ejs +23 -0
  42. package/dashboard/src/views/partials/sidebar.ejs +67 -22
  43. package/dashboard/src/views/partials/todos-content.ejs +13 -3
  44. package/dashboard/src/views/partials/todos-done-content.ejs +44 -0
  45. package/dashboard/src/views/requirements.ejs +3 -0
  46. package/dashboard/src/views/research-detail.ejs +3 -0
  47. package/dashboard/src/views/research.ejs +3 -0
  48. package/dashboard/src/views/todos-done.ejs +3 -0
  49. package/package.json +1 -1
  50. package/plugins/copilot-pbr/agents/dev-sync.agent.md +114 -0
  51. package/plugins/copilot-pbr/agents/integration-checker.agent.md +9 -2
  52. package/plugins/copilot-pbr/agents/planner.agent.md +19 -0
  53. package/plugins/copilot-pbr/agents/verifier.agent.md +22 -2
  54. package/plugins/copilot-pbr/hooks/hooks.json +12 -0
  55. package/plugins/copilot-pbr/plugin.json +1 -1
  56. package/plugins/copilot-pbr/references/plan-format.md +22 -0
  57. package/plugins/copilot-pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
  58. package/plugins/copilot-pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
  59. package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
  60. package/plugins/cursor-pbr/agents/dev-sync.md +113 -0
  61. package/plugins/cursor-pbr/agents/integration-checker.md +9 -2
  62. package/plugins/cursor-pbr/agents/planner.md +19 -0
  63. package/plugins/cursor-pbr/agents/verifier.md +22 -2
  64. package/plugins/cursor-pbr/hooks/hooks.json +10 -0
  65. package/plugins/cursor-pbr/references/plan-format.md +22 -0
  66. package/plugins/cursor-pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
  67. package/plugins/cursor-pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
  68. package/plugins/pbr/.claude-plugin/plugin.json +1 -1
  69. package/plugins/pbr/agents/dev-sync.md +120 -0
  70. package/plugins/pbr/agents/integration-checker.md +9 -2
  71. package/plugins/pbr/agents/planner.md +19 -0
  72. package/plugins/pbr/agents/verifier.md +22 -2
  73. package/plugins/pbr/hooks/hooks.json +10 -0
  74. package/plugins/pbr/references/plan-format.md +22 -0
  75. package/plugins/pbr/scripts/check-plan-format.js +2 -2
  76. package/plugins/pbr/scripts/check-subagent-output.js +2 -2
  77. package/plugins/pbr/scripts/config-schema.json +4 -1
  78. package/plugins/pbr/scripts/local-llm/health.js +4 -1
  79. package/plugins/pbr/scripts/local-llm/operations/classify-commit.js +68 -0
  80. package/plugins/pbr/scripts/local-llm/operations/classify-file-intent.js +73 -0
  81. package/plugins/pbr/scripts/local-llm/operations/triage-test-output.js +72 -0
  82. package/plugins/pbr/scripts/post-bash-triage.js +132 -0
  83. package/plugins/pbr/scripts/post-write-dispatch.js +44 -0
  84. package/plugins/pbr/scripts/pre-bash-dispatch.js +17 -11
  85. package/plugins/pbr/scripts/status-line.js +50 -5
  86. package/plugins/pbr/scripts/validate-commit.js +66 -2
  87. package/plugins/pbr/scripts/validate-task.js +1 -1
  88. package/plugins/pbr/templates/INTEGRATION-REPORT.md.tmpl +18 -2
  89. package/plugins/pbr/templates/VERIFICATION-DETAIL.md.tmpl +2 -1
  90. package/dashboard/src/views/coming-soon.ejs +0 -11
@@ -0,0 +1,131 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Logs</h1>
3
+
4
+ <% if (!logFiles || logFiles.length === 0) { %>
5
+ <%- include('empty-state', { icon: 'L', title: 'No log files found', action: 'Log files appear in .planning/logs/ when PBR hooks run with local LLM routing enabled.' }) %>
6
+ <% } else { %>
7
+
8
+ <div class="logs-layout" style="display:flex;gap:var(--spacing-md);align-items:flex-start">
9
+
10
+ <%# File browser sidebar %>
11
+ <aside style="min-width:220px;max-width:260px;flex-shrink:0">
12
+ <article>
13
+ <header><strong>Log Files</strong></header>
14
+ <ul style="list-style:none;padding:0;margin:0">
15
+ <% logFiles.forEach(function(lf) { %>
16
+ <li style="padding:0.25rem 0;border-bottom:1px solid var(--card-border)">
17
+ <a href="/logs?file=<%= lf.name %>"
18
+ hx-get="/logs?file=<%= lf.name %>"
19
+ hx-target="#main-content"
20
+ hx-push-url="true"
21
+ <% if (selectedFile === lf.name) { %>aria-current="page"<% } %>>
22
+ <%= lf.name %>
23
+ </a>
24
+ <br>
25
+ <small><%= Math.round(lf.size / 1024) %> KB &middot; <%= lf.modified ? lf.modified.slice(0,10) : '' %></small>
26
+ </li>
27
+ <% }); %>
28
+ </ul>
29
+ </article>
30
+ </aside>
31
+
32
+ <%# Main log viewer %>
33
+ <div style="flex:1;min-width:0">
34
+ <% if (selectedFile) { %>
35
+
36
+ <%# Filter controls %>
37
+ <form hx-get="/logs"
38
+ hx-target="#main-content"
39
+ hx-push-url="true"
40
+ style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:var(--spacing-sm)">
41
+ <input type="hidden" name="file" value="<%= selectedFile %>">
42
+ <input type="text" name="type" placeholder="Type filter (e.g. tool_use)"
43
+ value="<%= typeof filters !== 'undefined' ? filters.type : '' %>"
44
+ style="width:200px">
45
+ <input type="search" name="q" placeholder="Search entries..."
46
+ value="<%= typeof filters !== 'undefined' ? filters.q : '' %>"
47
+ style="flex:1;min-width:160px">
48
+ <button type="submit">Filter</button>
49
+ <% if (typeof filters !== 'undefined' && (filters.type || filters.q)) { %>
50
+ <a href="/logs?file=<%= selectedFile %>"
51
+ hx-get="/logs?file=<%= selectedFile %>"
52
+ hx-target="#main-content"
53
+ hx-push-url="true"
54
+ role="button" class="secondary">Clear</a>
55
+ <% } %>
56
+ </form>
57
+
58
+ <%# Auto-scroll toggle + live indicator %>
59
+ <div style="display:flex;align-items:center;gap:1rem;margin-bottom:var(--spacing-sm)">
60
+ <label style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
61
+ <input type="checkbox" id="auto-scroll-toggle" checked style="margin:0">
62
+ <small>Auto-scroll</small>
63
+ </label>
64
+ <span id="live-indicator" class="status-badge" data-status="active" style="display:none">LIVE</span>
65
+ </div>
66
+
67
+ <%# Entry list: SSE target + initial content %>
68
+ <div id="log-entries-wrap"
69
+ hx-ext="sse"
70
+ sse-connect="/logs/stream?file=<%= selectedFile %>"
71
+ sse-swap="log-entry"
72
+ hx-swap="beforeend"
73
+ hx-target="#log-entries"
74
+ style="position:relative">
75
+ <%- include('log-entries-content') %>
76
+ </div>
77
+
78
+ <%# Pagination %>
79
+ <% if (logData && logData.total > logData.pageSize) { %>
80
+ <nav style="display:flex;gap:0.5rem;margin-top:var(--spacing-sm);align-items:center">
81
+ <% const totalPages = Math.ceil(logData.total / logData.pageSize); %>
82
+ <% if (logData.page > 1) { %>
83
+ <a href="/logs?file=<%= selectedFile %>&page=<%= logData.page - 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
84
+ hx-get="/logs?file=<%= selectedFile %>&page=<%= logData.page - 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
85
+ hx-target="#main-content"
86
+ hx-push-url="true"
87
+ role="button" class="secondary">&larr; Prev</a>
88
+ <% } %>
89
+ <small>Page <%= logData.page %> of <%= totalPages %> (<%= logData.total %> entries)</small>
90
+ <% if (logData.page < totalPages) { %>
91
+ <a href="/logs?file=<%= selectedFile %>&page=<%= logData.page + 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
92
+ hx-get="/logs?file=<%= selectedFile %>&page=<%= logData.page + 1 %>&type=<%= (filters || {}).type || '' %>&q=<%= (filters || {}).q || '' %>"
93
+ hx-target="#main-content"
94
+ hx-push-url="true"
95
+ role="button" class="secondary">Next &rarr;</a>
96
+ <% } %>
97
+ </nav>
98
+ <% } %>
99
+
100
+ <% } else { %>
101
+ <p>Select a log file to view entries.</p>
102
+ <% } %>
103
+ </div>
104
+ </div>
105
+
106
+ <script>
107
+ (function () {
108
+ const wrap = document.getElementById('log-entries-wrap');
109
+ const toggle = document.getElementById('auto-scroll-toggle');
110
+ const indicator = document.getElementById('live-indicator');
111
+
112
+ if (!wrap || !toggle) return;
113
+
114
+ // Show live indicator when SSE connects
115
+ wrap.addEventListener('htmx:sseOpen', () => {
116
+ if (indicator) indicator.style.display = '';
117
+ });
118
+ wrap.addEventListener('htmx:sseClose', () => {
119
+ if (indicator) indicator.style.display = 'none';
120
+ });
121
+
122
+ // Auto-scroll: when a new log-entry SSE event adds content, scroll to bottom
123
+ wrap.addEventListener('htmx:afterSwap', () => {
124
+ if (toggle && toggle.checked) {
125
+ const list = document.getElementById('log-entries');
126
+ if (list) list.scrollTop = list.scrollHeight;
127
+ }
128
+ });
129
+ })();
130
+ </script>
131
+ <% } %>
@@ -0,0 +1,22 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>
3
+ <%= title %>
4
+ <% if (promoted) { %>
5
+ &nbsp;<span class="status-badge" data-status="complete">promoted</span>
6
+ <% } %>
7
+ </h1>
8
+
9
+ <% if (date) { %>
10
+ <p><small><%= date %></small></p>
11
+ <% } %>
12
+
13
+ <article>
14
+ <div class="markdown-body">
15
+ <%- html %>
16
+ </div>
17
+ </article>
18
+
19
+ <p><a href="/notes"
20
+ hx-get="/notes"
21
+ hx-target="#main-content"
22
+ hx-push-url="true">Back to Notes</a></p>
@@ -5,9 +5,15 @@
5
5
  <%- include('empty-state', { icon: 'N', title: 'No notes yet', action: 'Use /pbr:note to create project notes.' }) %>
6
6
  <% } else { %>
7
7
  <% notes.forEach(function(note) { %>
8
+ <% const noteSlug = note.filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, ''); %>
8
9
  <article>
9
10
  <header>
10
- <strong><%= note.title %></strong>
11
+ <strong>
12
+ <a href="/notes/<%= noteSlug %>"
13
+ hx-get="/notes/<%= noteSlug %>"
14
+ hx-target="#main-content"
15
+ hx-push-url="true"><%= note.title %></a>
16
+ </strong>
11
17
  <% if (note.promoted) { %>
12
18
  &nbsp; <span class="status-badge" data-status="complete">promoted</span>
13
19
  <% } %>
@@ -1,49 +1,84 @@
1
1
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
2
  <h1>Phase <%= phaseId %>: <%= phaseName %></h1>
3
3
 
4
- <p><a href="/phases" hx-get="/phases" hx-target="#main-content" hx-push-url="true">&larr; Back to Phases</a></p>
4
+ <nav class="phase-nav" aria-label="Phase navigation">
5
+ <% if (typeof prevPhase !== 'undefined' && prevPhase) { %>
6
+ <a href="/phases/<%= String(prevPhase.id).padStart(2, '0') %>"
7
+ hx-get="/phases/<%= String(prevPhase.id).padStart(2, '0') %>"
8
+ hx-target="#main-content"
9
+ hx-push-url="true">
10
+ &larr; Phase <%= String(prevPhase.id).padStart(2, '0') %>: <%= prevPhase.name %>
11
+ </a>
12
+ <% } else { %>
13
+ <span></span>
14
+ <% } %>
15
+
16
+ <a href="/phases"
17
+ hx-get="/phases"
18
+ hx-target="#main-content"
19
+ hx-push-url="true">All Phases</a>
20
+
21
+ <% if (typeof nextPhase !== 'undefined' && nextPhase) { %>
22
+ <a href="/phases/<%= String(nextPhase.id).padStart(2, '0') %>"
23
+ hx-get="/phases/<%= String(nextPhase.id).padStart(2, '0') %>"
24
+ hx-target="#main-content"
25
+ hx-push-url="true">
26
+ Phase <%= String(nextPhase.id).padStart(2, '0') %>: <%= nextPhase.name %> &rarr;
27
+ </a>
28
+ <% } else { %>
29
+ <span></span>
30
+ <% } %>
31
+ </nav>
5
32
 
6
33
  <% if (verification) { %>
7
- <!-- Verification Summary Card -->
8
- <article>
9
- <header>
34
+ <%
35
+ var vStatus = verification.result || verification.status || verification.verdict || 'unknown';
36
+ var verificationCssStatus = 'not-started';
37
+ if (vStatus === 'pass' || vStatus === 'passed') verificationCssStatus = 'complete';
38
+ else if (vStatus === 'fail' || vStatus === 'failed') verificationCssStatus = 'blocked';
39
+ else if (vStatus === 'partial') verificationCssStatus = 'in-progress';
40
+ %>
41
+ <div class="card">
42
+ <div class="card__header">
10
43
  <strong>Verification Status</strong>
11
- </header>
12
- <%
13
- // Map verification status to CSS status-badge data-status values
14
- var vStatus = verification.result || verification.status || verification.verdict || 'unknown';
15
- var verificationCssStatus = 'not-started';
16
- if (vStatus === 'pass' || vStatus === 'passed') verificationCssStatus = 'complete';
17
- else if (vStatus === 'fail' || vStatus === 'failed') verificationCssStatus = 'blocked';
18
- else if (vStatus === 'partial') verificationCssStatus = 'in-progress';
19
- %>
20
- <p>
21
- <span class="status-badge" data-status="<%= verificationCssStatus %>">
22
- <%= vStatus.toUpperCase() %>
44
+ <span style="float:right;">
45
+ <span class="status-badge" data-status="<%= verificationCssStatus %>"><%= vStatus.toUpperCase() %></span>
23
46
  </span>
47
+ </div>
48
+ <div class="card__body">
24
49
  <% if (verification.verified) { %>
25
- &nbsp; Verified: <%= new Date(verification.verified).toLocaleString() %>
50
+ <p style="margin:0 0 var(--space-sm);">Verified: <%= new Date(verification.verified).toLocaleString() %></p>
26
51
  <% } %>
27
- </p>
28
- <% if (verification.score) { %>
29
- <p>
30
- Score: <%= verification.score.verified %>/<%= verification.score.total_must_haves %> must-haves verified
31
- </p>
32
- <% if (verification.score.failed > 0) { %>
33
- <p><strong><%= verification.score.failed %> must-haves FAILED</strong></p>
52
+ <% if (verification.score) { %>
53
+ <p style="margin:0 0 var(--space-sm);">
54
+ Score: <%= verification.score.verified %>/<%= verification.score.total_must_haves %> must-haves verified
55
+ <% if (verification.score.failed > 0) { %>
56
+ &nbsp; <strong><%= verification.score.failed %> FAILED</strong>
57
+ <% } %>
58
+ </p>
34
59
  <% } %>
35
- <% } %>
36
- <% if (verification.gaps && verification.gaps.length > 0) { %>
37
- <details>
38
- <summary>Verification Gaps (<%= verification.gaps.length %>)</summary>
39
- <ul>
40
- <% verification.gaps.forEach(function(gap) { %>
41
- <li><%= gap %></li>
42
- <% }); %>
43
- </ul>
44
- </details>
45
- <% } %>
46
- </article>
60
+ <% if (verification.mustHaves && verification.mustHaves.length > 0) { %>
61
+ <details>
62
+ <summary>Must-Haves (<%= verification.mustHaves.length %>)</summary>
63
+ <ul style="list-style:none;padding:0;margin:var(--space-sm) 0 0;">
64
+ <% verification.mustHaves.forEach(function(mh) { %>
65
+ <li style="display:flex;gap:var(--space-xs);align-items:flex-start;padding:var(--space-xs) 0;border-bottom:1px solid var(--color-border);">
66
+ <span class="status-badge status-badge--sm" data-status="<%= mh.passed ? 'complete' : 'blocked' %>">
67
+ <%= mh.passed ? 'PASS' : 'FAIL' %>
68
+ </span>
69
+ <span style="flex:1;font-size:0.875rem;"><%= mh.text %></span>
70
+ </li>
71
+ <% }); %>
72
+ </ul>
73
+ </details>
74
+ <% } else if (verification.gaps && verification.gaps.length > 0) { %>
75
+ <details>
76
+ <summary>Verification Gaps (<%= verification.gaps.length %>)</summary>
77
+ <ul><% verification.gaps.forEach(function(gap) { %><li><%= gap %></li><% }); %></ul>
78
+ </details>
79
+ <% } %>
80
+ </div>
81
+ </div>
47
82
  <% } %>
48
83
 
49
84
  <% if (plans.length === 0) { %>
@@ -57,94 +92,92 @@
57
92
 
58
93
  <!-- Plan Cards -->
59
94
  <% plans.forEach(function(plan) { %>
60
- <article>
61
- <header>
62
- <strong>Plan <%= plan.planId %></strong>
63
- <% if (plan.summary && plan.summary.status) { %>
64
- &nbsp;
65
- <span class="status-badge" data-status="<%= plan.summary.status %>">
66
- <%= plan.summary.status.replace('-', ' ') %>
67
- </span>
68
- <% } %>
69
- </header>
95
+ <%
96
+ var planStatus = (plan.summary && plan.summary.status) ? plan.summary.status : 'planned';
97
+ var planCssStatus = planStatus === 'built' ? 'complete'
98
+ : planStatus === 'building' ? 'in-progress'
99
+ : planStatus === 'blocked' ? 'blocked'
100
+ : 'not-started';
101
+ var wave = (plan.summary && plan.summary.wave) ? plan.summary.wave : null;
102
+ var taskCount = plan.taskCount || 0;
103
+ var planTitle = plan.planTitle || ('Plan ' + plan.planId);
104
+ %>
105
+ <div class="card">
106
+ <div class="card__header">
107
+ <span><strong><%= planTitle %></strong></span>
108
+ <span style="display:inline-flex;gap:var(--space-xs);align-items:center;float:right;">
109
+ <% if (wave) { %>
110
+ <span class="status-badge status-badge--sm" data-status="not-started" title="Wave <%= wave %>">W<%= wave %></span>
111
+ <% } %>
112
+ <% if (taskCount > 0) { %>
113
+ <span class="status-badge status-badge--sm" data-status="not-started"><%= taskCount %> task<%= taskCount !== 1 ? 's' : '' %></span>
114
+ <% } %>
115
+ <span class="status-badge" data-status="<%= planCssStatus %>"><%= planStatus.replace(/-/g, ' ').toUpperCase() %></span>
116
+ </span>
117
+ </div>
118
+ <div class="card__body">
119
+ <p style="margin:0 0 var(--space-sm);">
120
+ <a href="/phases/<%= phaseId %>/<%= plan.planId %>/plan"
121
+ hx-get="/phases/<%= phaseId %>/<%= plan.planId %>/plan"
122
+ hx-target="#main-content" hx-push-url="true">View Plan</a>
123
+ <% if (plan.summary) { %>
124
+ &nbsp;|&nbsp;
125
+ <a href="/phases/<%= phaseId %>/<%= plan.planId %>/summary"
126
+ hx-get="/phases/<%= phaseId %>/<%= plan.planId %>/summary"
127
+ hx-target="#main-content" hx-push-url="true">View Summary</a>
128
+ <% } %>
129
+ <% if (verification) { %>
130
+ &nbsp;|&nbsp;
131
+ <a href="/phases/<%= phaseId %>/<%= plan.planId %>/verification"
132
+ hx-get="/phases/<%= phaseId %>/<%= plan.planId %>/verification"
133
+ hx-target="#main-content" hx-push-url="true">View Verification</a>
134
+ <% } %>
135
+ </p>
70
136
 
71
- <p>
72
- <a href="/phases/<%= phaseId %>/<%= plan.planId %>/plan">View Plan</a>
73
137
  <% if (plan.summary) { %>
74
- &nbsp;|&nbsp; <a href="/phases/<%= phaseId %>/<%= plan.planId %>/summary">View Summary</a>
75
- <% } %>
76
- <% if (verification) { %>
77
- &nbsp;|&nbsp; <a href="/phases/<%= phaseId %>/<%= plan.planId %>/verification">View Verification</a>
78
- <% } %>
79
- </p>
80
-
81
- <% if (plan.summary) { %>
82
-
83
- <% if (plan.summary.subsystem) { %>
84
- <p><strong>Subsystem:</strong> <%= plan.summary.subsystem %></p>
138
+ <% if (plan.summary.subsystem) { %>
139
+ <p><strong>Subsystem:</strong> <%= plan.summary.subsystem %></p>
140
+ <% } %>
141
+ <% if (plan.summary.key_decisions && plan.summary.key_decisions.length > 0) { %>
142
+ <details>
143
+ <summary>Key Decisions (<%= plan.summary.key_decisions.length %>)</summary>
144
+ <ul><% plan.summary.key_decisions.forEach(function(d) { %><li><%= d %></li><% }); %></ul>
145
+ </details>
146
+ <% } %>
147
+ <% if (plan.summary.key_files && plan.summary.key_files.length > 0) { %>
148
+ <details>
149
+ <summary>Key Files (<%= plan.summary.key_files.length %>)</summary>
150
+ <ul><% plan.summary.key_files.forEach(function(f) { %><li><code><%= f %></code></li><% }); %></ul>
151
+ </details>
152
+ <% } %>
153
+ <% if (plan.summary.deferred && plan.summary.deferred.length > 0) { %>
154
+ <details>
155
+ <summary>Deferred Items (<%= plan.summary.deferred.length %>)</summary>
156
+ <ul><% plan.summary.deferred.forEach(function(item) { %><li><%= item %></li><% }); %></ul>
157
+ </details>
158
+ <% } %>
159
+ <% if (plan.summary.metrics) { %>
160
+ <p style="margin-top:var(--space-sm);">
161
+ <small>
162
+ <% if (plan.summary.metrics.duration_minutes != null) { %>Duration: <%= plan.summary.metrics.duration_minutes %> min<% } %>
163
+ <% if (plan.summary.metrics.commits != null) { %> | Commits: <%= plan.summary.metrics.commits %><% } %>
164
+ <% if (plan.summary.metrics.files_created != null || plan.summary.metrics.files_modified != null) { %>
165
+ | Files: <%= plan.summary.metrics.files_created || 0 %> created, <%= plan.summary.metrics.files_modified || 0 %> modified
166
+ <% } %>
167
+ </small>
168
+ </p>
169
+ <% } %>
170
+ <% } else { %>
171
+ <p><em>Plan file exists but has not been executed yet. No summary available.</em></p>
85
172
  <% } %>
86
-
87
- <% if (plan.summary.key_decisions && plan.summary.key_decisions.length > 0) { %>
88
- <details>
89
- <summary>Key Decisions (<%= plan.summary.key_decisions.length %>)</summary>
90
- <ul>
91
- <% plan.summary.key_decisions.forEach(function(decision) { %>
92
- <li><%= decision %></li>
93
- <% }); %>
94
- </ul>
95
- </details>
96
- <% } %>
97
-
98
- <% if (plan.summary.key_files && plan.summary.key_files.length > 0) { %>
99
- <details>
100
- <summary>Key Files (<%= plan.summary.key_files.length %>)</summary>
101
- <ul>
102
- <% plan.summary.key_files.forEach(function(file) { %>
103
- <li><code><%= file %></code></li>
104
- <% }); %>
105
- </ul>
106
- </details>
107
- <% } %>
108
-
109
- <% if (plan.summary.deferred && plan.summary.deferred.length > 0) { %>
110
- <details>
111
- <summary>Deferred Items (<%= plan.summary.deferred.length %>)</summary>
112
- <ul>
113
- <% plan.summary.deferred.forEach(function(item) { %>
114
- <li><%= item %></li>
115
- <% }); %>
116
- </ul>
117
- </details>
118
- <% } %>
119
-
120
- <% if (plan.summary.metrics) { %>
121
- <p>
122
- <small>
123
- <% if (plan.summary.metrics.duration_minutes != null) { %>
124
- Duration: <%= plan.summary.metrics.duration_minutes %> min
125
- <% } %>
126
- <% if (plan.summary.metrics.commits != null) { %>
127
- | Commits: <%= plan.summary.metrics.commits %>
128
- <% } %>
129
- <% if (plan.summary.metrics.files_created != null || plan.summary.metrics.files_modified != null) { %>
130
- | Files: <%= plan.summary.metrics.files_created || 0 %> created, <%= plan.summary.metrics.files_modified || 0 %> modified
131
- <% } %>
132
- </small>
133
- </p>
134
- <% } %>
135
-
136
- <% } else { %>
137
- <!-- Plan exists but not executed yet -->
138
- <p><em>Plan file exists but has not been executed yet. No summary available.</em></p>
139
- <% } %>
140
- </article>
173
+ </div>
174
+ </div>
141
175
  <% }); %>
142
176
 
143
177
  <% } %>
144
178
 
145
179
  <!-- Commit History Section -->
146
180
  <%
147
- // Collect all commits across all plans, tagged with their planId
148
181
  var allCommits = [];
149
182
  plans.forEach(function(plan) {
150
183
  if (plan.commits && plan.commits.length > 0) {
@@ -153,41 +186,43 @@
153
186
  });
154
187
  }
155
188
  });
189
+
190
+ // Extract commit type from task string: "NN-NN-T1: feat(scope): msg" -> "feat"
191
+ function commitType(entry) {
192
+ var m = entry.commit.task && entry.commit.task.match(/\b(feat|fix|refactor|test|docs|chore|wip|perf)\b/i);
193
+ return m ? m[1].toLowerCase() : 'chore';
194
+ }
195
+
196
+ var typeStatusMap = {
197
+ feat: 'complete', fix: 'in-progress', refactor: 'not-started',
198
+ test: 'not-started', docs: 'not-started', chore: 'not-started',
199
+ wip: 'in-progress', perf: 'complete'
200
+ };
156
201
  %>
157
202
 
158
203
  <h2>Commit History (<%= allCommits.length %>)</h2>
159
204
 
160
205
  <% if (allCommits.length === 0) { %>
161
- <article>
162
- <p><em>No commits yet for this phase.</em></p>
163
- </article>
206
+ <div class="card"><div class="card__body"><p><em>No commits yet for this phase.</em></p></div></div>
164
207
  <% } else { %>
165
- <div class="table-wrap">
166
- <table>
167
- <thead>
168
- <tr>
169
- <th>Commit</th>
170
- <th>Task</th>
171
- <th>Plan</th>
172
- <th>Files</th>
173
- <th>Status</th>
174
- </tr>
175
- </thead>
176
- <tbody>
177
- <% allCommits.forEach(function(entry) { %>
178
- <tr>
179
- <td><code><%= entry.commit.hash %></code></td>
180
- <td><%= entry.commit.task %></td>
181
- <td><%= entry.planId %></td>
182
- <td><%= entry.commit.files %></td>
183
- <td>
184
- <span class="status-badge" data-status="<%= entry.commit.verify === 'passed' ? 'complete' : (entry.commit.verify === 'failed' ? 'blocked' : 'in-progress') %>">
185
- <%= entry.commit.verify %>
186
- </span>
187
- </td>
188
- </tr>
189
- <% }); %>
190
- </tbody>
191
- </table>
192
- </div>
208
+ <ol class="commit-timeline">
209
+ <% allCommits.forEach(function(entry) { %>
210
+ <%
211
+ var ctype = commitType(entry);
212
+ var cStatus = typeStatusMap[ctype] || 'not-started';
213
+ var verifyStatus = entry.commit.verify === 'passed' ? 'complete'
214
+ : entry.commit.verify === 'failed' ? 'blocked' : 'in-progress';
215
+ %>
216
+ <li class="commit-timeline__item">
217
+ <span class="status-badge status-badge--sm" data-status="<%= cStatus %>" title="<%= ctype %>"><%= ctype %></span>
218
+ <code class="commit-timeline__hash"><%= entry.commit.hash %></code>
219
+ <span class="commit-timeline__task"><%= entry.commit.task %></span>
220
+ <span class="commit-timeline__meta">
221
+ plan <strong><%= entry.planId %></strong>
222
+ &middot; <%= entry.commit.files %> file<%= entry.commit.files !== 1 ? 's' : '' %>
223
+ &middot; <span class="status-badge status-badge--sm" data-status="<%= verifyStatus %>"><%= entry.commit.verify %></span>
224
+ </span>
225
+ </li>
226
+ <% }); %>
227
+ </ol>
193
228
  <% } %>
@@ -0,0 +1,16 @@
1
+ <% if (!phases || phases.length === 0) { %>
2
+ <p class="muted">No phases found.</p>
3
+ <% } else { %>
4
+ <ol class="phase-timeline">
5
+ <% phases.forEach(function(phase) { %>
6
+ <li class="phase-step" data-status="<%= phase.status %>"<% if (currentPhase && phase.id === currentPhase.id) { %> aria-current="step"<% } %>>
7
+ <span class="step-indicator"></span>
8
+ <a class="step-label"
9
+ href="/phases/<%= String(phase.id).padStart(2, '0') %>"
10
+ hx-get="/phases/<%= String(phase.id).padStart(2, '0') %>"
11
+ hx-target="#main-content"
12
+ hx-push-url="true">Phase <%= phase.id %></a>
13
+ </li>
14
+ <% }); %>
15
+ </ol>
16
+ <% } %>
@@ -0,0 +1,44 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Requirements</h1>
3
+
4
+ <% if (totalCount === 0) { %>
5
+ <%- include('empty-state', { icon: 'R', title: 'No requirements found', action: 'Add a REQUIREMENTS.md file to your .planning/ directory.' }) %>
6
+ <% } else { %>
7
+ <p class="requirements-summary">
8
+ <strong><%= coveredCount %> / <%= totalCount %></strong> requirements covered by plans.
9
+ <% if (uncoveredCount > 0) { %>
10
+ <span class="status-badge" data-status="warning"><%= uncoveredCount %> uncovered</span>
11
+ <% } else { %>
12
+ <span class="status-badge" data-status="verified">All covered</span>
13
+ <% } %>
14
+ </p>
15
+
16
+ <% sections.forEach(function(section) { %>
17
+ <section class="requirements-section">
18
+ <h2><%= section.sectionTitle %></h2>
19
+ <% section.requirements.forEach(function(req) { %>
20
+ <article class="card requirements-card" data-covered="<%= req.covered %>">
21
+ <header class="card__header">
22
+ <code class="req-id"><%= req.id %></code>
23
+ <% if (req.covered) { %>
24
+ <span class="status-badge" data-status="verified">covered</span>
25
+ <% } else { %>
26
+ <span class="status-badge" data-status="warning">uncovered</span>
27
+ <% } %>
28
+ </header>
29
+ <div class="card__body">
30
+ <p><%= req.text %></p>
31
+ <% if (req.planRefs.length > 0) { %>
32
+ <p class="req-plan-refs">
33
+ <small>Plans: </small>
34
+ <% req.planRefs.forEach(function(planId) { %>
35
+ <code class="req-plan-ref"><%= planId %></code>
36
+ <% }); %>
37
+ </p>
38
+ <% } %>
39
+ </div>
40
+ </article>
41
+ <% }); %>
42
+ </section>
43
+ <% }); %>
44
+ <% } %>
@@ -0,0 +1,49 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Research</h1>
3
+
4
+ <h2>Research Documents</h2>
5
+ <% if (researchDocs.length === 0) { %>
6
+ <%- include('empty-state', { icon: 'R', title: 'No research documents', action: 'Run /pbr:explore or /pbr:scan to generate research.' }) %>
7
+ <% } else { %>
8
+ <% researchDocs.forEach(function(doc) { %>
9
+ <article class="card">
10
+ <header class="card__header">
11
+ <strong>
12
+ <a href="/research/<%= doc.slug %>"
13
+ hx-get="/research/<%= doc.slug %>"
14
+ hx-target="#main-content"
15
+ hx-push-url="true"><%= doc.title %></a>
16
+ </strong>
17
+ <% if (doc.date) { %><small style="float:right"><%= doc.date %></small><% } %>
18
+ </header>
19
+ <div class="card__body">
20
+ <% if (doc.topic) { %><p><strong>Topic:</strong> <%= doc.topic %></p><% } %>
21
+ <% if (doc.confidence || doc.coverage) { %>
22
+ <p>
23
+ <% if (doc.confidence) { %><span class="status-badge" data-status="<%= doc.confidence %>"><%= doc.confidence %></span>&nbsp;<% } %>
24
+ <% if (doc.coverage) { %><small>Coverage: <%= doc.coverage %></small><% } %>
25
+ </p>
26
+ <% } %>
27
+ </div>
28
+ </article>
29
+ <% }); %>
30
+ <% } %>
31
+
32
+ <h2>Codebase Documents</h2>
33
+ <% if (codebaseDocs.length === 0) { %>
34
+ <%- include('empty-state', { icon: 'C', title: 'No codebase documents', action: 'Run /pbr:scan to generate codebase analysis.' }) %>
35
+ <% } else { %>
36
+ <% codebaseDocs.forEach(function(doc) { %>
37
+ <article class="card">
38
+ <header class="card__header">
39
+ <strong>
40
+ <a href="/research/<%= doc.slug %>"
41
+ hx-get="/research/<%= doc.slug %>"
42
+ hx-target="#main-content"
43
+ hx-push-url="true"><%= doc.title %></a>
44
+ </strong>
45
+ <% if (doc.date) { %><small style="float:right"><%= doc.date %></small><% } %>
46
+ </header>
47
+ </article>
48
+ <% }); %>
49
+ <% } %>