specweave 0.21.3 → 0.22.2

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