@boyingliu01/xp-gate 0.10.2 → 0.10.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 (29) hide show
  1. package/build-integrity/__tests__/gate-10.test.ts +230 -0
  2. package/build-integrity/__tests__/import-resolver.test.ts +409 -0
  3. package/build-integrity/__tests__/orchestrator.test.ts +289 -0
  4. package/build-integrity/gate-10.ts +641 -0
  5. package/build-integrity/types.ts +51 -0
  6. package/mock-policy/AGENTS.md +2 -2
  7. package/mutation/AGENTS.md +2 -2
  8. package/mutation/__tests__/go-mutant-runner.test.ts +288 -0
  9. package/mutation/gate-m.ts +6 -0
  10. package/mutation/runners/go-mutant-runner.ts +175 -0
  11. package/mutation/runners/index.ts +3 -0
  12. package/package.json +3 -2
  13. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  14. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  15. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  16. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  17. package/plugins/opencode/__tests__/tui-plugin.test.ts +2 -2
  18. package/plugins/opencode/package.json +1 -1
  19. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  20. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  21. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  22. package/plugins/opencode/tui-plugin.ts +0 -19
  23. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  24. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  25. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  26. package/principles/AGENTS.md +2 -2
  27. package/skills/delphi-review/AGENTS.md +2 -2
  28. package/skills/sprint-flow/AGENTS.md +2 -2
  29. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -0,0 +1,288 @@
1
+ /**
2
+ * @test REQ-MUT-005 Go mutation runner
3
+ * @intent Verify GoMutantRunner registration, availability checks, and JSON report parsing
4
+ * @covers AC-MUT-005, AC-MUT-006
5
+ */
6
+
7
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ import { spawn, spawnSync, type ChildProcess } from 'child_process';
9
+ import { writeFileSync, mkdirSync } from 'fs';
10
+ import { tmpdir } from 'os';
11
+ import { join } from 'path';
12
+
13
+ vi.mock('child_process', () => ({
14
+ spawn: vi.fn(),
15
+ spawnSync: vi.fn(),
16
+ }));
17
+
18
+ describe('GoMutantRunner', () => {
19
+ let GoMutantRunner: typeof import('../runners/go-mutant-runner').GoMutantRunner;
20
+ let tmpDir: string;
21
+
22
+ beforeEach(async () => {
23
+ vi.clearAllMocks();
24
+ tmpDir = join(tmpdir(), `xp-gate-go-test-${Date.now()}`);
25
+ mkdirSync(tmpDir, { recursive: true });
26
+ GoMutantRunner = (await import('../runners/go-mutant-runner')).GoMutantRunner;
27
+ });
28
+
29
+ afterEach(() => {
30
+ try { process.kill(process.pid, 0); } catch { /* noop */ }
31
+ });
32
+
33
+ describe('name and extensions', () => {
34
+ it('should have name "gomutants"', () => {
35
+ const runner = new GoMutantRunner();
36
+ expect(runner.name).toBe('gomutants');
37
+ });
38
+
39
+ it('should handle .go extension', () => {
40
+ const runner = new GoMutantRunner();
41
+ expect(runner.extensions).toContain('go');
42
+ expect(runner.extensions).not.toContain('ts');
43
+ expect(runner.extensions).not.toContain('py');
44
+ });
45
+ });
46
+
47
+ describe('isAvailable', () => {
48
+ it('should return true when gomutants is installed', async () => {
49
+ vi.mocked(spawnSync).mockReturnValue({
50
+ status: 0,
51
+ output: [],
52
+ pid: 0,
53
+ signal: null,
54
+ } as unknown as ReturnType<typeof spawnSync>);
55
+
56
+ const runner = new GoMutantRunner();
57
+ const result = await runner.isAvailable();
58
+ expect(result).toBe(true);
59
+ expect(spawnSync).toHaveBeenCalledWith('gomutants', ['--version'], {
60
+ stdio: 'pipe',
61
+ timeout: 5000,
62
+ });
63
+ });
64
+
65
+ it('should return false when gomutants is not installed', async () => {
66
+ vi.mocked(spawnSync).mockReturnValue({
67
+ status: 1,
68
+ output: [],
69
+ pid: 0,
70
+ signal: null,
71
+ } as unknown as ReturnType<typeof spawnSync>);
72
+
73
+ const runner = new GoMutantRunner();
74
+ const result = await runner.isAvailable();
75
+ expect(result).toBe(false);
76
+ });
77
+
78
+ it('should return false when spawnSync throws', async () => {
79
+ vi.mocked(spawnSync).mockImplementation(() => {
80
+ throw new Error('ENOENT');
81
+ });
82
+
83
+ const runner = new GoMutantRunner();
84
+ const result = await runner.isAvailable();
85
+ expect(result).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('run', () => {
90
+ it('should spawn gomutants with correct args', async () => {
91
+ let closeCb: ((code: number | null) => void) | null = null;
92
+ const mockChild = {
93
+ stdout: { on: vi.fn() },
94
+ stderr: { on: vi.fn() },
95
+ on: vi.fn((event, cb) => {
96
+ if (event === 'close') closeCb = cb;
97
+ }),
98
+ kill: vi.fn(),
99
+ killed: false,
100
+ };
101
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
102
+
103
+ writeFileSync(join(tmpDir, '.gomutants-report.json'), JSON.stringify({
104
+ test_efficacy: 80,
105
+ mutants_total: 10,
106
+ mutants_killed: 8,
107
+ mutants_lived: 2,
108
+ mutants_not_viable: 0,
109
+ mutants_not_covered: 0,
110
+ }));
111
+
112
+ const runner = new GoMutantRunner();
113
+ const promise = runner.run({
114
+ files: ['src/main.go'],
115
+ timeoutMs: 60000,
116
+ cwd: tmpDir,
117
+ });
118
+
119
+ closeCb!(0);
120
+ const result = await promise;
121
+
122
+ expect(spawn).toHaveBeenCalledWith('gomutants', [
123
+ '--output',
124
+ expect.stringContaining('.gomutants-report.json'),
125
+ '--quiet',
126
+ ], expect.objectContaining({ cwd: tmpDir }));
127
+
128
+ expect(result.timedOut).toBe(false);
129
+ expect(result.report!.mutationScore).toBe(80);
130
+ expect(result.report!.nrOfMutants).toBe(10);
131
+ expect(result.report!.nrOfKilledMutants).toBe(8);
132
+ expect(result.report!.nrOfSurvivedMutants).toBe(2);
133
+ });
134
+
135
+ it('should return timedOut=true when killed', async () => {
136
+ const mockChild = {
137
+ stdout: { on: vi.fn() },
138
+ stderr: { on: vi.fn() },
139
+ on: vi.fn((event, cb) => {
140
+ if (event === 'close') setTimeout(() => cb(null), 10);
141
+ }),
142
+ kill: vi.fn(),
143
+ killed: false,
144
+ };
145
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
146
+
147
+ const runner = new GoMutantRunner();
148
+ const result = await runner.run({
149
+ files: ['src/main.go'],
150
+ timeoutMs: 100,
151
+ cwd: tmpDir,
152
+ });
153
+
154
+ expect(result.timedOut).toBe(true);
155
+ expect(result.report).toBeNull();
156
+ });
157
+
158
+ it('should return error when spawn fails', async () => {
159
+ const mockChild = {
160
+ stdout: { on: vi.fn() },
161
+ stderr: { on: vi.fn() },
162
+ on: vi.fn((event, cb) => {
163
+ if (event === 'error') setTimeout(() => cb(new Error('spawn ENOENT')), 10);
164
+ }),
165
+ kill: vi.fn(),
166
+ killed: false,
167
+ };
168
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
169
+
170
+ const runner = new GoMutantRunner();
171
+ const result = await runner.run({
172
+ files: ['src/main.go'],
173
+ timeoutMs: 60000,
174
+ cwd: tmpDir,
175
+ });
176
+
177
+ expect(result.report).toBeNull();
178
+ expect(result.timedOut).toBe(false);
179
+ expect(result.error).toBe('spawn ENOENT');
180
+ });
181
+
182
+ it('should handle gomutants exit code 10 (below threshold) with report', async () => {
183
+ let stderrCb: ((d: Buffer) => void) | null = null;
184
+ const mockChild = {
185
+ stdout: { on: vi.fn() },
186
+ stderr: { on: vi.fn((_event: string, cb: (d: Buffer) => void) => { stderrCb = cb; }) },
187
+ on: vi.fn((event, cb) => {
188
+ if (event === 'close') {
189
+ setTimeout(() => {
190
+ if (stderrCb) stderrCb(Buffer.from('below threshold'));
191
+ cb(10);
192
+ }, 10);
193
+ }
194
+ }),
195
+ kill: vi.fn(),
196
+ killed: false,
197
+ };
198
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
199
+
200
+ writeFileSync(join(tmpDir, '.gomutants-report.json'), JSON.stringify({
201
+ test_efficacy: 45,
202
+ mutants_total: 50,
203
+ mutants_killed: 22,
204
+ mutants_lived: 18,
205
+ mutants_not_covered: 10,
206
+ }));
207
+
208
+ const runner = new GoMutantRunner();
209
+ const result = await runner.run({
210
+ files: ['src/main.go'],
211
+ timeoutMs: 60000,
212
+ cwd: tmpDir,
213
+ });
214
+
215
+ expect(result.timedOut).toBe(false);
216
+ expect(result.report).not.toBeNull();
217
+ expect(result.report!.mutationScore).toBe(45);
218
+ expect(result.error).toBeTruthy();
219
+ });
220
+
221
+ it('should return null report when report file does not exist', async () => {
222
+ const mockChild = {
223
+ stdout: { on: vi.fn() },
224
+ stderr: { on: vi.fn() },
225
+ on: vi.fn((event, cb) => {
226
+ if (event === 'close') setTimeout(() => cb(0), 10);
227
+ }),
228
+ kill: vi.fn(),
229
+ killed: false,
230
+ };
231
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
232
+
233
+ const runner = new GoMutantRunner();
234
+ const result = await runner.run({
235
+ files: ['src/main.go'],
236
+ timeoutMs: 60000,
237
+ cwd: tmpDir,
238
+ });
239
+
240
+ expect(result.report).toBeNull();
241
+ expect(result.timedOut).toBe(false);
242
+ });
243
+
244
+ it('should parse files array from report', async () => {
245
+ let closeCb: ((code: number | null) => void) | null = null;
246
+ const mockChild = {
247
+ stdout: { on: vi.fn() },
248
+ stderr: { on: vi.fn() },
249
+ on: vi.fn((event, cb) => {
250
+ if (event === 'close') closeCb = cb;
251
+ }),
252
+ kill: vi.fn(),
253
+ killed: false,
254
+ };
255
+ vi.mocked(spawn).mockReturnValue(mockChild as unknown as ChildProcess);
256
+
257
+ writeFileSync(join(tmpDir, '.gomutants-report.json'), JSON.stringify({
258
+ test_efficacy: 80,
259
+ mutants_total: 20,
260
+ mutants_killed: 16,
261
+ mutants_lived: 4,
262
+ mutants_not_viable: 0,
263
+ mutants_not_covered: 0,
264
+ files: [
265
+ {
266
+ file_name: 'calculator.go',
267
+ mutations: Array(12).fill({ status: 'KILLED' }).concat(Array(8).fill({ status: 'LIVED' })),
268
+ },
269
+ ],
270
+ }));
271
+
272
+ const runner = new GoMutantRunner();
273
+ const promise = runner.run({
274
+ files: ['src/calculator.go'],
275
+ timeoutMs: 60000,
276
+ cwd: tmpDir,
277
+ });
278
+
279
+ closeCb!(0);
280
+ const result = await promise;
281
+
282
+ expect(result.report).not.toBeNull();
283
+ expect(result.report!.files).toBeDefined();
284
+ expect(result.report!.files!['calculator.go'].mutationScore).toBe(60);
285
+ expect(result.report!.files!['calculator.go'].nrOfMutants).toBe(20);
286
+ });
287
+ });
288
+ });
@@ -126,6 +126,12 @@ async function findTestFileForSource(sourceFile: string): Promise<string | null>
126
126
  if (await fileExists(testFile3)) return testFile3;
127
127
  }
128
128
 
129
+ if (ext === '.go') {
130
+ // Go: foo.go → foo_test.go (same directory, idiomatic convention)
131
+ const testFile1 = sourceFile.replace(/\.go$/, '_test.go');
132
+ if (await fileExists(testFile1)) return testFile1;
133
+ }
134
+
129
135
  return null;
130
136
  }
131
137
 
@@ -0,0 +1,175 @@
1
+ // @no-test-required: Go mutation wrapper — tested via gate-m integration + pre-push Gate M
2
+ import { spawn, spawnSync } from 'child_process';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import type {
6
+ MutationRunner,
7
+ RunMutationOptions,
8
+ MutationRunOutcome,
9
+ MutationRunResult,
10
+ } from './types';
11
+
12
+ const GOMUTANTS_REPORT_PATH = '.gomutants-report.json';
13
+
14
+ /**
15
+ * gomutants mutation runner for Go files.
16
+ *
17
+ * Tool: https://github.com/szhekpisov/gomutants
18
+ *
19
+ * Uses `gomutants run --output <json>` to generate mutants and parse report.
20
+ * Exit codes: 0=pass, 10=below threshold, 11=below coverage.
21
+ */
22
+ export class GoMutantRunner implements MutationRunner {
23
+ readonly name = 'gomutants';
24
+ readonly extensions = ['go'];
25
+
26
+ async isAvailable(): Promise<boolean> {
27
+ try {
28
+ const result = spawnSync('gomutants', ['--version'], {
29
+ stdio: 'pipe',
30
+ timeout: 5000,
31
+ });
32
+ return result.status !== null && result.status === 0;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async run(options: RunMutationOptions): Promise<MutationRunOutcome> {
39
+ return new Promise((resolve) => {
40
+ const reportPath = join(options.cwd, GOMUTANTS_REPORT_PATH);
41
+
42
+ const args = [
43
+ '--output',
44
+ reportPath,
45
+ '--quiet',
46
+ ];
47
+
48
+ const child = spawn('gomutants', args, {
49
+ stdio: 'pipe',
50
+ shell: false,
51
+ cwd: options.cwd,
52
+ });
53
+
54
+ let stderr = '';
55
+ let stdout = '';
56
+
57
+ child.stdout?.on('data', (data: Buffer) => {
58
+ stdout += data.toString();
59
+ });
60
+
61
+ child.stderr?.on('data', (data: Buffer) => {
62
+ stderr += data.toString();
63
+ });
64
+
65
+ const timeoutId = setTimeout(() => {
66
+ child.kill('SIGTERM');
67
+ setTimeout(() => {
68
+ if (!child.killed) child.kill('SIGKILL');
69
+ }, 5000);
70
+ }, options.timeoutMs);
71
+
72
+ child.on('close', (code) => {
73
+ clearTimeout(timeoutId);
74
+
75
+ if (code === null) {
76
+ resolve({ report: null, timedOut: true });
77
+ return;
78
+ }
79
+
80
+ // Exit codes: 0=pass, 10=below threshold, 11=below coverage
81
+ // All three produce a JSON report we can parse
82
+ const report = this.parseReport(reportPath);
83
+ resolve({
84
+ report,
85
+ timedOut: false,
86
+ error: code !== 0 ? stderr || stdout : undefined,
87
+ });
88
+ });
89
+
90
+ child.on('error', (err) => {
91
+ clearTimeout(timeoutId);
92
+ resolve({ report: null, timedOut: false, error: err.message });
93
+ });
94
+ });
95
+ }
96
+
97
+ private parseReport(reportPath: string): MutationRunResult | null {
98
+ try {
99
+ if (!existsSync(reportPath)) return null;
100
+ const content = readFileSync(reportPath, 'utf-8');
101
+ const parsed = JSON.parse(content) as Record<string, unknown>;
102
+ return this.normalizeReport(parsed);
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Normalize gomutants JSON into the shared MutationRunResult format.
110
+ *
111
+ * gomutants v0.4.0 output:
112
+ * test_efficacy: number (0-100) — KILLED/(KILLED+LIVED)
113
+ * mutations_coverage: number (0-100) — (KILLED+LIVED)/(KILLED+LIVED+NOT_COVERED)
114
+ * mutants_total: number
115
+ * mutants_killed: number
116
+ * mutants_lived: number
117
+ * mutants_not_viable: number
118
+ * mutants_not_covered: number
119
+ * files: [{ file_name: string, mutations: [{ status: "KILLED"|"LIVED"|... }] }]
120
+ */
121
+ private normalizeReport(parsed: Record<string, unknown>): MutationRunResult | null {
122
+ const mutationScore = asNumber(parsed.test_efficacy, NaN);
123
+ const nrOfMutants = asNumber(parsed.mutants_total, NaN);
124
+ const nrOfKilledMutants = asNumber(parsed.mutants_killed, 0);
125
+ const nrOfSurvivedMutants = asNumber(parsed.mutants_lived, 0) + asNumber(parsed.mutants_not_covered, 0);
126
+
127
+ if (isNaN(mutationScore) || isNaN(nrOfMutants)) return null;
128
+
129
+ const result: MutationRunResult = {
130
+ mutationScore,
131
+ nrOfMutants,
132
+ nrOfKilledMutants,
133
+ nrOfSurvivedMutants,
134
+ };
135
+
136
+ if (parsed.files && Array.isArray(parsed.files)) {
137
+ result.files = parseFilesArray(parsed.files as Array<Record<string, unknown>>);
138
+ }
139
+
140
+ return result;
141
+ }
142
+ }
143
+
144
+ function asNumber(value: unknown, fallback = 0): number {
145
+ return typeof value === 'number' ? value : fallback;
146
+ }
147
+
148
+ function parseFilesArray(
149
+ filesArr: Array<Record<string, unknown>>
150
+ ): Record<string, { mutationScore: number; nrOfMutants: number; nrOfKilledMutants: number; nrOfSurvivedMutants: number }> {
151
+ const files: Record<string, { mutationScore: number; nrOfMutants: number; nrOfKilledMutants: number; nrOfSurvivedMutants: number }> = {};
152
+ for (const entry of filesArr) {
153
+ const fileName = asString(entry.file_name);
154
+ if (!fileName) continue;
155
+ const mutations = Array.isArray(entry.mutations)
156
+ ? entry.mutations as Array<Record<string, unknown>>
157
+ : [];
158
+ const killed = mutations.filter(m => m.status === 'KILLED').length;
159
+ const lived = mutations.filter(m => m.status === 'LIVED').length;
160
+ const notCovered = mutations.filter(m => m.status === 'NOT_COVERED').length;
161
+ const total = mutations.length;
162
+
163
+ files[fileName] = {
164
+ mutationScore: total > 0 ? (killed / total) * 100 : 0,
165
+ nrOfMutants: total,
166
+ nrOfKilledMutants: killed,
167
+ nrOfSurvivedMutants: lived + notCovered,
168
+ };
169
+ }
170
+ return files;
171
+ }
172
+
173
+ function asString(value: unknown): string {
174
+ return typeof value === 'string' ? value : '';
175
+ }
@@ -11,13 +11,16 @@ export { registerRunner, resolveRunner, runnerRegistry } from './types';
11
11
 
12
12
  export { StrykerRunner } from './stryker-runner';
13
13
  export { MutmutRunner } from './mutmut-runner';
14
+ export { GoMutantRunner } from './go-mutant-runner';
14
15
 
15
16
  import { StrykerRunner } from './stryker-runner';
16
17
  import { MutmutRunner } from './mutmut-runner';
18
+ import { GoMutantRunner } from './go-mutant-runner';
17
19
  import { registerRunner } from './types';
18
20
 
19
21
  /** Auto-register all known runners. */
20
22
  export function registerAllRunners(): void {
21
23
  registerRunner(new StrykerRunner());
22
24
  registerRunner(new MutmutRunner());
25
+ registerRunner(new GoMutantRunner());
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
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"
@@ -15,7 +15,8 @@
15
15
  "adapter-common.sh",
16
16
  "principles/",
17
17
  "mutation/",
18
- "mock-policy/"
18
+ "mock-policy/",
19
+ "build-integrity/"
19
20
  ],
20
21
  "repository": {
21
22
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: 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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -12,7 +12,7 @@
12
12
  import { describe, it, before, after } from "node:test"
13
13
  import assert from "node:assert/strict"
14
14
  import { randomUUID } from "node:crypto"
15
- import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"
15
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs"
16
16
  import { join } from "node:path"
17
17
  import { tmpdir } from "node:os"
18
18
 
@@ -88,7 +88,7 @@ function makeSprintState(overrides: Record<string, unknown> = {}) {
88
88
  { phase: "2", status: "in_progress" as const, reqs: { "REQ-001": { name: "JWT auth", status: "completed" as const }, "REQ-002": { name: "OAuth2 flow", status: "in_progress" as const } } },
89
89
  ],
90
90
  }
91
- return { ...base, ...overrides } as any
91
+ return { ...base, ...overrides } as unknown as Parameters<typeof renderSprintSidebar>[0]
92
92
  }
93
93
 
94
94
  // ── isStale ──
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: 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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -38,25 +38,6 @@ function parseTime(value: unknown): number {
38
38
  return new Date(value as string).getTime();
39
39
  }
40
40
 
41
- function maxValid(current: number, candidate: unknown): number {
42
- if (!candidate) return current;
43
- const t = parseTime(candidate);
44
- return !isNaN(t) && t > current ? t : current;
45
- }
46
-
47
- function getLatestTimestamp(state: Record<string, unknown> | null): number {
48
- if (!state || !state.started_at) return 0;
49
- const started = parseTime(state.started_at);
50
- if (isNaN(started)) return 0;
51
- let latest = started;
52
- if (Array.isArray(state.phase_history)) {
53
- for (const ph of state.phase_history) {
54
- latest = maxValid(maxValid(latest, (ph as Record<string, unknown>).completed_at), (ph as Record<string, unknown>).started_at);
55
- }
56
- }
57
- return latest;
58
- }
59
-
60
41
  function isStale(state: SprintState | null): boolean {
61
42
  if (!state || !state.started_at) return false;
62
43
  const latest = sprintTimestamp(state);
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.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
3
  **Generated:** 2026-06-22
4
- **Commit:** dcb5154
4
+ **Commit:** 1cc434d
5
5
  **Branch:** main
6
- **Version:** 0.10.2.0
6
+ **Version:** 0.10.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.