@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,614 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Release Validator — Automated release readiness validation
5
+ *
6
+ * Runs security gates, tier checklist validation, and produces
7
+ * a Production Readiness Score (0-100) for /ez:release.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+ const TierManager = require('./tier-manager.cjs');
16
+
17
+ // ─────────────────────────────────────────────
18
+ // Security Helpers
19
+ // ─────────────────────────────────────────────
20
+
21
+ /**
22
+ * Calculate Shannon entropy for a string — used to detect high-entropy secrets
23
+ * @param {string} str
24
+ * @param {number} threshold
25
+ * @returns {boolean}
26
+ */
27
+ function hasHighEntropy(str, threshold = 4.5) {
28
+ const freq = {};
29
+ for (const c of str) freq[c] = (freq[c] || 0) + 1;
30
+ const len = str.length;
31
+ let entropy = 0;
32
+ for (const c in freq) {
33
+ const p = freq[c] / len;
34
+ entropy -= p * Math.log2(p);
35
+ }
36
+ return entropy > threshold;
37
+ }
38
+
39
+ // ─────────────────────────────────────────────
40
+ // Security Gates
41
+ // ─────────────────────────────────────────────
42
+
43
+ /**
44
+ * Run all security gates
45
+ * @param {string} cwd - Working directory
46
+ * @returns {{ passed: boolean, gates: object[] }}
47
+ */
48
+ function runSecurityGates(cwd = process.cwd()) {
49
+ const gates = [];
50
+
51
+ // Gate 1: Multi-pattern secret detection
52
+ try {
53
+ const secretPatterns = [
54
+ // Original + variants
55
+ '(api[_-]?key|api[_-]?k[e3]y|password|passw[0o]rd|s[e3]cr[e3]t|auth[_-]?token)',
56
+ // High-value secret types
57
+ '(bearer|private[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)',
58
+ // Known formats: AWS
59
+ 'AKIA[0-9A-Z]{16}',
60
+ // GitHub PAT
61
+ 'ghp_[a-zA-Z0-9]{36}',
62
+ // JWT pattern
63
+ 'eyJ[a-zA-Z0-9-_=]+\\.[a-zA-Z0-9-_=]+\\.'
64
+ ];
65
+ const secretPattern = secretPatterns.join('|');
66
+ const result = execSync(
67
+ `git grep -i -E "${secretPattern}" HEAD 2>/dev/null | grep -v "example\\|placeholder\\|your-key\\|process\\.env\\|env\\.\\|config\\." | wc -l`,
68
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
69
+ ).trim();
70
+ const count = parseInt(result) || 0;
71
+ gates.push({
72
+ name: 'no_secrets',
73
+ label: 'No secrets in committed files',
74
+ passed: count === 0,
75
+ blocking: true,
76
+ detail: count === 0 ? 'Clean' : `${count} potential secret(s) found`
77
+ });
78
+ } catch {
79
+ gates.push({ name: 'no_secrets', label: 'No secrets in committed files', passed: true, blocking: true, detail: 'Check skipped (git not available)' });
80
+ }
81
+
82
+ // Gate 2: npm audit
83
+ try {
84
+ execSync('npm audit --audit-level=critical 2>/dev/null', { cwd, stdio: 'pipe' });
85
+ gates.push({ name: 'npm_audit', label: 'npm audit — no critical vulnerabilities', passed: true, blocking: true, detail: 'Clean' });
86
+ } catch (err) {
87
+ const output = err.stdout ? err.stdout.toString() : '';
88
+ const criticals = (output.match(/critical/gi) || []).length;
89
+ gates.push({
90
+ name: 'npm_audit',
91
+ label: 'npm audit — no critical vulnerabilities',
92
+ passed: false,
93
+ blocking: true,
94
+ detail: `${criticals} critical vulnerability issue(s). Run: npm audit fix`
95
+ });
96
+ }
97
+
98
+ // Gate 3: No production TODOs
99
+ try {
100
+ const srcDirs = ['src', 'lib', 'app', 'server'].filter(d => fs.existsSync(path.join(cwd, d)));
101
+ if (srcDirs.length > 0) {
102
+ const searchDirs = srcDirs.join(' ');
103
+ const result = execSync(
104
+ `grep -rn "TODO\\|FIXME\\|HACK\\|XXX" ${searchDirs} --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | grep -v "test\\|spec\\|\\.test\\." | wc -l`,
105
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
106
+ ).trim();
107
+ const count = parseInt(result) || 0;
108
+ gates.push({
109
+ name: 'no_prod_todos',
110
+ label: 'No production TODO/FIXME in src/',
111
+ passed: count === 0,
112
+ blocking: false, // advisory
113
+ detail: count === 0 ? 'Clean' : `${count} TODO/FIXME found in production code`
114
+ });
115
+ } else {
116
+ gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'No src/ directory found — skipped' });
117
+ }
118
+ } catch {
119
+ gates.push({ name: 'no_prod_todos', label: 'No production TODO/FIXME in src/', passed: true, blocking: false, detail: 'Check skipped' });
120
+ }
121
+
122
+ // Gate 4: .env in .gitignore
123
+ try {
124
+ const gitignore = fs.readFileSync(path.join(cwd, '.gitignore'), 'utf8');
125
+ const protected_ = gitignore.match(/^\.env/m) !== null;
126
+ gates.push({
127
+ name: 'env_protected',
128
+ label: '.env files in .gitignore',
129
+ passed: protected_,
130
+ blocking: true,
131
+ detail: protected_ ? 'Protected' : '.env not found in .gitignore — add it before releasing'
132
+ });
133
+ } catch {
134
+ gates.push({ name: 'env_protected', label: '.env files in .gitignore', passed: false, blocking: true, detail: '.gitignore not found or .env not listed' });
135
+ }
136
+
137
+ // Gate 5: MoSCoW coverage advisory (non-blocking)
138
+ try {
139
+ const featuresDir = path.join(cwd, 'features');
140
+ const testDir = path.join(cwd, 'test');
141
+ const specDir = path.join(cwd, 'spec');
142
+ const bddDir = [featuresDir, testDir, specDir].find(d => fs.existsSync(d));
143
+ if (bddDir) {
144
+ const featureFiles = fs.readdirSync(bddDir).filter(f => f.endsWith('.feature'));
145
+ if (featureFiles.length > 0) {
146
+ let totalScenarios = 0;
147
+ let taggedScenarios = 0;
148
+ for (const file of featureFiles) {
149
+ const content = fs.readFileSync(path.join(bddDir, file), 'utf8');
150
+ const scenarioMatches = content.match(/^\s*Scenario/gm) || [];
151
+ const mustMatches = content.match(/@must|@should|@could|@wont/g) || [];
152
+ totalScenarios += scenarioMatches.length;
153
+ taggedScenarios += mustMatches.length;
154
+ }
155
+ const untaggedPct = totalScenarios > 0 ? ((totalScenarios - taggedScenarios) / totalScenarios) * 100 : 0;
156
+ gates.push({
157
+ name: 'moscow_coverage',
158
+ label: 'MoSCoW tag coverage in BDD scenarios',
159
+ passed: untaggedPct <= 20,
160
+ blocking: false, // advisory
161
+ detail: untaggedPct <= 20
162
+ ? `${Math.round(100 - untaggedPct)}% scenarios tagged`
163
+ : `${Math.round(untaggedPct)}% scenarios missing @must/@should tags`
164
+ });
165
+ } else {
166
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No .feature files found — skipped' });
167
+ }
168
+ } else {
169
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'No BDD directory found — skipped' });
170
+ }
171
+ } catch {
172
+ gates.push({ name: 'moscow_coverage', label: 'MoSCoW tag coverage in BDD scenarios', passed: true, blocking: false, detail: 'Check skipped' });
173
+ }
174
+
175
+ const passed = gates.filter(g => g.passed && g.blocking).length;
176
+ const total = gates.filter(g => g.blocking).length;
177
+ const allPassed = gates.filter(g => g.blocking).every(g => g.passed);
178
+
179
+ return { passed: allPassed, gates, score: `${passed}/${total}` };
180
+ }
181
+
182
+ // ─────────────────────────────────────────────
183
+ // Tier Checklist
184
+ // ─────────────────────────────────────────────
185
+
186
+ const MVP_CHECKLIST = [
187
+ { id: 'bdd_must', label: 'All @must BDD scenarios passing', auto: true },
188
+ { id: 'npm_audit', label: 'npm audit — no critical vulnerabilities', auto: true },
189
+ { id: 'health_endpoint', label: 'Health endpoint returns 200', auto: true },
190
+ { id: 'no_secrets', label: 'No secrets in committed files', auto: true },
191
+ { id: 'app_starts', label: 'Application starts without errors', auto: true },
192
+ { id: 'rollback_documented', label: 'Rollback procedure documented', auto: true }
193
+ ];
194
+
195
+ const MEDIUM_EXTRA = [
196
+ { id: 'bdd_should', label: 'All @should BDD scenarios passing', auto: true },
197
+ { id: 'coverage_80', label: 'Test coverage ≥ 80%', auto: true },
198
+ { id: 'staging_parity', label: 'Staging environment parity verified', auto: false },
199
+ { id: 'monitoring', label: 'Monitoring/alerts configured', auto: false },
200
+ { id: 'structured_logging', label: 'Structured logging (no console.log in prod)', auto: true },
201
+ { id: 'perf_baseline', label: 'Performance baseline documented', auto: false },
202
+ { id: 'error_tracking', label: 'Error tracking configured', auto: false },
203
+ { id: 'db_migrations', label: 'Database migrations tested on staging', auto: false },
204
+ { id: 'api_docs', label: 'API documentation current', auto: false },
205
+ { id: 'env_example', label: '.env.example up to date', auto: true },
206
+ { id: 'graceful_shutdown', label: 'Graceful shutdown handled', auto: true },
207
+ { id: 'rate_limiting', label: 'Rate limiting on public API endpoints', auto: true }
208
+ ];
209
+
210
+ const ENTERPRISE_EXTRA = [
211
+ { id: 'bdd_could', label: 'All @could BDD scenarios passing', auto: true },
212
+ { id: 'coverage_95', label: 'Test coverage ≥ 95%', auto: true },
213
+ { id: 'security_audit', label: 'Security audit completed', auto: false },
214
+ { id: 'compliance_docs', label: 'Compliance documentation updated', auto: false },
215
+ { id: 'load_test', label: 'Load test results documented', auto: false },
216
+ { id: 'dr_tested', label: 'Disaster recovery tested', auto: false },
217
+ { id: 'data_retention', label: 'Data retention policy configured', auto: false },
218
+ { id: 'audit_logging', label: 'Audit logging enabled', auto: true },
219
+ { id: 'pentest', label: 'Penetration test completed or scheduled', auto: false },
220
+ { id: 'soc2_gdpr', label: 'SOC2/GDPR controls validated', auto: false },
221
+ { id: 'change_ticket', label: 'Change management ticket filed', auto: false },
222
+ { id: 'incident_runbook', label: 'Incident runbook up to date', auto: false }
223
+ ];
224
+
225
+ // ─────────────────────────────────────────────
226
+ // Rollback Validation
227
+ // ─────────────────────────────────────────────
228
+
229
+ /**
230
+ * Validate rollback plan content — checks for unfilled placeholders
231
+ * @param {string} cwd
232
+ * @returns {{ status: string, detail: string }}
233
+ */
234
+ function validateRollbackContent(cwd) {
235
+ const releasesDir = path.join(cwd, '.planning', 'releases');
236
+ if (!fs.existsSync(releasesDir)) {
237
+ return { status: 'fail', detail: 'No .planning/releases/ directory found' };
238
+ }
239
+
240
+ const rollbackFiles = fs.readdirSync(releasesDir)
241
+ .filter(f => f.includes('ROLLBACK') && f.endsWith('.md'));
242
+
243
+ if (rollbackFiles.length === 0) {
244
+ return { status: 'fail', detail: 'No rollback plan found' };
245
+ }
246
+
247
+ const latest = rollbackFiles.sort().pop();
248
+ const content = fs.readFileSync(path.join(releasesDir, latest), 'utf8');
249
+
250
+ // Detect unfilled placeholders like {name}, {migration_name}, {your-domain}
251
+ const placeholders = content.match(/\{[a-z_-]+\}/gi) || [];
252
+ if (placeholders.length > 0) {
253
+ return {
254
+ status: 'fail',
255
+ detail: `Rollback plan has ${placeholders.length} unfilled placeholder(s): ${placeholders.slice(0, 3).join(', ')}`
256
+ };
257
+ }
258
+
259
+ return { status: 'pass', detail: `Rollback plan validated: ${latest}` };
260
+ }
261
+
262
+ // ─────────────────────────────────────────────
263
+ // Manual Checklist State Persistence (Fix 11)
264
+ // ─────────────────────────────────────────────
265
+
266
+ /**
267
+ * Load persisted manual checklist state
268
+ * @param {string} cwd
269
+ * @returns {object}
270
+ */
271
+ function loadChecklistState(cwd) {
272
+ const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
273
+ if (!fs.existsSync(statePath)) return {};
274
+ try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); }
275
+ catch { return {}; }
276
+ }
277
+
278
+ /**
279
+ * Mark a manual checklist item as complete with approver and timestamp
280
+ * @param {string} itemId
281
+ * @param {string} approver
282
+ * @param {string} cwd
283
+ */
284
+ function markManualItemComplete(itemId, approver, cwd = process.cwd()) {
285
+ const state = loadChecklistState(cwd);
286
+ state[itemId] = {
287
+ approved: true,
288
+ approver: approver || 'unknown',
289
+ timestamp: new Date().toISOString(),
290
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days
291
+ };
292
+ const statePath = path.join(cwd, '.planning', 'releases', 'checklist-state.json');
293
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
294
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
295
+ }
296
+
297
+ /**
298
+ * Get full checklist for a tier
299
+ * @param {string} tier
300
+ * @returns {object[]}
301
+ */
302
+ function getChecklist(tier) {
303
+ const t = tier.toLowerCase();
304
+ if (t === 'mvp') return [...MVP_CHECKLIST];
305
+ if (t === 'medium') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA];
306
+ if (t === 'enterprise') return [...MVP_CHECKLIST, ...MEDIUM_EXTRA, ...ENTERPRISE_EXTRA];
307
+ throw new Error(`Unknown tier: ${tier}`);
308
+ }
309
+
310
+ /**
311
+ * Run automated checklist items
312
+ * @param {string} tier
313
+ * @param {string} cwd
314
+ * @param {object} context - additional context (coverage, bddResults, etc.)
315
+ * @returns {{ items: object[], passed: number, total: number, score: number }}
316
+ */
317
+ function runChecklist(tier, cwd = process.cwd(), context = {}) {
318
+ const items = getChecklist(tier);
319
+ const results = [];
320
+
321
+ for (const item of items) {
322
+ let result;
323
+ if (!item.auto) {
324
+ // Fix 11: Check persisted state for manual items
325
+ const state = loadChecklistState(cwd);
326
+ const saved = state[item.id];
327
+ if (saved && saved.approved) {
328
+ const age = Date.now() - new Date(saved.timestamp).getTime();
329
+ const ageDays = Math.floor(age / (1000 * 60 * 60 * 24));
330
+ if (ageDays > 30) {
331
+ result = { ...item, status: 'fail', detail: `Manual check expired ${ageDays}d ago — re-verify required` };
332
+ } else {
333
+ result = { ...item, status: 'pass', detail: `Verified by ${saved.approver} on ${saved.timestamp.split('T')[0]}` };
334
+ }
335
+ } else {
336
+ result = { ...item, status: 'manual', detail: 'Requires manual verification (run: ez checklist mark <id> <approver>)' };
337
+ }
338
+ } else {
339
+ result = runChecklistItem(item, cwd, context);
340
+ }
341
+ results.push(result);
342
+ }
343
+
344
+ const autoItems = results.filter(r => r.auto);
345
+ const passed = autoItems.filter(r => r.status === 'pass').length;
346
+ const total = autoItems.length;
347
+
348
+ // Compute readiness score: blocking failures cost 10, advisory failures cost 2
349
+ let score = 100;
350
+ for (const r of results) {
351
+ if (r.status === 'fail') {
352
+ score -= r.blocking !== false ? 10 : 2;
353
+ }
354
+ }
355
+ score = Math.max(0, score);
356
+
357
+ return { items: results, passed, total, score };
358
+ }
359
+
360
+ function runChecklistItem(item, cwd, context) {
361
+ try {
362
+ switch (item.id) {
363
+ case 'npm_audit':
364
+ case 'no_secrets':
365
+ // Already handled in security gates — check from context
366
+ return { ...item, status: 'pass', detail: 'Verified in security gates' };
367
+
368
+ case 'coverage_80': {
369
+ const cov = context.coverage;
370
+ if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
371
+ return { ...item, status: cov >= 80 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
372
+ }
373
+
374
+ case 'coverage_95': {
375
+ const cov = context.coverage;
376
+ if (cov === undefined) return { ...item, status: 'skip', detail: 'No coverage data available' };
377
+ return { ...item, status: cov >= 95 ? 'pass' : 'fail', detail: `Coverage: ${cov}%` };
378
+ }
379
+
380
+ case 'bdd_must': {
381
+ const { bddPassed, moscowTagged, totalScenarios } = context;
382
+ // Hard gate: fail if there are too many untagged scenarios
383
+ if (moscowTagged !== undefined && totalScenarios > 0) {
384
+ const untaggedPct = ((totalScenarios - moscowTagged) / totalScenarios) * 100;
385
+ if (untaggedPct > 20) { // > 20% untagged = blocking
386
+ return { ...item, status: 'fail', detail: `${Math.round(untaggedPct)}% scenarios missing @must/@should tags — BDD coverage unverifiable` };
387
+ }
388
+ }
389
+ if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD results — run test suite first' };
390
+ return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
391
+ }
392
+
393
+ case 'bdd_should':
394
+ case 'bdd_could': {
395
+ const bddPassed = context.bddPassed;
396
+ if (bddPassed === undefined) return { ...item, status: 'skip', detail: 'No BDD test results available' };
397
+ return { ...item, status: bddPassed ? 'pass' : 'fail', detail: bddPassed ? 'All scenarios passing' : 'Some scenarios failing' };
398
+ }
399
+
400
+ case 'rollback_documented': {
401
+ const rollbackResult = validateRollbackContent(cwd);
402
+ return { ...item, status: rollbackResult.status, detail: rollbackResult.detail };
403
+ }
404
+
405
+ case 'env_example': {
406
+ const hasExample = fs.existsSync(path.join(cwd, '.env.example'));
407
+ return { ...item, status: hasExample ? 'pass' : 'fail', detail: hasExample ? '.env.example found' : '.env.example missing' };
408
+ }
409
+
410
+ case 'structured_logging': {
411
+ try {
412
+ const srcDirs = ['src', 'lib', 'app'].filter(d => fs.existsSync(path.join(cwd, d)));
413
+ if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
414
+ const result = execSync(
415
+ `grep -rn "console\\.log" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | grep -v "test\\|spec" | wc -l`,
416
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
417
+ ).trim();
418
+ const count = parseInt(result) || 0;
419
+ return { ...item, status: count === 0 ? 'pass' : 'fail', detail: count === 0 ? 'No console.log in prod' : `${count} console.log found` };
420
+ } catch {
421
+ return { ...item, status: 'skip', detail: 'Check failed' };
422
+ }
423
+ }
424
+
425
+ case 'health_endpoint': {
426
+ // Try to detect health endpoint in source
427
+ try {
428
+ const srcDirs = ['src', 'app', 'server', 'pages/api'].filter(d => fs.existsSync(path.join(cwd, d)));
429
+ if (srcDirs.length === 0) return { ...item, status: 'skip', detail: 'No src/ found' };
430
+ const result = execSync(
431
+ `grep -rn "health\\|/ping\\|/status" ${srcDirs.join(' ')} --include="*.ts" --include="*.js" 2>/dev/null | wc -l`,
432
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
433
+ ).trim();
434
+ const found = parseInt(result) > 0;
435
+ return { ...item, status: found ? 'pass' : 'skip', detail: found ? 'Health endpoint found in source' : 'No health endpoint found (optional for MVP)' };
436
+ } catch {
437
+ return { ...item, status: 'skip', detail: 'Check failed' };
438
+ }
439
+ }
440
+
441
+ default:
442
+ return { ...item, status: 'skip', detail: 'Automated check not implemented' };
443
+ }
444
+ } catch (err) {
445
+ return { ...item, status: 'error', detail: err.message };
446
+ }
447
+ }
448
+
449
+ // ─────────────────────────────────────────────
450
+ // Full Validation
451
+ // ─────────────────────────────────────────────
452
+
453
+ /**
454
+ * Run full release validation
455
+ * @param {string} tier
456
+ * @param {string} version
457
+ * @param {string} cwd
458
+ * @param {object} context
459
+ * @returns {{ valid: boolean, blockers: string[], warnings: string[], score: number, securityGates: object, checklist: object }}
460
+ */
461
+ function validateRelease(tier, version, cwd = process.cwd(), context = {}) {
462
+ const securityGates = runSecurityGates(cwd);
463
+ const checklist = runChecklist(tier, cwd, context);
464
+
465
+ const blockers = [];
466
+ const warnings = [];
467
+
468
+ // Security gate failures
469
+ for (const gate of securityGates.gates) {
470
+ if (!gate.passed && gate.blocking) blockers.push(`Security: ${gate.label} — ${gate.detail}`);
471
+ if (!gate.passed && !gate.blocking) warnings.push(`Security: ${gate.label} — ${gate.detail}`);
472
+ }
473
+
474
+ // Checklist failures
475
+ for (const item of checklist.items) {
476
+ if (item.status === 'fail') {
477
+ if (item.blocking !== false) warnings.push(`Checklist: ${item.label}`);
478
+ }
479
+ }
480
+
481
+ const valid = blockers.length === 0;
482
+ const readinessScore = Math.min(checklist.score, securityGates.passed ? 100 : 50);
483
+
484
+ const readinessStatus = readinessScore >= 90 ? 'READY'
485
+ : readinessScore >= 70 ? 'CONDITIONAL'
486
+ : 'NOT READY';
487
+
488
+ return {
489
+ valid,
490
+ tier,
491
+ version,
492
+ blockers,
493
+ warnings,
494
+ score: readinessScore,
495
+ readinessStatus,
496
+ securityGates,
497
+ checklist
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Format validation result as markdown
503
+ */
504
+ function formatValidation(result) {
505
+ const lines = [];
506
+ const icon = result.valid ? '✓' : '✗';
507
+
508
+ lines.push(`## Release Validation: v${result.version} (${result.tier})`);
509
+ lines.push(`**Status:** ${icon} ${result.valid ? 'READY' : 'BLOCKED'}`);
510
+ lines.push(`**Production Readiness Score:** ${result.score}/100 — ${result.readinessStatus}`);
511
+ lines.push('');
512
+
513
+ lines.push('### Security Gates');
514
+ for (const g of result.securityGates.gates) {
515
+ lines.push(`- ${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
516
+ }
517
+ lines.push('');
518
+
519
+ lines.push(`### Checklist (${result.tier})`);
520
+ for (const item of result.checklist.items) {
521
+ const icon2 = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : item.status === 'manual' ? '?' : '✗';
522
+ lines.push(`- ${icon2} ${item.label}${item.detail ? ` — ${item.detail}` : ''}`);
523
+ }
524
+ lines.push('');
525
+
526
+ if (result.blockers.length > 0) {
527
+ lines.push('### Blockers (must fix)');
528
+ result.blockers.forEach(b => lines.push(`- 🛑 ${b}`));
529
+ lines.push('');
530
+ }
531
+
532
+ if (result.warnings.length > 0) {
533
+ lines.push('### Warnings (advisory)');
534
+ result.warnings.forEach(w => lines.push(`- ⚠️ ${w}`));
535
+ }
536
+
537
+ return lines.join('\n');
538
+ }
539
+
540
+ // ─────────────────────────────────────────────
541
+ // CLI Interface
542
+ // ─────────────────────────────────────────────
543
+
544
+ if (require.main === module) {
545
+ const args = process.argv.slice(2);
546
+ const cmd = args[0];
547
+
548
+ try {
549
+ if (cmd === 'security-gates') {
550
+ const result = runSecurityGates(process.cwd());
551
+ if (args.includes('--json')) {
552
+ console.log(JSON.stringify(result, null, 2));
553
+ } else {
554
+ for (const g of result.gates) {
555
+ console.log(`${g.passed ? '✓' : '✗'} ${g.label}: ${g.detail}`);
556
+ }
557
+ process.exit(result.passed ? 0 : 1);
558
+ }
559
+ } else if (cmd === 'checklist') {
560
+ const tier = args[1];
561
+ if (!tier) { console.error('Usage: release-validator.cjs checklist <tier>'); process.exit(1); }
562
+ const result = runChecklist(tier, process.cwd());
563
+ if (args.includes('--json')) {
564
+ console.log(JSON.stringify(result, null, 2));
565
+ } else {
566
+ for (const item of result.items) {
567
+ const icon = item.status === 'pass' ? '✓' : item.status === 'skip' ? '○' : '✗';
568
+ console.log(`${icon} ${item.label}`);
569
+ }
570
+ console.log(`\nScore: ${result.score}/100`);
571
+ }
572
+ } else if (cmd === 'validate') {
573
+ const tier = args[1];
574
+ const version = args[2] || '0.0.0';
575
+ if (!tier) { console.error('Usage: release-validator.cjs validate <tier> [version]'); process.exit(1); }
576
+ const result = validateRelease(tier, version, process.cwd());
577
+ if (args.includes('--json')) {
578
+ console.log(JSON.stringify(result, null, 2));
579
+ } else {
580
+ console.log(formatValidation(result));
581
+ process.exit(result.valid ? 0 : 1);
582
+ }
583
+ } else if (cmd === 'checklist-mark') {
584
+ // Fix 11: ez checklist mark <id> <approver>
585
+ const itemId = args[1];
586
+ const approver = args[2];
587
+ if (!itemId || !approver) {
588
+ console.error('Usage: release-validator.cjs checklist-mark <item-id> <approver>');
589
+ process.exit(1);
590
+ }
591
+ markManualItemComplete(itemId, approver, process.cwd());
592
+ console.log(JSON.stringify({ marked: true, item: itemId, approver, timestamp: new Date().toISOString() }));
593
+ } else {
594
+ console.error(`Unknown command: ${cmd}`);
595
+ console.error('Commands: security-gates, checklist, validate, checklist-mark');
596
+ process.exit(1);
597
+ }
598
+ } catch (err) {
599
+ console.error(`Error: ${err.message}`);
600
+ process.exit(1);
601
+ }
602
+ }
603
+
604
+ module.exports = {
605
+ runSecurityGates,
606
+ getChecklist,
607
+ runChecklist,
608
+ validateRelease,
609
+ formatValidation,
610
+ validateRollbackContent,
611
+ loadChecklistState,
612
+ markManualItemComplete,
613
+ hasHighEntropy
614
+ };