@boyingliu01/xp-gate 0.15.4 → 0.18.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 (117) hide show
  1. package/bin/xp-gate.js +29 -1
  2. package/hooks/lib/now-ms.sh +33 -0
  3. package/hooks/pre-commit +63 -47
  4. package/hooks/sprint-gate.sh +291 -0
  5. package/lib/__tests__/check-version.test.js +122 -0
  6. package/lib/__tests__/doctor.test.js +13 -15
  7. package/lib/__tests__/gate-audit.test.ts +38 -0
  8. package/lib/__tests__/phase-transition.test.js +241 -1
  9. package/lib/__tests__/retro.test.js +131 -0
  10. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  11. package/lib/check-version.js +9 -0
  12. package/lib/doctor-tui.js +79 -9
  13. package/lib/doctor.js +15 -3
  14. package/lib/gate-audit.ts +18 -7
  15. package/lib/gate-runner.js +77 -30
  16. package/lib/init.js +10 -0
  17. package/lib/mattpocock-license-verdict.json +7 -0
  18. package/lib/phase-transition.js +251 -1
  19. package/lib/retro.js +222 -0
  20. package/lib/shared-utils.js +7 -0
  21. package/lib/sprint-init.js +207 -0
  22. package/lib/sprint-state-migrator.js +1 -0
  23. package/lib/sprint-status.js +111 -0
  24. package/mock-policy/AGENTS.md +3 -3
  25. package/mutation/AGENTS.md +3 -3
  26. package/package.json +3 -2
  27. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  28. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  29. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  30. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  31. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  32. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  33. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  34. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  35. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  36. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  37. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  38. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  39. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  40. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  41. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  42. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  43. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  44. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  45. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  46. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  47. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  48. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  49. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  50. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  51. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  52. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  53. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  54. package/plugins/opencode/index.ts +1 -0
  55. package/plugins/opencode/package.json +1 -1
  56. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  57. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  58. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  59. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  60. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  61. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  62. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  63. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  64. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  65. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  66. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  67. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  68. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  69. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  70. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  71. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  72. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  73. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  74. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  75. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  76. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  77. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  78. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  79. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  80. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  81. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  82. package/plugins/qoder/plugin.json +1 -1
  83. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/principles/AGENTS.md +3 -3
  87. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  88. package/scripts/__tests__/sync-package-content.test.js +50 -0
  89. package/scripts/check-mattpocock-license.cjs +302 -0
  90. package/scripts/migrate-audit-anomalies.mjs +78 -0
  91. package/scripts/sync-package-content.js +52 -5
  92. package/skills/batch-grill-me/SKILL.md +15 -0
  93. package/skills/delphi-review/AGENTS.md +3 -3
  94. package/skills/delphi-review/SKILL.md +59 -4
  95. package/skills/delphi-review/references/requirements.md +390 -0
  96. package/skills/delphi-review/references/round-templates.md +67 -0
  97. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  98. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  99. package/skills/domain-modeling/SKILL.md +74 -0
  100. package/skills/grill-with-docs/SKILL.md +7 -0
  101. package/skills/grilling/SKILL.md +12 -0
  102. package/skills/sprint-flow/AGENTS.md +3 -3
  103. package/skills/sprint-flow/SKILL.md +168 -43
  104. package/skills/sprint-flow/references/components/memory.md +1 -1
  105. package/skills/sprint-flow/references/components/middleware.md +1 -1
  106. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  107. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  108. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  109. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  110. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  111. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  112. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  113. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  114. package/skills/sprint-flow/references/phase-overview.md +58 -53
  115. package/skills/test-specification-alignment/AGENTS.md +3 -3
  116. package/skills/test-specification-alignment/SKILL.md +8 -3
  117. package/skills/to-issues/SKILL.md +1 -1
@@ -24,6 +24,8 @@
24
24
 
25
25
  const path = require('path');
26
26
  const fs = require('fs');
27
+ const crypto = require('crypto');
28
+ const { execSync } = require('child_process');
27
29
  const { SprintStateManager } = require('./sprint-state-manager');
28
30
 
29
31
  /**
@@ -35,6 +37,214 @@ const PHASE_NAMES = {
35
37
  4: 'VERIFY', 5: 'SHIP', 6: 'CLOSE',
36
38
  };
37
39
 
40
+ /**
41
+ * Phase-specific evidence file requirements.
42
+ * Each entry defines the evidence file path, required fields, and a blocking check.
43
+ * Phases NOT in this map require no evidence validation in v0.17.1.
44
+ */
45
+ const EVIDENCE_FILES = {
46
+ 2: {
47
+ path: '.sprint-state/phase-outputs/requirements-reviewed.json',
48
+ requiredFields: ['verdict', 'requirements_hash'],
49
+ blockingCheck: (data) => data.verdict === 'APPROVED',
50
+ blockingMessage: 'Requirements review verdict is not APPROVED',
51
+ },
52
+ 4: {
53
+ path: '.sprint-state/phase-outputs/test-alignment-report.json',
54
+ requiredFields: ['alignment_status', 'head_commit', 'spec_hash'],
55
+ blockingCheck: (data) => data.alignment_status === 'PASS',
56
+ blockingMessage: 'Test alignment status is not PASS',
57
+ },
58
+ };
59
+
60
+ /**
61
+ * Get current HEAD commit hash from a project directory.
62
+ * Returns 'unknown' if not in a git repo.
63
+ * @param {string} projectDir
64
+ * @returns {string}
65
+ */
66
+ function getCurrentHeadCommit(projectDir) {
67
+ try {
68
+ return execSync('git rev-parse HEAD', {
69
+ cwd: projectDir,
70
+ encoding: 'utf8',
71
+ stdio: ['pipe', 'pipe', 'pipe'],
72
+ }).trim();
73
+ } catch {
74
+ return 'unknown';
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Compute SHA-256 hash of a file's contents.
80
+ * @param {string} filePath
81
+ * @returns {string} hex digest
82
+ */
83
+ function computeFileHash(filePath) {
84
+ const content = fs.readFileSync(filePath, 'utf8');
85
+ return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
86
+ }
87
+
88
+ /**
89
+ * Validate phase evidence files for a given phase transition.
90
+ *
91
+ * Returns { ok: boolean, errors: string[], warnings: string[] }.
92
+ * - For phases not in EVIDENCE_FILES: ok=true, no errors.
93
+ * - For phases with evidence requirements:
94
+ * - If evidence_schema_version >= 2 (new sprint): missing/invalid evidence → ok=false (BLOCK)
95
+ * - If evidence_schema_version missing or < 2 (legacy sprint): missing/invalid → ok=true with WARNING
96
+ *
97
+ * @param {number} phase - Phase number (1-6)
98
+ * @param {string} projectDir - Project root directory
99
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
100
+ */
101
+ function validateEvidence(phase, projectDir) {
102
+ const evidenceConfig = EVIDENCE_FILES[phase];
103
+ if (!evidenceConfig) {
104
+ return { ok: true, errors: [], warnings: [] };
105
+ }
106
+
107
+ // Read sprint state to determine evidence_schema_version
108
+ let evidenceSchemaVersion = 0;
109
+ try {
110
+ const manager = new SprintStateManager(projectDir);
111
+ const state = manager.read();
112
+ if (state && typeof state.evidence_schema_version === 'number') {
113
+ evidenceSchemaVersion = state.evidence_schema_version;
114
+ }
115
+ } catch {
116
+ // If state can't be read, treat as legacy (version 0)
117
+ }
118
+
119
+ const isLegacySprint = evidenceSchemaVersion < 2;
120
+ const errors = [];
121
+ const warnings = [];
122
+
123
+ const evidenceFilePath = path.join(projectDir, evidenceConfig.path);
124
+ const fileName = path.basename(evidenceConfig.path);
125
+
126
+ // Check file existence
127
+ if (!fs.existsSync(evidenceFilePath)) {
128
+ const msg = `Evidence file missing: ${evidenceConfig.path}`;
129
+ if (isLegacySprint) {
130
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
131
+ return { ok: true, errors: [], warnings };
132
+ }
133
+ errors.push(msg);
134
+ return { ok: false, errors, warnings };
135
+ }
136
+
137
+ // Parse JSON
138
+ let data;
139
+ try {
140
+ const raw = fs.readFileSync(evidenceFilePath, 'utf8');
141
+ data = JSON.parse(raw);
142
+ } catch {
143
+ const msg = `${fileName} is malformed JSON`;
144
+ if (isLegacySprint) {
145
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
146
+ return { ok: true, errors: [], warnings };
147
+ }
148
+ errors.push(msg);
149
+ return { ok: false, errors, warnings };
150
+ }
151
+
152
+ // Check required fields
153
+ for (const field of evidenceConfig.requiredFields) {
154
+ if (!(field in data)) {
155
+ const msg = `${fileName} is missing required field: ${field}`;
156
+ if (isLegacySprint) {
157
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
158
+ } else {
159
+ errors.push(msg);
160
+ }
161
+ }
162
+ }
163
+
164
+ // If any required fields missing for new sprint, block early
165
+ if (errors.length > 0) {
166
+ return { ok: false, errors, warnings };
167
+ }
168
+
169
+ // Run blocking check
170
+ if (!evidenceConfig.blockingCheck(data)) {
171
+ const msg = `${fileName}: ${evidenceConfig.blockingMessage} (got: ${JSON.stringify(data[evidenceConfig.requiredFields[0]])})`;
172
+ if (isLegacySprint) {
173
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
174
+ return { ok: true, errors: [], warnings };
175
+ }
176
+ errors.push(msg);
177
+ return { ok: false, errors, warnings };
178
+ }
179
+
180
+ // Phase-specific anti-staleness checks
181
+ if (phase === 4) {
182
+ // head_commit check
183
+ const currentHead = getCurrentHeadCommit(projectDir);
184
+ if (data.head_commit !== currentHead) {
185
+ const msg = `${fileName}: head_commit mismatch — report has "${data.head_commit}", current HEAD is "${currentHead}"`;
186
+ if (isLegacySprint) {
187
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
188
+ } else {
189
+ errors.push(msg);
190
+ }
191
+ }
192
+
193
+ // spec_hash check (only if specification.yaml exists)
194
+ const specPath = path.join(projectDir, 'specification.yaml');
195
+ if (fs.existsSync(specPath)) {
196
+ const expectedHash = computeFileHash(specPath);
197
+ if (data.spec_hash !== expectedHash) {
198
+ const msg = `${fileName}: spec_hash mismatch — specification.yaml has changed since report was generated`;
199
+ if (isLegacySprint) {
200
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
201
+ } else {
202
+ errors.push(msg);
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ // Phase 2: requirements_hash existence check (full verification in v0.18.0)
209
+ if (phase === 2) {
210
+ if (typeof data.requirements_hash !== 'string' || data.requirements_hash.length === 0) {
211
+ const msg = `${fileName}: requirements_hash must be a non-empty string`;
212
+ if (isLegacySprint) {
213
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
214
+ } else {
215
+ errors.push(msg);
216
+ }
217
+ }
218
+ }
219
+
220
+ if (errors.length > 0) {
221
+ return { ok: false, errors, warnings };
222
+ }
223
+
224
+ return { ok: true, errors: [], warnings };
225
+ }
226
+
227
+ /**
228
+ * Append an evidence-skipped audit entry to .xp-gate/audit.jsonl.
229
+ * @param {string} projectDir
230
+ * @param {number} phase
231
+ * @param {string} reason
232
+ */
233
+ function logEvidenceSkip(projectDir, phase, reason) {
234
+ const auditDir = path.join(projectDir, '.xp-gate');
235
+ if (!fs.existsSync(auditDir)) {
236
+ fs.mkdirSync(auditDir, { recursive: true });
237
+ }
238
+ const entry = {
239
+ timestamp: new Date().toISOString(),
240
+ event: 'evidence_skipped',
241
+ phase,
242
+ reason,
243
+ commit_hash: getCurrentHeadCommit(projectDir),
244
+ };
245
+ fs.appendFileSync(path.join(auditDir, 'audit.jsonl'), JSON.stringify(entry) + '\n', 'utf8');
246
+ }
247
+
38
248
  /**
39
249
  * Render ASCII dashboard from sprint state.
40
250
  * Reuses the formatSprintTable logic from sprint-status.
@@ -152,11 +362,13 @@ async function handlePhaseTransition(args = []) {
152
362
  console.log(' --outputs <json> JSON object of outputs to record');
153
363
  console.log(' --render Render ASCII dashboard after transition');
154
364
  console.log(' --dir <path> Project directory (default: cwd)');
365
+ console.log(' --skip-evidence <reason> Skip evidence validation (requires reason)');
155
366
  console.log('');
156
367
  console.log('Examples:');
157
368
  console.log(' xp-gate phase-transition 1 completed');
158
369
  console.log(' xp-gate phase-transition 3 completed --render');
159
370
  console.log(' xp-gate phase-transition 2 in_progress --outputs \'{"spec":"path.yaml"}\'');
371
+ console.log(' xp-gate phase-transition 4 completed --skip-evidence "Emergency hotfix"');
160
372
  return 0;
161
373
  }
162
374
 
@@ -182,6 +394,19 @@ async function handlePhaseTransition(args = []) {
182
394
  }
183
395
  }
184
396
 
397
+ // Parse --skip-evidence flag
398
+ const skipEvidenceIdx = args.indexOf('--skip-evidence');
399
+ let skipEvidence = false;
400
+ let skipEvidenceReason = '';
401
+ if (skipEvidenceIdx >= 0) {
402
+ skipEvidence = true;
403
+ const nextArg = args[skipEvidenceIdx + 1];
404
+ // Reason is the next arg if it exists and doesn't start with '--'
405
+ if (nextArg && !nextArg.startsWith('--')) {
406
+ skipEvidenceReason = nextArg;
407
+ }
408
+ }
409
+
185
410
  // Validate inputs
186
411
  if (phase < 1 || phase > 6) {
187
412
  console.error(`Error: Phase must be 1-6, got ${phase}`);
@@ -221,6 +446,31 @@ async function handlePhaseTransition(args = []) {
221
446
  // If no sprint-state.json exists yet, skip check (no previous phase to validate)
222
447
  }
223
448
 
449
+ // ── Layer 2: Evidence validation (blocks on status === 'completed') ──
450
+ if (status === 'completed' && EVIDENCE_FILES[phase]) {
451
+ if (skipEvidence) {
452
+ if (!skipEvidenceReason) {
453
+ console.error('Error: --skip-evidence requires --reason "<text>" to be provided');
454
+ return 1;
455
+ }
456
+ logEvidenceSkip(projectDir, phase, skipEvidenceReason);
457
+ console.warn('⚠️ Evidence skipped. This will appear in retro reports. Single-sprint limit: >2 skips per sprint will trigger an alert.');
458
+ } else {
459
+ const evidence = validateEvidence(phase, projectDir);
460
+ for (const w of evidence.warnings) {
461
+ console.warn(`⚠️ [sprint-audit] ${w}`);
462
+ }
463
+ if (!evidence.ok) {
464
+ for (const e of evidence.errors) {
465
+ console.error(`❌ [sprint-audit] ${e}`);
466
+ }
467
+ console.error(` Phase ${phase} (${PHASE_NAMES[phase]}) cannot transition to 'completed' without valid evidence.`);
468
+ console.error(` Use --skip-evidence "<reason>" to bypass (audited).`);
469
+ return 1;
470
+ }
471
+ }
472
+ }
473
+
224
474
  const newState = manager.transitionPhase(phase, status, { outputs });
225
475
 
226
476
  console.log(`✅ Phase ${phase} transitioned to '${status}'`);
@@ -246,4 +496,4 @@ async function handlePhaseTransition(args = []) {
246
496
  }
247
497
  }
248
498
 
249
- module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES };
499
+ module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES, validateEvidence };
package/lib/retro.js ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * xp-gate retro — Sprint engineering retrospective CLI (#369, design §8.1)
3
+ *
4
+ * Reads git log + .xp-gate/audit.jsonl + .quality-history.jsonl + .sprint-history/
5
+ * and generates a Markdown retrospective report with rework rate trend (#369)
6
+ * and --skip-evidence exposure (§8.4) sections.
7
+ *
8
+ * Zero npm dependencies (Node built-ins only).
9
+ * @module retro
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execSync } = require('child_process');
17
+
18
+ /**
19
+ * Get git commit subjects within a date window.
20
+ * @param {string} dir - Repository root
21
+ * @param {number} days - Lookback window in days
22
+ * @returns {Array<{hash: string, date: string, subject: string}>}
23
+ */
24
+ function getGitCommits(dir, days) {
25
+ try {
26
+ const out = execSync(
27
+ `git log --since="${days} days ago" --pretty=format:"%H|%ai|%s"`,
28
+ { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
29
+ );
30
+ return out.split('\n').filter(Boolean).map(l => {
31
+ const [hash, date, ...rest] = l.split('|');
32
+ return { hash, date, subject: rest.join('|') };
33
+ });
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Parse .xp-gate/audit.jsonl entries.
41
+ * @param {string} dir - Repository root
42
+ * @returns {object[]} Parsed audit entries
43
+ */
44
+ function readAuditEntries(dir) {
45
+ const p = path.join(dir, '.xp-gate', 'audit.jsonl');
46
+ if (!fs.existsSync(p)) return [];
47
+ return fs.readFileSync(p, 'utf8').split('\n')
48
+ .filter(l => l.trim())
49
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
50
+ .filter(Boolean);
51
+ }
52
+
53
+ /**
54
+ * Read last 10 entries from .quality-history.jsonl.
55
+ * @param {string} dir - Repository root
56
+ * @returns {object[]} Recent quality entries
57
+ */
58
+ function readQualityHistory(dir) {
59
+ const p = path.join(dir, '.quality-history.jsonl');
60
+ if (!fs.existsSync(p)) return [];
61
+ const entries = fs.readFileSync(p, 'utf8').split('\n')
62
+ .filter(l => l.trim())
63
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
64
+ .filter(Boolean);
65
+ return entries.slice(-10);
66
+ }
67
+
68
+ /**
69
+ * Read sprint history JSON files from .sprint-state/sprint-history/.
70
+ * @param {string} dir - Repository root
71
+ * @param {string|null} sprintFilter - Optional sprint ID filter
72
+ * @returns {object[]} Sprint state objects
73
+ */
74
+ function readSprintHistory(dir, sprintFilter) {
75
+ const histDir = path.join(dir, '.sprint-state', 'sprint-history');
76
+ const results = [];
77
+ if (fs.existsSync(histDir)) {
78
+ for (const f of fs.readdirSync(histDir).filter(f => f.endsWith('.json'))) {
79
+ try {
80
+ const s = JSON.parse(fs.readFileSync(path.join(histDir, f), 'utf8'));
81
+ if (!sprintFilter || s.id === sprintFilter) results.push(s);
82
+ } catch { /* skip */ }
83
+ }
84
+ }
85
+ return results;
86
+ }
87
+
88
+ /**
89
+ * Build the Markdown retrospective report.
90
+ * @param {object} opts - Report data
91
+ * @returns {string} Markdown report
92
+ */
93
+ function buildReport(opts) {
94
+ const { days, commits, audit, quality, sprints } = opts;
95
+ const now = new Date();
96
+ const start = new Date(now.getTime() - days * 86400000);
97
+ const L = [];
98
+
99
+ L.push('# Sprint Engineering Retrospective');
100
+ L.push('');
101
+ L.push(`Period: ${start.toISOString().slice(0, 10)} to ${now.toISOString().slice(0, 10)}`);
102
+ L.push('');
103
+
104
+ // Activity Summary
105
+ L.push('## Activity Summary');
106
+ const gateEntries = audit.filter(e => e.gate_id);
107
+ const passed = gateEntries.filter(e => e.passed).length;
108
+ const normal = gateEntries.filter(e => !e.duration_anomaly);
109
+ const anomalous = gateEntries.length - normal.length;
110
+ const avgMs = normal.length > 0
111
+ ? Math.round(normal.reduce((s, e) => s + (e.duration_ms || 0), 0) / normal.length)
112
+ : 0;
113
+ L.push(`- Total commits in window: ${commits.length}`);
114
+ L.push(`- Sprint transitions recorded: ${sprints.length}`);
115
+ L.push(`- Gate pass rate: ${gateEntries.length > 0 ? ((passed / gateEntries.length) * 100).toFixed(1) : '0.0'}% (${gateEntries.length} gates total, ${gateEntries.length - passed} failed)`);
116
+ if (gateEntries.length > 0) {
117
+ L.push(`- Avg gate duration: ${avgMs}ms (${anomalous} anomal${anomalous === 1 ? 'y' : 'ies'} excluded)`);
118
+ }
119
+ L.push('');
120
+
121
+ // Rework Rate Trend (#369)
122
+ L.push('## Rework Rate Trend (#369)');
123
+ const withRate = sprints.filter(s => s.metrics && s.metrics.rework_rate != null);
124
+ if (withRate.length === 0) {
125
+ L.push('No data');
126
+ } else {
127
+ L.push('| Sprint | Rework Rate | Fix Commits | Total Commits |');
128
+ L.push('|--------|-------------|-------------|---------------|');
129
+ for (const s of withRate) {
130
+ const pct = (s.metrics.rework_rate * 100).toFixed(1);
131
+ const fix = s.metrics.total_sprint_commits != null
132
+ ? Math.round(s.metrics.rework_rate * s.metrics.total_sprint_commits)
133
+ : '?';
134
+ L.push(`| ${s.id || 'unknown'} | ${pct}% | ${fix} | ${s.metrics.total_sprint_commits ?? '?'} |`);
135
+ if (s.metrics.rework_rate > 0.30) {
136
+ L.push(`⚠️ Sprint ${s.id || 'unknown'}: ${pct}% rework — exceeds 30% threshold`);
137
+ }
138
+ }
139
+ }
140
+ L.push('');
141
+
142
+ // Evidence Skip Exposure (§8.4)
143
+ L.push('## Evidence Skip Exposure (§8.4)');
144
+ const skipped = audit.filter(e => e.event === 'evidence_skipped');
145
+ if (skipped.length === 0) {
146
+ L.push('No data');
147
+ } else {
148
+ L.push('| Phase | Reason | Timestamp |');
149
+ L.push('|-------|--------|-----------|');
150
+ for (const e of skipped) {
151
+ L.push(`| ${e.phase || '?'} | ${e.reason || '?'} | ${(e.timestamp || '').slice(0, 19)} |`);
152
+ }
153
+ const bySprint = {};
154
+ for (const e of skipped) {
155
+ const k = e.sprint_id || 'unknown';
156
+ bySprint[k] = (bySprint[k] || 0) + 1;
157
+ }
158
+ for (const [sid, count] of Object.entries(bySprint)) {
159
+ if (count > 2) {
160
+ L.push(`⚠️ Sprint ${sid} used skip ${count} times`);
161
+ }
162
+ }
163
+ }
164
+ L.push('');
165
+
166
+ // Quality Trend
167
+ L.push('## Quality Trend');
168
+ if (quality.length === 0) {
169
+ L.push('No data');
170
+ } else {
171
+ for (const q of quality) {
172
+ L.push(`- ${(q.timestamp || '').slice(0, 10)}: score ${q.score ?? '?'}`);
173
+ }
174
+ }
175
+
176
+ return L.join('\n');
177
+ }
178
+
179
+ /**
180
+ * CLI entry point for xp-gate retro.
181
+ * @param {string[]} args - CLI subargs (without 'retro')
182
+ * @returns {Promise<number>} Exit code
183
+ */
184
+ async function handleRetro(args = []) {
185
+ const allFlag = args.includes('--all');
186
+ const jsonFlag = args.includes('--json');
187
+ const daysIdx = args.indexOf('--days');
188
+ const sprintIdx = args.indexOf('--sprint');
189
+
190
+ let days = allFlag ? 30 : 7;
191
+ if (daysIdx >= 0 && args[daysIdx + 1]) {
192
+ days = parseInt(args[daysIdx + 1], 10);
193
+ if (isNaN(days) || days < 1) days = 7;
194
+ }
195
+
196
+ const sprintFilter = (sprintIdx >= 0 && args[sprintIdx + 1]) ? args[sprintIdx + 1] : null;
197
+ const dir = process.cwd();
198
+
199
+ const commits = getGitCommits(dir, days);
200
+ const audit = readAuditEntries(dir);
201
+ const quality = readQualityHistory(dir);
202
+ const sprints = readSprintHistory(dir, sprintFilter);
203
+
204
+ if (jsonFlag) {
205
+ const gateEntries = audit.filter(e => e.gate_id);
206
+ const passed = gateEntries.filter(e => e.passed).length;
207
+ console.log(JSON.stringify({
208
+ period: { days, from: new Date(Date.now() - days * 86400000).toISOString(), to: new Date().toISOString() },
209
+ total_commits: commits.length,
210
+ gate_pass_rate: gateEntries.length > 0 ? passed / gateEntries.length : null,
211
+ rework_rates: sprints.filter(s => s.metrics?.rework_rate != null).map(s => ({ id: s.id, rate: s.metrics.rework_rate })),
212
+ evidence_skipped: audit.filter(e => e.event === 'evidence_skipped'),
213
+ quality: quality.map(q => ({ timestamp: q.timestamp, score: q.score })),
214
+ }, null, 2));
215
+ return 0;
216
+ }
217
+
218
+ console.log(buildReport({ days, commits, audit, quality, sprints }));
219
+ return 0;
220
+ }
221
+
222
+ module.exports = { handleRetro };
@@ -42,6 +42,13 @@ function copyHooks(srcDir, destDir) {
42
42
  fs.chmodSync(dest, 0o755);
43
43
  }
44
44
  });
45
+
46
+ // Copy lib/ directory (e.g. now-ms.sh) — sourced by pre-commit at runtime
47
+ const libSrc = path.join(srcDir, 'hooks', 'lib');
48
+ const libDest = path.join(destDir, 'lib');
49
+ if (fs.existsSync(libSrc)) {
50
+ copyDirRecursive(libSrc, libDest);
51
+ }
45
52
  }
46
53
 
47
54
  function copyAdapters(srcDir, destDir) {