@boyingliu01/xp-gate 0.12.3 → 0.12.4

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 (49) hide show
  1. package/adapters/cpp.sh +0 -0
  2. package/adapters/dart.sh +0 -0
  3. package/adapters/flutter.sh +0 -0
  4. package/adapters/go.sh +0 -0
  5. package/adapters/java.sh +0 -0
  6. package/adapters/kotlin.sh +0 -0
  7. package/adapters/objectivec.sh +0 -0
  8. package/adapters/powershell.sh +0 -0
  9. package/adapters/python.sh +0 -0
  10. package/adapters/shell.sh +0 -0
  11. package/adapters/swift.sh +0 -0
  12. package/adapters/typescript.sh +0 -0
  13. package/bin/xp-gate.js +0 -8
  14. package/gate-3.sh +0 -0
  15. package/gate-4.sh +0 -0
  16. package/gate-7.sh +0 -0
  17. package/gate-8.sh +0 -0
  18. package/gate-9.sh +0 -0
  19. package/lib/init.js +9 -6
  20. package/lib/sprint-status.js +35 -1
  21. package/lib/update-hooks.js +46 -46
  22. package/mock-policy/AGENTS.md +2 -2
  23. package/mutation/AGENTS.md +2 -2
  24. package/package.json +1 -1
  25. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  27. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  28. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  29. package/plugins/opencode/package.json +1 -1
  30. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  31. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  32. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  33. package/plugins/qoder/plugin.json +1 -1
  34. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  35. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  36. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  37. package/principles/AGENTS.md +2 -2
  38. package/skills/delphi-review/AGENTS.md +2 -2
  39. package/skills/sprint-flow/AGENTS.md +2 -2
  40. package/skills/test-specification-alignment/AGENTS.md +2 -2
  41. package/lib/__tests__/next-sprint.test.js +0 -231
  42. package/lib/next-sprint.js +0 -129
  43. package/lib/shared-phase-constants.d.ts +0 -17
  44. package/lib/shared-phase-constants.js +0 -77
  45. package/plugins/opencode/__tests__/tui-plugin.test.ts +0 -543
  46. package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +0 -562
  47. package/plugins/opencode/__tests__/xp-gate-update.test.ts +0 -518
  48. package/plugins/opencode/tui-plugin.ts +0 -414
  49. package/plugins/opencode/tui-plugin.tsx +0 -306
@@ -1,231 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- describe('next-sprint', () => {
6
- let tmpDir;
7
- let consoleSpy;
8
- let processExitSpy;
9
-
10
- beforeEach(() => {
11
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'next-sprint-test-'));
12
- consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
13
- vi.spyOn(console, 'error').mockImplementation(() => {});
14
- processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
15
- // Clear require cache to ensure fresh module for each test
16
- delete require.cache[require.resolve('../next-sprint')];
17
- });
18
-
19
- afterEach(() => {
20
- fs.rmSync(tmpDir, { recursive: true, force: true });
21
- vi.restoreAllMocks();
22
- });
23
-
24
- function createState(overrides = {}) {
25
- const stateDir = path.join(tmpDir, '.sprint-state');
26
- fs.mkdirSync(stateDir, { recursive: true });
27
- const state = {
28
- id: 'sprint-2026-06-30-01',
29
- task_description: 'Test Sprint',
30
- phase: 4,
31
- status: 'completed',
32
- started_at: new Date().toISOString(),
33
- phase_history: [
34
- {
35
- phase: 2,
36
- phase_name: 'BUILD',
37
- status: 'completed',
38
- reqs: {
39
- 'REQ-001': { name: 'Implement login', status: 'completed' },
40
- 'REQ-002': { name: 'Implement logout', status: 'completed' },
41
- },
42
- },
43
- ],
44
- ...overrides,
45
- };
46
- fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
47
- return state;
48
- }
49
-
50
- function mockGhCli(issues) {
51
- const { execSync } = require('child_process');
52
- vi.spyOn(require('child_process'), 'execSync').mockImplementation((cmd) => {
53
- if (cmd.includes('gh issue list')) {
54
- return JSON.stringify(issues);
55
- }
56
- return '';
57
- });
58
- }
59
-
60
- describe('readSprintState', () => {
61
- it('returns null when no sprint state exists', () => {
62
- const { readSprintState } = require('../next-sprint');
63
- const result = readSprintState(tmpDir);
64
- expect(result).toBeNull();
65
- });
66
-
67
- it('returns parsed state when sprint state exists', () => {
68
- const state = createState();
69
- const { readSprintState } = require('../next-sprint');
70
- const result = readSprintState(tmpDir);
71
- expect(result).toBeDefined();
72
- expect(result.id).toBe('sprint-2026-06-30-01');
73
- });
74
- });
75
-
76
- describe('getCurrentSprintReqs', () => {
77
- it('extracts requirements from sprint state', () => {
78
- createState();
79
- const { readSprintState, getCurrentSprintReqs } = require('../next-sprint');
80
- const state = readSprintState(tmpDir);
81
- const reqs = getCurrentSprintReqs(state);
82
- expect(reqs).toHaveLength(2);
83
- expect(reqs[0].name).toBe('Implement login');
84
- expect(reqs[1].name).toBe('Implement logout');
85
- });
86
-
87
- it('returns empty array when no phase history', () => {
88
- const { getCurrentSprintReqs } = require('../next-sprint');
89
- const reqs = getCurrentSprintReqs({ phase_history: [] });
90
- expect(reqs).toEqual([]);
91
- });
92
- });
93
-
94
- describe('filterRemainingIssues', () => {
95
- it('filters out issues already in sprint', () => {
96
- const issues = [
97
- { number: 1, title: 'Implement login', labels: [], createdAt: '2026-06-30T00:00:00Z' },
98
- { number: 2, title: 'Implement logout', labels: [], createdAt: '2026-06-30T00:00:00Z' },
99
- { number: 3, title: 'Implement signup', labels: [], createdAt: '2026-06-30T00:00:00Z' },
100
- ];
101
- const sprintReqs = [{ name: 'Implement login' }, { name: 'Implement logout' }];
102
- const { filterRemainingIssues } = require('../next-sprint');
103
- const result = filterRemainingIssues(issues, sprintReqs);
104
- expect(result).toHaveLength(1);
105
- expect(result[0].number).toBe(3);
106
- });
107
-
108
- it('returns all issues when no sprint reqs', () => {
109
- const issues = [
110
- { number: 1, title: 'Implement login', labels: [], createdAt: '2026-06-30T00:00:00Z' },
111
- { number: 2, title: 'Implement logout', labels: [], createdAt: '2026-06-30T00:00:00Z' },
112
- ];
113
- const { filterRemainingIssues } = require('../next-sprint');
114
- const result = filterRemainingIssues(issues, []);
115
- expect(result).toHaveLength(2);
116
- });
117
- });
118
-
119
- describe('formatIssuesTable', () => {
120
- it('formats issues as table', () => {
121
- const issues = [
122
- { number: 1, title: 'Implement login', labels: [{ name: 'feature' }], createdAt: '2026-06-30T00:00:00Z' },
123
- { number: 2, title: 'Implement logout', labels: [{ name: 'bug' }], createdAt: '2026-06-30T00:00:00Z' },
124
- ];
125
- const { formatIssuesTable } = require('../next-sprint');
126
- const result = formatIssuesTable(issues);
127
- expect(result).toContain('Remaining Open Issues');
128
- expect(result).toContain('Implement login');
129
- expect(result).toContain('feature');
130
- });
131
-
132
- it('handles empty issues', () => {
133
- const { formatIssuesTable } = require('../next-sprint');
134
- const result = formatIssuesTable([]);
135
- expect(result).toBe('No remaining issues found.');
136
- });
137
- });
138
-
139
- describe('generateSprintPlan', () => {
140
- it('generates sprint plan for remaining issues', () => {
141
- const issues = [
142
- { number: 1, title: 'Implement login', labels: [], createdAt: '2026-06-30T00:00:00Z' },
143
- { number: 2, title: 'Implement logout', labels: [], createdAt: '2026-06-30T00:00:00Z' },
144
- ];
145
- const { generateSprintPlan } = require('../next-sprint');
146
- const result = generateSprintPlan(issues);
147
- expect(result).toContain('Suggested Next Sprint Plan');
148
- expect(result).toContain('#1');
149
- expect(result).toContain('sprint-flow');
150
- });
151
-
152
- it('returns null when no issues', () => {
153
- const { generateSprintPlan } = require('../next-sprint');
154
- const result = generateSprintPlan([]);
155
- expect(result).toBeNull();
156
- });
157
- });
158
-
159
- describe('handleNextSprint', () => {
160
- it('outputs remaining issues in table format', async () => {
161
- createState();
162
- const { execSync } = require('child_process');
163
- vi.spyOn(require('child_process'), 'execSync').mockImplementation((cmd) => {
164
- if (cmd.includes('gh issue list')) {
165
- return JSON.stringify([
166
- { number: 1, title: 'Implement login', labels: [{ name: 'feature' }], createdAt: '2026-06-30T00:00:00Z' },
167
- { number: 2, title: 'Implement logout', labels: [{ name: 'bug' }], createdAt: '2026-06-30T00:00:00Z' },
168
- { number: 3, title: 'Implement signup', labels: [{ name: 'enhancement' }], createdAt: '2026-06-30T00:00:00Z' },
169
- ]);
170
- }
171
- return '';
172
- });
173
- delete require.cache[require.resolve('../next-sprint')];
174
- const { handleNextSprint } = require('../next-sprint');
175
- const result = await handleNextSprint(['--dir', tmpDir]);
176
- expect(result).toBe(0);
177
- expect(consoleSpy).toHaveBeenCalled();
178
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
179
- expect(output).toContain('Remaining Open Issues');
180
- expect(output).toContain('Implement signup');
181
- });
182
-
183
- it('outputs JSON when --json flag', async () => {
184
- createState();
185
- vi.spyOn(require('child_process'), 'execSync').mockImplementation((cmd) => {
186
- if (cmd.includes('gh issue list')) {
187
- return JSON.stringify([
188
- { number: 1, title: 'Implement login', labels: [], createdAt: '2026-06-30T00:00:00Z' },
189
- ]);
190
- }
191
- return '';
192
- });
193
- delete require.cache[require.resolve('../next-sprint')];
194
- const { handleNextSprint } = require('../next-sprint');
195
- const result = await handleNextSprint(['--json', '--dir', tmpDir]);
196
- expect(result).toBe(0);
197
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
198
- const parsed = JSON.parse(output);
199
- expect(parsed).toHaveProperty('current_sprint');
200
- expect(parsed).toHaveProperty('remaining_issues');
201
- });
202
-
203
- it('outputs plan when --plan flag', async () => {
204
- createState();
205
- vi.spyOn(require('child_process'), 'execSync').mockImplementation((cmd) => {
206
- if (cmd.includes('gh issue list')) {
207
- return JSON.stringify([
208
- { number: 5, title: 'Implement password reset', labels: [], createdAt: '2026-06-30T00:00:00Z' },
209
- ]);
210
- }
211
- return '';
212
- });
213
- delete require.cache[require.resolve('../next-sprint')];
214
- const { handleNextSprint } = require('../next-sprint');
215
- const result = await handleNextSprint(['--plan', '--dir', tmpDir]);
216
- expect(result).toBe(0);
217
- const output = consoleSpy.mock.calls.map(c => c[0]).join('\n');
218
- expect(output).toContain('Suggested Next Sprint Plan');
219
- });
220
-
221
- it('returns 1 when gh CLI not available', async () => {
222
- vi.spyOn(require('child_process'), 'execSync').mockImplementation(() => {
223
- throw new Error('gh: command not found');
224
- });
225
- delete require.cache[require.resolve('../next-sprint')];
226
- const { handleNextSprint } = require('../next-sprint');
227
- const result = await handleNextSprint(['--dir', tmpDir]);
228
- expect(result).toBe(1);
229
- });
230
- });
231
- });
@@ -1,129 +0,0 @@
1
- const childProcess = require('child_process');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- function readSprintState(dir) {
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'));
10
- } catch {
11
- return null;
12
- }
13
- }
14
-
15
- function getCurrentSprintReqs(state) {
16
- if (!state?.phase_history) return [];
17
- const reqs = [];
18
- for (const phase of state.phase_history) {
19
- if (phase.reqs) {
20
- for (const [id, req] of Object.entries(phase.reqs)) {
21
- reqs.push({ id, name: req.name, status: req.status });
22
- }
23
- }
24
- }
25
- return reqs;
26
- }
27
-
28
- function fetchOpenIssues(repoRoot) {
29
- try {
30
- const result = childProcess.execSync('gh issue list --state open --json number,title,labels,createdAt', {
31
- cwd: repoRoot,
32
- encoding: 'utf8',
33
- timeout: 30000,
34
- });
35
- return JSON.parse(result);
36
- } catch (err) {
37
- if (err.message.includes('gh: command not found')) {
38
- console.error('Error: gh CLI not found. Install it from https://cli.github.com/');
39
- return null;
40
- }
41
- if (err.message.includes('not logged in')) {
42
- console.error('Error: Not logged in to GitHub. Run "gh auth login" first.');
43
- return null;
44
- }
45
- console.error('Error fetching issues:', err.message);
46
- return null;
47
- }
48
- }
49
-
50
- function filterRemainingIssues(issues, sprintReqs) {
51
- const sprintTitles = new Set(sprintReqs.map(r => r.name?.toLowerCase()));
52
- return issues.filter(issue => {
53
- const titleLower = issue.title.toLowerCase();
54
- return !sprintTitles.has(titleLower);
55
- });
56
- }
57
-
58
- function formatIssuesTable(issues) {
59
- if (issues.length === 0) return 'No remaining issues found.';
60
- const lines = ['Remaining Open Issues:', ''];
61
- lines.push('┌──────┬────────────────────────────────────────────┬─────────────────────┬───────────┐');
62
- lines.push('│ # │ Title │ Labels │ Created │');
63
- lines.push('├──────┼────────────────────────────────────────────┼─────────────────────┼───────────┤');
64
- for (const issue of issues) {
65
- const num = String(issue.number).padEnd(4);
66
- const title = issue.title.slice(0, 40).padEnd(40);
67
- const labels = (issue.labels || []).map(l => l.name).join(', ').slice(0, 19).padEnd(19);
68
- const created = issue.createdAt ? issue.createdAt.slice(0, 10) : 'N/A';
69
- lines.push(`│ ${num} │ ${title} │ ${labels} │ ${created} │`);
70
- }
71
- lines.push('└──────┴────────────────────────────────────────────┴─────────────────────┴───────────┘');
72
- lines.push(`Total: ${issues.length} remaining issues`);
73
- return lines.join('\n');
74
- }
75
-
76
- function generateSprintPlan(issues) {
77
- if (issues.length === 0) return null;
78
- const lines = ['Suggested Next Sprint Plan:', ''];
79
- const prioritized = issues.slice(0, 5);
80
- for (let i = 0; i < prioritized.length; i++) {
81
- const issue = prioritized[i];
82
- lines.push(`${i + 1}. #${issue.number} - ${issue.title}`);
83
- }
84
- if (issues.length > 5) {
85
- lines.push(`... and ${issues.length - 5} more issues`);
86
- }
87
- lines.push('');
88
- lines.push('To start a sprint, run:');
89
- lines.push(` /sprint-flow "Implement #${prioritized[0].number}: ${prioritized[0].title}"`);
90
- return lines.join('\n');
91
- }
92
-
93
- async function handleNextSprint(args = []) {
94
- const jsonFlag = args.includes('--json');
95
- const planFlag = args.includes('--plan');
96
- const dirIdx = args.indexOf('--dir');
97
- let repoRoot = process.cwd();
98
- if (dirIdx >= 0 && dirIdx + 1 < args.length) {
99
- repoRoot = path.resolve(args[dirIdx + 1]);
100
- }
101
- const state = readSprintState(repoRoot);
102
- const sprintReqs = state ? getCurrentSprintReqs(state) : [];
103
- const issues = fetchOpenIssues(repoRoot);
104
- if (issues === null) return 1;
105
- const remaining = filterRemainingIssues(issues, sprintReqs);
106
- if (jsonFlag) {
107
- console.log(JSON.stringify({ current_sprint: state?.id || null, sprint_reqs: sprintReqs, remaining_issues: remaining }, null, 2));
108
- return 0;
109
- }
110
- console.log(formatIssuesTable(remaining));
111
- if (planFlag) {
112
- const plan = generateSprintPlan(remaining);
113
- if (plan) {
114
- console.log('');
115
- console.log(plan);
116
- }
117
- }
118
- return 0;
119
- }
120
-
121
- module.exports = {
122
- handleNextSprint,
123
- fetchOpenIssues,
124
- filterRemainingIssues,
125
- formatIssuesTable,
126
- generateSprintPlan,
127
- readSprintState,
128
- getCurrentSprintReqs,
129
- };
@@ -1,17 +0,0 @@
1
- /**
2
- * Type declarations for shared-phase-constants (CommonJS module).
3
- * Consumed by both sprint-status.js (CJS) and tui-plugin.ts (ESM via tsx).
4
- */
5
-
6
- export const PHASE_NAMES: Record<string, string>;
7
- export const PHASE_ORDER: string[];
8
-
9
- /**
10
- * Find the most recent timestamp across started_at and phase_history.
11
- */
12
- export function getLatestTimestamp(state: object): number;
13
-
14
- /**
15
- * Check if the sprint state is stale (>1h since last activity).
16
- */
17
- export function isStale(state: object): boolean;
@@ -1,77 +0,0 @@
1
- /**
2
- * Shared Sprint Flow phase constants.
3
- *
4
- * Single source of truth for PHASE_NAMES and PHASE_ORDER, consumed by both
5
- * the CLI (sprint-status.js, CommonJS) and the OpenCode TUI plugin
6
- * (tui-plugin.ts, ESM via tsx).
7
- *
8
- * @module shared-phase-constants
9
- */
10
-
11
- const PHASE_NAMES = {
12
- '-1': 'ISOLATE',
13
- '-0.5': 'AUTO-ESTIMATE',
14
- '0': 'THINK',
15
- '1': 'PLAN',
16
- '2': 'BUILD',
17
- '3': 'REVIEW',
18
- '4': 'USER ACCEPT',
19
- '5': 'FEEDBACK',
20
- '6': 'SHIP',
21
- '7': 'LAND',
22
- '8': 'CLEANUP',
23
- };
24
-
25
- const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
26
-
27
- /**
28
- * Safely parse a date-like value into epoch ms.
29
- * @param {*} value - Timestamp to parse
30
- * @returns {number} Epoch ms, or NaN if invalid
31
- */
32
- function parseTime(value) {
33
- return new Date(value).getTime();
34
- }
35
-
36
- /**
37
- * Get the max of an existing value and a new timestamp (if valid and larger).
38
- * @param {number} current - Current best value
39
- * @param {*} candidate - Candidate timestamp
40
- * @returns {number} Max value
41
- */
42
- function maxValid(current, candidate) {
43
- if (!candidate) return current;
44
- const t = parseTime(candidate);
45
- return !isNaN(t) && t > current ? t : current;
46
- }
47
-
48
- /**
49
- * Find the most recent timestamp across started_at and phase_history.
50
- * @param {object} state - Sprint state object
51
- * @returns {number} Latest timestamp in ms, or 0 if none found
52
- */
53
- function getLatestTimestamp(state) {
54
- if (!state || !state.started_at) return 0;
55
- const started = parseTime(state.started_at);
56
- if (isNaN(started)) return 0;
57
- let latest = started;
58
- if (Array.isArray(state.phase_history)) {
59
- for (const ph of state.phase_history) {
60
- latest = maxValid(maxValid(latest, ph.completed_at), ph.started_at);
61
- }
62
- }
63
- return latest;
64
- }
65
-
66
- /**
67
- * Check if the sprint state is stale (>1h since last activity).
68
- * @param {object} state - Sprint state object
69
- * @returns {boolean}
70
- */
71
- function isStale(state) {
72
- if (!state || !state.started_at) return false;
73
- const latest = getLatestTimestamp(state);
74
- return latest > 0 && Date.now() - latest > 3600000;
75
- }
76
-
77
- module.exports = { PHASE_NAMES, PHASE_ORDER, getLatestTimestamp, isStale };