specweave 1.0.263 β 1.0.264
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/CLAUDE.md +31 -27
- package/dist/src/cli/commands/docs.js +4 -4
- package/dist/src/cli/commands/docs.js.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.d.ts +14 -6
- package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/config-generator.js +504 -126
- package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.d.ts +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/docusaurus-setup.js +92 -48
- package/dist/src/utils/docs-preview/docusaurus-setup.js.map +1 -1
- package/dist/src/utils/docs-preview/index.d.ts +2 -1
- package/dist/src/utils/docs-preview/index.d.ts.map +1 -1
- package/dist/src/utils/docs-preview/index.js +2 -1
- package/dist/src/utils/docs-preview/index.js.map +1 -1
- package/dist/src/utils/docs-preview/project-detector.d.ts +16 -0
- package/dist/src/utils/docs-preview/project-detector.d.ts.map +1 -0
- package/dist/src/utils/docs-preview/project-detector.js +215 -0
- package/dist/src/utils/docs-preview/project-detector.js.map +1 -0
- package/dist/src/utils/docs-preview/types.d.ts +26 -0
- package/dist/src/utils/docs-preview/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/scripts/progress.js +148 -50
- package/plugins/specweave/scripts/read-progress.sh +128 -52
- package/plugins/specweave/scripts/rebuild-dashboard-cache.sh +23 -17
- package/plugins/specweave/skills/progress/SKILL.md +7 -21
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
export interface DocCategory {
|
|
2
|
+
/** Folder name (e.g., 'strategy') */
|
|
3
|
+
id: string;
|
|
4
|
+
/** Human-readable label (e.g., 'Strategy') */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Short description for landing page */
|
|
7
|
+
description: string;
|
|
8
|
+
/** Emoji icon for the category */
|
|
9
|
+
icon: string;
|
|
10
|
+
/** Number of markdown documents found */
|
|
11
|
+
docCount: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectMetadata {
|
|
14
|
+
/** Project name */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Project description / tagline */
|
|
17
|
+
description: string;
|
|
18
|
+
/** 1-2 letter initials for logo generation */
|
|
19
|
+
initials: string;
|
|
20
|
+
/** Detected doc categories that actually exist */
|
|
21
|
+
categories: DocCategory[];
|
|
22
|
+
/** Source of the project name detection */
|
|
23
|
+
source: 'config' | 'package' | 'directory';
|
|
24
|
+
}
|
|
1
25
|
export interface DocusaurusConfig {
|
|
2
26
|
title: string;
|
|
3
27
|
tagline: string;
|
|
@@ -7,6 +31,8 @@ export interface DocusaurusConfig {
|
|
|
7
31
|
port?: number;
|
|
8
32
|
theme?: 'default' | 'classic' | 'dark';
|
|
9
33
|
excludeFolders?: string[];
|
|
34
|
+
/** Auto-detected project metadata */
|
|
35
|
+
projectMetadata?: ProjectMetadata;
|
|
10
36
|
}
|
|
11
37
|
export interface SidebarItem {
|
|
12
38
|
type: 'doc' | 'category';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/utils/docs-preview/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/utils/docs-preview/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,2CAA2C;IAC3C,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,qCAAqC;IACrC,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specweave",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.264",
|
|
4
4
|
"description": "Spec-driven development framework for AI coding agents. First-class support for Claude Code β compatible with any LLM-powered coding tool. Living documentation, autonomous execution, quality gates, and multilingual support (9 languages).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Instant Increment Progress
|
|
3
|
+
* Instant Increment Progress (Node.js fallback)
|
|
4
4
|
*
|
|
5
|
-
* Executed by UserPromptSubmit hook for /sw:progress
|
|
6
|
-
*
|
|
5
|
+
* Executed by UserPromptSubmit hook for /sw:progress when jq is unavailable.
|
|
6
|
+
* Reads files directly (no cache needed). Bypasses LLM entirely.
|
|
7
7
|
*
|
|
8
8
|
* Usage: node progress.js [incrementId] [--help]
|
|
9
9
|
*/
|
|
@@ -67,34 +67,80 @@ if (incrementFolders.length === 0) {
|
|
|
67
67
|
function parseIncrement(folder) {
|
|
68
68
|
const metaPath = path.join(incrementsDir, folder, 'metadata.json');
|
|
69
69
|
const tasksPath = path.join(incrementsDir, folder, 'tasks.md');
|
|
70
|
-
|
|
70
|
+
const specPath = path.join(incrementsDir, folder, 'spec.md');
|
|
71
|
+
|
|
71
72
|
let metadata = { status: 'unknown' };
|
|
72
73
|
if (fs.existsSync(metaPath)) {
|
|
73
74
|
try {
|
|
74
75
|
metadata = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
75
76
|
} catch {}
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
let totalTasks = 0;
|
|
79
80
|
let completedTasks = 0;
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
if (fs.existsSync(tasksPath)) {
|
|
82
83
|
const content = fs.readFileSync(tasksPath, 'utf-8');
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const headingTasks = (content.match(/^#{2,} T-\d+/gm) || []).length;
|
|
85
|
+
const checklistTasks = (content.match(/^- \[[x ]\] T-\d+/gm) || []).length;
|
|
86
|
+
totalTasks = headingTasks + checklistTasks;
|
|
87
|
+
|
|
88
|
+
const completedStatus = (content.match(/\*\*Status\*\*:\s*\[x\]/gi) || []).length;
|
|
89
|
+
const completedDate = (content.match(/\*\*Completed\*\*:\s*\d/gi) || []).length;
|
|
90
|
+
const completedChecklist = (content.match(/^- \[x\] T-\d+/gm) || []).length;
|
|
91
|
+
completedTasks = completedStatus + completedDate + completedChecklist;
|
|
88
92
|
}
|
|
89
|
-
|
|
93
|
+
|
|
94
|
+
let totalAcs = 0;
|
|
95
|
+
let completedAcs = 0;
|
|
96
|
+
if (fs.existsSync(specPath)) {
|
|
97
|
+
const specContent = fs.readFileSync(specPath, 'utf-8');
|
|
98
|
+
const acMatches = specContent.match(/- \[.\] \*\*AC-/g);
|
|
99
|
+
totalAcs = acMatches ? acMatches.length : 0;
|
|
100
|
+
const acDone = specContent.match(/- \[x\] \*\*AC-/g);
|
|
101
|
+
completedAcs = acDone ? acDone.length : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get last activity from newest file mtime
|
|
105
|
+
let lastActivity = 0;
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(metaPath)) lastActivity = Math.max(lastActivity, fs.statSync(metaPath).mtimeMs);
|
|
108
|
+
if (fs.existsSync(tasksPath)) lastActivity = Math.max(lastActivity, fs.statSync(tasksPath).mtimeMs);
|
|
109
|
+
if (fs.existsSync(specPath)) lastActivity = Math.max(lastActivity, fs.statSync(specPath).mtimeMs);
|
|
110
|
+
} catch {}
|
|
111
|
+
|
|
90
112
|
return {
|
|
91
113
|
id: folder,
|
|
92
114
|
status: metadata.status || 'unknown',
|
|
93
115
|
type: metadata.type || 'feature',
|
|
116
|
+
priority: metadata.priority || 'P1',
|
|
94
117
|
totalTasks,
|
|
95
118
|
completedTasks,
|
|
96
|
-
|
|
119
|
+
totalAcs,
|
|
120
|
+
completedAcs,
|
|
121
|
+
percentage: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0,
|
|
122
|
+
lastActivity
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Helpers
|
|
127
|
+
function createProgressBar(pct, width = 20) {
|
|
128
|
+
const filled = Math.round((pct / 100) * width);
|
|
129
|
+
const empty = width - filled;
|
|
130
|
+
return `[${'β'.repeat(filled)}${'β'.repeat(empty)}]`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatStatus(status) {
|
|
134
|
+
const icons = {
|
|
135
|
+
'active': 'π active',
|
|
136
|
+
'planning': 'π planning',
|
|
137
|
+
'backlog': 'π backlog',
|
|
138
|
+
'ready_for_review': 'π ready for review',
|
|
139
|
+
'paused': 'βΈοΈ paused',
|
|
140
|
+
'completed': 'β
completed',
|
|
141
|
+
'abandoned': 'β abandoned'
|
|
97
142
|
};
|
|
143
|
+
return icons[status] || status;
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
// If specific increment requested
|
|
@@ -104,67 +150,139 @@ if (specificId) {
|
|
|
104
150
|
console.log(`Increment not found: ${specificId}`);
|
|
105
151
|
process.exit(1);
|
|
106
152
|
}
|
|
107
|
-
|
|
153
|
+
|
|
108
154
|
const inc = parseIncrement(folder);
|
|
109
155
|
const bar = createProgressBar(inc.percentage);
|
|
110
|
-
|
|
156
|
+
|
|
111
157
|
console.log(`\nπ Progress: ${inc.id}\n`);
|
|
112
158
|
console.log(`Status: ${formatStatus(inc.status)}`);
|
|
113
159
|
console.log(`Type: ${inc.type}`);
|
|
114
160
|
console.log(`Tasks: ${inc.completedTasks}/${inc.totalTasks} (${inc.percentage}%)`);
|
|
115
161
|
console.log(`Progress: ${bar}`);
|
|
162
|
+
if (inc.totalAcs > 0) {
|
|
163
|
+
const acPct = Math.round((inc.completedAcs / inc.totalAcs) * 100);
|
|
164
|
+
console.log(`ACs: ${inc.completedAcs}/${inc.totalAcs} (${acPct}%)`);
|
|
165
|
+
}
|
|
116
166
|
process.exit(0);
|
|
117
167
|
}
|
|
118
168
|
|
|
119
169
|
// Show all active increments
|
|
120
170
|
const increments = incrementFolders.map(parseIncrement);
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
|
|
172
|
+
// Categorize increments
|
|
173
|
+
const needsClosure = increments
|
|
174
|
+
.filter(i => ['active', 'planning', 'planned'].includes(i.status) && i.totalTasks > 0 && i.completedTasks === i.totalTasks)
|
|
175
|
+
.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
176
|
+
|
|
177
|
+
const readyForReview = increments
|
|
178
|
+
.filter(i => i.status === 'ready_for_review')
|
|
179
|
+
.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
180
|
+
|
|
181
|
+
const active = increments
|
|
182
|
+
.filter(i => ['active', 'planning', 'planned'].includes(i.status) &&
|
|
183
|
+
(i.totalTasks > 0 || i.totalAcs > 0) &&
|
|
184
|
+
!(i.totalTasks > 0 && i.completedTasks === i.totalTasks))
|
|
185
|
+
.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
186
|
+
|
|
187
|
+
const emptyActive = increments
|
|
188
|
+
.filter(i => ['active', 'planning', 'planned'].includes(i.status) && i.totalTasks === 0 && i.totalAcs === 0);
|
|
189
|
+
|
|
190
|
+
const backlog = increments
|
|
191
|
+
.filter(i => i.status === 'backlog')
|
|
192
|
+
.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
193
|
+
|
|
123
194
|
const paused = increments.filter(i => i.status === 'paused');
|
|
124
195
|
|
|
125
196
|
console.log('\nπ Increment Progress\n');
|
|
126
197
|
|
|
127
|
-
//
|
|
198
|
+
// Section 1: Needs Closure
|
|
199
|
+
if (needsClosure.length > 0) {
|
|
200
|
+
console.log('β οΈ Needs Closure (100% tasks done):');
|
|
201
|
+
for (const inc of needsClosure) {
|
|
202
|
+
console.log(` ${inc.id} β /sw:done ${inc.id}`);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Section 2: Ready for Review
|
|
128
208
|
if (readyForReview.length > 0) {
|
|
129
|
-
console.log(
|
|
209
|
+
console.log('π Ready for Review:');
|
|
130
210
|
for (const inc of readyForReview) {
|
|
131
|
-
const bar = createProgressBar(inc.percentage,
|
|
132
|
-
|
|
133
|
-
|
|
211
|
+
const bar = createProgressBar(inc.percentage, 12);
|
|
212
|
+
const priBadge = inc.priority === 'P0' ? 'π΄' : inc.priority === 'P1' ? 'π ' : '';
|
|
213
|
+
let acInfo = '';
|
|
214
|
+
if (inc.totalAcs > 0) {
|
|
215
|
+
const acPct = Math.round((inc.completedAcs / inc.totalAcs) * 100);
|
|
216
|
+
acInfo = ` | ${inc.completedAcs}/${inc.totalAcs} ACs (${acPct}%)`;
|
|
217
|
+
}
|
|
218
|
+
console.log(` ${priBadge} ${inc.id}`);
|
|
219
|
+
console.log(` ${bar} ${inc.completedTasks}/${inc.totalTasks} tasks${acInfo}`);
|
|
134
220
|
console.log(` β /sw:done ${inc.id}`);
|
|
135
221
|
}
|
|
136
222
|
console.log('');
|
|
137
223
|
}
|
|
138
224
|
|
|
225
|
+
// Section 3: Active (with work, not done, not empty)
|
|
139
226
|
if (active.length > 0) {
|
|
140
|
-
console.log(
|
|
227
|
+
console.log('π Active:');
|
|
141
228
|
for (const inc of active) {
|
|
142
|
-
const bar = createProgressBar(inc.percentage,
|
|
143
|
-
|
|
144
|
-
|
|
229
|
+
const bar = createProgressBar(inc.percentage, 12);
|
|
230
|
+
const priBadge = inc.priority === 'P0' ? 'π΄' : inc.priority === 'P1' ? 'π ' : '';
|
|
231
|
+
const statusInd = ['planning', 'planned'].includes(inc.status) ? ' (π planning)' : '';
|
|
232
|
+
let acInfo = '';
|
|
233
|
+
if (inc.totalAcs > 0) {
|
|
234
|
+
const acPct = Math.round((inc.completedAcs / inc.totalAcs) * 100);
|
|
235
|
+
acInfo = ` | ${inc.completedAcs}/${inc.totalAcs} ACs (${acPct}%)`;
|
|
236
|
+
}
|
|
237
|
+
console.log(` ${priBadge} ${inc.id}${statusInd}`);
|
|
238
|
+
console.log(` ${bar} ${inc.completedTasks}/${inc.totalTasks} tasks${acInfo}`);
|
|
145
239
|
}
|
|
146
240
|
console.log('');
|
|
147
241
|
}
|
|
148
242
|
|
|
243
|
+
// Section 4: Planning (no tasks) β collapsed
|
|
244
|
+
if (emptyActive.length > 0) {
|
|
245
|
+
const ids = emptyActive.map(i => i.id).join(', ');
|
|
246
|
+
console.log(`π Planning (no tasks): ${ids}`);
|
|
247
|
+
console.log('');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Section 5: Backlog
|
|
251
|
+
if (backlog.length > 0) {
|
|
252
|
+
console.log('π Backlog:');
|
|
253
|
+
for (const inc of backlog) {
|
|
254
|
+
const priBadge = inc.priority === 'P0' ? 'π΄' : inc.priority === 'P1' ? 'π ' : '';
|
|
255
|
+
console.log(` ${priBadge} ${inc.id} - ${inc.percentage}% planned`);
|
|
256
|
+
}
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Section 6: Paused
|
|
149
261
|
if (paused.length > 0) {
|
|
150
262
|
console.log(`βΈοΈ Paused (${paused.length}):`);
|
|
151
263
|
for (const inc of paused) {
|
|
152
|
-
console.log(` ${inc.id} - ${inc.percentage}
|
|
264
|
+
console.log(` ${inc.id} - ${inc.percentage}% tasks`);
|
|
153
265
|
}
|
|
154
266
|
console.log('');
|
|
155
267
|
}
|
|
156
268
|
|
|
157
|
-
// Summary
|
|
158
|
-
|
|
269
|
+
// Summary
|
|
270
|
+
const totalActionable = needsClosure.length + readyForReview.length + active.length + emptyActive.length + backlog.length + paused.length;
|
|
271
|
+
if (totalActionable > 0) {
|
|
159
272
|
console.log('β'.repeat(40));
|
|
160
273
|
const parts = [];
|
|
274
|
+
if (needsClosure.length > 0) parts.push(`${needsClosure.length} needs closure`);
|
|
161
275
|
if (readyForReview.length > 0) parts.push(`${readyForReview.length} ready for review`);
|
|
162
276
|
if (active.length > 0) parts.push(`${active.length} active`);
|
|
277
|
+
if (emptyActive.length > 0) parts.push(`${emptyActive.length} planning`);
|
|
278
|
+
if (backlog.length > 0) parts.push(`${backlog.length} backlog`);
|
|
163
279
|
if (paused.length > 0) parts.push(`${paused.length} paused`);
|
|
164
280
|
console.log(`Summary: ${parts.join(', ')}`);
|
|
165
281
|
console.log('');
|
|
166
282
|
|
|
167
|
-
if (
|
|
283
|
+
if (needsClosure.length > 0) {
|
|
284
|
+
console.log('π‘ Run /sw:done <id> to close completed increments');
|
|
285
|
+
} else if (readyForReview.length > 0) {
|
|
168
286
|
console.log('π‘ Run /sw:done <id> to close reviewed increments');
|
|
169
287
|
} else {
|
|
170
288
|
console.log('π‘ For details: /sw:progress <incrementId>');
|
|
@@ -178,23 +296,3 @@ if (readyForReview.length > 0 || active.length > 0 || paused.length > 0) {
|
|
|
178
296
|
console.log('');
|
|
179
297
|
console.log('π‘ Run /sw:increment to start new work');
|
|
180
298
|
}
|
|
181
|
-
|
|
182
|
-
// Helpers
|
|
183
|
-
function createProgressBar(pct, width = 20) {
|
|
184
|
-
const filled = Math.round((pct / 100) * width);
|
|
185
|
-
const empty = width - filled;
|
|
186
|
-
return `[${'β'.repeat(filled)}${'β'.repeat(empty)}]`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function formatStatus(status) {
|
|
190
|
-
const icons = {
|
|
191
|
-
'active': 'π active',
|
|
192
|
-
'planning': 'π planning',
|
|
193
|
-
'backlog': 'π backlog',
|
|
194
|
-
'ready_for_review': 'π ready for review',
|
|
195
|
-
'paused': 'βΈοΈ paused',
|
|
196
|
-
'completed': 'β
completed',
|
|
197
|
-
'abandoned': 'β abandoned'
|
|
198
|
-
};
|
|
199
|
-
return icons[status] || status;
|
|
200
|
-
}
|
|
@@ -38,7 +38,8 @@ if [[ ! -d "$PROJECT_ROOT/.specweave" ]]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
CACHE_FILE="$PROJECT_ROOT/.specweave/state/dashboard.json"
|
|
41
|
-
|
|
41
|
+
# Resolve scripts dir relative to this script (works in both dev and plugin cache)
|
|
42
|
+
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
43
|
|
|
43
44
|
# Check if jq is available, fall back to Node if not
|
|
44
45
|
if ! command -v jq >/dev/null 2>&1; then
|
|
@@ -60,6 +61,36 @@ elif ! jq -e '.' "$CACHE_FILE" >/dev/null 2>&1; then
|
|
|
60
61
|
NEED_REBUILD=true
|
|
61
62
|
fi
|
|
62
63
|
|
|
64
|
+
# Staleness check: if any increment file is newer than cache, rebuild
|
|
65
|
+
if [[ "$NEED_REBUILD" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then
|
|
66
|
+
CACHE_MTIME=0
|
|
67
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
68
|
+
CACHE_MTIME=$(stat -f "%m" "$CACHE_FILE" 2>/dev/null || echo "0")
|
|
69
|
+
else
|
|
70
|
+
CACHE_MTIME=$(stat -c "%Y" "$CACHE_FILE" 2>/dev/null || echo "0")
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
INC_DIR="$PROJECT_ROOT/.specweave/increments"
|
|
74
|
+
if [[ -d "$INC_DIR" ]]; then
|
|
75
|
+
for d in "$INC_DIR"/[0-9]*/; do
|
|
76
|
+
[[ -d "$d" ]] || continue
|
|
77
|
+
for f in "$d"metadata.json "$d"tasks.md "$d"spec.md; do
|
|
78
|
+
[[ -f "$f" ]] || continue
|
|
79
|
+
FILE_MTIME=0
|
|
80
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
81
|
+
FILE_MTIME=$(stat -f "%m" "$f" 2>/dev/null || echo "0")
|
|
82
|
+
else
|
|
83
|
+
FILE_MTIME=$(stat -c "%Y" "$f" 2>/dev/null || echo "0")
|
|
84
|
+
fi
|
|
85
|
+
if [[ "$FILE_MTIME" -gt "$CACHE_MTIME" ]]; then
|
|
86
|
+
NEED_REBUILD=true
|
|
87
|
+
break 2
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
63
94
|
if [[ "$NEED_REBUILD" == "true" ]]; then
|
|
64
95
|
bash "$SCRIPTS_DIR/rebuild-dashboard-cache.sh" --quiet 2>/dev/null || true
|
|
65
96
|
fi
|
|
@@ -170,7 +201,9 @@ if [[ -n "$INCREMENT_ID" ]]; then
|
|
|
170
201
|
echo "Priority: $PRIORITY_FMT"
|
|
171
202
|
echo ""
|
|
172
203
|
echo "Tasks: $COMPLETED/$TOTAL ($TASK_PCT%) $BAR"
|
|
173
|
-
|
|
204
|
+
if [[ "$AC_TOTAL" -gt 0 ]]; then
|
|
205
|
+
echo "ACs: $AC_COMPLETED/$AC_TOTAL ($AC_PCT%)"
|
|
206
|
+
fi
|
|
174
207
|
exit 0
|
|
175
208
|
fi
|
|
176
209
|
|
|
@@ -179,17 +212,39 @@ echo ""
|
|
|
179
212
|
echo "π Increment Progress"
|
|
180
213
|
echo ""
|
|
181
214
|
|
|
182
|
-
#
|
|
215
|
+
# ββ Section 1: Needs Closure (100% done but still active/planning) ββ
|
|
216
|
+
NEEDS_CLOSURE=$(jq -r '
|
|
217
|
+
[.increments | to_entries[] |
|
|
218
|
+
select((.value.status == "active" or .value.status == "planning" or .value.status == "planned") and
|
|
219
|
+
.value.tasks.total > 0 and
|
|
220
|
+
.value.tasks.completed == .value.tasks.total)] |
|
|
221
|
+
sort_by(.value.lastActivityEpoch // 0) | reverse | .[] |
|
|
222
|
+
.key
|
|
223
|
+
' "$CACHE_FILE" 2>/dev/null)
|
|
224
|
+
|
|
225
|
+
CLOSURE_COUNT=0
|
|
226
|
+
if [[ -n "$NEEDS_CLOSURE" ]]; then
|
|
227
|
+
echo "β οΈ Needs Closure (100% tasks done):"
|
|
228
|
+
while IFS= read -r id; do
|
|
229
|
+
[[ -z "$id" ]] && continue
|
|
230
|
+
CLOSURE_COUNT=$((CLOSURE_COUNT + 1))
|
|
231
|
+
echo " $id β /sw:done $id"
|
|
232
|
+
done <<< "$NEEDS_CLOSURE"
|
|
233
|
+
echo ""
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# ββ Section 2: Ready for Review ββ
|
|
183
237
|
READY_FOR_REVIEW=$(jq -r '
|
|
184
|
-
.increments | to_entries[] |
|
|
185
|
-
|
|
186
|
-
|
|
238
|
+
[.increments | to_entries[] |
|
|
239
|
+
select(.value.status == "ready_for_review")] |
|
|
240
|
+
sort_by(.value.lastActivityEpoch // 0) | reverse | .[] |
|
|
241
|
+
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.acs.completed)|\(.value.acs.total)|\(.value.priority)"
|
|
187
242
|
' "$CACHE_FILE" 2>/dev/null)
|
|
188
243
|
|
|
189
244
|
REVIEW_COUNT=0
|
|
190
245
|
if [[ -n "$READY_FOR_REVIEW" ]]; then
|
|
191
246
|
echo "π Ready for Review:"
|
|
192
|
-
while IFS='|' read -r id completed total ac_completed ac_total priority
|
|
247
|
+
while IFS='|' read -r id completed total ac_completed ac_total priority; do
|
|
193
248
|
[[ -z "$id" ]] && continue
|
|
194
249
|
REVIEW_COUNT=$((REVIEW_COUNT + 1))
|
|
195
250
|
if [[ "$total" -gt 0 ]]; then
|
|
@@ -197,34 +252,36 @@ if [[ -n "$READY_FOR_REVIEW" ]]; then
|
|
|
197
252
|
else
|
|
198
253
|
pct=0
|
|
199
254
|
fi
|
|
200
|
-
if [[ "$ac_total" -gt 0 ]]; then
|
|
201
|
-
ac_pct=$((ac_completed * 100 / ac_total))
|
|
202
|
-
else
|
|
203
|
-
ac_pct=0
|
|
204
|
-
fi
|
|
205
255
|
bar=$(progress_bar "$pct" 12)
|
|
206
|
-
# Format priority badge
|
|
207
256
|
pri_badge=""
|
|
208
257
|
case "$priority" in P0|critical) pri_badge="π΄" ;; P1|high) pri_badge="π " ;; esac
|
|
258
|
+
ac_info=""
|
|
259
|
+
if [[ "$ac_total" -gt 0 ]]; then
|
|
260
|
+
ac_pct=$((ac_completed * 100 / ac_total))
|
|
261
|
+
ac_info=" | $ac_completed/$ac_total ACs ($ac_pct%)"
|
|
262
|
+
fi
|
|
209
263
|
echo " $pri_badge $id"
|
|
210
|
-
echo " $bar $completed/$total tasks
|
|
264
|
+
echo " $bar $completed/$total tasks$ac_info"
|
|
211
265
|
echo " β /sw:done $id"
|
|
212
266
|
done <<< "$READY_FOR_REVIEW"
|
|
213
267
|
echo ""
|
|
214
268
|
fi
|
|
215
269
|
|
|
216
|
-
#
|
|
217
|
-
#
|
|
270
|
+
# ββ Section 3: Active (with real work, not 100% done, not empty) ββ
|
|
271
|
+
# Excludes: 100%-done (shown in Needs Closure), 0-tasks-0-ACs (shown in Planning)
|
|
218
272
|
ACTIVE=$(jq -r '
|
|
219
|
-
.increments | to_entries[] |
|
|
220
|
-
|
|
221
|
-
|
|
273
|
+
[.increments | to_entries[] |
|
|
274
|
+
select((.value.status == "active" or .value.status == "planning" or .value.status == "planned") and
|
|
275
|
+
(.value.tasks.total > 0 or .value.acs.total > 0) and
|
|
276
|
+
((.value.tasks.total == 0) or (.value.tasks.completed < .value.tasks.total)))] |
|
|
277
|
+
sort_by(.value.lastActivityEpoch // 0) | reverse | .[] |
|
|
278
|
+
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.acs.completed)|\(.value.acs.total)|\(.value.status)|\(.value.priority)"
|
|
222
279
|
' "$CACHE_FILE" 2>/dev/null)
|
|
223
280
|
|
|
224
281
|
ACTIVE_COUNT=0
|
|
225
282
|
if [[ -n "$ACTIVE" ]]; then
|
|
226
283
|
echo "π Active:"
|
|
227
|
-
while IFS='|' read -r id completed total ac_completed ac_total inc_status priority
|
|
284
|
+
while IFS='|' read -r id completed total ac_completed ac_total inc_status priority; do
|
|
228
285
|
[[ -z "$id" ]] && continue
|
|
229
286
|
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
|
230
287
|
if [[ "$total" -gt 0 ]]; then
|
|
@@ -232,37 +289,57 @@ if [[ -n "$ACTIVE" ]]; then
|
|
|
232
289
|
else
|
|
233
290
|
pct=0
|
|
234
291
|
fi
|
|
235
|
-
if [[ "$ac_total" -gt 0 ]]; then
|
|
236
|
-
ac_pct=$((ac_completed * 100 / ac_total))
|
|
237
|
-
else
|
|
238
|
-
ac_pct=0
|
|
239
|
-
fi
|
|
240
292
|
bar=$(progress_bar "$pct" 12)
|
|
241
|
-
# Format priority badge
|
|
242
293
|
pri_badge=""
|
|
243
294
|
case "$priority" in P0|critical) pri_badge="π΄" ;; P1|high) pri_badge="π " ;; esac
|
|
244
|
-
# Show status indicator if planning
|
|
245
295
|
status_indicator=""
|
|
246
296
|
case "$inc_status" in
|
|
247
297
|
planning|planned) status_indicator=" (π planning)" ;;
|
|
248
298
|
esac
|
|
299
|
+
ac_info=""
|
|
300
|
+
if [[ "$ac_total" -gt 0 ]]; then
|
|
301
|
+
ac_pct=$((ac_completed * 100 / ac_total))
|
|
302
|
+
ac_info=" | $ac_completed/$ac_total ACs ($ac_pct%)"
|
|
303
|
+
fi
|
|
249
304
|
echo " $pri_badge $id$status_indicator"
|
|
250
|
-
echo " $bar $completed/$total tasks
|
|
305
|
+
echo " $bar $completed/$total tasks$ac_info"
|
|
251
306
|
done <<< "$ACTIVE"
|
|
252
307
|
echo ""
|
|
253
308
|
fi
|
|
254
309
|
|
|
255
|
-
#
|
|
256
|
-
|
|
310
|
+
# ββ Section 4: Planning (no tasks yet) β collapsed single line ββ
|
|
311
|
+
EMPTY_ACTIVE=$(jq -r '
|
|
257
312
|
.increments | to_entries[] |
|
|
258
|
-
select(.value.status == "
|
|
259
|
-
|
|
313
|
+
select((.value.status == "active" or .value.status == "planning" or .value.status == "planned") and
|
|
314
|
+
.value.tasks.total == 0 and .value.acs.total == 0) |
|
|
315
|
+
.key
|
|
316
|
+
' "$CACHE_FILE" 2>/dev/null)
|
|
317
|
+
|
|
318
|
+
EMPTY_COUNT=0
|
|
319
|
+
if [[ -n "$EMPTY_ACTIVE" ]]; then
|
|
320
|
+
EMPTY_LIST=""
|
|
321
|
+
while IFS= read -r id; do
|
|
322
|
+
[[ -z "$id" ]] && continue
|
|
323
|
+
EMPTY_COUNT=$((EMPTY_COUNT + 1))
|
|
324
|
+
[[ -n "$EMPTY_LIST" ]] && EMPTY_LIST="$EMPTY_LIST, "
|
|
325
|
+
EMPTY_LIST="$EMPTY_LIST$id"
|
|
326
|
+
done <<< "$EMPTY_ACTIVE"
|
|
327
|
+
echo "π Planning (no tasks): $EMPTY_LIST"
|
|
328
|
+
echo ""
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# ββ Section 5: Backlog ββ
|
|
332
|
+
BACKLOG=$(jq -r '
|
|
333
|
+
[.increments | to_entries[] |
|
|
334
|
+
select(.value.status == "backlog")] |
|
|
335
|
+
sort_by(.value.lastActivityEpoch // 0) | reverse | .[] |
|
|
336
|
+
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.priority)"
|
|
260
337
|
' "$CACHE_FILE" 2>/dev/null)
|
|
261
338
|
|
|
262
339
|
BACKLOG_COUNT=0
|
|
263
340
|
if [[ -n "$BACKLOG" ]]; then
|
|
264
341
|
echo "π Backlog:"
|
|
265
|
-
while IFS='|' read -r id completed total
|
|
342
|
+
while IFS='|' read -r id completed total priority; do
|
|
266
343
|
[[ -z "$id" ]] && continue
|
|
267
344
|
BACKLOG_COUNT=$((BACKLOG_COUNT + 1))
|
|
268
345
|
if [[ "$total" -gt 0 ]]; then
|
|
@@ -270,7 +347,6 @@ if [[ -n "$BACKLOG" ]]; then
|
|
|
270
347
|
else
|
|
271
348
|
pct=0
|
|
272
349
|
fi
|
|
273
|
-
# Format priority badge
|
|
274
350
|
pri_badge=""
|
|
275
351
|
case "$priority" in P0|critical) pri_badge="π΄" ;; P1|high) pri_badge="π " ;; esac
|
|
276
352
|
echo " $pri_badge $id - $pct% planned"
|
|
@@ -278,48 +354,46 @@ if [[ -n "$BACKLOG" ]]; then
|
|
|
278
354
|
echo ""
|
|
279
355
|
fi
|
|
280
356
|
|
|
281
|
-
#
|
|
357
|
+
# ββ Section 6: Paused ββ
|
|
282
358
|
PAUSED=$(jq -r '
|
|
283
359
|
.increments | to_entries[] |
|
|
284
360
|
select(.value.status == "paused") |
|
|
285
|
-
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)
|
|
361
|
+
"\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)"
|
|
286
362
|
' "$CACHE_FILE" 2>/dev/null)
|
|
287
363
|
|
|
364
|
+
PAUSED_COUNT=0
|
|
288
365
|
if [[ -n "$PAUSED" ]]; then
|
|
289
|
-
|
|
366
|
+
while IFS='|' read -r id completed total; do
|
|
367
|
+
[[ -z "$id" ]] && continue
|
|
368
|
+
PAUSED_COUNT=$((PAUSED_COUNT + 1))
|
|
369
|
+
done <<< "$PAUSED"
|
|
290
370
|
echo "βΈοΈ Paused ($PAUSED_COUNT):"
|
|
291
|
-
while IFS='|' read -r id completed total
|
|
371
|
+
while IFS='|' read -r id completed total; do
|
|
292
372
|
[[ -z "$id" ]] && continue
|
|
293
373
|
if [[ "$total" -gt 0 ]]; then
|
|
294
374
|
pct=$((completed * 100 / total))
|
|
295
375
|
else
|
|
296
376
|
pct=0
|
|
297
377
|
fi
|
|
298
|
-
|
|
299
|
-
ac_pct=$((ac_completed * 100 / ac_total))
|
|
300
|
-
else
|
|
301
|
-
ac_pct=0
|
|
302
|
-
fi
|
|
303
|
-
echo " $id - $pct% tasks | $ac_pct% ACs"
|
|
378
|
+
echo " $id - $pct% tasks"
|
|
304
379
|
done <<< "$PAUSED"
|
|
305
380
|
echo ""
|
|
306
381
|
fi
|
|
307
382
|
|
|
308
|
-
# Summary
|
|
309
|
-
|
|
310
|
-
|
|
383
|
+
# ββ Summary ββ
|
|
384
|
+
TOTAL_ACTIONABLE=$((CLOSURE_COUNT + REVIEW_COUNT + ACTIVE_COUNT + EMPTY_COUNT + BACKLOG_COUNT + PAUSED_COUNT))
|
|
385
|
+
if [[ "$TOTAL_ACTIONABLE" -gt 0 ]]; then
|
|
311
386
|
echo "ββββββββββββββββββββββββββββββββββββββββ"
|
|
312
387
|
SUMMARY_PARTS=()
|
|
388
|
+
[[ "$CLOSURE_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$CLOSURE_COUNT needs closure")
|
|
313
389
|
[[ "$REVIEW_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$REVIEW_COUNT ready for review")
|
|
314
390
|
[[ "$ACTIVE_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$ACTIVE_COUNT active")
|
|
391
|
+
[[ "$EMPTY_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$EMPTY_COUNT planning")
|
|
315
392
|
[[ "$BACKLOG_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$BACKLOG_COUNT backlog")
|
|
316
|
-
PAUSED_COUNT=0
|
|
317
|
-
[[ -n "$PAUSED" ]] && PAUSED_COUNT=$(echo "$PAUSED" | grep -c '|' || echo 0)
|
|
318
393
|
[[ "$PAUSED_COUNT" -gt 0 ]] && SUMMARY_PARTS+=("$PAUSED_COUNT paused")
|
|
319
394
|
|
|
320
395
|
IFS=', '; echo "Summary: ${SUMMARY_PARTS[*]}"
|
|
321
396
|
else
|
|
322
|
-
# No work in progress
|
|
323
397
|
echo "No active increments."
|
|
324
398
|
COMPLETED_COUNT=$(jq '.summary.completed // 0' "$CACHE_FILE")
|
|
325
399
|
if [[ "$COMPLETED_COUNT" -gt 0 ]]; then
|
|
@@ -328,9 +402,11 @@ else
|
|
|
328
402
|
fi
|
|
329
403
|
|
|
330
404
|
echo ""
|
|
331
|
-
if [[ "$
|
|
405
|
+
if [[ "$CLOSURE_COUNT" -gt 0 ]]; then
|
|
406
|
+
echo "π‘ Run /sw:done <id> to close completed increments"
|
|
407
|
+
elif [[ "$REVIEW_COUNT" -gt 0 ]]; then
|
|
332
408
|
echo "π‘ Run /sw:done <id> to close reviewed increments"
|
|
333
|
-
elif [[ "$ACTIVE_COUNT" -eq 0 ]]; then
|
|
409
|
+
elif [[ "$ACTIVE_COUNT" -eq 0 ]] && [[ "$EMPTY_COUNT" -eq 0 ]]; then
|
|
334
410
|
echo "π‘ Run /sw:increment to start new work"
|
|
335
411
|
else
|
|
336
412
|
echo "π‘ For details: /sw:progress <incrementId>"
|