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.
Files changed (26) hide show
  1. package/CLAUDE.md +31 -27
  2. package/dist/src/cli/commands/docs.js +4 -4
  3. package/dist/src/cli/commands/docs.js.map +1 -1
  4. package/dist/src/utils/docs-preview/config-generator.d.ts +14 -6
  5. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  6. package/dist/src/utils/docs-preview/config-generator.js +504 -126
  7. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  8. package/dist/src/utils/docs-preview/docusaurus-setup.d.ts +1 -1
  9. package/dist/src/utils/docs-preview/docusaurus-setup.d.ts.map +1 -1
  10. package/dist/src/utils/docs-preview/docusaurus-setup.js +92 -48
  11. package/dist/src/utils/docs-preview/docusaurus-setup.js.map +1 -1
  12. package/dist/src/utils/docs-preview/index.d.ts +2 -1
  13. package/dist/src/utils/docs-preview/index.d.ts.map +1 -1
  14. package/dist/src/utils/docs-preview/index.js +2 -1
  15. package/dist/src/utils/docs-preview/index.js.map +1 -1
  16. package/dist/src/utils/docs-preview/project-detector.d.ts +16 -0
  17. package/dist/src/utils/docs-preview/project-detector.d.ts.map +1 -0
  18. package/dist/src/utils/docs-preview/project-detector.js +215 -0
  19. package/dist/src/utils/docs-preview/project-detector.js.map +1 -0
  20. package/dist/src/utils/docs-preview/types.d.ts +26 -0
  21. package/dist/src/utils/docs-preview/types.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/specweave/scripts/progress.js +148 -50
  24. package/plugins/specweave/scripts/read-progress.sh +128 -52
  25. package/plugins/specweave/scripts/rebuild-dashboard-cache.sh +23 -17
  26. 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;CAC3B;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"}
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.263",
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
- * Bypasses LLM entirely - output shown directly to user
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 taskMatches = content.match(/### T-\d+/g);
84
- totalTasks = taskMatches ? taskMatches.length : 0;
85
-
86
- const completedMatches = content.match(/\*\*Status\*\*:\s*\[x\]/gi);
87
- completedTasks = completedMatches ? completedMatches.length : 0;
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
- percentage: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
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
- const readyForReview = increments.filter(i => i.status === 'ready_for_review');
122
- const active = increments.filter(i => ['active', 'planning', 'backlog'].includes(i.status));
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
- // Show ready_for_review FIRST (needs attention!)
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(`πŸ‘€ Ready for Review (${readyForReview.length}):`);
209
+ console.log('πŸ‘€ Ready for Review:');
130
210
  for (const inc of readyForReview) {
131
- const bar = createProgressBar(inc.percentage, 15);
132
- console.log(` ${inc.id}`);
133
- console.log(` ${bar} ${inc.completedTasks}/${inc.totalTasks} (${inc.percentage}%)`);
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(`πŸ”„ Active (${active.length}):`);
227
+ console.log('πŸ”„ Active:');
141
228
  for (const inc of active) {
142
- const bar = createProgressBar(inc.percentage, 15);
143
- console.log(` ${inc.id}`);
144
- console.log(` ${bar} ${inc.completedTasks}/${inc.totalTasks} (${inc.percentage}%)`);
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 section
158
- if (readyForReview.length > 0 || active.length > 0 || paused.length > 0) {
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 (readyForReview.length > 0) {
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
- SCRIPTS_DIR="$PROJECT_ROOT/plugins/specweave/scripts"
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
- echo "ACs: $AC_COMPLETED/$AC_TOTAL ($AC_PCT%)"
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
- # Get ready_for_review increments FIRST (needs attention!)
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
- select(.value.status == "ready_for_review") |
186
- "\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.acs.completed)|\(.value.acs.total)|\(.value.priority)|\(.value.type)"
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 inc_type; do
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 | $ac_completed/$ac_total ACs ($ac_pct%)"
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
- # Get active increments (excluding ready_for_review and backlog)
217
- # Note: "planned" is a legacy typo for "planning" - support both for backwards compatibility
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
- select(.value.status == "active" or .value.status == "planning" or .value.status == "planned") |
221
- "\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.acs.completed)|\(.value.acs.total)|\(.value.status)|\(.value.priority)|\(.value.type)"
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 inc_type; do
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 | $ac_completed/$ac_total ACs ($ac_pct%)"
305
+ echo " $bar $completed/$total tasks$ac_info"
251
306
  done <<< "$ACTIVE"
252
307
  echo ""
253
308
  fi
254
309
 
255
- # Get backlog increments (separate from active)
256
- BACKLOG=$(jq -r '
310
+ # ── Section 4: Planning (no tasks yet) β€” collapsed single line ──
311
+ EMPTY_ACTIVE=$(jq -r '
257
312
  .increments | to_entries[] |
258
- select(.value.status == "backlog") |
259
- "\(.key)|\(.value.tasks.completed)|\(.value.tasks.total)|\(.value.acs.completed)|\(.value.acs.total)|\(.value.priority)"
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 ac_completed ac_total priority; do
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
- # Get paused increments
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)|\(.value.acs.completed)|\(.value.acs.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
- PAUSED_COUNT=$(echo "$PAUSED" | wc -l | tr -d ' ')
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 ac_completed ac_total; do
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
- if [[ "$ac_total" -gt 0 ]]; then
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 section
309
- if [[ "$REVIEW_COUNT" -gt 0 ]] || [[ "$ACTIVE_COUNT" -gt 0 ]] || [[ "$BACKLOG_COUNT" -gt 0 ]] || [[ -n "$PAUSED" ]]; then
310
- # Has work - show summary
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 [[ "$REVIEW_COUNT" -gt 0 ]]; then
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>"