specweave 0.16.11 → 0.16.12

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 (47) hide show
  1. package/CLAUDE.md +11 -6
  2. package/bin/specweave.js +9 -0
  3. package/dist/cli/commands/revert-wip-limit.js +60 -0
  4. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  5. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +38 -2
  6. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  7. package/dist/src/cli/commands/migrate-to-profiles.d.ts +32 -0
  8. package/dist/src/cli/commands/migrate-to-profiles.d.ts.map +1 -1
  9. package/dist/src/cli/commands/migrate-to-profiles.js +8 -6
  10. package/dist/src/cli/commands/migrate-to-profiles.js.map +1 -1
  11. package/dist/src/cli/commands/revert-wip-limit.d.ts +8 -0
  12. package/dist/src/cli/commands/revert-wip-limit.d.ts.map +1 -0
  13. package/dist/src/cli/commands/revert-wip-limit.js +61 -0
  14. package/dist/src/cli/commands/revert-wip-limit.js.map +1 -0
  15. package/dist/src/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
  16. package/dist/src/cli/helpers/issue-tracker/ado.js +5 -0
  17. package/dist/src/cli/helpers/issue-tracker/ado.js.map +1 -1
  18. package/dist/src/cli/helpers/issue-tracker/jira.d.ts.map +1 -1
  19. package/dist/src/cli/helpers/issue-tracker/jira.js +5 -0
  20. package/dist/src/cli/helpers/issue-tracker/jira.js.map +1 -1
  21. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  22. package/dist/src/core/increment/metadata-manager.js +6 -0
  23. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  24. package/dist/src/core/repo-structure/setup-state-manager.d.ts +7 -0
  25. package/dist/src/core/repo-structure/setup-state-manager.d.ts.map +1 -1
  26. package/dist/src/core/repo-structure/setup-state-manager.js +28 -2
  27. package/dist/src/core/repo-structure/setup-state-manager.js.map +1 -1
  28. package/dist/src/utils/external-resource-validator.d.ts +6 -0
  29. package/dist/src/utils/external-resource-validator.d.ts.map +1 -1
  30. package/dist/src/utils/external-resource-validator.js +298 -57
  31. package/dist/src/utils/external-resource-validator.js.map +1 -1
  32. package/package.json +1 -1
  33. package/plugins/specweave/.claude-plugin/plugin.json +2 -1
  34. package/plugins/specweave/commands/revert-wip-limit.md +82 -0
  35. package/plugins/specweave/hooks/hooks.json +10 -0
  36. package/plugins/specweave/hooks/lib/migrate-increment-work.sh +245 -0
  37. package/plugins/specweave/hooks/post-increment-planning.sh +26 -40
  38. package/plugins/specweave/hooks/user-prompt-submit.sh +119 -12
  39. package/plugins/specweave/lib/hooks/sync-living-docs.js +113 -129
  40. package/plugins/specweave/lib/hooks/sync-living-docs.ts +46 -2
  41. package/plugins/specweave-ado/.claude-plugin/plugin.json +2 -1
  42. package/plugins/specweave-ado/skills/ado-resource-validator/SKILL.md +27 -1
  43. package/plugins/specweave-github/.claude-plugin/plugin.json +2 -1
  44. package/plugins/specweave-jira/.claude-plugin/plugin.json +2 -1
  45. package/plugins/specweave-jira/skills/jira-resource-validator/SKILL.md +31 -1
  46. package/plugins/specweave-release/.claude-plugin/plugin.json +2 -1
  47. package/dist/core/increment/metadata-manager.js +0 -335
@@ -1,335 +0,0 @@
1
- /**
2
- * Metadata Manager
3
- *
4
- * Handles CRUD operations for increment metadata (status, type, timestamps).
5
- * Part of increment 0007: Smart Status Management
6
- */
7
- import fs from 'fs-extra';
8
- import path from 'path';
9
- import { IncrementStatus, IncrementType, createDefaultMetadata, isValidTransition, isStale, shouldAutoAbandon } from '../types/increment-metadata.js';
10
- import { ActiveIncrementManager } from './active-increment-manager.js';
11
- /**
12
- * Error thrown when metadata operations fail
13
- */
14
- export class MetadataError extends Error {
15
- constructor(message, incrementId, cause) {
16
- super(message);
17
- this.incrementId = incrementId;
18
- this.cause = cause;
19
- this.name = 'MetadataError';
20
- }
21
- }
22
- /**
23
- * Metadata Manager
24
- *
25
- * Provides CRUD operations and queries for increment metadata
26
- */
27
- export class MetadataManager {
28
- /**
29
- * Get metadata file path for increment
30
- */
31
- static getMetadataPath(incrementId) {
32
- const specweavePath = path.join(process.cwd(), '.specweave');
33
- return path.join(specweavePath, 'increments', incrementId, 'metadata.json');
34
- }
35
- /**
36
- * Get increment directory path
37
- */
38
- static getIncrementPath(incrementId) {
39
- const specweavePath = path.join(process.cwd(), '.specweave');
40
- return path.join(specweavePath, 'increments', incrementId);
41
- }
42
- /**
43
- * Check if metadata file exists
44
- */
45
- static exists(incrementId) {
46
- const metadataPath = this.getMetadataPath(incrementId);
47
- return fs.existsSync(metadataPath);
48
- }
49
- /**
50
- * Read metadata from file
51
- * Creates default metadata if file doesn't exist (lazy initialization)
52
- */
53
- static read(incrementId) {
54
- const metadataPath = this.getMetadataPath(incrementId);
55
- // Lazy initialization: Create metadata if doesn't exist
56
- if (!fs.existsSync(metadataPath)) {
57
- // Check if increment folder exists
58
- const incrementPath = this.getIncrementPath(incrementId);
59
- if (!fs.existsSync(incrementPath)) {
60
- throw new MetadataError(`Increment not found: ${incrementId}`, incrementId);
61
- }
62
- // Create default metadata
63
- const defaultMetadata = createDefaultMetadata(incrementId);
64
- this.write(incrementId, defaultMetadata);
65
- return defaultMetadata;
66
- }
67
- try {
68
- const content = fs.readFileSync(metadataPath, 'utf-8');
69
- const metadata = JSON.parse(content);
70
- // Validate schema
71
- this.validate(metadata);
72
- return metadata;
73
- }
74
- catch (error) {
75
- const errorMessage = error instanceof Error ? error.message : String(error);
76
- throw new MetadataError(`Failed to read metadata for ${incrementId}: ${errorMessage}`, incrementId, error instanceof Error ? error : new Error(String(error)));
77
- }
78
- }
79
- /**
80
- * Write metadata to file
81
- * Uses atomic write (temp file → rename)
82
- */
83
- static write(incrementId, metadata) {
84
- const metadataPath = this.getMetadataPath(incrementId);
85
- const incrementPath = this.getIncrementPath(incrementId);
86
- // Ensure increment directory exists
87
- if (!fs.existsSync(incrementPath)) {
88
- throw new MetadataError(`Increment directory not found: ${incrementId}`, incrementId);
89
- }
90
- try {
91
- // Validate before writing
92
- this.validate(metadata);
93
- // Atomic write: temp file → rename
94
- const tempPath = `${metadataPath}.tmp`;
95
- fs.writeFileSync(tempPath, JSON.stringify(metadata, null, 2), 'utf-8');
96
- fs.renameSync(tempPath, metadataPath);
97
- }
98
- catch (error) {
99
- const errorMessage = error instanceof Error ? error.message : String(error);
100
- throw new MetadataError(`Failed to write metadata for ${incrementId}: ${errorMessage}`, incrementId, error instanceof Error ? error : new Error(String(error)));
101
- }
102
- }
103
- /**
104
- * Delete metadata file
105
- */
106
- static delete(incrementId) {
107
- const metadataPath = this.getMetadataPath(incrementId);
108
- if (!fs.existsSync(metadataPath)) {
109
- return; // Already deleted
110
- }
111
- try {
112
- fs.unlinkSync(metadataPath);
113
- }
114
- catch (error) {
115
- const errorMessage = error instanceof Error ? error.message : String(error);
116
- throw new MetadataError(`Failed to delete metadata for ${incrementId}: ${errorMessage}`, incrementId, error instanceof Error ? error : new Error(String(error)));
117
- }
118
- }
119
- /**
120
- * Update increment status
121
- * Validates transition and updates timestamps
122
- *
123
- * **CRITICAL**: Also updates active increment state automatically!
124
- */
125
- static updateStatus(incrementId, newStatus, reason) {
126
- const metadata = this.read(incrementId);
127
- // Validate transition
128
- if (!isValidTransition(metadata.status, newStatus)) {
129
- throw new MetadataError(`Invalid status transition: ${metadata.status} → ${newStatus}`, incrementId);
130
- }
131
- // Update status
132
- metadata.status = newStatus;
133
- metadata.lastActivity = new Date().toISOString();
134
- // Update status-specific fields
135
- if (newStatus === IncrementStatus.PAUSED) {
136
- metadata.pausedReason = reason || 'No reason provided';
137
- metadata.pausedAt = new Date().toISOString();
138
- }
139
- else if (newStatus === IncrementStatus.ACTIVE) {
140
- // Clear paused fields when resuming
141
- metadata.pausedReason = undefined;
142
- metadata.pausedAt = undefined;
143
- }
144
- else if (newStatus === IncrementStatus.ABANDONED) {
145
- metadata.abandonedReason = reason || 'No reason provided';
146
- metadata.abandonedAt = new Date().toISOString();
147
- }
148
- this.write(incrementId, metadata);
149
- // **CRITICAL**: Update active increment state
150
- const activeManager = new ActiveIncrementManager();
151
- if (newStatus === IncrementStatus.ACTIVE) {
152
- // Increment became active → set as active
153
- activeManager.setActive(incrementId);
154
- }
155
- else if (newStatus === IncrementStatus.COMPLETED ||
156
- newStatus === IncrementStatus.PAUSED ||
157
- newStatus === IncrementStatus.ABANDONED) {
158
- // Increment no longer active → smart update (find next active or clear)
159
- activeManager.smartUpdate();
160
- }
161
- return metadata;
162
- }
163
- /**
164
- * Update increment type
165
- */
166
- static updateType(incrementId, type) {
167
- const metadata = this.read(incrementId);
168
- metadata.type = type;
169
- metadata.lastActivity = new Date().toISOString();
170
- this.write(incrementId, metadata);
171
- return metadata;
172
- }
173
- /**
174
- * Touch increment (update lastActivity)
175
- */
176
- static touch(incrementId) {
177
- const metadata = this.read(incrementId);
178
- metadata.lastActivity = new Date().toISOString();
179
- this.write(incrementId, metadata);
180
- return metadata;
181
- }
182
- /**
183
- * Get all increments
184
- */
185
- static getAll() {
186
- const incrementsPath = path.join(process.cwd(), '.specweave', 'increments');
187
- if (!fs.existsSync(incrementsPath)) {
188
- return [];
189
- }
190
- const incrementFolders = fs.readdirSync(incrementsPath)
191
- .filter(name => {
192
- const folderPath = path.join(incrementsPath, name);
193
- return fs.statSync(folderPath).isDirectory() && !name.startsWith('_');
194
- });
195
- return incrementFolders
196
- .map(folder => {
197
- try {
198
- return this.read(folder);
199
- }
200
- catch (error) {
201
- // Skip increments with invalid/missing metadata
202
- return null;
203
- }
204
- })
205
- .filter((m) => m !== null);
206
- }
207
- /**
208
- * Get increments by status
209
- */
210
- static getByStatus(status) {
211
- return this.getAll().filter(m => m.status === status);
212
- }
213
- /**
214
- * Get active increments
215
- */
216
- static getActive() {
217
- return this.getByStatus(IncrementStatus.ACTIVE);
218
- }
219
- /**
220
- * Get paused increments
221
- */
222
- static getPaused() {
223
- return this.getByStatus(IncrementStatus.PAUSED);
224
- }
225
- /**
226
- * Get completed increments
227
- */
228
- static getCompleted() {
229
- return this.getByStatus(IncrementStatus.COMPLETED);
230
- }
231
- /**
232
- * Get abandoned increments
233
- */
234
- static getAbandoned() {
235
- return this.getByStatus(IncrementStatus.ABANDONED);
236
- }
237
- /**
238
- * Get increments by type
239
- */
240
- static getByType(type) {
241
- return this.getAll().filter(m => m.type === type);
242
- }
243
- /**
244
- * Get stale increments (paused >7 days or active >30 days)
245
- */
246
- static getStale() {
247
- return this.getAll().filter(m => isStale(m));
248
- }
249
- /**
250
- * Get increments that should be auto-abandoned (experiments inactive >14 days)
251
- */
252
- static getShouldAutoAbandon() {
253
- return this.getAll().filter(m => shouldAutoAbandon(m));
254
- }
255
- /**
256
- * Get extended metadata with computed fields (progress, age, etc.)
257
- */
258
- static getExtended(incrementId) {
259
- const metadata = this.read(incrementId);
260
- const extended = { ...metadata };
261
- // Calculate progress from tasks.md
262
- try {
263
- const tasksPath = path.join(this.getIncrementPath(incrementId), 'tasks.md');
264
- if (fs.existsSync(tasksPath)) {
265
- const tasksContent = fs.readFileSync(tasksPath, 'utf-8');
266
- // Count completed tasks: [x] or [X]
267
- const completedMatches = tasksContent.match(/\[x\]/gi);
268
- extended.completedTasks = completedMatches ? completedMatches.length : 0;
269
- // Count total tasks: [ ] or [x]
270
- const totalMatches = tasksContent.match(/\[ \]|\[x\]/gi);
271
- extended.totalTasks = totalMatches ? totalMatches.length : 0;
272
- // Calculate progress percentage
273
- if (extended.totalTasks > 0) {
274
- extended.progress = Math.round((extended.completedTasks / extended.totalTasks) * 100);
275
- }
276
- }
277
- }
278
- catch (error) {
279
- // Ignore errors reading tasks.md
280
- }
281
- // Calculate age in days
282
- const now = new Date();
283
- const createdDate = new Date(metadata.created);
284
- extended.ageInDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24));
285
- // Calculate days paused
286
- if (metadata.status === IncrementStatus.PAUSED && metadata.pausedAt) {
287
- const pausedDate = new Date(metadata.pausedAt);
288
- extended.daysPaused = Math.floor((now.getTime() - pausedDate.getTime()) / (1000 * 60 * 60 * 24));
289
- }
290
- return extended;
291
- }
292
- /**
293
- * Validate metadata schema
294
- */
295
- static validate(metadata) {
296
- if (!metadata.id) {
297
- throw new Error('Metadata missing required field: id');
298
- }
299
- if (!metadata.status || !Object.values(IncrementStatus).includes(metadata.status)) {
300
- throw new Error(`Invalid status: ${metadata.status}`);
301
- }
302
- if (!metadata.type || !Object.values(IncrementType).includes(metadata.type)) {
303
- throw new Error(`Invalid type: ${metadata.type}`);
304
- }
305
- if (!metadata.created) {
306
- throw new Error('Metadata missing required field: created');
307
- }
308
- if (!metadata.lastActivity) {
309
- throw new Error('Metadata missing required field: lastActivity');
310
- }
311
- return true;
312
- }
313
- /**
314
- * Check if status transition is allowed
315
- */
316
- static canTransition(from, to) {
317
- return isValidTransition(from, to);
318
- }
319
- /**
320
- * Get human-readable status transition error message
321
- */
322
- static getTransitionError(from, to) {
323
- if (from === IncrementStatus.COMPLETED) {
324
- return `Cannot transition from completed state. Increment is already complete.`;
325
- }
326
- if (to === IncrementStatus.PAUSED && from === IncrementStatus.ABANDONED) {
327
- return `Cannot pause an abandoned increment. Resume it first with /resume.`;
328
- }
329
- if (to === IncrementStatus.COMPLETED && from === IncrementStatus.ABANDONED) {
330
- return `Cannot complete an abandoned increment. Resume it first with /resume.`;
331
- }
332
- return `Invalid transition: ${from} → ${to}`;
333
- }
334
- }
335
- //# sourceMappingURL=metadata-manager.js.map