specweave 1.0.255 → 1.0.256

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 (92) hide show
  1. package/CLAUDE.md +24 -24
  2. package/README.md +138 -203
  3. package/dist/src/core/ac-checkbox-formatter.d.ts +24 -0
  4. package/dist/src/core/ac-checkbox-formatter.d.ts.map +1 -0
  5. package/dist/src/core/ac-checkbox-formatter.js +35 -0
  6. package/dist/src/core/ac-checkbox-formatter.js.map +1 -0
  7. package/dist/src/core/ac-progress-sync.d.ts +116 -0
  8. package/dist/src/core/ac-progress-sync.d.ts.map +1 -0
  9. package/dist/src/core/ac-progress-sync.js +272 -0
  10. package/dist/src/core/ac-progress-sync.js.map +1 -0
  11. package/dist/src/core/fabric/registry-schema.d.ts +79 -0
  12. package/dist/src/core/fabric/registry-schema.d.ts.map +1 -0
  13. package/dist/src/core/fabric/registry-schema.js +6 -0
  14. package/dist/src/core/fabric/registry-schema.js.map +1 -0
  15. package/dist/src/core/fabric/security-scanner.d.ts +12 -0
  16. package/dist/src/core/fabric/security-scanner.d.ts.map +1 -0
  17. package/dist/src/core/fabric/security-scanner.js +219 -0
  18. package/dist/src/core/fabric/security-scanner.js.map +1 -0
  19. package/dist/src/core/types/sync-profile.d.ts +44 -0
  20. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  21. package/dist/src/core/types/sync-profile.js.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +4 -4
  24. package/plugins/{specweave-github/hooks/github-ac-sync-handler.sh → specweave/hooks/v2/handlers/ac-sync-dispatcher.sh} +96 -92
  25. package/plugins/specweave/skills/architect/SKILL.md +1 -1
  26. package/plugins/specweave/skills/auto/SKILL.md +1 -1
  27. package/plugins/specweave/skills/cancel-auto/SKILL.md +1 -1
  28. package/plugins/specweave/skills/code-simplifier/SKILL.md +1 -1
  29. package/plugins/specweave/skills/do/SKILL.md +1 -1
  30. package/plugins/specweave/skills/docs/SKILL.md +1 -1
  31. package/plugins/specweave/skills/docs-updater/SKILL.md +1 -1
  32. package/plugins/specweave/skills/done/SKILL.md +13 -70
  33. package/plugins/specweave/skills/framework/SKILL.md +1 -1
  34. package/plugins/specweave/skills/grill/SKILL.md +1 -1
  35. package/plugins/specweave/skills/increment/SKILL.md +1 -1
  36. package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
  37. package/plugins/specweave/skills/lsp/SKILL.md +1 -1
  38. package/plugins/specweave/skills/pm/SKILL.md +1 -1
  39. package/plugins/specweave/skills/progress/SKILL.md +1 -1
  40. package/plugins/specweave/skills/save/SKILL.md +1 -1
  41. package/plugins/specweave/skills/security/SKILL.md +1 -1
  42. package/plugins/specweave/skills/security-patterns/SKILL.md +1 -1
  43. package/plugins/specweave/skills/tdd-cycle/SKILL.md +1 -1
  44. package/plugins/specweave/skills/tdd-green/SKILL.md +1 -1
  45. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  46. package/plugins/specweave/skills/tdd-red/SKILL.md +1 -1
  47. package/plugins/specweave/skills/validate/SKILL.md +1 -1
  48. package/plugins/specweave-github/commands/sync.md +1 -22
  49. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +0 -205
  50. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +0 -1
  51. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -685
  52. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +0 -1
  53. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  54. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  55. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -28
  56. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  57. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +0 -21
  58. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +0 -1
  59. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -471
  60. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +0 -1
  61. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +0 -53
  62. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +0 -1
  63. package/dist/plugins/specweave-github/lib/github-status-sync.js +0 -120
  64. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +0 -1
  65. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  66. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  67. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -297
  68. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  69. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +0 -94
  70. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +0 -1
  71. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +0 -385
  72. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +0 -1
  73. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -611
  74. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +0 -909
  75. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.js +0 -1
  76. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  77. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  78. package/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -17
  79. package/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  80. package/plugins/specweave-github/lib/cli-sync-increment-changes.ts +0 -33
  81. package/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -474
  82. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +0 -616
  83. package/plugins/specweave-github/lib/github-status-sync.js +0 -107
  84. package/plugins/specweave-github/lib/github-status-sync.ts +0 -163
  85. package/plugins/specweave-github/lib/github-sync-increment-changes.d.js +0 -0
  86. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  87. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  88. package/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -253
  89. package/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  90. package/plugins/specweave-github/lib/github-sync-increment-changes.ts +0 -391
  91. package/plugins/specweave-github/lib/increment-issue-builder.js +0 -402
  92. package/plugins/specweave-github/lib/increment-issue-builder.ts +0 -520
@@ -1,909 +0,0 @@
1
- /**
2
- * Three-Layer Full Sync Manager (All Permissions Enabled)
3
- *
4
- * Handles synchronization across three layers:
5
- * - Layer 1: GitHub Issue (stakeholder UI)
6
- * - Layer 2: Living Docs User Story files (intermediate representation)
7
- * - Layer 3: Increment spec.md + tasks.md (source of truth)
8
- *
9
- * Supports two sync flows:
10
- * 1. GitHub → Living Docs → Increment (stakeholder checks checkbox)
11
- * 2. Increment → Living Docs → GitHub (developer completes work)
12
- *
13
- * Features:
14
- * - Code validation (reopen tasks if code doesn't exist)
15
- * - Completion propagation (Tasks → ACs → User Stories)
16
- * - Conflict resolution (Increment always wins)
17
- *
18
- * @module ThreeLayerSyncManager
19
- */
20
-
21
- import * as fs from '../../../src/utils/fs-native.js';
22
- import path from 'path';
23
- import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
24
- import { CodeValidator, type TaskValidationResult } from './CodeValidator.js';
25
- import { getGitHubAuthFromProject } from '../../../src/utils/auth-helpers.js';
26
-
27
- /**
28
- * Acceptance Criterion with completion state
29
- */
30
- export interface AcceptanceCriterion {
31
- id: string; // e.g., "AC-US1-01"
32
- userStoryId: string; // e.g., "US1"
33
- description: string;
34
- completed: boolean;
35
- projects: string[]; // ["backend", "frontend", "mobile"]
36
- rawLine: string; // Original markdown line
37
- }
38
-
39
- /**
40
- * Task with completion state and AC references
41
- */
42
- export interface Task {
43
- id: string; // e.g., "T-001"
44
- title: string;
45
- completed: boolean;
46
- completedDate?: string;
47
- acIds: string[]; // e.g., ["AC-US1-01", "AC-US1-02"]
48
- filePaths?: string[]; // For code validation
49
- }
50
-
51
- /**
52
- * Change detected during sync
53
- */
54
- export interface SyncChange {
55
- type: 'ac' | 'task';
56
- id: string;
57
- completed: boolean;
58
- layer: 'github' | 'living-docs' | 'increment';
59
- userStoryPath?: string;
60
- incrementPath?: string;
61
- }
62
-
63
- /**
64
- * Sync result
65
- */
66
- export interface SyncResult {
67
- acsUpdated: number;
68
- tasksUpdated: number;
69
- tasksReopened: number;
70
- conflicts: string[];
71
- errors: string[];
72
- }
73
-
74
- export class ThreeLayerSyncManager {
75
- private codeValidator: CodeValidator;
76
- private userStoryFileCache: Map<string, string | null> = new Map();
77
- private projectRoot: string;
78
- private token?: string;
79
-
80
- constructor(projectRoot: string = process.cwd()) {
81
- this.projectRoot = projectRoot;
82
- this.codeValidator = new CodeValidator({ projectRoot });
83
- this.token = getGitHubAuthFromProject(projectRoot).token;
84
- }
85
-
86
- /**
87
- * Get environment object with GH_TOKEN for gh CLI commands.
88
- */
89
- private getGhEnv(): NodeJS.ProcessEnv {
90
- return this.token
91
- ? { ...process.env, GH_TOKEN: this.token }
92
- : process.env;
93
- }
94
-
95
- /**
96
- * Clear User Story file cache
97
- * Call this when file structure changes or for testing
98
- */
99
- clearCache(): void {
100
- this.userStoryFileCache.clear();
101
- }
102
-
103
- /**
104
- * Sync from GitHub to Increment (Flow 1: GitHub → Living Docs → Increment)
105
- *
106
- * Triggered when stakeholder checks AC/Task checkbox in GitHub issue.
107
- */
108
- async syncGitHubToIncrement(
109
- issueNumber: number,
110
- incrementPath: string,
111
- livingDocsPath: string
112
- ): Promise<SyncResult> {
113
- const result: SyncResult = {
114
- acsUpdated: 0,
115
- tasksUpdated: 0,
116
- tasksReopened: 0,
117
- conflicts: [],
118
- errors: []
119
- };
120
-
121
- try {
122
- console.log(`🔄 Syncing GitHub issue #${issueNumber} → Increment`);
123
-
124
- // 1. Fetch GitHub issue state
125
- const githubState = await this.fetchGitHubIssueState(issueNumber);
126
-
127
- // 2. Extract AC and Task completion state from issue body
128
- const githubAcs = this.extractAcsFromIssue(githubState.body);
129
- const githubTasks = this.extractTasksFromIssue(githubState.body);
130
-
131
- // 3. Update Living Docs User Stories (Layer 2) - PARALLEL I/O for performance
132
- const acUpdatePromises = githubAcs.map(async (ac) => {
133
- const userStoryPath = await this.findUserStoryFile(livingDocsPath, ac.userStoryId);
134
- if (userStoryPath) {
135
- await this.updateUserStoryAc(userStoryPath, ac.id, ac.completed);
136
- result.acsUpdated++;
137
- }
138
- });
139
-
140
- const taskUpdatePromises = githubTasks.map(async (task) => {
141
- // Find User Story that contains this task
142
- const userStoryPath = await this.findUserStoryFileForTask(livingDocsPath, task.id);
143
- if (userStoryPath) {
144
- await this.updateUserStoryTask(userStoryPath, task.id, task.completed);
145
- result.tasksUpdated++;
146
- }
147
- });
148
-
149
- // Wait for all updates to complete in parallel
150
- await Promise.all([...acUpdatePromises, ...taskUpdatePromises]);
151
-
152
- // 4. Update Increment (Layer 3 - source of truth) with conflict resolution
153
- await this.updateIncrementAcsWithConflictResolution(incrementPath, githubAcs, result);
154
- await this.updateIncrementTasksWithConflictResolution(incrementPath, githubTasks, result);
155
-
156
- // 5. Code validation: If task marked complete, check if code exists - PARALLEL for performance
157
- const completedTasks = githubTasks.filter(t => t.completed);
158
- const validationPromises = completedTasks.map(async (task) => {
159
- const codeExists = await this.validateCodeExists(task);
160
- if (!codeExists) {
161
- console.log(`⚠️ Task ${task.id} marked complete but code missing - reopening`);
162
- await this.reopenTask(task.id, incrementPath, livingDocsPath, issueNumber);
163
- result.tasksReopened++;
164
- }
165
- });
166
-
167
- await Promise.all(validationPromises);
168
-
169
- console.log(`✅ Sync complete: ${result.acsUpdated} ACs, ${result.tasksUpdated} tasks updated`);
170
- if (result.tasksReopened > 0) {
171
- console.log(` ${result.tasksReopened} tasks reopened due to missing code`);
172
- }
173
-
174
- } catch (error) {
175
- const errorMsg = error instanceof Error ? error.message : String(error);
176
- result.errors.push(errorMsg);
177
- console.error(`❌ Error syncing GitHub → Increment:`, error);
178
- }
179
-
180
- return result;
181
- }
182
-
183
- /**
184
- * Sync from Increment to GitHub (Flow 2: Increment → Living Docs → GitHub)
185
- *
186
- * Triggered when developer completes work and updates increment files.
187
- */
188
- async syncIncrementToGitHub(
189
- incrementPath: string,
190
- livingDocsPath: string,
191
- issueNumber: number
192
- ): Promise<SyncResult> {
193
- const result: SyncResult = {
194
- acsUpdated: 0,
195
- tasksUpdated: 0,
196
- tasksReopened: 0,
197
- conflicts: [],
198
- errors: []
199
- };
200
-
201
- try {
202
- console.log(`🔄 Syncing Increment → GitHub issue #${issueNumber}`);
203
-
204
- // 1. Read increment spec.md and tasks.md
205
- const specPath = path.join(incrementPath, 'spec.md');
206
- const tasksPath = path.join(incrementPath, 'tasks.md');
207
-
208
- const specContent = await fs.readFile(specPath, 'utf-8');
209
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
210
-
211
- // 2. Parse ACs and Tasks
212
- const acs = this.parseAcceptanceCriteria(specContent);
213
- const tasks = this.parseTasks(tasksContent);
214
-
215
- // 3. Update Living Docs User Stories (Layer 2)
216
- const acsByUserStory = this.groupAcsByUserStory(acs);
217
-
218
- for (const [userStoryId, userStoryAcs] of Object.entries(acsByUserStory)) {
219
- const userStoryPath = await this.findUserStoryFile(livingDocsPath, userStoryId);
220
- if (userStoryPath) {
221
- // Update ACs in User Story
222
- await this.updateUserStoryAcs(userStoryPath, userStoryAcs);
223
- result.acsUpdated += userStoryAcs.length;
224
-
225
- // Update Tasks in User Story (filter by AC IDs)
226
- const acIds = userStoryAcs.map(ac => ac.id);
227
- const userStoryTasks = this.filterTasksByAcIds(tasks, acIds);
228
- await this.updateUserStoryTasks(userStoryPath, userStoryTasks);
229
- result.tasksUpdated += userStoryTasks.length;
230
- }
231
- }
232
-
233
- // 4. Update GitHub issue (Layer 1)
234
- await this.updateGitHubIssue(issueNumber, acs, tasks);
235
-
236
- console.log(`✅ Sync complete: ${result.acsUpdated} ACs, ${result.tasksUpdated} tasks updated`);
237
-
238
- } catch (error) {
239
- const errorMsg = error instanceof Error ? error.message : String(error);
240
- result.errors.push(errorMsg);
241
- console.error(`❌ Error syncing Increment → GitHub:`, error);
242
- }
243
-
244
- return result;
245
- }
246
-
247
- /**
248
- * Validate that code exists for completed task
249
- * Uses CodeValidator for comprehensive validation
250
- */
251
- private async validateCodeExists(task: Task): Promise<boolean> {
252
- // Use CodeValidator for robust validation
253
- const validationResult = await this.codeValidator.validateTask(task.title, task.id);
254
- return validationResult.valid;
255
- }
256
-
257
- /**
258
- * Reopen task if code validation fails
259
- */
260
- private async reopenTask(
261
- taskId: string,
262
- incrementPath: string,
263
- livingDocsPath: string,
264
- issueNumber: number
265
- ): Promise<void> {
266
- // 1. Reopen in increment tasks.md
267
- const tasksPath = path.join(incrementPath, 'tasks.md');
268
- let tasksContent = await fs.readFile(tasksPath, 'utf-8');
269
-
270
- // Find task and mark as incomplete
271
- const taskRegex = new RegExp(`(### ${taskId}:[^#]*?)\\*\\*Completed\\*\\*:[^\\n]*\\n`, 's');
272
- tasksContent = tasksContent.replace(taskRegex, '$1');
273
-
274
- await fs.writeFile(tasksPath, tasksContent, 'utf-8');
275
-
276
- // 2. Propagate to Living Docs
277
- const userStoryPath = await this.findUserStoryFileForTask(livingDocsPath, taskId);
278
- if (userStoryPath) {
279
- await this.updateUserStoryTask(userStoryPath, taskId, false);
280
- }
281
-
282
- // 3. Propagate to GitHub (add comment explaining why)
283
- await this.addGitHubComment(
284
- issueNumber,
285
- `⚠️ Task ${taskId} reopened: Code validation failed (missing or empty file). Please implement the required code.`
286
- );
287
- }
288
-
289
- /**
290
- * Propagate completion from Tasks → ACs → User Stories (bottom-up)
291
- */
292
- async propagateCompletion(
293
- incrementPath: string,
294
- livingDocsPath: string
295
- ): Promise<void> {
296
- // 1. Read increment files
297
- const specPath = path.join(incrementPath, 'spec.md');
298
- const tasksPath = path.join(incrementPath, 'tasks.md');
299
-
300
- const specContent = await fs.readFile(specPath, 'utf-8');
301
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
302
-
303
- const acs = this.parseAcceptanceCriteria(specContent);
304
- const tasks = this.parseTasks(tasksContent);
305
-
306
- // 2. For each AC, check if all its tasks are complete
307
- for (const ac of acs) {
308
- const acTasks = tasks.filter(t => t.acIds.includes(ac.id));
309
- const allTasksComplete = acTasks.length > 0 && acTasks.every(t => t.completed);
310
-
311
- if (allTasksComplete && !ac.completed) {
312
- // Mark AC as complete
313
- console.log(`✅ All tasks for ${ac.id} complete - marking AC as complete`);
314
- await this.updateIncrementAc(incrementPath, ac.id, true);
315
-
316
- // Propagate to Living Docs
317
- const userStoryPath = await this.findUserStoryFile(livingDocsPath, ac.userStoryId);
318
- if (userStoryPath) {
319
- await this.updateUserStoryAc(userStoryPath, ac.id, true);
320
- }
321
- }
322
- }
323
-
324
- // 3. For each User Story, check if all ACs are complete
325
- const acsByUserStory = this.groupAcsByUserStory(acs);
326
-
327
- for (const [userStoryId, userStoryAcs] of Object.entries(acsByUserStory)) {
328
- const allAcsComplete = userStoryAcs.every(ac => ac.completed);
329
-
330
- if (allAcsComplete) {
331
- console.log(`✅ All ACs for ${userStoryId} complete - User Story complete`);
332
- // Could trigger User Story completion workflow here
333
- }
334
- }
335
- }
336
-
337
- // ==================== Helper Methods ====================
338
-
339
- /**
340
- * Fetch GitHub issue state
341
- */
342
- private async fetchGitHubIssueState(issueNumber: number): Promise<{ body: string; state: string }> {
343
- const result = await execFileNoThrow('gh', [
344
- 'issue',
345
- 'view',
346
- String(issueNumber),
347
- '--json',
348
- 'body,state'
349
- ], { env: this.getGhEnv() });
350
-
351
- if (result.stderr) {
352
- throw new Error(`Failed to fetch GitHub issue: ${result.stderr}`);
353
- }
354
-
355
- return JSON.parse(result.stdout);
356
- }
357
-
358
- /**
359
- * Extract ACs from GitHub issue body
360
- *
361
- * CRITICAL FIX (v1.0.59): Handle multiple checkbox formats:
362
- * - Bold format: `- [ ] **AC-US5-01**: Description` (SpecWeave standard)
363
- * - Plain format: `- [ ] AC-US5-01: Description` (legacy/external)
364
- */
365
- private extractAcsFromIssue(body: string): AcceptanceCriterion[] {
366
- const acs: AcceptanceCriterion[] = [];
367
- const lines = body.split('\n');
368
-
369
- // Pattern 1: Bold format `- [x] **AC-US5-01**: Description` (SpecWeave standard)
370
- const boldAcRegex = /^- \[([ x])\] \*\*(AC-[A-Z0-9]+-\d+)\*\*:\s*(.+)$/;
371
-
372
- // Pattern 2: Plain format `- [x] AC-US5-01: Description` (legacy)
373
- const plainAcRegex = /^- \[([ x])\] (AC-[A-Z0-9]+-\d+):\s*(.+)$/;
374
-
375
- for (const line of lines) {
376
- // Try bold format first (more common in SpecWeave)
377
- let match = line.match(boldAcRegex);
378
- if (!match) {
379
- match = line.match(plainAcRegex);
380
- }
381
-
382
- if (match) {
383
- const completed = match[1] === 'x';
384
- const id = match[2];
385
- const description = match[3];
386
-
387
- const userStoryMatch = id.match(/AC-([A-Z0-9]+)-\d+/);
388
- const userStoryId = userStoryMatch ? userStoryMatch[1] : '';
389
-
390
- acs.push({
391
- id,
392
- userStoryId,
393
- description,
394
- completed,
395
- projects: [],
396
- rawLine: line
397
- });
398
- }
399
- }
400
-
401
- return acs;
402
- }
403
-
404
- /**
405
- * Extract Tasks from GitHub issue body
406
- *
407
- * CRITICAL FIX (v1.0.59): Handle multiple checkbox formats:
408
- * - Bold format: `- [ ] **T-001**: Description` (SpecWeave standard)
409
- * - Plain format: `- [ ] T-001: Description` (legacy/external)
410
- */
411
- private extractTasksFromIssue(body: string): Task[] {
412
- const tasks: Task[] = [];
413
- const lines = body.split('\n');
414
-
415
- // Pattern 1: Bold format `- [x] **T-001**: Description` (SpecWeave standard)
416
- const boldTaskRegex = /^- \[([ x])\] \*\*(T-\d+)\*\*:\s*(.+)$/;
417
-
418
- // Pattern 2: Plain format `- [x] T-001: Description` (legacy)
419
- const plainTaskRegex = /^- \[([ x])\] (T-\d+):\s*(.+)$/;
420
-
421
- for (const line of lines) {
422
- // Try bold format first
423
- let match = line.match(boldTaskRegex);
424
- if (!match) {
425
- match = line.match(plainTaskRegex);
426
- }
427
-
428
- if (match) {
429
- const completed = match[1] === 'x';
430
- const id = match[2];
431
- const title = match[3];
432
-
433
- tasks.push({
434
- id,
435
- title,
436
- completed,
437
- acIds: []
438
- });
439
- }
440
- }
441
-
442
- return tasks;
443
- }
444
-
445
- /**
446
- * Parse ACs from spec.md
447
- *
448
- * CRITICAL FIX (v1.0.59): Handle multiple checkbox formats in spec.md:
449
- * - Bold format: `- [x] **AC-US5-01**: Description` (SpecWeave standard)
450
- * - Plain format: `- [x] AC-US5-01: Description` (legacy)
451
- */
452
- private parseAcceptanceCriteria(specContent: string): AcceptanceCriterion[] {
453
- const acs: AcceptanceCriterion[] = [];
454
- const lines = specContent.split('\n');
455
-
456
- // Pattern 1: Bold format `- [x] **AC-US5-01**: Description` (SpecWeave standard)
457
- const boldAcRegex = /^- \[([ x])\] \*\*(AC-[A-Z0-9]+-\d+)\*\*:\s*(.+)$/;
458
-
459
- // Pattern 2: Plain format `- [x] AC-US5-01: Description` (legacy)
460
- const plainAcRegex = /^- \[([ x])\] (AC-[A-Z0-9]+-\d+):\s*(.+)$/;
461
-
462
- for (const line of lines) {
463
- // Try bold format first (SpecWeave standard)
464
- let match = line.match(boldAcRegex);
465
- if (!match) {
466
- match = line.match(plainAcRegex);
467
- }
468
-
469
- if (match) {
470
- const completed = match[1] === 'x';
471
- const id = match[2];
472
- const description = match[3];
473
-
474
- const userStoryMatch = id.match(/AC-([A-Z0-9]+)-\d+/);
475
- const userStoryId = userStoryMatch ? userStoryMatch[1] : '';
476
-
477
- acs.push({
478
- id,
479
- userStoryId,
480
- description,
481
- completed,
482
- projects: [],
483
- rawLine: line
484
- });
485
- }
486
- }
487
-
488
- return acs;
489
- }
490
-
491
- /**
492
- * Parse Tasks from tasks.md
493
- */
494
- private parseTasks(tasksContent: string): Task[] {
495
- const tasks: Task[] = [];
496
- const lines = tasksContent.split('\n');
497
-
498
- let currentTask: Partial<Task> | null = null;
499
-
500
- for (const line of lines) {
501
- // Match task header: ### T-001: Title (P1)
502
- const headerMatch = line.match(/^### (T-\d+):\s*(.+?)\s*\(P\d+\)$/);
503
- if (headerMatch) {
504
- if (currentTask && currentTask.id) {
505
- tasks.push(currentTask as Task);
506
- }
507
-
508
- currentTask = {
509
- id: headerMatch[1],
510
- title: headerMatch[2],
511
- completed: false,
512
- acIds: []
513
- };
514
- continue;
515
- }
516
-
517
- // Check for completion
518
- const completedMatch = line.match(/^\*\*Completed\*\*:\s*(.+)$/);
519
- if (completedMatch && currentTask) {
520
- currentTask.completed = true;
521
- currentTask.completedDate = completedMatch[1];
522
- }
523
-
524
- // Extract AC IDs
525
- const acMatch = line.match(/\*\*AC\*\*:\s*(.+)$/);
526
- if (acMatch && currentTask) {
527
- const acIds = acMatch[1].split(',').map(id => id.trim());
528
- currentTask.acIds = acIds;
529
- }
530
- }
531
-
532
- if (currentTask && currentTask.id) {
533
- tasks.push(currentTask as Task);
534
- }
535
-
536
- return tasks;
537
- }
538
-
539
- /**
540
- * Group ACs by User Story ID
541
- */
542
- private groupAcsByUserStory(acs: AcceptanceCriterion[]): Record<string, AcceptanceCriterion[]> {
543
- const grouped: Record<string, AcceptanceCriterion[]> = {};
544
-
545
- for (const ac of acs) {
546
- if (!grouped[ac.userStoryId]) {
547
- grouped[ac.userStoryId] = [];
548
- }
549
- grouped[ac.userStoryId].push(ac);
550
- }
551
-
552
- return grouped;
553
- }
554
-
555
- /**
556
- * Filter tasks by AC IDs
557
- */
558
- private filterTasksByAcIds(tasks: Task[], acIds: string[]): Task[] {
559
- return tasks.filter(task =>
560
- task.acIds.some(acId => acIds.includes(acId))
561
- );
562
- }
563
-
564
- /**
565
- * Find User Story file by ID (with caching for performance)
566
- */
567
- private async findUserStoryFile(livingDocsPath: string, userStoryId: string): Promise<string | null> {
568
- // Check cache first
569
- const cacheKey = `${livingDocsPath}:${userStoryId}`;
570
- if (this.userStoryFileCache.has(cacheKey)) {
571
- return this.userStoryFileCache.get(cacheKey)!;
572
- }
573
-
574
- // Search in specs directory
575
- const specsPath = path.join(livingDocsPath, 'internal', 'specs');
576
-
577
- // Use glob to find user story files
578
- const pattern = `**/us-*${userStoryId.toLowerCase()}*.md`;
579
- const { glob } = await import('glob');
580
- const files = await glob(pattern, { cwd: specsPath, absolute: true });
581
-
582
- const result = files.length > 0 ? files[0] : null;
583
-
584
- // Cache the result
585
- this.userStoryFileCache.set(cacheKey, result);
586
-
587
- return result;
588
- }
589
-
590
- /**
591
- * Find User Story file that contains specific task
592
- */
593
- private async findUserStoryFileForTask(livingDocsPath: string, taskId: string): Promise<string | null> {
594
- // Search all user story files for the task
595
- const specsPath = path.join(livingDocsPath, 'internal', 'specs');
596
- const { glob } = await import('glob');
597
- const files = await glob('**/us-*.md', { cwd: specsPath, absolute: true });
598
-
599
- for (const file of files) {
600
- const content = await fs.readFile(file, 'utf-8');
601
- if (content.includes(taskId)) {
602
- return file;
603
- }
604
- }
605
-
606
- return null;
607
- }
608
-
609
- /**
610
- * Update AC in User Story file
611
- */
612
- private async updateUserStoryAc(userStoryPath: string, acId: string, completed: boolean): Promise<void> {
613
- let content = await fs.readFile(userStoryPath, 'utf-8');
614
-
615
- // Update checkbox state
616
- const checkboxState = completed ? 'x' : ' ';
617
- const regex = new RegExp(`(- \\[)[ x](\\] ${acId}:)`, 'g');
618
- content = content.replace(regex, `$1${checkboxState}$2`);
619
-
620
- await fs.writeFile(userStoryPath, content, 'utf-8');
621
- }
622
-
623
- /**
624
- * Update Task in User Story file
625
- */
626
- private async updateUserStoryTask(userStoryPath: string, taskId: string, completed: boolean): Promise<void> {
627
- let content = await fs.readFile(userStoryPath, 'utf-8');
628
-
629
- // Update checkbox state
630
- const checkboxState = completed ? 'x' : ' ';
631
- const regex = new RegExp(`(- \\[)[ x](\\] ${taskId}:)`, 'g');
632
- content = content.replace(regex, `$1${checkboxState}$2`);
633
-
634
- await fs.writeFile(userStoryPath, content, 'utf-8');
635
- }
636
-
637
- /**
638
- * Update multiple ACs in User Story file
639
- */
640
- private async updateUserStoryAcs(userStoryPath: string, acs: AcceptanceCriterion[]): Promise<void> {
641
- let content = await fs.readFile(userStoryPath, 'utf-8');
642
-
643
- for (const ac of acs) {
644
- const checkboxState = ac.completed ? 'x' : ' ';
645
- const regex = new RegExp(`(- \\[)[ x](\\] ${ac.id}:)`, 'g');
646
- content = content.replace(regex, `$1${checkboxState}$2`);
647
- }
648
-
649
- await fs.writeFile(userStoryPath, content, 'utf-8');
650
- }
651
-
652
- /**
653
- * Update multiple Tasks in User Story file
654
- */
655
- private async updateUserStoryTasks(userStoryPath: string, tasks: Task[]): Promise<void> {
656
- let content = await fs.readFile(userStoryPath, 'utf-8');
657
-
658
- for (const task of tasks) {
659
- const checkboxState = task.completed ? 'x' : ' ';
660
- const regex = new RegExp(`(- \\[)[ x](\\] ${task.id}:)`, 'g');
661
- content = content.replace(regex, `$1${checkboxState}$2`);
662
- }
663
-
664
- await fs.writeFile(userStoryPath, content, 'utf-8');
665
- }
666
-
667
- /**
668
- * Update AC in increment spec.md
669
- */
670
- private async updateIncrementAc(incrementPath: string, acId: string, completed: boolean): Promise<void> {
671
- const specPath = path.join(incrementPath, 'spec.md');
672
- let content = await fs.readFile(specPath, 'utf-8');
673
-
674
- const checkboxState = completed ? 'x' : ' ';
675
- const regex = new RegExp(`(- \\[)[ x](\\] ${acId}:)`, 'g');
676
- content = content.replace(regex, `$1${checkboxState}$2`);
677
-
678
- await fs.writeFile(specPath, content, 'utf-8');
679
- }
680
-
681
- /**
682
- * Update multiple ACs in increment spec.md
683
- */
684
- private async updateIncrementAcs(incrementPath: string, acs: AcceptanceCriterion[]): Promise<void> {
685
- const specPath = path.join(incrementPath, 'spec.md');
686
- let content = await fs.readFile(specPath, 'utf-8');
687
-
688
- for (const ac of acs) {
689
- const checkboxState = ac.completed ? 'x' : ' ';
690
- const regex = new RegExp(`(- \\[)[ x](\\] ${ac.id}:)`, 'g');
691
- content = content.replace(regex, `$1${checkboxState}$2`);
692
- }
693
-
694
- await fs.writeFile(specPath, content, 'utf-8');
695
- }
696
-
697
- /**
698
- * Update multiple Tasks in increment tasks.md
699
- */
700
- private async updateIncrementTasks(incrementPath: string, tasks: Task[]): Promise<void> {
701
- const tasksPath = path.join(incrementPath, 'tasks.md');
702
- let content = await fs.readFile(tasksPath, 'utf-8');
703
-
704
- for (const task of tasks) {
705
- if (task.completed) {
706
- // Add **Completed** marker if not present
707
- const taskHeader = `### ${task.id}:`;
708
- const completedMarker = `**Completed**: ${new Date().toISOString().split('T')[0]}`;
709
-
710
- if (!content.includes(`${taskHeader}`) || !content.includes(`**Completed**`)) {
711
- // Find task section and add completed marker
712
- const regex = new RegExp(`(### ${task.id}:[^#]*?)(###|$)`, 's');
713
- content = content.replace(regex, `$1\n${completedMarker}\n\n$2`);
714
- }
715
- }
716
- }
717
-
718
- await fs.writeFile(tasksPath, content, 'utf-8');
719
- }
720
-
721
- /**
722
- * Update ACs in increment with conflict resolution (Increment wins)
723
- *
724
- * If GitHub and Increment disagree, Increment state wins as it's the source of truth.
725
- * Conflicts are logged for transparency.
726
- */
727
- private async updateIncrementAcsWithConflictResolution(
728
- incrementPath: string,
729
- githubAcs: AcceptanceCriterion[],
730
- result: SyncResult
731
- ): Promise<void> {
732
- const specPath = path.join(incrementPath, 'spec.md');
733
- const currentContent = await fs.readFile(specPath, 'utf-8');
734
-
735
- // Parse current state from increment
736
- const currentAcs = this.parseAcceptanceCriteria(currentContent);
737
-
738
- let content = currentContent;
739
-
740
- for (const githubAc of githubAcs) {
741
- const currentAc = currentAcs.find(ac => ac.id === githubAc.id);
742
-
743
- if (currentAc && currentAc.completed !== githubAc.completed) {
744
- // CONFLICT: Increment state differs from GitHub state
745
- const conflictMsg = `AC ${githubAc.id}: GitHub says ${githubAc.completed ? 'complete' : 'incomplete'}, Increment says ${currentAc.completed ? 'complete' : 'incomplete'} → Increment wins`;
746
- result.conflicts.push(conflictMsg);
747
- console.log(`⚠️ CONFLICT RESOLVED: ${conflictMsg}`);
748
-
749
- // Keep increment state (do not update)
750
- continue;
751
- }
752
-
753
- // No conflict - apply GitHub change
754
- const checkboxState = githubAc.completed ? 'x' : ' ';
755
- const regex = new RegExp(`(- \\[)[ x](\\] ${githubAc.id}:)`, 'g');
756
- content = content.replace(regex, `$1${checkboxState}$2`);
757
- }
758
-
759
- await fs.writeFile(specPath, content, 'utf-8');
760
- }
761
-
762
- /**
763
- * Update Tasks in increment with conflict resolution (Increment wins)
764
- */
765
- private async updateIncrementTasksWithConflictResolution(
766
- incrementPath: string,
767
- githubTasks: Task[],
768
- result: SyncResult
769
- ): Promise<void> {
770
- const tasksPath = path.join(incrementPath, 'tasks.md');
771
- const currentContent = await fs.readFile(tasksPath, 'utf-8');
772
-
773
- // Parse current state from increment
774
- const currentTasks = this.parseTasks(currentContent);
775
-
776
- let content = currentContent;
777
-
778
- for (const githubTask of githubTasks) {
779
- const currentTask = currentTasks.find(t => t.id === githubTask.id);
780
-
781
- if (currentTask && currentTask.completed !== githubTask.completed) {
782
- // CONFLICT: Increment state differs from GitHub state
783
- const conflictMsg = `Task ${githubTask.id}: GitHub says ${githubTask.completed ? 'complete' : 'incomplete'}, Increment says ${currentTask.completed ? 'complete' : 'incomplete'} → Increment wins`;
784
- result.conflicts.push(conflictMsg);
785
- console.log(`⚠️ CONFLICT RESOLVED: ${conflictMsg}`);
786
-
787
- // Keep increment state (do not update)
788
- continue;
789
- }
790
-
791
- // No conflict - apply GitHub change
792
- if (githubTask.completed) {
793
- const taskHeader = `### ${githubTask.id}:`;
794
- const completedMarker = `**Completed**: ${new Date().toISOString().split('T')[0]}`;
795
-
796
- if (!content.includes(`${taskHeader}`) || !content.includes(`**Completed**`)) {
797
- const regex = new RegExp(`(### ${githubTask.id}:[^#]*?)(###|$)`, 's');
798
- content = content.replace(regex, `$1\n${completedMarker}\n\n$2`);
799
- }
800
- }
801
- }
802
-
803
- await fs.writeFile(tasksPath, content, 'utf-8');
804
- }
805
-
806
- /**
807
- * Update GitHub issue with ACs and Tasks
808
- *
809
- * CRITICAL FIX (v1.0.59): Handle multiple checkbox formats in GitHub issues:
810
- * - Bold format: `- [ ] **AC-US5-01**: Description` (SpecWeave standard)
811
- * - Plain format: `- [ ] AC-US5-01: Description` (legacy/external)
812
- *
813
- * The regex must match BOTH formats to correctly sync checkboxes.
814
- */
815
- private async updateGitHubIssue(issueNumber: number, acs: AcceptanceCriterion[], tasks: Task[]): Promise<void> {
816
- // Fetch current issue
817
- const result = await execFileNoThrow('gh', [
818
- 'issue',
819
- 'view',
820
- String(issueNumber),
821
- '--json',
822
- 'body'
823
- ], { env: this.getGhEnv() });
824
-
825
- if (result.stderr || !result.stdout) {
826
- console.warn(`⚠️ Could not fetch GitHub issue #${issueNumber}: ${result.stderr}`);
827
- return;
828
- }
829
-
830
- const { body } = JSON.parse(result.stdout);
831
- if (!body) {
832
- console.warn(`⚠️ GitHub issue #${issueNumber} has no body`);
833
- return;
834
- }
835
-
836
- // Update AC checkboxes - handle BOTH bold and plain formats
837
- let updatedBody = body;
838
- let changesCount = 0;
839
-
840
- for (const ac of acs) {
841
- const checkboxState = ac.completed ? 'x' : ' ';
842
- // Escape AC ID for use in regex (handle hyphens)
843
- const escapedAcId = ac.id.replace(/-/g, '\\-');
844
-
845
- // Pattern 1: Bold format `- [ ] **AC-US5-01**: Description` (SpecWeave standard)
846
- const boldRegex = new RegExp(`(- \\[)[ x](\\] \\*\\*${escapedAcId}\\*\\*:)`, 'g');
847
-
848
- // Pattern 2: Plain format `- [ ] AC-US5-01: Description` (legacy)
849
- const plainRegex = new RegExp(`(- \\[)[ x](\\] ${escapedAcId}:)`, 'g');
850
-
851
- const beforeLength = updatedBody.length;
852
- updatedBody = updatedBody.replace(boldRegex, `$1${checkboxState}$2`);
853
- updatedBody = updatedBody.replace(plainRegex, `$1${checkboxState}$2`);
854
-
855
- if (updatedBody.length !== beforeLength || updatedBody !== body) {
856
- changesCount++;
857
- }
858
- }
859
-
860
- // Update Task checkboxes - handle BOTH bold and plain formats
861
- for (const task of tasks) {
862
- const checkboxState = task.completed ? 'x' : ' ';
863
- const escapedTaskId = task.id.replace(/-/g, '\\-');
864
-
865
- // Pattern 1: Bold format `- [ ] **T-001**: Description`
866
- const boldRegex = new RegExp(`(- \\[)[ x](\\] \\*\\*${escapedTaskId}\\*\\*:)`, 'g');
867
-
868
- // Pattern 2: Plain format `- [ ] T-001: Description`
869
- const plainRegex = new RegExp(`(- \\[)[ x](\\] ${escapedTaskId}:)`, 'g');
870
-
871
- updatedBody = updatedBody.replace(boldRegex, `$1${checkboxState}$2`);
872
- updatedBody = updatedBody.replace(plainRegex, `$1${checkboxState}$2`);
873
- }
874
-
875
- // Only update if there are changes
876
- if (updatedBody === body) {
877
- console.log(` ⏭️ No checkbox changes for issue #${issueNumber}`);
878
- return;
879
- }
880
-
881
- // Update issue via gh CLI
882
- const updateResult = await execFileNoThrow('gh', [
883
- 'issue',
884
- 'edit',
885
- String(issueNumber),
886
- '--body',
887
- updatedBody
888
- ], { env: this.getGhEnv() });
889
-
890
- if (updateResult.stderr) {
891
- console.warn(`⚠️ Failed to update GitHub issue #${issueNumber}: ${updateResult.stderr}`);
892
- } else {
893
- console.log(` ✅ Updated checkboxes in GitHub issue #${issueNumber}`);
894
- }
895
- }
896
-
897
- /**
898
- * Add comment to GitHub issue
899
- */
900
- private async addGitHubComment(issueNumber: number, comment: string): Promise<void> {
901
- await execFileNoThrow('gh', [
902
- 'issue',
903
- 'comment',
904
- String(issueNumber),
905
- '--body',
906
- comment
907
- ], { env: this.getGhEnv() });
908
- }
909
- }