@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,137 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { readMarkdownFile } from '../repositories/planning.repository.js';
4
+
5
+ /**
6
+ * List all research docs from .planning/research/*.md, sorted by filename descending.
7
+ *
8
+ * @param {string} projectDir - Absolute path to the project root
9
+ * @returns {Promise<Array>}
10
+ */
11
+ export async function listResearchDocs(projectDir) {
12
+ const dir = join(projectDir, '.planning', 'research');
13
+ let entries;
14
+ try {
15
+ entries = await readdir(dir);
16
+ } catch (err) {
17
+ if (err.code === 'ENOENT') return [];
18
+ throw err;
19
+ }
20
+ const mdFiles = entries.filter(f => f.endsWith('.md')).sort().reverse();
21
+ const results = await Promise.allSettled(
22
+ mdFiles.map(f => readMarkdownFile(join(dir, f)))
23
+ );
24
+ const docs = [];
25
+ for (let i = 0; i < mdFiles.length; i++) {
26
+ if (results[i].status !== 'fulfilled') continue;
27
+ const { frontmatter, html } = results[i].value;
28
+ const filename = mdFiles[i];
29
+ const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
30
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
31
+ docs.push({
32
+ filename,
33
+ slug,
34
+ title,
35
+ topic: frontmatter.topic || null,
36
+ date: frontmatter.research_date
37
+ ? (frontmatter.research_date instanceof Date
38
+ ? frontmatter.research_date.toISOString().slice(0, 10)
39
+ : String(frontmatter.research_date))
40
+ : null,
41
+ confidence: frontmatter.confidence || null,
42
+ coverage: frontmatter.coverage || null,
43
+ html
44
+ });
45
+ }
46
+ return docs;
47
+ }
48
+
49
+ /**
50
+ * List all codebase docs from .planning/codebase/*.md, sorted by filename descending.
51
+ *
52
+ * @param {string} projectDir - Absolute path to the project root
53
+ * @returns {Promise<Array>}
54
+ */
55
+ export async function listCodebaseDocs(projectDir) {
56
+ const dir = join(projectDir, '.planning', 'codebase');
57
+ let entries;
58
+ try {
59
+ entries = await readdir(dir);
60
+ } catch (err) {
61
+ if (err.code === 'ENOENT') return [];
62
+ throw err;
63
+ }
64
+ const mdFiles = entries.filter(f => f.endsWith('.md')).sort().reverse();
65
+ const results = await Promise.allSettled(
66
+ mdFiles.map(f => readMarkdownFile(join(dir, f)))
67
+ );
68
+ const docs = [];
69
+ for (let i = 0; i < mdFiles.length; i++) {
70
+ if (results[i].status !== 'fulfilled') continue;
71
+ const { frontmatter, html } = results[i].value;
72
+ const filename = mdFiles[i];
73
+ const slug = filename.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '');
74
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
75
+ docs.push({
76
+ filename,
77
+ slug,
78
+ title,
79
+ date: frontmatter.scan_date
80
+ ? (frontmatter.scan_date instanceof Date
81
+ ? frontmatter.scan_date.toISOString().slice(0, 10)
82
+ : String(frontmatter.scan_date))
83
+ : null,
84
+ html
85
+ });
86
+ }
87
+ return docs;
88
+ }
89
+
90
+ /**
91
+ * Get a single research or codebase doc by slug.
92
+ * Searches research/ first, then codebase/.
93
+ *
94
+ * @param {string} projectDir - Absolute path to the project root
95
+ * @param {string} slug - Slug derived from filename (without date prefix and .md extension)
96
+ * @returns {Promise<object|null>}
97
+ */
98
+ export async function getResearchDocBySlug(projectDir, slug) {
99
+ for (const subdir of ['research', 'codebase']) {
100
+ const dir = join(projectDir, '.planning', subdir);
101
+ let entries;
102
+ try {
103
+ entries = await readdir(dir);
104
+ } catch (err) {
105
+ if (err.code === 'ENOENT') continue;
106
+ throw err;
107
+ }
108
+ const filename = entries.find(
109
+ f => f.endsWith('.md') &&
110
+ f.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.md$/, '') === slug
111
+ );
112
+ if (!filename) continue;
113
+ const { frontmatter, html } = await readMarkdownFile(join(dir, filename));
114
+ const title = slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
115
+ return {
116
+ filename,
117
+ slug,
118
+ title,
119
+ topic: frontmatter.topic || null,
120
+ date: frontmatter.research_date
121
+ ? (frontmatter.research_date instanceof Date
122
+ ? frontmatter.research_date.toISOString().slice(0, 10)
123
+ : String(frontmatter.research_date))
124
+ : frontmatter.scan_date
125
+ ? (frontmatter.scan_date instanceof Date
126
+ ? frontmatter.scan_date.toISOString().slice(0, 10)
127
+ : String(frontmatter.scan_date))
128
+ : null,
129
+ confidence: frontmatter.confidence || null,
130
+ sources_checked: frontmatter.sources_checked || null,
131
+ coverage: frontmatter.coverage || null,
132
+ section: subdir,
133
+ html
134
+ };
135
+ }
136
+ return null;
137
+ }
@@ -1,17 +1,7 @@
1
1
  import { readFile, readdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { parseRoadmapFile } from './dashboard.service.js';
4
-
5
- /**
6
- * Strip UTF-8 BOM from file content.
7
- * Duplicated intentionally -- this service reads raw text, not via the repository layer.
8
- *
9
- * @param {string} content - Raw file content
10
- * @returns {string} Content without BOM
11
- */
12
- function stripBOM(content) {
13
- return content.replace(/^\uFEFF/, '');
14
- }
4
+ import { stripBOM } from '../utils/strip-bom.js';
15
5
 
16
6
  /**
17
7
  * Count the number of PLAN.md files in a phase directory.
@@ -230,6 +230,36 @@ export async function createTodo(projectDir, todoData) {
230
230
  });
231
231
  }
232
232
 
233
+ export async function listDoneTodos(projectDir) {
234
+ const doneDir = join(projectDir, '.planning', 'todos', 'done');
235
+ let files;
236
+ try {
237
+ files = await readdir(doneDir);
238
+ } catch (err) {
239
+ if (err.code === 'ENOENT') return [];
240
+ throw err;
241
+ }
242
+ const mdFiles = files.filter(f => f.endsWith('.md')).sort().reverse();
243
+ const todos = [];
244
+ for (const filename of mdFiles) {
245
+ const match = filename.match(/^(\d{3})-(.+)\.md$/);
246
+ if (!match) continue;
247
+ const [, id, slugPart] = match;
248
+ try {
249
+ const raw = await readFile(join(doneDir, filename), 'utf-8');
250
+ const { data } = matter(raw);
251
+ todos.push({
252
+ id, filename,
253
+ title: data.title || slugPart,
254
+ priority: data.priority || '',
255
+ phase: data.phase || '',
256
+ completedAt: data.completed_at || null
257
+ });
258
+ } catch { /* skip */ }
259
+ }
260
+ return todos;
261
+ }
262
+
233
263
  export async function completeTodo(projectDir, todoId) {
234
264
  return writeQueue.enqueue(async () => {
235
265
  const pendingDir = join(projectDir, '.planning', 'todos', 'pending');
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strip UTF-8 BOM (Byte Order Mark) if present.
3
+ * @param {string} content
4
+ * @returns {string}
5
+ */
6
+ export function stripBOM(content) {
7
+ return content.replace(/^\uFEFF/, '');
8
+ }
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: title, activePage: 'audits' }) %>
2
+
3
+ <%- include('partials/audit-detail-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Audit Reports', activePage: 'audits' }) %>
2
+
3
+ <%- include('partials/audits-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,5 @@
1
+ <%- include('partials/layout-top', { title: 'Config', activePage: 'config' }) %>
2
+
3
+ <%- include('partials/config-content') %>
4
+
5
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,3 @@
1
+ <%- include('partials/layout-top', { title: 'Logs', activePage: 'logs' }) %>
2
+ <%- include('partials/logs-content') %>
3
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,3 @@
1
+ <%- include('partials/layout-top', { title: title, activePage: 'notes' }) %>
2
+ <%- include('partials/note-detail-content') %>
3
+ <%- include('partials/layout-bottom') %>
@@ -0,0 +1,12 @@
1
+ <% if (!recentActivity || recentActivity.length === 0) { %>
2
+ <p class="muted">No recent activity.</p>
3
+ <% } else { %>
4
+ <ul class="activity-feed">
5
+ <% recentActivity.forEach(function(item) { %>
6
+ <li class="activity-item">
7
+ <span class="activity-path"><%= item.path %></span>
8
+ <time class="activity-time" datetime="<%= item.timestamp %>"><%= item.timestamp %></time>
9
+ </li>
10
+ <% }); %>
11
+ </ul>
12
+ <% } %>
@@ -0,0 +1,12 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1><%= title %></h1>
3
+
4
+ <p><a href="/audits">&larr; Back to Audit Reports</a></p>
5
+
6
+ <% if (typeof date !== 'undefined' && date) { %>
7
+ <p><small>Date: <%= date %></small></p>
8
+ <% } %>
9
+
10
+ <article class="markdown-body">
11
+ <%- html %>
12
+ </article>
@@ -0,0 +1,34 @@
1
+ <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
+ <h1>Audit Reports</h1>
3
+
4
+ <% if (typeof reports !== 'undefined' && reports.length > 0) { %>
5
+ <article>
6
+ <div class="table-wrap">
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th scope="col">Date</th>
11
+ <th scope="col">Report</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% reports.forEach(function(report) { %>
16
+ <tr>
17
+ <td><%= report.date || '—' %></td>
18
+ <td>
19
+ <a href="/audits/<%= report.filename %>"
20
+ hx-get="/audits/<%= report.filename %>"
21
+ hx-target="#main-content"
22
+ hx-push-url="true">
23
+ <%= report.title %>
24
+ </a>
25
+ </td>
26
+ </tr>
27
+ <% }); %>
28
+ </tbody>
29
+ </table>
30
+ </div>
31
+ </article>
32
+ <% } else { %>
33
+ <%- include('empty-state', { icon: '🔍', title: 'No audit reports found', action: 'Run /pbr:audit to generate a session audit report.' }) %>
34
+ <% } %>
@@ -0,0 +1,196 @@
1
+ <div class="config-page">
2
+ <div class="page-header">
3
+ <h1>Config</h1>
4
+ <p class="subtitle">Manage <code>.planning/config.json</code></p>
5
+ </div>
6
+
7
+ <% if (!config || !config.version) { %>
8
+ <div class="card">
9
+ <div class="card__body">
10
+ <p>No <code>.planning/config.json</code> found for this project.</p>
11
+ </div>
12
+ </div>
13
+ <% } else { %>
14
+
15
+ <form
16
+ hx-post="/api/config"
17
+ hx-target="#config-feedback"
18
+ hx-swap="innerHTML"
19
+ id="config-form"
20
+ >
21
+ <%# ── Common Settings ─────────────────────────────────────────── %>
22
+ <details class="card config-section" open>
23
+ <summary class="card__header"><h2>Common Settings</h2></summary>
24
+ <div class="card__body">
25
+
26
+ <%# Mode & Depth %>
27
+ <div class="config-row">
28
+ <label for="cfg-mode">Mode</label>
29
+ <select id="cfg-mode" name="mode">
30
+ <% ['normal', 'strict', 'relaxed'].forEach(opt => { %>
31
+ <option value="<%= opt %>" <%= config.mode === opt ? 'selected' : '' %>><%= opt %></option>
32
+ <% }); %>
33
+ </select>
34
+ </div>
35
+
36
+ <div class="config-row">
37
+ <label for="cfg-depth">Depth</label>
38
+ <select id="cfg-depth" name="depth">
39
+ <% ['minimal', 'standard', 'comprehensive'].forEach(opt => { %>
40
+ <option value="<%= opt %>" <%= config.depth === opt ? 'selected' : '' %>><%= opt %></option>
41
+ <% }); %>
42
+ </select>
43
+ </div>
44
+
45
+ <div class="config-row">
46
+ <label for="cfg-context-strategy">Context Strategy</label>
47
+ <select id="cfg-context-strategy" name="context_strategy">
48
+ <% ['conservative', 'balanced', 'aggressive'].forEach(opt => { %>
49
+ <option value="<%= opt %>" <%= config.context_strategy === opt ? 'selected' : '' %>><%= opt %></option>
50
+ <% }); %>
51
+ </select>
52
+ </div>
53
+
54
+ <%# Features toggles %>
55
+ <% if (config.features && typeof config.features === 'object') { %>
56
+ <fieldset class="config-fieldset">
57
+ <legend>Features</legend>
58
+ <% for (const [key, val] of Object.entries(config.features)) { %>
59
+ <div class="config-toggle-row">
60
+ <label>
61
+ <input
62
+ type="checkbox"
63
+ role="switch"
64
+ name="features.<%= key %>"
65
+ value="on"
66
+ <%= val ? 'checked' : '' %>
67
+ />
68
+ <%= key %>
69
+ </label>
70
+ </div>
71
+ <% } %>
72
+ </fieldset>
73
+ <% } %>
74
+
75
+ <%# Gates toggles %>
76
+ <% if (config.gates && typeof config.gates === 'object') { %>
77
+ <fieldset class="config-fieldset">
78
+ <legend>Gates</legend>
79
+ <% for (const [key, val] of Object.entries(config.gates)) { %>
80
+ <div class="config-toggle-row">
81
+ <label>
82
+ <input
83
+ type="checkbox"
84
+ role="switch"
85
+ name="gates.<%= key %>"
86
+ value="on"
87
+ <%= val ? 'checked' : '' %>
88
+ />
89
+ <%= key %>
90
+ </label>
91
+ </div>
92
+ <% } %>
93
+ </fieldset>
94
+ <% } %>
95
+
96
+ <%# Safety toggles %>
97
+ <% if (config.safety && typeof config.safety === 'object') { %>
98
+ <fieldset class="config-fieldset">
99
+ <legend>Safety</legend>
100
+ <% for (const [key, val] of Object.entries(config.safety)) { %>
101
+ <div class="config-toggle-row">
102
+ <label>
103
+ <input
104
+ type="checkbox"
105
+ role="switch"
106
+ name="safety.<%= key %>"
107
+ value="on"
108
+ <%= val ? 'checked' : '' %>
109
+ />
110
+ <%= key %>
111
+ </label>
112
+ </div>
113
+ <% } %>
114
+ </fieldset>
115
+ <% } %>
116
+
117
+ <%# Model selections %>
118
+ <% if (config.models && typeof config.models === 'object') { %>
119
+ <fieldset class="config-fieldset">
120
+ <legend>Models</legend>
121
+ <% for (const [key, val] of Object.entries(config.models)) { %>
122
+ <div class="config-row">
123
+ <label for="cfg-model-<%= key %>"><%= key %></label>
124
+ <input
125
+ id="cfg-model-<%= key %>"
126
+ type="text"
127
+ name="models.<%= key %>"
128
+ value="<%= val %>"
129
+ />
130
+ </div>
131
+ <% } %>
132
+ </fieldset>
133
+ <% } %>
134
+
135
+ <%# Parallelization number inputs %>
136
+ <% if (config.parallelization && typeof config.parallelization === 'object') { %>
137
+ <fieldset class="config-fieldset">
138
+ <legend>Parallelization</legend>
139
+ <% for (const [key, val] of Object.entries(config.parallelization)) { %>
140
+ <div class="config-row">
141
+ <label for="cfg-par-<%= key %>"><%= key %></label>
142
+ <% if (typeof val === 'number') { %>
143
+ <input id="cfg-par-<%= key %>" type="number" name="parallelization.<%= key %>" value="<%= val %>" min="1" />
144
+ <% } else { %>
145
+ <input id="cfg-par-<%= key %>" type="text" name="parallelization.<%= key %>" value="<%= val %>" />
146
+ <% } %>
147
+ </div>
148
+ <% } %>
149
+ </fieldset>
150
+ <% } %>
151
+
152
+ <%# Planning number/text inputs %>
153
+ <% if (config.planning && typeof config.planning === 'object') { %>
154
+ <fieldset class="config-fieldset">
155
+ <legend>Planning</legend>
156
+ <% for (const [key, val] of Object.entries(config.planning)) { %>
157
+ <div class="config-row">
158
+ <label for="cfg-plan-<%= key %>"><%= key %></label>
159
+ <% if (typeof val === 'number') { %>
160
+ <input id="cfg-plan-<%= key %>" type="number" name="planning.<%= key %>" value="<%= val %>" />
161
+ <% } else { %>
162
+ <input id="cfg-plan-<%= key %>" type="text" name="planning.<%= key %>" value="<%= val %>" />
163
+ <% } %>
164
+ </div>
165
+ <% } %>
166
+ </fieldset>
167
+ <% } %>
168
+
169
+ </div><%# end card__body %>
170
+ </details>
171
+
172
+ <%# ── Advanced (raw JSON) ─────────────────────────────────────── %>
173
+ <details class="card config-section">
174
+ <summary class="card__header"><h2>Advanced (Raw JSON)</h2></summary>
175
+ <div class="card__body">
176
+ <p class="config-hint">Edit the full config as JSON. On save, this value overrides the form above.</p>
177
+ <textarea
178
+ id="cfg-raw-json"
179
+ name="rawJson"
180
+ class="config-raw-json"
181
+ rows="24"
182
+ spellcheck="false"
183
+ ><%= JSON.stringify(config, null, 2) %></textarea>
184
+ <p class="config-hint">Tip: clear this field to save using the form controls above.</p>
185
+ </div>
186
+ </details>
187
+
188
+ <div class="config-actions">
189
+ <button type="submit">Save Config</button>
190
+ <span id="config-feedback"></span>
191
+ </div>
192
+
193
+ </form>
194
+
195
+ <% } %>
196
+ </div>
@@ -1,37 +1,49 @@
1
1
  <%- include('breadcrumbs', { breadcrumbs: typeof breadcrumbs !== 'undefined' ? breadcrumbs : [] }) %>
2
2
  <h1><%= projectName %></h1>
3
3
 
4
- <!-- Hero Stat Row -->
5
- <div class="hero-stats">
6
- <!-- Current Phase -->
7
- <article>
4
+ <!-- Status Cards -->
5
+ <div class="status-cards">
6
+ <!-- Current Phase Card -->
7
+ <article class="status-card">
8
8
  <header><strong>Current Phase</strong></header>
9
9
  <% if (currentPhase.id > 0) { %>
10
- <p class="stat-value">
10
+ <p>
11
11
  <a href="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
12
12
  hx-get="/phases/<%= String(currentPhase.id).padStart(2, '0') %>"
13
13
  hx-target="#main-content"
14
14
  hx-push-url="true">
15
- <span class="status-badge" data-status="in-progress">Phase <%= currentPhase.id %></span>
15
+ <span class="status-badge" data-status="<%= currentPhase.status %>">Phase <%= currentPhase.id %></span>
16
16
  <%= currentPhase.name %>
17
17
  </a>
18
18
  </p>
19
19
  <% } else { %>
20
- <p class="stat-value">No active phase</p>
20
+ <p>No active phase</p>
21
21
  <% } %>
22
+ <footer><small><%= currentPhase.planStatus %></small></footer>
22
23
  </article>
23
24
 
24
- <!-- Progress -->
25
- <article>
26
- <header><strong>Progress</strong></header>
27
- <p class="stat-value"><%= progress %>%</p>
28
- <progress value="<%= progress %>" max="100"></progress>
25
+ <!-- Milestone Progress Card -->
26
+ <article class="status-card">
27
+ <header><strong>Milestone Progress</strong></header>
28
+ <p>
29
+ <progress value="<%= progress %>" max="100"></progress>
30
+ <span><%= progress %>%</span>
31
+ </p>
32
+ <footer>
33
+ <small>
34
+ <% if (phases && phases.length > 0) { %>
35
+ <%= phases.filter(function(p) { return p.status === 'complete'; }).length %> of <%= phases.length %> phases complete
36
+ <% } else { %>
37
+ &mdash;
38
+ <% } %>
39
+ </small>
40
+ </footer>
29
41
  </article>
30
42
 
31
- <!-- Pending Todos -->
32
- <article>
43
+ <!-- Pending Todos Card -->
44
+ <article class="status-card">
33
45
  <header><strong>Pending Todos</strong></header>
34
- <p class="stat-value">
46
+ <p>
35
47
  <a href="/todos"
36
48
  hx-get="/todos"
37
49
  hx-target="#main-content"
@@ -39,46 +51,59 @@
39
51
  <%= typeof pendingTodoCount !== 'undefined' ? pendingTodoCount : 0 %>
40
52
  </a>
41
53
  </p>
54
+ <footer><small>P0/P1 items need attention</small></footer>
42
55
  </article>
43
56
 
44
- <!-- Last Activity -->
45
- <article>
57
+ <!-- Last Activity Card -->
58
+ <article class="status-card">
46
59
  <header><strong>Last Activity</strong></header>
47
60
  <% if (lastActivity.date) { %>
48
- <p class="stat-value"><%= lastActivity.date %></p>
49
- <p><small><%= lastActivity.description %></small></p>
61
+ <p><%= lastActivity.date %></p>
62
+ <footer><small><%= lastActivity.description %></small></footer>
50
63
  <% } else { %>
51
- <p class="stat-value">None</p>
64
+ <p>&mdash;</p>
52
65
  <% } %>
53
66
  </article>
54
67
  </div>
55
68
 
69
+ <!-- Phase Timeline -->
70
+ <section class="section-timeline">
71
+ <h2>Phase Timeline</h2>
72
+ <%- include('phase-timeline', { phases: phases, currentPhase: currentPhase }) %>
73
+ </section>
74
+
75
+ <!-- Activity Feed -->
76
+ <section class="section-activity">
77
+ <h2>Recent Activity</h2>
78
+ <%- include('activity-feed', { recentActivity: typeof recentActivity !== 'undefined' ? recentActivity : [] }) %>
79
+ </section>
80
+
56
81
  <!-- Quick Actions -->
57
82
  <div class="quick-actions">
58
- <% var phaseId = String(currentPhase.id).padStart(2, '0'); %>
59
- <a role="button" class="outline"
60
- href="/phases/<%= phaseId %>"
61
- hx-get="/phases/<%= phaseId %>"
62
- hx-target="#main-content"
63
- hx-push-url="true">Current Phase</a>
64
- <a role="button" class="outline"
65
- href="/roadmap"
66
- hx-get="/roadmap"
67
- hx-target="#main-content"
68
- hx-push-url="true">Roadmap</a>
69
- <a role="button" class="outline"
70
- href="/todos/new"
71
- hx-get="/todos/new"
72
- hx-target="#main-content"
73
- hx-push-url="true">Create Todo</a>
83
+ <% if (typeof quickActions !== 'undefined' && quickActions && quickActions.length > 0) { %>
84
+ <% quickActions.forEach(function(action) { %>
85
+ <a role="button" class="<%= action.primary ? '' : 'outline' %>"
86
+ href="<%= action.href %>"
87
+ hx-get="<%= action.href %>"
88
+ hx-target="#main-content"
89
+ hx-push-url="true"><%= action.label %></a>
90
+ <% }); %>
91
+ <% } else { %>
92
+ <% var phaseId = String(currentPhase.id).padStart(2, '0'); %>
93
+ <a role="button" class="outline"
94
+ href="/phases/<%= phaseId %>"
95
+ hx-get="/phases/<%= phaseId %>"
96
+ hx-target="#main-content"
97
+ hx-push-url="true">Current Phase</a>
98
+ <a role="button" class="outline"
99
+ href="/roadmap"
100
+ hx-get="/roadmap"
101
+ hx-target="#main-content"
102
+ hx-push-url="true">Roadmap</a>
103
+ <a role="button" class="outline"
104
+ href="/todos/new"
105
+ hx-get="/todos/new"
106
+ hx-target="#main-content"
107
+ hx-push-url="true">Create Todo</a>
108
+ <% } %>
74
109
  </div>
75
-
76
- <!-- Project Notes (if available) -->
77
- <% if (content && content !== '<p>No project README found.</p>') { %>
78
- <article>
79
- <header>
80
- <strong>Project Notes</strong>
81
- </header>
82
- <%- content %>
83
- </article>
84
- <% } %>
@@ -0,0 +1,17 @@
1
+ <div id="log-entries"
2
+ style="max-height:60vh;overflow-y:auto;font-family:var(--font-mono,monospace);font-size:0.78rem;background:var(--card-bg);border:1px solid var(--card-border);border-radius:var(--card-radius);padding:0.5rem">
3
+ <% if (typeof logData !== 'undefined' && logData && logData.entries.length > 0) { %>
4
+ <% logData.entries.forEach(function(entry) { %>
5
+ <div class="log-entry" style="padding:0.2rem 0;border-bottom:1px solid var(--card-border);word-break:break-all">
6
+ <span style="color:var(--muted-fg,#888)"><%= entry.timestamp ? String(entry.timestamp).slice(0,19).replace('T',' ') : '' %></span>
7
+ <% if (entry.type) { %><span class="status-badge status-badge--sm" data-status="<%= entry.type %>"><%= entry.type %></span><% } %>
8
+ <% if (entry.tool) { %><strong><%= entry.tool %></strong><% } %>
9
+ <% if (entry.message) { %><span><%= String(entry.message).slice(0,200) %></span><% } %>
10
+ </div>
11
+ <% }); %>
12
+ <% } else if (typeof logData !== 'undefined' && logData) { %>
13
+ <p style="padding:0.5rem;margin:0;color:var(--muted-fg,#888)">No entries match the current filter.</p>
14
+ <% } else { %>
15
+ <p style="padding:0.5rem;margin:0;color:var(--muted-fg,#888)">Waiting for live entries...</p>
16
+ <% } %>
17
+ </div>