@howlil/ez-agents 3.4.1 → 3.5.0

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 (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -0,0 +1,1157 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Git Workflow Engine
5
+ *
6
+ * Enterprise-grade Git workflow management with branch hierarchy,
7
+ * validation gates, and automated merging.
8
+ *
9
+ * Branch Hierarchy:
10
+ * main (production) ← develop (staging) ← phase/* ← {feature,fix,docs,refactor}/*
11
+ */
12
+
13
+ const GitUtils = require('./git-utils.cjs');
14
+ const Logger = require('./logger.cjs');
15
+ const {
16
+ GitWorkflowError,
17
+ BranchExistsError,
18
+ BranchNotFoundError,
19
+ MergeConflictError,
20
+ ValidationFailedError
21
+ } = require('./git-errors.cjs');
22
+
23
+ class GitWorkflowEngine {
24
+ constructor(config = {}) {
25
+ this.git = new GitUtils(process.cwd());
26
+ this.logger = new Logger();
27
+ this.config = config;
28
+ this.validationLevels = {
29
+ minimal: ['format', 'lint'],
30
+ standard: ['format', 'lint', 'test'],
31
+ full: ['format', 'lint', 'test', 'security', 'performance']
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Detect branch type from name
37
+ */
38
+ detectBranchType(branchName) {
39
+ const patterns = {
40
+ feature: /^feature\//,
41
+ fix: /^fix\//,
42
+ docs: /^docs\//,
43
+ refactor: /^refactor\//,
44
+ phase: /^phase\//,
45
+ release: /^release\//,
46
+ hotfix: /^hotfix\//
47
+ };
48
+
49
+ for (const [type, pattern] of Object.entries(patterns)) {
50
+ if (pattern.test(branchName)) {
51
+ return type;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /**
58
+ * Validate branch naming convention
59
+ */
60
+ validateBranchNaming(branchName, type) {
61
+ const patterns = {
62
+ feature: /^feature\/[a-zA-Z0-9\-_]+$/,
63
+ fix: /^fix\/[a-zA-Z0-9\-_]+$/,
64
+ docs: /^docs\/[a-zA-Z0-9\-_]+$/,
65
+ refactor: /^refactor\/[a-zA-Z0-9\-_]+$/,
66
+ phase: /^phase\/\d+-[a-zA-Z0-9\-_]+$/,
67
+ release: /^release\/v\d+\.\d+\.\d+$/,
68
+ hotfix: /^hotfix\/[a-zA-Z0-9\-_]+$/
69
+ };
70
+
71
+ const pattern = patterns[type];
72
+ if (!pattern) {
73
+ throw new ValidationFailedError('branch_type', [`Unknown branch type: ${type}`]);
74
+ }
75
+
76
+ if (!pattern.test(branchName)) {
77
+ throw new ValidationFailedError('branch_naming', [
78
+ `Branch '${branchName}' does not match ${type} pattern: ${pattern}`
79
+ ]);
80
+ }
81
+
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Validate merge strategy
87
+ */
88
+ _validateStrategy(strategy) {
89
+ const validStrategies = ['merge', 'squash', 'rebase'];
90
+ if (!validStrategies.includes(strategy)) {
91
+ throw new ValidationFailedError('merge_strategy', [
92
+ `Invalid merge strategy: ${strategy}. Must be one of: ${validStrategies.join(', ')}`
93
+ ]);
94
+ }
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * Create phase branch from develop
100
+ * PHASE-GIT-01: Auto-create phase branch from develop
101
+ */
102
+ async createPhaseBranch(phaseNumber, phaseSlug) {
103
+ const branchName = `phase/${phaseNumber}-${phaseSlug}`;
104
+
105
+ // Validate naming convention
106
+ this.validateBranchNaming(branchName, 'phase');
107
+
108
+ // Check if branch exists
109
+ if (await this.git.branchExists(branchName)) {
110
+ throw new BranchExistsError(branchName);
111
+ }
112
+
113
+ // Determine source branch (develop or main)
114
+ let sourceBranch = 'develop';
115
+ if (!(await this.git.branchExists('develop'))) {
116
+ this.logger.warn('develop branch not found, using main', { sourceBranch: 'main' });
117
+ sourceBranch = 'main';
118
+ }
119
+
120
+ // Create branch
121
+ await this.git.createBranch(branchName, sourceBranch);
122
+
123
+ this.logger.info('Phase branch created', {
124
+ branch: branchName,
125
+ phaseNumber,
126
+ phaseSlug,
127
+ source: sourceBranch
128
+ });
129
+
130
+ return branchName;
131
+ }
132
+
133
+ /**
134
+ * Create feature/fix/docs/refactor branch
135
+ * PHASE-GIT-02: Auto-create feature/fix/docs/refactor branches within phase
136
+ */
137
+ async createWorkBranch(type, ticketId = null, slug) {
138
+ const validTypes = ['feature', 'fix', 'docs', 'refactor'];
139
+
140
+ if (!validTypes.includes(type)) {
141
+ throw new ValidationFailedError('branch_type', [
142
+ `Invalid branch type: ${type}. Must be one of: ${validTypes.join(', ')}`
143
+ ]);
144
+ }
145
+
146
+ // Build branch name
147
+ let branchName;
148
+ if (ticketId && ['feature', 'fix'].includes(type)) {
149
+ branchName = `${type}/${ticketId}-${slug}`;
150
+ } else {
151
+ branchName = `${type}/${slug}`;
152
+ }
153
+
154
+ // Validate naming convention
155
+ this.validateBranchNaming(branchName, type);
156
+
157
+ // Check if branch exists
158
+ if (await this.git.branchExists(branchName)) {
159
+ throw new BranchExistsError(branchName);
160
+ }
161
+
162
+ // Source from current branch (should be phase branch or develop)
163
+ const sourceBranch = await this.git.getCurrentBranch();
164
+
165
+ // Create branch
166
+ await this.git.createBranch(branchName, sourceBranch);
167
+
168
+ this.logger.info('Work branch created', {
169
+ branch: branchName,
170
+ type,
171
+ ticketId,
172
+ slug,
173
+ source: sourceBranch
174
+ });
175
+
176
+ return branchName;
177
+ }
178
+
179
+ /**
180
+ * Create atomic commit for task completion
181
+ * PHASE-GIT-03: Auto-commit after each task completion
182
+ * PHASE-GIT-04: Commit message format: <type>(scope): <description> [TASK-XX]
183
+ */
184
+ async commitTask(taskDescription, taskId, files = []) {
185
+ // Parse task ID to number for formatting
186
+ const taskNum = parseInt(taskId.replace('TASK-', ''), 10);
187
+ const formattedTaskId = `TASK-${String(taskNum).padStart(2, '0')}`;
188
+
189
+ // Extract commit type from task description
190
+ const typePatterns = {
191
+ feat: /add|implement|create|new|enable/i,
192
+ fix: /fix|resolve|patch|correct/i,
193
+ docs: /document|update docs|readme/i,
194
+ refactor: /refactor|restructure|reorganize/i,
195
+ test: /test|add tests/i,
196
+ chore: /update|configure|setup|clean/i
197
+ };
198
+
199
+ let commitType = 'chore';
200
+ for (const [type, pattern] of Object.entries(typePatterns)) {
201
+ if (pattern.test(taskDescription)) {
202
+ commitType = type;
203
+ break;
204
+ }
205
+ }
206
+
207
+ // Extract scope (optional)
208
+ const scopePattern = /\[([^\]]+)\]/;
209
+ const scopeMatch = taskDescription.match(scopePattern);
210
+ const scope = scopeMatch ? `(${scopeMatch[1]})` : '';
211
+
212
+ // Clean description
213
+ let cleanDescription = taskDescription
214
+ .replace(scopePattern, '')
215
+ .replace(/^(add|implement|create|fix|update|resolve)\s+/i, '')
216
+ .trim();
217
+
218
+ // Build commit message
219
+ const commitMessage = `${commitType}${scope}: ${cleanDescription} [${formattedTaskId}]`;
220
+
221
+ // Create commit
222
+ const commitHash = await this.git.commitAtomic(commitMessage, files);
223
+
224
+ this.logger.info('Task commit created', {
225
+ hash: commitHash,
226
+ message: commitMessage,
227
+ taskId: formattedTaskId
228
+ });
229
+
230
+ return commitHash;
231
+ }
232
+
233
+ /**
234
+ * Validate planning file consistency
235
+ */
236
+ async _validatePlanningFiles() {
237
+ const fs = require('fs');
238
+ const path = require('path');
239
+ const errors = [];
240
+
241
+ // Check STATE.md exists
242
+ const statePath = path.join(process.cwd(), '.planning', 'STATE.md');
243
+ if (!fs.existsSync(statePath)) {
244
+ errors.push('STATE.md not found');
245
+ }
246
+
247
+ // Check STATE.md has required frontmatter
248
+ if (fs.existsSync(statePath)) {
249
+ const stateContent = fs.readFileSync(statePath, 'utf-8');
250
+ if (!stateContent.includes('current_phase:') || !stateContent.includes('status:')) {
251
+ errors.push('STATE.md missing required frontmatter fields');
252
+ }
253
+ }
254
+
255
+ return {
256
+ name: 'planning_consistency',
257
+ passed: errors.length === 0,
258
+ message: errors.length === 0 ? 'Planning files consistent' : errors.join('; ')
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Run format check (Prettier)
264
+ */
265
+ async _runFormatCheck() {
266
+ const { execFile } = require('child_process');
267
+ const { promisify } = require('util');
268
+ const execFileAsync = promisify(execFile);
269
+
270
+ try {
271
+ await execFileAsync('npx', ['prettier', '--check', '.'], { cwd: process.cwd() });
272
+ return { name: 'format', passed: true, message: 'Format check passed' };
273
+ } catch (err) {
274
+ return { name: 'format', passed: false, message: 'Format check failed' };
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Run lint check (ESLint)
280
+ */
281
+ async _runLintCheck() {
282
+ const { execFile } = require('child_process');
283
+ const { promisify } = require('util');
284
+ const execFileAsync = promisify(execFile);
285
+
286
+ try {
287
+ await execFileAsync('npx', ['eslint', '.'], { cwd: process.cwd() });
288
+ return { name: 'lint', passed: true, message: 'Lint check passed' };
289
+ } catch (err) {
290
+ return { name: 'lint', passed: false, message: 'Lint check failed' };
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Run test check
296
+ */
297
+ async _runTestCheck() {
298
+ const { execFile } = require('child_process');
299
+ const { promisify } = require('util');
300
+ const execFileAsync = promisify(execFile);
301
+
302
+ try {
303
+ await execFileAsync('npm', ['test'], { cwd: process.cwd() });
304
+ return { name: 'test', passed: true, message: 'Test check passed' };
305
+ } catch (err) {
306
+ return { name: 'test', passed: false, message: 'Test check failed' };
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Run security check (npm audit)
312
+ */
313
+ async _runSecurityCheck() {
314
+ const { execFile } = require('child_process');
315
+ const { promisify } = require('util');
316
+ const execFileAsync = promisify(execFile);
317
+
318
+ try {
319
+ await execFileAsync('npm', ['audit', '--audit-level=critical'], { cwd: process.cwd() });
320
+ return { name: 'security', passed: true, message: 'Security check passed' };
321
+ } catch (err) {
322
+ return { name: 'security', passed: false, message: 'Security check failed: critical vulnerabilities found' };
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Validate branch before merge
328
+ * PHASE-GIT-05: Validate feature/fix branches before merge to phase
329
+ */
330
+ async validateBeforeMerge(branch, validationLevel = 'standard') {
331
+ const currentBranch = await this.git.getCurrentBranch();
332
+ const checks = [];
333
+ let passed = true;
334
+
335
+ try {
336
+ // Switch to branch for validation
337
+ await this.git.checkout(branch);
338
+
339
+ // Get validation checks based on level
340
+ const requiredChecks = this.validationLevels[validationLevel] || this.validationLevels.standard;
341
+
342
+ // Planning file consistency check (always run)
343
+ const planningCheck = await this._validatePlanningFiles();
344
+ checks.push(planningCheck);
345
+ if (!planningCheck.passed) passed = false;
346
+
347
+ // Format check
348
+ if (requiredChecks.includes('format')) {
349
+ const formatCheck = await this._runFormatCheck();
350
+ checks.push(formatCheck);
351
+ if (!formatCheck.passed) passed = false;
352
+ }
353
+
354
+ // Lint check
355
+ if (requiredChecks.includes('lint')) {
356
+ const lintCheck = await this._runLintCheck();
357
+ checks.push(lintCheck);
358
+ if (!lintCheck.passed) passed = false;
359
+ }
360
+
361
+ // Test check
362
+ if (requiredChecks.includes('test')) {
363
+ const testCheck = await this._runTestCheck();
364
+ checks.push(testCheck);
365
+ if (!testCheck.passed) passed = false;
366
+ }
367
+
368
+ // Security check (full level only)
369
+ if (requiredChecks.includes('security')) {
370
+ const securityCheck = await this._runSecurityCheck();
371
+ checks.push(securityCheck);
372
+ if (!securityCheck.passed) passed = false;
373
+ }
374
+
375
+ const result = {
376
+ branch,
377
+ validationLevel,
378
+ passed,
379
+ checks,
380
+ timestamp: new Date().toISOString()
381
+ };
382
+
383
+ this.logger.info('Validation completed', result);
384
+
385
+ if (!passed) {
386
+ throw new ValidationFailedError('pre_merge', checks.filter(c => !c.passed).map(c => c.message));
387
+ }
388
+
389
+ return result;
390
+ } finally {
391
+ // Restore original branch
392
+ if (currentBranch !== branch) {
393
+ await this.git.checkout(currentBranch);
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Merge feature/fix branch to phase branch
400
+ * PHASE-GIT-06: Auto-merge feature/fix branches to phase branch after validation
401
+ */
402
+ async mergeToPhase(featureBranch, phaseBranch) {
403
+ const branchType = this.detectBranchType(featureBranch);
404
+ const strategy = this.config.git?.merge_strategies?.[branchType] || 'squash';
405
+
406
+ this.logger.info('Merging to phase', {
407
+ source: featureBranch,
408
+ target: phaseBranch,
409
+ strategy
410
+ });
411
+
412
+ // Validate before merge
413
+ await this.validateBeforeMerge(featureBranch, 'standard');
414
+
415
+ // Check for conflicts
416
+ const hasConflicts = await this.git.hasConflicts(featureBranch, phaseBranch);
417
+ if (hasConflicts) {
418
+ throw new MergeConflictError(featureBranch, phaseBranch);
419
+ }
420
+
421
+ // Perform merge
422
+ await this.git.mergeWithStrategy(featureBranch, phaseBranch, strategy);
423
+
424
+ this.logger.info('Merge to phase completed', {
425
+ source: featureBranch,
426
+ target: phaseBranch
427
+ });
428
+
429
+ return { success: true, source: featureBranch, target: phaseBranch };
430
+ }
431
+
432
+ /**
433
+ * Merge phase branch to develop
434
+ * PHASE-GIT-08: Auto-merge phase branch to develop after validation
435
+ */
436
+ async mergePhaseToDevelop(phaseBranch) {
437
+ const strategy = this.config.git?.merge_strategies?.phase || 'merge';
438
+ const targetBranch = 'develop';
439
+
440
+ this.logger.info('Merging phase to develop', {
441
+ source: phaseBranch,
442
+ target: targetBranch,
443
+ strategy
444
+ });
445
+
446
+ // Validate before merge
447
+ await this.validateBeforeMerge(phaseBranch, 'full');
448
+
449
+ // Check for conflicts
450
+ const hasConflicts = await this.git.hasConflicts(phaseBranch, targetBranch);
451
+ if (hasConflicts) {
452
+ throw new MergeConflictError(phaseBranch, targetBranch);
453
+ }
454
+
455
+ // Perform merge
456
+ await this.git.mergeWithStrategy(phaseBranch, targetBranch, strategy);
457
+
458
+ this.logger.info('Phase merge to develop completed', {
459
+ source: phaseBranch,
460
+ target: targetBranch
461
+ });
462
+
463
+ return { success: true, source: phaseBranch, target: targetBranch };
464
+ }
465
+
466
+ /**
467
+ * Create release branch from develop
468
+ * PHASE-GIT-09: Create release branch from develop for stabilization
469
+ */
470
+ async createReleaseBranch(version) {
471
+ const semver = require('semver');
472
+
473
+ // Validate version format
474
+ const validVersion = semver.valid(version);
475
+ if (!validVersion) {
476
+ throw new ValidationFailedError('version_format', [
477
+ `Invalid version format: ${version}. Must be valid semver (e.g., 2.0.0)`
478
+ ]);
479
+ }
480
+
481
+ const branchName = `release/v${validVersion}`;
482
+
483
+ // Check if branch exists
484
+ if (await this.git.branchExists(branchName)) {
485
+ throw new BranchExistsError(branchName);
486
+ }
487
+
488
+ // Source from develop
489
+ if (!(await this.git.branchExists('develop'))) {
490
+ throw new BranchNotFoundError('develop');
491
+ }
492
+
493
+ // Create branch
494
+ await this.git.createBranch(branchName, 'develop');
495
+
496
+ this.logger.info('Release branch created', {
497
+ branch: branchName,
498
+ version: validVersion
499
+ });
500
+
501
+ return branchName;
502
+ }
503
+
504
+ /**
505
+ * Run integration tests
506
+ */
507
+ async _runIntegrationTestCheck() {
508
+ const { execFile } = require('child_process');
509
+ const { promisify } = require('util');
510
+ const execFileAsync = promisify(execFile);
511
+
512
+ try {
513
+ // Try to run integration tests if they exist
514
+ await execFileAsync('npm', ['run', 'test:integration'], { cwd: process.cwd() });
515
+ return { name: 'integration_tests', passed: true, message: 'Integration tests passed' };
516
+ } catch (err) {
517
+ // If integration test script doesn't exist, skip
518
+ if (err.code === 1 || err.stderr?.includes('Missing script')) {
519
+ return { name: 'integration_tests', passed: true, message: 'Integration tests not configured, skipped' };
520
+ }
521
+ return { name: 'integration_tests', passed: false, message: 'Integration tests failed' };
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Run dependency audit
527
+ */
528
+ async _runDependencyAudit() {
529
+ const { execFile } = require('child_process');
530
+ const { promisify } = require('util');
531
+ const execFileAsync = promisify(execFile);
532
+
533
+ try {
534
+ await execFileAsync('npm', ['audit', '--production', '--audit-level=high'], { cwd: process.cwd() });
535
+ return { name: 'dependency_audit', passed: true, message: 'Dependency audit passed' };
536
+ } catch (err) {
537
+ return { name: 'dependency_audit', passed: false, message: 'Dependency audit failed: high/critical vulnerabilities' };
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Run critical bug detection
543
+ */
544
+ async _runCriticalBugDetection() {
545
+ // Check for common critical bug patterns in code
546
+ const fs = require('fs');
547
+ const path = require('path');
548
+ const errors = [];
549
+
550
+ // Example: Check for console.log in production code (excluding tests)
551
+ const srcDir = path.join(process.cwd(), 'ez-agents', 'bin', 'lib');
552
+ if (fs.existsSync(srcDir)) {
553
+ const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.cjs'));
554
+ for (const file of files) {
555
+ const content = fs.readFileSync(path.join(srcDir, file), 'utf-8');
556
+ if (content.includes('console.log(') && !content.includes('// console.log')) {
557
+ errors.push(`Potential debug logging in ${file}`);
558
+ }
559
+ }
560
+ }
561
+
562
+ return {
563
+ name: 'critical_bug_detection',
564
+ passed: errors.length === 0,
565
+ message: errors.length === 0 ? 'No critical bugs detected' : errors.join('; ')
566
+ };
567
+ }
568
+
569
+ /**
570
+ * Validate release branch stability
571
+ * PHASE-GIT-10: Run full test suite, integration tests, security scans on release branch
572
+ * PHASE-GIT-11: Validate release branch stability (zero critical bugs, all tests green)
573
+ */
574
+ async validateReleaseBranch(releaseBranch) {
575
+ const currentBranch = await this.git.getCurrentBranch();
576
+ const checks = [];
577
+ let passed = true;
578
+ let criticalFailures = 0;
579
+
580
+ try {
581
+ // Switch to release branch
582
+ await this.git.checkout(releaseBranch);
583
+
584
+ // Full test suite
585
+ this.logger.info('Running full test suite', { branch: releaseBranch });
586
+ const testCheck = await this._runTestCheck();
587
+ testCheck.name = 'full_test_suite';
588
+ testCheck.critical = true;
589
+ checks.push(testCheck);
590
+ if (!testCheck.passed) {
591
+ passed = false;
592
+ criticalFailures++;
593
+ }
594
+
595
+ // Integration tests
596
+ this.logger.info('Running integration tests', { branch: releaseBranch });
597
+ const integrationCheck = await this._runIntegrationTestCheck();
598
+ integrationCheck.critical = true;
599
+ checks.push(integrationCheck);
600
+ if (!integrationCheck.passed) {
601
+ passed = false;
602
+ criticalFailures++;
603
+ }
604
+
605
+ // Security scan - npm audit
606
+ this.logger.info('Running security audit', { branch: releaseBranch });
607
+ const securityCheck = await this._runSecurityCheck();
608
+ securityCheck.critical = true;
609
+ checks.push(securityCheck);
610
+ if (!securityCheck.passed) {
611
+ passed = false;
612
+ criticalFailures++;
613
+ }
614
+
615
+ // Dependency vulnerability scan
616
+ this.logger.info('Scanning dependencies', { branch: releaseBranch });
617
+ const dependencyCheck = await this._runDependencyAudit();
618
+ dependencyCheck.critical = true;
619
+ checks.push(dependencyCheck);
620
+ if (!dependencyCheck.passed) {
621
+ passed = false;
622
+ criticalFailures++;
623
+ }
624
+
625
+ // Critical bug detection (check for known critical patterns)
626
+ this.logger.info('Checking for critical bugs', { branch: releaseBranch });
627
+ const criticalBugCheck = await this._runCriticalBugDetection();
628
+ criticalBugCheck.critical = true;
629
+ checks.push(criticalBugCheck);
630
+ if (!criticalBugCheck.passed) {
631
+ passed = false;
632
+ criticalFailures++;
633
+ }
634
+
635
+ const result = {
636
+ branch: releaseBranch,
637
+ passed,
638
+ criticalFailures,
639
+ checks,
640
+ timestamp: new Date().toISOString()
641
+ };
642
+
643
+ this.logger.info('Release validation completed', result);
644
+
645
+ if (criticalFailures > 0) {
646
+ throw new ValidationFailedError('release_stability', [
647
+ `${criticalFailures} critical check(s) failed`,
648
+ ...checks.filter(c => c.critical && !c.passed).map(c => c.message)
649
+ ]);
650
+ }
651
+
652
+ return result;
653
+ } finally {
654
+ // Restore original branch
655
+ if (currentBranch !== releaseBranch) {
656
+ await this.git.checkout(currentBranch);
657
+ }
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Bump version in package.json
663
+ */
664
+ async _bumpVersion(newVersion) {
665
+ const fs = require('fs');
666
+ const path = require('path');
667
+
668
+ const packagePath = path.join(process.cwd(), 'package.json');
669
+ if (fs.existsSync(packagePath)) {
670
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
671
+ packageJson.version = newVersion;
672
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
673
+
674
+ // Commit version bump
675
+ await this.git.add('package.json');
676
+ await this.git.commitAtomic(`chore: bump version to ${newVersion} [RELEASE]`, ['package.json']);
677
+
678
+ this.logger.info('Version bumped', { version: newVersion });
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Merge release to main with version tag
684
+ * PHASE-GIT-12: Merge release to main with version tag
685
+ * PHASE-GIT-13: Merge release back to develop with version bump
686
+ */
687
+ async mergeReleaseToMain(releaseBranch) {
688
+ const semver = require('semver');
689
+
690
+ // Extract version from branch name
691
+ const versionMatch = releaseBranch.match(/^release\/v(\d+\.\d+\.\d+)$/);
692
+ if (!versionMatch) {
693
+ throw new ValidationFailedError('release_branch_format', [
694
+ `Invalid release branch format: ${releaseBranch}. Expected: release/vX.Y.Z`
695
+ ]);
696
+ }
697
+
698
+ const version = versionMatch[1];
699
+ const tagName = `v${version}`;
700
+
701
+ this.logger.info('Merging release to main', {
702
+ releaseBranch,
703
+ version,
704
+ tagName
705
+ });
706
+
707
+ // Validate release branch
708
+ await this.validateReleaseBranch(releaseBranch);
709
+
710
+ // Merge to main
711
+ await this.git.checkout('main');
712
+ await this.git.mergeWithStrategy(releaseBranch, 'main', 'merge');
713
+
714
+ // Create tag
715
+ await this.git.tagRelease(tagName, `Release ${tagName}`);
716
+
717
+ this.logger.info('Release merged to main', {
718
+ branch: 'main',
719
+ tag: tagName
720
+ });
721
+
722
+ // Merge back to develop with version bump
723
+ await this.git.checkout('develop');
724
+ await this.git.mergeWithStrategy(releaseBranch, 'develop', 'merge');
725
+
726
+ // Bump version in package.json
727
+ await this._bumpVersion(version);
728
+
729
+ this.logger.info('Release merged to develop', {
730
+ branch: 'develop',
731
+ version
732
+ });
733
+
734
+ return {
735
+ success: true,
736
+ releaseBranch,
737
+ mainTag: tagName,
738
+ version
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Create pull request for enterprise mode
744
+ */
745
+ async _createPullRequest(source, target, options = {}) {
746
+ const { Octokit } = require('@octokit/rest');
747
+
748
+ // Check if GitHub token is configured
749
+ const githubToken = process.env.GITHUB_TOKEN;
750
+ if (!githubToken) {
751
+ throw new ValidationFailedError('github_auth', [
752
+ 'GITHUB_TOKEN environment variable not set. Required for enterprise PR workflow.'
753
+ ]);
754
+ }
755
+
756
+ const octokit = new Octokit({ auth: githubToken });
757
+
758
+ // Get repository info
759
+ const repoPath = process.cwd();
760
+ const { execFile } = require('child_process');
761
+ const { promisify } = require('util');
762
+ const execFileAsync = promisify(execFile);
763
+
764
+ try {
765
+ const { stdout: remoteUrl } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd: repoPath });
766
+ const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)\.git/);
767
+
768
+ if (!repoMatch) {
769
+ throw new Error('Could not parse repository URL');
770
+ }
771
+
772
+ const [, owner, repo] = repoMatch;
773
+
774
+ // Create PR
775
+ const prTitle = options.title || `Merge '${source}' into '${target}'`;
776
+ const prBody = options.body || `Automated PR for merging ${source} into ${target}`;
777
+
778
+ const { data: pr } = await octokit.pulls.create({
779
+ owner,
780
+ repo,
781
+ title: prTitle,
782
+ body: prBody,
783
+ head: source,
784
+ base: target
785
+ });
786
+
787
+ this.logger.info('Pull request created', {
788
+ number: pr.number,
789
+ url: pr.html_url,
790
+ source,
791
+ target
792
+ });
793
+
794
+ return {
795
+ success: true,
796
+ mode: 'enterprise',
797
+ pullRequest: pr.number,
798
+ url: pr.html_url,
799
+ requiredReviewers: options.requiredReviewers,
800
+ createdAt: new Date().toISOString()
801
+ };
802
+ } catch (err) {
803
+ this.logger.error('Failed to create pull request', { error: err.message });
804
+ throw new GitWorkflowError(`Failed to create PR: ${err.message}`, {
805
+ code: 'PR_CREATION_FAILED',
806
+ details: { source, target }
807
+ });
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Merge branch with enterprise/open source mode support
813
+ * PHASE-GIT-14: Support enterprise workflow (protected branches, PR required, code review)
814
+ * PHASE-GIT-15: Support open source workflow (direct merge after automated validation)
815
+ */
816
+ async mergeBranch(source, target, options = {}) {
817
+ const enterpriseMode = this.config.git?.enterprise_mode?.require_pull_request || false;
818
+ const requiredReviewers = this.config.git?.enterprise_mode?.required_reviewers || 1;
819
+
820
+ // Validate strategy
821
+ const strategy = options.strategy || this.config.git?.merge_strategies?.[this.detectBranchType(source)] || 'merge';
822
+ this._validateStrategy(strategy);
823
+
824
+ this.logger.info('Merge branch requested', {
825
+ source,
826
+ target,
827
+ enterpriseMode,
828
+ requiredReviewers,
829
+ strategy
830
+ });
831
+
832
+ if (enterpriseMode) {
833
+ // Enterprise mode: Create PR and require approval
834
+ return await this._createPullRequest(source, target, {
835
+ ...options,
836
+ requiredReviewers
837
+ });
838
+ } else {
839
+ // Open source mode: Direct merge after validation
840
+ await this.validateBeforeMerge(source, options.validationLevel || 'standard');
841
+
842
+ await this.git.mergeWithStrategy(source, target, strategy);
843
+
844
+ this.logger.info('Direct merge completed (open source mode)', {
845
+ source,
846
+ target
847
+ });
848
+
849
+ return {
850
+ success: true,
851
+ mode: 'open_source',
852
+ source,
853
+ target,
854
+ mergedAt: new Date().toISOString()
855
+ };
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Create and merge Hotfix
861
+ * PHASE-GIT-17: Hotfix workflow (create from main, merge to main + develop)
862
+ */
863
+ async createHotfix(description) {
864
+ // Create slug from description
865
+ const slug = description
866
+ .toLowerCase()
867
+ .replace(/[^a-z0-9]+/g, '-')
868
+ .replace(/^-|-$/g, '');
869
+
870
+ const branchName = `hotfix/${slug}`;
871
+
872
+ // Check if branch exists
873
+ if (await this.git.branchExists(branchName)) {
874
+ throw new BranchExistsError(branchName);
875
+ }
876
+
877
+ // Create hotfix branch from main
878
+ await this.git.createBranch(branchName, 'main');
879
+
880
+ this.logger.info('Hotfix branch created', {
881
+ branch: branchName,
882
+ description
883
+ });
884
+
885
+ return branchName;
886
+ }
887
+
888
+ /**
889
+ * Merge hotfix to main and develop
890
+ */
891
+ async mergeHotfix(hotfixBranch, version = null) {
892
+ const branchType = this.detectBranchType(hotfixBranch);
893
+ if (branchType !== 'hotfix') {
894
+ throw new ValidationFailedError('branch_type', [
895
+ `Expected hotfix branch, got: ${hotfixBranch}`
896
+ ]);
897
+ }
898
+
899
+ this.logger.info('Merging hotfix', { branch: hotfixBranch });
900
+
901
+ // Validate hotfix
902
+ await this.validateBeforeMerge(hotfixBranch, 'standard');
903
+
904
+ // Merge to main
905
+ await this.git.checkout('main');
906
+ await this.git.mergeWithStrategy(hotfixBranch, 'main', 'squash');
907
+
908
+ // Create tag if version provided
909
+ if (version) {
910
+ const tagName = `v${version}`;
911
+ await this.git.tagRelease(tagName, `Hotfix ${version}`);
912
+ this.logger.info('Hotfix tagged', { tag: tagName });
913
+ }
914
+
915
+ // Merge to develop
916
+ await this.git.checkout('develop');
917
+ await this.git.mergeWithStrategy(hotfixBranch, 'develop', 'squash');
918
+
919
+ this.logger.info('Hotfix merged to main and develop', {
920
+ branch: hotfixBranch
921
+ });
922
+
923
+ return {
924
+ success: true,
925
+ hotfixBranch,
926
+ mergedTo: ['main', 'develop'],
927
+ version
928
+ };
929
+ }
930
+
931
+ /**
932
+ * Rollback phase
933
+ * PHASE-GIT-18: Rollback capability with auto-revert if phase introduces regressions
934
+ */
935
+ async rollbackPhase(phaseNumber) {
936
+ const phasePattern = `phase/${phaseNumber}-`;
937
+ const branches = await this.git.listBranches(phasePattern + '*');
938
+
939
+ if (branches.length === 0) {
940
+ throw new BranchNotFoundError(`phase/${phaseNumber}-*`);
941
+ }
942
+
943
+ const phaseBranch = branches[0];
944
+ this.logger.info('Rolling back phase', { phaseNumber, phaseBranch });
945
+
946
+ // Create rollback branch for safety
947
+ const rollbackBranch = `rollback/phase-${phaseNumber}-${Date.now()}`;
948
+ await this.git.createBranch(rollbackBranch, 'develop');
949
+
950
+ // Check if phase was merged to develop
951
+ const currentBranch = await this.git.getCurrentBranch();
952
+ await this.git.checkout('develop');
953
+
954
+ // Find merge commit
955
+ const { execFile } = require('child_process');
956
+ const { promisify } = require('util');
957
+ const execFileAsync = promisify(execFile);
958
+
959
+ try {
960
+ const { stdout } = await execFileAsync('git', [
961
+ 'log', '--oneline', '--grep', phaseBranch, '-n', '1'
962
+ ], { cwd: process.cwd() });
963
+
964
+ if (stdout) {
965
+ const mergeCommit = stdout.split(' ')[0];
966
+
967
+ // Revert the merge commit
968
+ await this.git.revertCommit(mergeCommit);
969
+
970
+ this.logger.info('Phase rollback completed', {
971
+ phaseNumber,
972
+ revertedCommit: mergeCommit,
973
+ rollbackBranch
974
+ });
975
+
976
+ return {
977
+ success: true,
978
+ phaseNumber,
979
+ revertedCommit: mergeCommit,
980
+ rollbackBranch
981
+ };
982
+ } else {
983
+ // Phase not merged yet, just delete the branch
984
+ await this.git.deleteBranch(phaseBranch, true);
985
+
986
+ this.logger.info('Phase branch deleted (not merged)', {
987
+ phaseNumber,
988
+ phaseBranch
989
+ });
990
+
991
+ return {
992
+ success: true,
993
+ phaseNumber,
994
+ deleted: true,
995
+ rollbackBranch
996
+ };
997
+ }
998
+ } finally {
999
+ await this.git.checkout(currentBranch);
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Check branch protection rules
1005
+ * PHASE-GIT-19: Branch protection rules enforcement (require PR, reviews, status checks)
1006
+ */
1007
+ async checkBranchProtection(branch) {
1008
+ const { Octokit } = require('@octokit/rest');
1009
+
1010
+ const githubToken = process.env.GITHUB_TOKEN;
1011
+ if (!githubToken) {
1012
+ this.logger.warn('GITHUB_TOKEN not set, skipping branch protection check');
1013
+ return { protected: false, reason: 'no_token' };
1014
+ }
1015
+
1016
+ const octokit = new Octokit({ auth: githubToken });
1017
+
1018
+ // Get repository info
1019
+ const { execFile } = require('child_process');
1020
+ const { promisify } = require('util');
1021
+ const execFileAsync = promisify(execFile);
1022
+
1023
+ try {
1024
+ const { stdout: remoteUrl } = await execFileAsync('git', ['remote', 'get-url', 'origin'], { cwd: process.cwd() });
1025
+ const repoMatch = remoteUrl.match(/github\.com[:/]([^/]+)\/([^.]+)\.git/);
1026
+
1027
+ if (!repoMatch) {
1028
+ throw new Error('Could not parse repository URL');
1029
+ }
1030
+
1031
+ const [, owner, repo] = repoMatch;
1032
+
1033
+ // Get branch protection rules
1034
+ try {
1035
+ const { data: protection } = await octokit.repos.getBranchProtection({
1036
+ owner,
1037
+ repo,
1038
+ branch
1039
+ });
1040
+
1041
+ const result = {
1042
+ protected: true,
1043
+ requiredStatusChecks: protection.required_status_checks?.strict || false,
1044
+ requiredPullRequestReviews: protection.required_pull_request_reviews || null,
1045
+ requiredLinearHistory: protection.required_linear_history?.enabled || false,
1046
+ allowForcePushes: protection.allow_force_pushes?.enabled || false,
1047
+ allowDeletions: protection.allow_deletions?.enabled || false
1048
+ };
1049
+
1050
+ this.logger.info('Branch protection status', { branch, ...result });
1051
+
1052
+ // Validate enterprise mode requirements
1053
+ if (this.config.git?.enterprise_mode?.require_pull_request) {
1054
+ if (!result.requiredPullRequestReviews) {
1055
+ throw new ValidationFailedError('branch_protection', [
1056
+ `Branch '${branch}' does not require pull request reviews (enterprise mode requires it)`
1057
+ ]);
1058
+ }
1059
+ }
1060
+
1061
+ return result;
1062
+ } catch (err) {
1063
+ if (err.status === 404) {
1064
+ // Branch not protected
1065
+ return { protected: false, reason: 'not_protected' };
1066
+ }
1067
+ throw err;
1068
+ }
1069
+ } catch (err) {
1070
+ this.logger.error('Failed to check branch protection', { error: err.message });
1071
+ throw new GitWorkflowError(`Failed to check branch protection: ${err.message}`, {
1072
+ code: 'PROTECTION_CHECK_FAILED',
1073
+ details: { branch }
1074
+ });
1075
+ }
1076
+ }
1077
+
1078
+ /**
1079
+ * Enhance changelog with task IDs
1080
+ */
1081
+ _enhanceChangelogWithTaskIds(changelog) {
1082
+ // Add header
1083
+ const header = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n`;
1084
+
1085
+ // Parse task IDs from commits and add to changelog
1086
+ const taskPattern = /\[TASK-(\d+)\]/g;
1087
+ const enhanced = changelog.replace(taskPattern, (match, taskId) => {
1088
+ return `[Task #${taskId}](https://github.com/howlil/ez-agents/issues/${taskId})`;
1089
+ });
1090
+
1091
+ return header + enhanced;
1092
+ }
1093
+
1094
+ /**
1095
+ * Generate changelog from commits
1096
+ * PHASE-GIT-20: Automated changelog generation from commits on merge to main
1097
+ */
1098
+ async generateChangelog(fromTag, toTag = 'HEAD') {
1099
+ const conventionalChangelog = require('conventional-changelog');
1100
+ const fs = require('fs');
1101
+ const path = require('path');
1102
+
1103
+ this.logger.info('Generating changelog', { fromTag, toTag });
1104
+
1105
+ return new Promise((resolve, reject) => {
1106
+ const changelogStream = conventionalChangelog({
1107
+ preset: 'angular',
1108
+ releaseCount: 1
1109
+ }, {
1110
+ from: fromTag,
1111
+ to: toTag
1112
+ }, {
1113
+ commits: true,
1114
+ commitsPath: process.cwd()
1115
+ });
1116
+
1117
+ let changelog = '';
1118
+
1119
+ changelogStream.on('data', (chunk) => {
1120
+ changelog += chunk.toString();
1121
+ });
1122
+
1123
+ changelogStream.on('end', () => {
1124
+ // Parse and enhance with task IDs
1125
+ const enhancedChangelog = this._enhanceChangelogWithTaskIds(changelog);
1126
+
1127
+ // Append to CHANGELOG.md
1128
+ const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
1129
+
1130
+ if (fs.existsSync(changelogPath)) {
1131
+ const existingContent = fs.readFileSync(changelogPath, 'utf-8');
1132
+ fs.writeFileSync(changelogPath, enhancedChangelog + '\n' + existingContent);
1133
+ } else {
1134
+ fs.writeFileSync(changelogPath, enhancedChangelog);
1135
+ }
1136
+
1137
+ this.logger.info('Changelog generated', { path: changelogPath });
1138
+
1139
+ resolve({
1140
+ success: true,
1141
+ fromTag,
1142
+ toTag,
1143
+ path: changelogPath
1144
+ });
1145
+ });
1146
+
1147
+ changelogStream.on('error', (err) => {
1148
+ this.logger.error('Changelog generation failed', { error: err.message });
1149
+ reject(new GitWorkflowError(`Changelog generation failed: ${err.message}`, {
1150
+ code: 'CHANGELOG_GENERATION_FAILED'
1151
+ }));
1152
+ });
1153
+ });
1154
+ }
1155
+ }
1156
+
1157
+ module.exports = GitWorkflowEngine;