@boyingliu01/xp-gate 0.14.6 → 0.14.8
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.
- package/adapters/plugins/whalecloud-java/scripts/install-maven-whalecloud.sh +4 -2
- package/bin/xp-gate.js +8 -0
- package/lib/__tests__/next-sprint.test.js +231 -0
- package/lib/__tests__/sprint-status.test.ts +9 -18
- package/lib/next-sprint.js +129 -0
- package/lib/sprint-status.js +3 -4
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/__tests__/gate-m3.test.ts +384 -0
- package/mock-policy/__tests__/schema.test.ts +239 -0
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +62 -3
- package/plugins/claude-code/skills/delphi-review/hooks.md +19 -0
- package/plugins/claude-code/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/claude-code/skills/delphi-review/tools.md +29 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/hooks.md +18 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/claude-code/skills/sprint-flow/tools.md +29 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +14 -0
- package/plugins/opencode/index.ts +72 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +62 -3
- package/plugins/opencode/skills/delphi-review/hooks.md +19 -0
- package/plugins/opencode/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/plugins/opencode/skills/delphi-review/tools.md +29 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/hooks.md +18 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/plugins/opencode/skills/sprint-flow/tools.md +29 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +62 -3
- package/skills/delphi-review/hooks.md +19 -0
- package/skills/delphi-review/scripts/verify-consensus.sh +43 -0
- package/skills/delphi-review/tools.md +29 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/hooks.md +18 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
- package/skills/sprint-flow/tools.md +29 -0
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -74,8 +74,10 @@ else
|
|
|
74
74
|
<!-- Security: J000078 SQL injection -->
|
|
75
75
|
<rule ref="category/java/security.xml/MethodRequiresCheck"/>
|
|
76
76
|
|
|
77
|
-
<!--
|
|
78
|
-
|
|
77
|
+
<!-- Deferred: Custom XPath rules for J-rules not covered by standard PMD categories.
|
|
78
|
+
All 106 whalecloud rules currently map to standard PMD 7.x category XMLs.
|
|
79
|
+
Custom XPath rules will be added here when enterprise-specific rules
|
|
80
|
+
cannot be expressed via built-in PMD checks. Track via issue backlog. -->
|
|
79
81
|
|
|
80
82
|
</ruleset>
|
|
81
83
|
RULESET
|
package/bin/xp-gate.js
CHANGED
|
@@ -137,6 +137,14 @@ const COMMANDS = {
|
|
|
137
137
|
},
|
|
138
138
|
usage: 'xp-gate sprint-status [--json] [--watch] [--dir <path>]'
|
|
139
139
|
},
|
|
140
|
+
'next-sprint': {
|
|
141
|
+
description: 'Analyze remaining open issues and plan next iteration',
|
|
142
|
+
run: subargs => {
|
|
143
|
+
const { handleNextSprint } = require('../lib/next-sprint.js');
|
|
144
|
+
handleNextSprint(subargs).then(code => process.exit(code));
|
|
145
|
+
},
|
|
146
|
+
usage: 'xp-gate next-sprint [--json] [--plan] [--dir <path>]'
|
|
147
|
+
},
|
|
140
148
|
'update-hooks': {
|
|
141
149
|
description: 'Sync latest hooks from xp-gate package to project',
|
|
142
150
|
run: subargs => {
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
});
|
|
@@ -16,11 +16,11 @@ 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)
|
|
19
|
+
// Active sprint state (canonical format per SKILL.md L893-942, 6-phase schema)
|
|
20
20
|
const ACTIVE_STATE = {
|
|
21
21
|
id: 'sprint-2026-06-16-01',
|
|
22
22
|
task_description: 'Test sprint',
|
|
23
|
-
phase:
|
|
23
|
+
phase: 3,
|
|
24
24
|
status: 'running',
|
|
25
25
|
started_at: '2026-06-16T10:00:00Z',
|
|
26
26
|
isolation: {
|
|
@@ -29,22 +29,13 @@ const ACTIVE_STATE = {
|
|
|
29
29
|
created_from: 'main',
|
|
30
30
|
created_from_commit: 'abc123'
|
|
31
31
|
},
|
|
32
|
-
auto_estimate: {
|
|
33
|
-
change_type: '新增功能',
|
|
34
|
-
estimated_level: '轻量',
|
|
35
|
-
user_decision: 'accepted'
|
|
36
|
-
},
|
|
37
32
|
phase_history: [
|
|
38
|
-
{ phase:
|
|
33
|
+
{ phase: 1, phase_name: 'PREP', status: 'completed',
|
|
39
34
|
started_at: '2026-06-16T10:00:00Z', completed_at: '2026-06-16T10:03:00Z', duration_seconds: 180 },
|
|
40
|
-
{ phase:
|
|
41
|
-
started_at: '2026-06-16T10:03:00Z', completed_at: '2026-06-16T10:
|
|
42
|
-
{ phase:
|
|
43
|
-
started_at: '2026-06-16T10:
|
|
44
|
-
{ phase: 1, phase_name: 'PLAN', status: 'completed',
|
|
45
|
-
started_at: '2026-06-16T10:15:00Z', completed_at: '2026-06-16T10:25:00Z', duration_seconds: 600 },
|
|
46
|
-
{ phase: 2, phase_name: 'BUILD', status: 'in_progress',
|
|
47
|
-
started_at: '2026-06-16T10:25:00Z', completed_at: null, duration_seconds: null,
|
|
35
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'completed',
|
|
36
|
+
started_at: '2026-06-16T10:03:00Z', completed_at: '2026-06-16T10:15:00Z', duration_seconds: 720 },
|
|
37
|
+
{ phase: 3, phase_name: 'BUILD', status: 'in_progress',
|
|
38
|
+
started_at: '2026-06-16T10:15:00Z', completed_at: null, duration_seconds: null,
|
|
48
39
|
reqs: {
|
|
49
40
|
'REQ-001': { name: 'Feature A', status: 'completed' },
|
|
50
41
|
'REQ-002': { name: 'Feature B', status: 'in_progress' }
|
|
@@ -103,7 +94,7 @@ describe('sprint-status: readSprintState', () => {
|
|
|
103
94
|
expect(state).not.toBeNull();
|
|
104
95
|
const stateObj = state as Record<string, unknown>;
|
|
105
96
|
expect(stateObj.task_description).toBe('Test sprint');
|
|
106
|
-
expect(stateObj.phase).toBe(
|
|
97
|
+
expect(stateObj.phase).toBe(3);
|
|
107
98
|
expect(stateObj.status).toBe('running');
|
|
108
99
|
expect(Array.isArray(stateObj.phase_history)).toBe(true);
|
|
109
100
|
});
|
|
@@ -216,6 +207,6 @@ describe('sprint-status: jsonMode', () => {
|
|
|
216
207
|
const json = sprintStatus!.jsonMode(ACTIVE_STATE);
|
|
217
208
|
const parsed = JSON.parse(json);
|
|
218
209
|
expect(parsed.task_description).toBe('Test sprint');
|
|
219
|
-
expect(parsed.phase).toBe(
|
|
210
|
+
expect(parsed.phase).toBe(3);
|
|
220
211
|
});
|
|
221
212
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
};
|
package/lib/sprint-status.js
CHANGED
|
@@ -13,11 +13,10 @@ const { discoverActiveSprints } = require('./sprint-discovery');
|
|
|
13
13
|
|
|
14
14
|
// Phase constants (inlined; was shared-phase-constants.js, removed in v0.13.0 slimming)
|
|
15
15
|
const PHASE_NAMES = {
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'4': 'USER ACCEPTANCE', '5': 'FEEDBACK', '6': 'SHIP', '7': 'LAND', '8': 'CLEANUP',
|
|
16
|
+
'1': 'PREP', '2': 'DESIGN', '3': 'BUILD',
|
|
17
|
+
'4': 'VERIFY', '5': 'SHIP', '6': 'CLOSE',
|
|
19
18
|
};
|
|
20
|
-
const PHASE_ORDER = ['
|
|
19
|
+
const PHASE_ORDER = ['1', '2', '3', '4', '5', '6'];
|
|
21
20
|
|
|
22
21
|
function parseTime(value) {
|
|
23
22
|
if (!value) return 0;
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** d83bb96
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.8.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.
|