@boyingliu01/xp-gate 0.14.9 → 0.14.11

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 (30) hide show
  1. package/lib/__tests__/install-skill.test.js +2 -1
  2. package/lib/__tests__/sprint-discovery.test.ts +6 -4
  3. package/lib/__tests__/sprint-state-manager.test.js +279 -0
  4. package/lib/__tests__/sprint-status.test.js +10 -13
  5. package/lib/__tests__/sprint-status.test.ts +4 -3
  6. package/lib/install-skill.js +14 -1
  7. package/lib/next-sprint.js +3 -4
  8. package/lib/sprint-discovery.js +4 -4
  9. package/lib/sprint-state-manager.js +122 -0
  10. package/lib/sprint-state-migrator.js +153 -0
  11. package/lib/sprint-status.js +4 -4
  12. package/mock-policy/AGENTS.md +4 -4
  13. package/mutation/AGENTS.md +4 -4
  14. package/package.json +1 -1
  15. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  17. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  18. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  19. package/plugins/opencode/package.json +1 -1
  20. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  21. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  22. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  23. package/plugins/qoder/plugin.json +1 -1
  24. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  25. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  26. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  27. package/principles/AGENTS.md +4 -4
  28. package/skills/delphi-review/AGENTS.md +4 -4
  29. package/skills/sprint-flow/AGENTS.md +4 -4
  30. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -178,7 +178,8 @@ describe('install-skill', () => {
178
178
  expect(fs.existsSync(configFile)).toBe(true);
179
179
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
180
180
  expect(config.installedSkills).toHaveProperty('delphi-review');
181
- expect(config.installedSkills['delphi-review'].version).toBe('1.0.0');
181
+ // Version should be actual CLI version (0.14.10.0), not hardcoded '1.0.0'
182
+ expect(config.installedSkills['delphi-review'].version).toBe('0.14.10.0');
182
183
  expect(config.installedSkills['delphi-review'].installedAt).toMatch(/^\d{4}-/);
183
184
  });
184
185
 
@@ -322,10 +322,12 @@ describe('sprint-discovery: discoverActiveSprints', () => {
322
322
  // no id, no status, no started_at
323
323
  task_description: 'Partial fields only',
324
324
  });
325
- // Should not throw, result should be skipped (no valid id)
325
+ // After migration, missing id gets auto-generated, so result is found
326
326
  const results = sprintDiscovery.discoverActiveSprints(TMP_ROOT);
327
327
  const partials = results.filter(r => r.state.task_description === 'Partial fields only');
328
- expect(partials.length).toBe(0);
328
+ expect(partials.length).toBe(1);
329
+ // Migrated state should have auto-generated id
330
+ expect(partials[0].state.id).toMatch(/^sprint-\d+$/);
329
331
  });
330
332
 
331
333
  test('handles EACCES on .sprint-state/ directory gracefully', () => {
@@ -359,7 +361,7 @@ describe('sprint-discovery: discoverActiveSprints', () => {
359
361
  const results = sprintDiscovery.discoverActiveSprints(TMP_ROOT);
360
362
  const desc = results.filter(r => r.state.id === 'sprint-2026-06-23-nodesc');
361
363
  expect(desc.length).toBe(1);
362
- // task_description will be null/undefined, but entry should still be returned
363
- expect(desc[0].state.task_description).toBeUndefined();
364
+ // After migration, missing task_description gets default '-'
365
+ expect(desc[0].state.task_description).toBe('-');
364
366
  });
365
367
  });
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Tests for Sprint State Manager
3
+ * @test REQ-001 Sprint State Manager
4
+ * @intent Verify read/write/migration/transitionPhase/rollback
5
+ * @covers AC-001-01, AC-001-02, AC-001-03
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import os from 'node:os';
12
+ import { SprintStateManager, LEGACY_PHASE_MAP } from '../sprint-state-manager.js';
13
+ import { migrateState, createInitialState } from '../sprint-state-migrator.js';
14
+
15
+ describe('SprintStateManager', () => {
16
+ let tmpDir;
17
+ let manager;
18
+
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-state-test-'));
21
+ manager = new SprintStateManager(tmpDir);
22
+ });
23
+
24
+ afterEach(() => {
25
+ fs.rmSync(tmpDir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe('read()', () => {
29
+ it('returns null when file does not exist', () => {
30
+ const state = manager.read();
31
+ expect(state).toBeNull();
32
+ });
33
+
34
+ it('reads valid state file', () => {
35
+ const state = {
36
+ _schema_version: 1,
37
+ id: 'sprint-123',
38
+ task_description: 'Test sprint',
39
+ phase: 2,
40
+ status: 'in_progress',
41
+ started_at: '2026-07-14T10:00:00Z',
42
+ phase_history: [],
43
+ isolation: { worktree_path: tmpDir, branch: 'test' },
44
+ };
45
+ fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
46
+ fs.writeFileSync(
47
+ path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
48
+ JSON.stringify(state, null, 2)
49
+ );
50
+
51
+ const result = manager.read();
52
+ expect(result.id).toBe('sprint-123');
53
+ expect(result._schema_version).toBe(1);
54
+ });
55
+
56
+ it('auto-migrates legacy state and creates backup', () => {
57
+ const legacyState = {
58
+ id: 'sprint-legacy',
59
+ task_description: 'Legacy sprint',
60
+ phase: -1, // Legacy ISOLATE → 1 (PREP)
61
+ status: 'completed',
62
+ phase_history: [
63
+ { phase: -1, phase_name: 'ISOLATE', status: 'completed', timestamp: '2026-07-14T10:00:00Z' },
64
+ ],
65
+ isolation: { worktree_path: tmpDir, branch: 'test' },
66
+ };
67
+ fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
68
+ fs.writeFileSync(
69
+ path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
70
+ JSON.stringify(legacyState, null, 2)
71
+ );
72
+
73
+ const result = manager.read();
74
+
75
+ // Should be migrated
76
+ expect(result._schema_version).toBe(1);
77
+ expect(result.phase).toBe(1); // -1 → 1
78
+ expect(result.phase_history[0].phase).toBe(1);
79
+ expect(result.phase_history[0].phase_name).toBe('PREP');
80
+ expect(result.phase_history[0].started_at).toBe('2026-07-14T10:00:00Z'); // timestamp → started_at
81
+
82
+ // Backup should exist
83
+ const backupExists = fs.existsSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json.backup'));
84
+ expect(backupExists).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('write()', () => {
89
+ it('writes state atomically', () => {
90
+ const state = {
91
+ _schema_version: 1,
92
+ id: 'sprint-test',
93
+ task_description: 'Test',
94
+ phase: 1,
95
+ status: 'in_progress',
96
+ started_at: new Date().toISOString(),
97
+ phase_history: [],
98
+ isolation: { worktree_path: tmpDir, branch: 'test' },
99
+ };
100
+
101
+ manager.write(state);
102
+
103
+ const written = JSON.parse(
104
+ fs.readFileSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json'), 'utf8')
105
+ );
106
+ expect(written.id).toBe('sprint-test');
107
+
108
+ // Tmp file should not exist (atomic write completed)
109
+ const tmpExists = fs.existsSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json.tmp'));
110
+ expect(tmpExists).toBe(false);
111
+ });
112
+ });
113
+
114
+ describe('transitionPhase()', () => {
115
+ it('creates new phase_history entry', () => {
116
+ // Create initial state
117
+ const initialState = createInitialState(tmpDir);
118
+ manager.write(initialState);
119
+
120
+ // Transition to phase 2
121
+ const result = manager.transitionPhase(2, 'in_progress');
122
+
123
+ expect(result.phase).toBe(2);
124
+ expect(result.phase_history.length).toBe(1);
125
+ expect(result.phase_history[0].phase).toBe(2);
126
+ expect(result.phase_history[0].phase_name).toBe('DESIGN');
127
+ expect(result.phase_history[0].status).toBe('in_progress');
128
+ expect(result.phase_history[0].started_at).toBeDefined();
129
+ });
130
+
131
+ it('updates existing phase_history entry', () => {
132
+ const initialState = createInitialState(tmpDir);
133
+ manager.write(initialState);
134
+
135
+ // Start phase 2
136
+ manager.transitionPhase(2, 'in_progress');
137
+
138
+ // Complete phase 2
139
+ const result = manager.transitionPhase(2, 'completed');
140
+
141
+ expect(result.phase_history[0].status).toBe('completed');
142
+ expect(result.phase_history[0].completed_at).toBeDefined();
143
+ expect(result.phase_history[0].duration_seconds).toBeGreaterThanOrEqual(0);
144
+ });
145
+
146
+ it('calls onTransition callback', () => {
147
+ const initialState = createInitialState(tmpDir);
148
+ manager.write(initialState);
149
+
150
+ let callbackCalled = false;
151
+ let callbackState = null;
152
+
153
+ manager.transitionPhase(2, 'in_progress', {
154
+ onTransition: (state) => {
155
+ callbackCalled = true;
156
+ callbackState = state;
157
+ },
158
+ });
159
+
160
+ expect(callbackCalled).toBe(true);
161
+ expect(callbackState.phase).toBe(2);
162
+ });
163
+
164
+ it('handles onTransition callback errors gracefully', () => {
165
+ const initialState = createInitialState(tmpDir);
166
+ manager.write(initialState);
167
+
168
+ // Should not throw
169
+ expect(() => {
170
+ manager.transitionPhase(2, 'in_progress', {
171
+ onTransition: () => {
172
+ throw new Error('Render failed');
173
+ },
174
+ });
175
+ }).not.toThrow();
176
+
177
+ // State should still be updated
178
+ const state = manager.read();
179
+ expect(state.phase).toBe(2);
180
+ });
181
+
182
+ it('updates outputs when provided', () => {
183
+ const initialState = createInitialState(tmpDir);
184
+ manager.write(initialState);
185
+
186
+ const result = manager.transitionPhase(2, 'completed', {
187
+ outputs: { specification: 'spec.yaml' },
188
+ });
189
+
190
+ expect(result.outputs.specification).toBe('spec.yaml');
191
+ });
192
+ });
193
+
194
+ describe('migrate()', () => {
195
+ it('adds _schema_version', () => {
196
+ const state = { id: 'test', phase: 1, phase_history: [] };
197
+ const migrated = migrateState(state, manager.stateDir);
198
+ expect(migrated._schema_version).toBe(1);
199
+ });
200
+
201
+ it('sets default task_description if missing', () => {
202
+ const state = { id: 'test', phase: 1, phase_history: [] };
203
+ const migrated = migrateState(state, manager.stateDir);
204
+ expect(migrated.task_description).toBe('-');
205
+ });
206
+
207
+ it('migrates all legacy phase numbers', () => {
208
+ for (const [legacy, expected] of Object.entries(LEGACY_PHASE_MAP)) {
209
+ const state = {
210
+ id: 'test',
211
+ phase: Number(legacy),
212
+ phase_history: [{ phase: Number(legacy), status: 'completed' }],
213
+ };
214
+ const migrated = migrateState(state, manager.stateDir);
215
+ expect(migrated.phase).toBe(expected.phase);
216
+ expect(migrated.phase_history[0].phase).toBe(expected.phase);
217
+ expect(migrated.phase_history[0].phase_name).toBe(expected.phase_name);
218
+ }
219
+ });
220
+
221
+ it('preserves unknown fields', () => {
222
+ const state = {
223
+ id: 'test',
224
+ phase: 1,
225
+ phase_history: [],
226
+ custom_field: 'preserved',
227
+ };
228
+ const migrated = migrateState(state, manager.stateDir);
229
+ expect(migrated.custom_field).toBe('preserved');
230
+ });
231
+ });
232
+
233
+ describe('rollback()', () => {
234
+ it('returns false when no backup exists', () => {
235
+ const result = manager.rollback();
236
+ expect(result).toBe(false);
237
+ });
238
+
239
+ it('restores from backup', () => {
240
+ // Create initial state and backup
241
+ const originalState = {
242
+ _schema_version: 1,
243
+ id: 'original',
244
+ task_description: 'Original',
245
+ phase: 1,
246
+ status: 'in_progress',
247
+ started_at: '2026-07-14T10:00:00Z',
248
+ phase_history: [],
249
+ isolation: { worktree_path: tmpDir, branch: 'test' },
250
+ };
251
+ fs.mkdirSync(path.join(tmpDir, '.sprint-state'), { recursive: true });
252
+ fs.writeFileSync(
253
+ path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
254
+ JSON.stringify(originalState, null, 2)
255
+ );
256
+ fs.writeFileSync(
257
+ path.join(tmpDir, '.sprint-state', 'sprint-state.json.backup'),
258
+ JSON.stringify(originalState, null, 2)
259
+ );
260
+
261
+ // Modify current state
262
+ const modifiedState = { ...originalState, id: 'modified' };
263
+ fs.writeFileSync(
264
+ path.join(tmpDir, '.sprint-state', 'sprint-state.json'),
265
+ JSON.stringify(modifiedState, null, 2)
266
+ );
267
+
268
+ // Rollback
269
+ const result = manager.rollback();
270
+ expect(result).toBe(true);
271
+
272
+ // Verify restored
273
+ const restored = JSON.parse(
274
+ fs.readFileSync(path.join(tmpDir, '.sprint-state', 'sprint-state.json'), 'utf8')
275
+ );
276
+ expect(restored.id).toBe('original');
277
+ });
278
+ });
279
+ });
@@ -14,12 +14,13 @@ const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-status-test-'));
14
14
  const STATE_DIR = path.join(TMP_DIR, '.sprint-state');
15
15
  const STATE_FILE = path.join(STATE_DIR, 'sprint-state.json');
16
16
 
17
- // Active sprint state (canonical format per SKILL.md L893-942)
17
+ // Active sprint state (v1 schema per sprint-state-manager.js)
18
18
  const ACTIVE_STATE = {
19
+ _schema_version: 1,
19
20
  id: 'sprint-2026-06-16-01',
20
21
  task_description: 'Test sprint',
21
- phase: 2,
22
- status: 'running',
22
+ phase: 3,
23
+ status: 'in_progress',
23
24
  started_at: '2026-06-16T10:00:00Z',
24
25
  isolation: {
25
26
  worktree_path: '.worktrees/sprint/sprint-2026-06-16-01',
@@ -33,15 +34,11 @@ const ACTIVE_STATE = {
33
34
  user_decision: 'accepted'
34
35
  },
35
36
  phase_history: [
36
- { phase: -1, phase_name: 'ISOLATE', status: 'completed',
37
+ { phase: 1, phase_name: 'PREP', status: 'completed',
37
38
  started_at: '2026-06-16T10:00:00Z', completed_at: '2026-06-16T10:03:00Z', duration_seconds: 180 },
38
- { phase: -0.5, phase_name: 'AUTO-ESTIMATE', status: 'completed',
39
- started_at: '2026-06-16T10:03:00Z', completed_at: '2026-06-16T10:05:00Z', duration_seconds: 120 },
40
- { phase: 0, phase_name: 'THINK', status: 'completed',
39
+ { phase: 2, phase_name: 'DESIGN', status: 'completed',
41
40
  started_at: '2026-06-16T10:05:00Z', completed_at: '2026-06-16T10:15:00Z', duration_seconds: 600 },
42
- { phase: 1, phase_name: 'PLAN', status: 'completed',
43
- started_at: '2026-06-16T10:15:00Z', completed_at: '2026-06-16T10:25:00Z', duration_seconds: 600 },
44
- { phase: 2, phase_name: 'BUILD', status: 'in_progress',
41
+ { phase: 3, phase_name: 'BUILD', status: 'in_progress',
45
42
  started_at: '2026-06-16T10:25:00Z', completed_at: null, duration_seconds: null,
46
43
  reqs: {
47
44
  'REQ-001': { name: 'Feature A', status: 'completed' },
@@ -87,8 +84,8 @@ describe('sprint-status: readSprintState', () => {
87
84
  const state = sprintStatus.readSprintState(TMP_DIR);
88
85
  expect(state).not.toBeNull();
89
86
  expect(state.task_description).toBe('Test sprint');
90
- expect(state.phase).toBe(2);
91
- expect(state.status).toBe('running');
87
+ expect(state.phase).toBe(3);
88
+ expect(state.status).toBe('in_progress');
92
89
  expect(Array.isArray(state.phase_history)).toBe(true);
93
90
  expect(state.phase_history.length).toBeGreaterThan(0);
94
91
  });
@@ -166,7 +163,7 @@ describe('sprint-status: formatSprintTable', () => {
166
163
  const json = sprintStatus.jsonMode(ACTIVE_STATE);
167
164
  const parsed = JSON.parse(json);
168
165
  expect(parsed.task_description).toBe('Test sprint');
169
- expect(parsed.phase).toBe(2);
166
+ expect(parsed.phase).toBe(3);
170
167
  });
171
168
 
172
169
  // AC-SPRINTSTATUS-001-09: Stale state detection
@@ -16,12 +16,13 @@ const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-status-test-'));
16
16
  const STATE_DIR = path.join(TMP_DIR, '.sprint-state');
17
17
  const STATE_FILE = path.join(STATE_DIR, 'sprint-state.json');
18
18
 
19
- // Active sprint state (canonical format per SKILL.md L893-942, 6-phase schema)
19
+ // Active sprint state (v1 schema per sprint-state-manager.js, 6-phase schema)
20
20
  const ACTIVE_STATE = {
21
+ _schema_version: 1,
21
22
  id: 'sprint-2026-06-16-01',
22
23
  task_description: 'Test sprint',
23
24
  phase: 3,
24
- status: 'running',
25
+ status: 'in_progress',
25
26
  started_at: '2026-06-16T10:00:00Z',
26
27
  isolation: {
27
28
  worktree_path: '.worktrees/sprint/sprint-2026-06-16-01',
@@ -95,7 +96,7 @@ describe('sprint-status: readSprintState', () => {
95
96
  const stateObj = state as Record<string, unknown>;
96
97
  expect(stateObj.task_description).toBe('Test sprint');
97
98
  expect(stateObj.phase).toBe(3);
98
- expect(stateObj.status).toBe('running');
99
+ expect(stateObj.status).toBe('in_progress');
99
100
  expect(Array.isArray(stateObj.phase_history)).toBe(true);
100
101
  });
101
102
 
@@ -20,6 +20,15 @@ function getSkillsDir() {
20
20
  return path.join(HOME_DIR, '.config', 'opencode', 'skills');
21
21
  }
22
22
 
23
+ function getCliVersion() {
24
+ try {
25
+ const versionFile = path.join(__dirname, '..', '..', '..', 'VERSION');
26
+ return fs.readFileSync(versionFile, 'utf8').trim();
27
+ } catch {
28
+ return '0.0.0';
29
+ }
30
+ }
31
+
23
32
  const SKILLS_REGISTRY = {
24
33
  'sprint-flow': { repo: 'boyingliu01/xp-gate', path: 'skills/sprint-flow' },
25
34
  'delphi-review': { repo: 'boyingliu01/xp-gate', path: 'skills/delphi-review' },
@@ -123,10 +132,14 @@ async function performInstall(skillInfo, name, targetDir, offline, verbose) {
123
132
  }
124
133
 
125
134
  ensureConfigDir();
135
+
136
+ // Read actual CLI version from VERSION file
137
+ const version = getCliVersion();
138
+
126
139
  updateConfig({
127
140
  installedSkills: {
128
141
  ...(getConfig().installedSkills || {}),
129
- [name]: { version: '1.0.0', installedAt: new Date().toISOString() }
142
+ [name]: { version, installedAt: new Date().toISOString() }
130
143
  }
131
144
  });
132
145
 
@@ -1,12 +1,11 @@
1
1
  const childProcess = require('child_process');
2
- const fs = require('fs');
3
2
  const path = require('path');
3
+ const { SprintStateManager } = require('./sprint-state-manager');
4
4
 
5
5
  function readSprintState(dir) {
6
6
  try {
7
- const stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
8
- if (!fs.existsSync(stateFile)) return null;
9
- return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
7
+ const manager = new SprintStateManager(dir);
8
+ return manager.read();
10
9
  } catch {
11
10
  return null;
12
11
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { SprintStateManager } = require('./sprint-state-manager');
16
17
 
17
18
  /**
18
19
  * Maximum number of active sprints to return.
@@ -62,16 +63,15 @@ function findGitRoot(startDir) {
62
63
 
63
64
  /**
64
65
  * Read and parse a sprint-state.json from a given project directory.
66
+ * Uses SprintStateManager for schema validation + auto-migration.
65
67
  *
66
68
  * @param {string} dir - Directory containing .sprint-state/
67
69
  * @returns {object|null} Parsed sprint state, or null if not found or malformed
68
70
  */
69
71
  function readSprintState(dir) {
70
72
  try {
71
- const stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
72
- if (!fs.existsSync(stateFile)) return null;
73
- const raw = fs.readFileSync(stateFile, 'utf8');
74
- return JSON.parse(raw);
73
+ const manager = new SprintStateManager(dir);
74
+ return manager.read();
75
75
  } catch {
76
76
  return null;
77
77
  }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Sprint State Manager — centralized read/write/transition for sprint state.
3
+ *
4
+ * Resolves #343 (schema drift) and #338 (auto-render enforcement).
5
+ *
6
+ * @module sprint-state-manager
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { migrateState, createInitialState, rollbackState, LEGACY_PHASE_MAP } = require('./sprint-state-migrator');
14
+
15
+ class SprintStateManager {
16
+ constructor(projectRoot) {
17
+ this.projectRoot = projectRoot;
18
+ this.stateDir = path.join(projectRoot, '.sprint-state');
19
+ this.stateFile = path.join(this.stateDir, 'sprint-state.json');
20
+ this.backupFile = path.join(this.stateDir, 'sprint-state.json.backup');
21
+ }
22
+
23
+ read() {
24
+ if (!fs.existsSync(this.stateFile)) {
25
+ return null;
26
+ }
27
+
28
+ const raw = fs.readFileSync(this.stateFile, 'utf8');
29
+ let state;
30
+ try {
31
+ state = JSON.parse(raw);
32
+ } catch (err) {
33
+ throw new Error(`Failed to parse sprint-state.json: ${err.message}`);
34
+ }
35
+
36
+ // Check if migration needed
37
+ if (!state._schema_version) {
38
+ // Create backup before first migration
39
+ if (!fs.existsSync(this.backupFile)) {
40
+ fs.copyFileSync(this.stateFile, this.backupFile);
41
+ }
42
+ state = migrateState(state, this.stateDir);
43
+ // Write migrated state atomically
44
+ this.write(state);
45
+ }
46
+
47
+ return state;
48
+ }
49
+
50
+ write(state) {
51
+ // Ensure state directory exists
52
+ if (!fs.existsSync(this.stateDir)) {
53
+ fs.mkdirSync(this.stateDir, { recursive: true });
54
+ }
55
+
56
+ // Atomic write: write to tmp, then rename
57
+ const tmpFile = this.stateFile + '.tmp';
58
+ fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8');
59
+ fs.renameSync(tmpFile, this.stateFile);
60
+ }
61
+
62
+ transitionPhase(phase, status, options = {}) {
63
+ const state = this.read() || createInitialState(this.projectRoot);
64
+
65
+ // Update current phase
66
+ state.phase = phase;
67
+
68
+ // Find or create phase_history entry
69
+ const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
70
+ const phaseName = phaseNames[phase - 1];
71
+
72
+ let entry = state.phase_history.find(e => e.phase === phase);
73
+ if (!entry) {
74
+ entry = {
75
+ phase,
76
+ phase_name: phaseName,
77
+ status,
78
+ };
79
+ state.phase_history.push(entry);
80
+ }
81
+
82
+ // Update entry
83
+ entry.status = status;
84
+ if (status === 'in_progress' && !entry.started_at) {
85
+ entry.started_at = new Date().toISOString();
86
+ }
87
+ if (status === 'completed' || status === 'skipped') {
88
+ entry.completed_at = new Date().toISOString();
89
+ if (entry.started_at) {
90
+ const startMs = new Date(entry.started_at).getTime();
91
+ const endMs = new Date(entry.completed_at).getTime();
92
+ entry.duration_seconds = Math.round((endMs - startMs) / 1000);
93
+ }
94
+ }
95
+
96
+ // Update outputs if provided
97
+ if (options.outputs) {
98
+ state.outputs = { ...state.outputs, ...options.outputs };
99
+ }
100
+
101
+ // Write state atomically
102
+ this.write(state);
103
+
104
+ // Trigger onTransition callback (for auto-render)
105
+ if (typeof options.onTransition === 'function') {
106
+ try {
107
+ options.onTransition(state);
108
+ } catch (err) {
109
+ // Render failure is WARNING, not BLOCK
110
+ console.warn(`[WARN] onTransition callback failed: ${err.message}`);
111
+ }
112
+ }
113
+
114
+ return state;
115
+ }
116
+
117
+ rollback() {
118
+ return rollbackState(this.stateFile, this.backupFile);
119
+ }
120
+ }
121
+
122
+ module.exports = { SprintStateManager, LEGACY_PHASE_MAP };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Sprint State Migrator — legacy phase migration + rollback helpers.
3
+ *
4
+ * Extracted from SprintStateManager to satisfy god-class rule (≤15 methods).
5
+ *
6
+ * @module sprint-state-migrator
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Legacy phase number → v1 phase mapping
16
+ */
17
+ const LEGACY_PHASE_MAP = {
18
+ '-1': { phase: 1, phase_name: 'PREP' },
19
+ '-0.5': { phase: 1, phase_name: 'PREP' },
20
+ '0': { phase: 2, phase_name: 'DESIGN' },
21
+ '1': { phase: 2, phase_name: 'DESIGN' },
22
+ '2': { phase: 3, phase_name: 'BUILD' },
23
+ '3': { phase: 4, phase_name: 'VERIFY' },
24
+ '4': { phase: 4, phase_name: 'VERIFY' },
25
+ '5': { phase: 5, phase_name: 'SHIP' },
26
+ '6': { phase: 5, phase_name: 'SHIP' },
27
+ '7': { phase: 6, phase_name: 'CLOSE' },
28
+ '8': { phase: 6, phase_name: 'CLOSE' },
29
+ };
30
+
31
+ const PHASE_NAMES = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
32
+
33
+ /**
34
+ * Migrate legacy sprint-state.json to v1 schema.
35
+ *
36
+ * @param {Object} state - Raw state from file
37
+ * @param {string} stateDir - Path to .sprint-state directory
38
+ * @returns {Object} Migrated state with _schema_version: 1
39
+ */
40
+ function migrateState(state, stateDir) {
41
+ const warnings = [];
42
+
43
+ state._schema_version = 1;
44
+
45
+ if (!state.task_description) {
46
+ state.task_description = '-';
47
+ warnings.push('task_description was missing, set to "-"');
48
+ }
49
+
50
+ if (!state.id) {
51
+ state.id = `sprint-${Date.now()}`;
52
+ warnings.push('id was missing, generated from timestamp');
53
+ }
54
+
55
+ if (!state.status) { state.status = 'in_progress'; }
56
+ if (!state.started_at) { state.started_at = new Date().toISOString(); }
57
+ if (!state.phase_history) { state.phase_history = []; }
58
+ if (!state.isolation) { state.isolation = { worktree_path: null, branch: null }; }
59
+
60
+ state.phase_history = state.phase_history.map(entry => {
61
+ const phaseNum = String(entry.phase);
62
+ const mapped = LEGACY_PHASE_MAP[phaseNum];
63
+ if (mapped) {
64
+ warnings.push(`Migrated legacy phase ${entry.phase} → ${mapped.phase} (${mapped.phase_name})`);
65
+ return { ...entry, phase: mapped.phase, phase_name: mapped.phase_name };
66
+ }
67
+ if (!entry.phase_name && entry.phase >= 1 && entry.phase <= 6) {
68
+ entry.phase_name = PHASE_NAMES[entry.phase - 1];
69
+ }
70
+ return entry;
71
+ });
72
+
73
+ const currentPhaseNum = String(state.phase);
74
+ const currentMapped = LEGACY_PHASE_MAP[currentPhaseNum];
75
+ if (currentMapped) {
76
+ state.phase = currentMapped.phase;
77
+ warnings.push(`Migrated current phase ${currentPhaseNum} → ${currentMapped.phase}`);
78
+ }
79
+
80
+ state.phase_history = state.phase_history.map(entry => {
81
+ if (entry.timestamp && !entry.started_at) {
82
+ entry.started_at = entry.timestamp;
83
+ }
84
+ return entry;
85
+ });
86
+
87
+ if (warnings.length > 0) {
88
+ logMigrationWarnings(warnings, stateDir);
89
+ }
90
+
91
+ return state;
92
+ }
93
+
94
+ /**
95
+ * Create initial sprint state for new sprints.
96
+ *
97
+ * @param {string} projectRoot - Project root directory
98
+ * @returns {Object} Initial state
99
+ */
100
+ function createInitialState(projectRoot) {
101
+ return {
102
+ _schema_version: 1,
103
+ id: `sprint-${Date.now()}`,
104
+ task_description: '-',
105
+ phase: 1,
106
+ status: 'in_progress',
107
+ started_at: new Date().toISOString(),
108
+ phase_history: [],
109
+ isolation: { worktree_path: projectRoot, branch: null },
110
+ outputs: {},
111
+ metrics: {},
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Log migration warnings to file.
117
+ *
118
+ * @param {string[]} warnings - Array of warning messages
119
+ * @param {string} stateDir - Path to .sprint-state directory
120
+ */
121
+ function logMigrationWarnings(warnings, stateDir) {
122
+ if (!fs.existsSync(stateDir)) {
123
+ fs.mkdirSync(stateDir, { recursive: true });
124
+ }
125
+
126
+ const warningsFile = path.join(stateDir, 'migration-warnings.json');
127
+ const existing = fs.existsSync(warningsFile)
128
+ ? JSON.parse(fs.readFileSync(warningsFile, 'utf8'))
129
+ : [];
130
+
131
+ existing.push({ timestamp: new Date().toISOString(), warnings });
132
+ fs.writeFileSync(warningsFile, JSON.stringify(existing, null, 2), 'utf8');
133
+ }
134
+
135
+ /**
136
+ * Rollback to backup state.
137
+ *
138
+ * @param {string} stateFile - Path to sprint-state.json
139
+ * @param {string} backupFile - Path to sprint-state.json.backup
140
+ * @returns {boolean} True if rollback succeeded
141
+ */
142
+ function rollbackState(stateFile, backupFile) {
143
+ if (!fs.existsSync(backupFile)) { return false; }
144
+ fs.copyFileSync(backupFile, stateFile);
145
+ return true;
146
+ }
147
+
148
+ module.exports = {
149
+ LEGACY_PHASE_MAP,
150
+ migrateState,
151
+ createInitialState,
152
+ rollbackState,
153
+ };
@@ -10,6 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { discoverActiveSprints } = require('./sprint-discovery');
13
+ const { SprintStateManager } = require('./sprint-state-manager');
13
14
 
14
15
  // Phase constants (inlined; was shared-phase-constants.js, removed in v0.13.0 slimming)
15
16
  const PHASE_NAMES = {
@@ -47,15 +48,14 @@ function isStale(state) {
47
48
 
48
49
  /**
49
50
  * Read and parse sprint-state.json from a project directory.
51
+ * Uses SprintStateManager for schema validation + auto-migration.
50
52
  * @param {string} dir - Project root directory
51
53
  * @returns {object|null} Parsed sprint state, or null if not found or malformed
52
54
  */
53
55
  function readSprintState(dir) {
54
56
  try {
55
- const stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
56
- if (!fs.existsSync(stateFile)) return null;
57
- const raw = fs.readFileSync(stateFile, 'utf8');
58
- return JSON.parse(raw);
57
+ const manager = new SprintStateManager(dir);
58
+ return manager.read();
59
59
  } catch {
60
60
  return null;
61
61
  }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.14.9",
3
+ "version": "0.14.11",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.14.9",
3
+ "version": "0.14.11",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.14.9",
3
+ "version": "0.14.11",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.14.9",
3
+ "version": "0.14.11",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
6
6
  "author": {
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-13
4
- **Commit:** cb954fe
5
- **Branch:** sprint/2026-07-13-01
6
- **Version:** 0.14.9.0
3
+ **Generated:** 2026-07-16
4
+ **Commit:** 2f70dd5
5
+ **Branch:** main
6
+ **Version:** 0.14.11.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.