@boyingliu01/xp-gate 0.14.23 → 0.14.25
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/bin/xp-gate.js +8 -0
- package/lib/__tests__/init.test.js +77 -0
- package/lib/__tests__/phase-transition.test.js +174 -0
- package/lib/init.js +48 -0
- package/lib/phase-transition.js +211 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +2 -2
- 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 +24 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +20 -5
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +11 -16
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- 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 +24 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +20 -5
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +11 -16
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/agents/delphi-architecture.md +48 -0
- package/plugins/qoder/agents/delphi-feasibility.md +48 -0
- package/plugins/qoder/agents/delphi-technical.md +48 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +276 -167
- 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/scripts/delphi-external-review.cjs +15 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +24 -4
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +20 -5
- package/skills/sprint-flow/references/orchestration-rules.md +11 -16
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/bin/xp-gate.js
CHANGED
|
@@ -195,6 +195,14 @@ const COMMANDS = {
|
|
|
195
195
|
description: 'Run all invokable quality gates',
|
|
196
196
|
run: subargs => check(['--all', ...subargs]).then(code => process.exit(code)),
|
|
197
197
|
usage: 'xp-gate gate-all [<path>]'
|
|
198
|
+
},
|
|
199
|
+
'phase-transition': {
|
|
200
|
+
description: 'Transition sprint phase + auto-render dashboard (resolves #338)',
|
|
201
|
+
run: subargs => {
|
|
202
|
+
const { handlePhaseTransition } = require('../lib/phase-transition.js');
|
|
203
|
+
handlePhaseTransition(subargs).then(code => process.exit(code));
|
|
204
|
+
},
|
|
205
|
+
usage: 'xp-gate phase-transition <phase> <status> [--render] [--outputs <json>]'
|
|
198
206
|
}
|
|
199
207
|
};
|
|
200
208
|
|
|
@@ -370,4 +370,81 @@ describe('init', () => {
|
|
|
370
370
|
const result = await init(['--core-only']);
|
|
371
371
|
expect(result).toBe(0);
|
|
372
372
|
});
|
|
373
|
+
|
|
374
|
+
describe('configureQoderDelphiAgents', () => {
|
|
375
|
+
function qoderSkillsDir() {
|
|
376
|
+
return path.join(tmpHome, '.qoder', 'skills');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function setupQoderPlatform() {
|
|
380
|
+
fs.mkdirSync(qoderSkillsDir(), { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function agentTemplateDir() {
|
|
384
|
+
// srcDir = path.dirname(__dirname) = src/npm-package/lib -> src/npm-package
|
|
385
|
+
return path.join(__dirname, '..', '..', 'plugins', 'qoder', 'agents');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
it('deploys Delphi agents when platform is Qoder and agents do not exist', async () => {
|
|
389
|
+
fs.mkdirSync(path.join(tmpProject, '.git', 'hooks'), { recursive: true });
|
|
390
|
+
mockExecSuccess();
|
|
391
|
+
setupQoderPlatform();
|
|
392
|
+
|
|
393
|
+
// Ensure agent templates exist in the npm package
|
|
394
|
+
const templateDir = agentTemplateDir();
|
|
395
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
396
|
+
fs.writeFileSync(path.join(templateDir, 'delphi-architecture.md'), '---\nname: test\n---\narch expert');
|
|
397
|
+
|
|
398
|
+
vi.resetModules();
|
|
399
|
+
delete require.cache[require.resolve('../init')];
|
|
400
|
+
delete require.cache[require.resolve('../detect-deps.js')];
|
|
401
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
402
|
+
|
|
403
|
+
const { init } = require('../init');
|
|
404
|
+
const result = await init(['--core-only']);
|
|
405
|
+
expect(result).toBe(0);
|
|
406
|
+
|
|
407
|
+
const deployedAgent = path.join(tmpProject, '.qoder', 'agents', 'delphi-architecture.md');
|
|
408
|
+
expect(fs.existsSync(deployedAgent)).toBe(true);
|
|
409
|
+
expect(fs.readFileSync(deployedAgent, 'utf8')).toContain('arch expert');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('does not overwrite existing agent files (preserves user customizations)', async () => {
|
|
413
|
+
fs.mkdirSync(path.join(tmpProject, '.git', 'hooks'), { recursive: true });
|
|
414
|
+
mockExecSuccess();
|
|
415
|
+
setupQoderPlatform();
|
|
416
|
+
|
|
417
|
+
// Pre-existing user-customized agent
|
|
418
|
+
const agentsDir = path.join(tmpProject, '.qoder', 'agents');
|
|
419
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
420
|
+
fs.writeFileSync(path.join(agentsDir, 'delphi-architecture.md'), 'CUSTOM USER CONTENT');
|
|
421
|
+
|
|
422
|
+
// Template in npm package
|
|
423
|
+
const templateDir = agentTemplateDir();
|
|
424
|
+
fs.mkdirSync(templateDir, { recursive: true });
|
|
425
|
+
fs.writeFileSync(path.join(templateDir, 'delphi-architecture.md'), 'TEMPLATE CONTENT');
|
|
426
|
+
|
|
427
|
+
vi.resetModules();
|
|
428
|
+
delete require.cache[require.resolve('../init')];
|
|
429
|
+
delete require.cache[require.resolve('../detect-deps.js')];
|
|
430
|
+
delete require.cache[require.resolve('../shared-paths')];
|
|
431
|
+
|
|
432
|
+
const { init } = require('../init');
|
|
433
|
+
await init(['--core-only']);
|
|
434
|
+
|
|
435
|
+
// User content preserved
|
|
436
|
+
expect(fs.readFileSync(path.join(agentsDir, 'delphi-architecture.md'), 'utf8')).toBe('CUSTOM USER CONTENT');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('skips deployment when platform is not Qoder', async () => {
|
|
440
|
+
fs.mkdirSync(path.join(tmpProject, '.git', 'hooks'), { recursive: true });
|
|
441
|
+
mockExecSuccess();
|
|
442
|
+
// Default platform is 'opencode' (no .qoder/skills dir)
|
|
443
|
+
|
|
444
|
+
const { init } = require('../init');
|
|
445
|
+
await init(['--core-only']);
|
|
446
|
+
|
|
447
|
+
expect(fs.existsSync(path.join(tmpProject, '.qoder', 'agents'))).toBe(false);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
373
450
|
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for phase-transition CLI command
|
|
3
|
+
* @test REQ-338 Phase transition CLI + dashboard rendering
|
|
4
|
+
* @intent Verify phase-transition command updates state and renders dashboard
|
|
5
|
+
* @covers AC-338-01 (CLI transitions phase and writes state)
|
|
6
|
+
* @covers AC-338-02 (--render flag outputs ASCII dashboard)
|
|
7
|
+
* @covers AC-338-03 (invalid inputs return error exit code)
|
|
8
|
+
* @covers AC-338-04 (--outputs flag records outputs in state)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import { handlePhaseTransition, renderDashboard } from '../phase-transition.js';
|
|
16
|
+
|
|
17
|
+
describe('phase-transition', () => {
|
|
18
|
+
let tmpDir;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-transition-test-'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('handlePhaseTransition()', () => {
|
|
29
|
+
it('returns 0 and shows help with --help', async () => {
|
|
30
|
+
const code = await handlePhaseTransition(['--help']);
|
|
31
|
+
expect(code).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns 1 for missing arguments', async () => {
|
|
35
|
+
const code = await handlePhaseTransition([]);
|
|
36
|
+
expect(code).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns 1 for invalid phase number', async () => {
|
|
40
|
+
const code = await handlePhaseTransition(['7', 'completed']);
|
|
41
|
+
expect(code).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns 1 for invalid status', async () => {
|
|
45
|
+
const code = await handlePhaseTransition(['1', 'invalid_status']);
|
|
46
|
+
expect(code).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('transitions phase and writes state file', async () => {
|
|
50
|
+
const code = await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
51
|
+
expect(code).toBe(0);
|
|
52
|
+
|
|
53
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
54
|
+
expect(fs.existsSync(stateFile)).toBe(true);
|
|
55
|
+
|
|
56
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
57
|
+
expect(state.phase).toBe(1);
|
|
58
|
+
expect(state.phase_history).toHaveLength(1);
|
|
59
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
60
|
+
expect(state.phase_history[0].phase_name).toBe('PREP');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('records outputs when --outputs flag is provided', async () => {
|
|
64
|
+
const outputs = JSON.stringify({ spec: 'path/to/spec.yaml' });
|
|
65
|
+
const code = await handlePhaseTransition(['2', 'completed', '--outputs', outputs, '--dir', tmpDir]);
|
|
66
|
+
expect(code).toBe(0);
|
|
67
|
+
|
|
68
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
69
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
70
|
+
expect(state.outputs.spec).toBe('path/to/spec.yaml');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns 1 for invalid --outputs JSON', async () => {
|
|
74
|
+
const code = await handlePhaseTransition(['1', 'completed', '--outputs', 'not-json', '--dir', tmpDir]);
|
|
75
|
+
expect(code).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('transitions through multiple phases sequentially', async () => {
|
|
79
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
80
|
+
await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
|
|
81
|
+
await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
|
|
82
|
+
await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
|
|
83
|
+
|
|
84
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
85
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
86
|
+
|
|
87
|
+
expect(state.phase).toBe(3);
|
|
88
|
+
expect(state.phase_history).toHaveLength(3);
|
|
89
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
90
|
+
expect(state.phase_history[1].status).toBe('completed');
|
|
91
|
+
expect(state.phase_history[2].status).toBe('in_progress');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('updates existing entry when same phase is transitioned again', async () => {
|
|
95
|
+
await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
|
|
96
|
+
await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
|
|
97
|
+
|
|
98
|
+
const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
|
|
99
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
100
|
+
|
|
101
|
+
// Should NOT have duplicate entries
|
|
102
|
+
expect(state.phase_history).toHaveLength(1);
|
|
103
|
+
expect(state.phase_history[0].status).toBe('completed');
|
|
104
|
+
expect(state.phase_history[0].completed_at).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('renderDashboard()', () => {
|
|
109
|
+
it('returns "No sprint state found" for null state', () => {
|
|
110
|
+
const result = renderDashboard(null);
|
|
111
|
+
expect(result).toBe('No sprint state found');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders dashboard with all 6 phases', () => {
|
|
115
|
+
const state = {
|
|
116
|
+
id: 'sprint-test-123',
|
|
117
|
+
task_description: 'Test feature',
|
|
118
|
+
phase: 3,
|
|
119
|
+
status: 'in_progress',
|
|
120
|
+
started_at: '2026-07-21T10:00:00Z',
|
|
121
|
+
phase_history: [
|
|
122
|
+
{ phase: 1, phase_name: 'PREP', status: 'completed', duration_seconds: 120 },
|
|
123
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'completed', duration_seconds: 300 },
|
|
124
|
+
{ phase: 3, phase_name: 'BUILD', status: 'in_progress' },
|
|
125
|
+
],
|
|
126
|
+
isolation: { branch: 'sprint/test-branch' },
|
|
127
|
+
outputs: {},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = renderDashboard(state);
|
|
131
|
+
|
|
132
|
+
expect(result).toContain('SPRINT PROGRESS');
|
|
133
|
+
expect(result).toContain('sprint-test-123');
|
|
134
|
+
expect(result).toContain('Test feature');
|
|
135
|
+
expect(result).toContain('sprint/test-branch');
|
|
136
|
+
expect(result).toContain('Phase 1/6');
|
|
137
|
+
expect(result).toContain('Phase 6/6');
|
|
138
|
+
expect(result).toContain('✅'); // completed phases
|
|
139
|
+
expect(result).toContain('🔄'); // in_progress phase
|
|
140
|
+
expect(result).toContain('⏳'); // pending phases
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('shows outputs section when outputs exist', () => {
|
|
144
|
+
const state = {
|
|
145
|
+
id: 'sprint-out',
|
|
146
|
+
task_description: 'Feature',
|
|
147
|
+
phase: 2,
|
|
148
|
+
status: 'in_progress',
|
|
149
|
+
started_at: '2026-07-21T10:00:00Z',
|
|
150
|
+
phase_history: [],
|
|
151
|
+
isolation: { branch: 'test' },
|
|
152
|
+
outputs: { spec: 'design.yaml', pr: '#42' },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = renderDashboard(state);
|
|
156
|
+
expect(result).toContain('输出物');
|
|
157
|
+
expect(result).toContain('spec: design.yaml');
|
|
158
|
+
expect(result).toContain('pr: #42');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('handles minimal state gracefully', () => {
|
|
162
|
+
const state = {
|
|
163
|
+
id: 'sprint-min',
|
|
164
|
+
phase: 1,
|
|
165
|
+
status: 'in_progress',
|
|
166
|
+
phase_history: [],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = renderDashboard(state);
|
|
170
|
+
expect(result).toContain('SPRINT PROGRESS');
|
|
171
|
+
expect(result).toContain('sprint-min');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
package/lib/init.js
CHANGED
|
@@ -402,6 +402,7 @@ async function installLocal(args) {
|
|
|
402
402
|
|
|
403
403
|
injectKarpathyPrinciples(projectRoot);
|
|
404
404
|
configureOpenCodePlugin(srcDir, projectRoot);
|
|
405
|
+
configureQoderDelphiAgents(srcDir, projectRoot);
|
|
405
406
|
|
|
406
407
|
// Auto-register TUI plugin globally (idempotent)
|
|
407
408
|
try {
|
|
@@ -503,6 +504,53 @@ async function setupGlobal(args) {
|
|
|
503
504
|
return 0;
|
|
504
505
|
}
|
|
505
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Deploy Qoder-native Delphi review agents when platform is Qoder.
|
|
509
|
+
* Copies agent templates from the bundled qoder plugin to the project's
|
|
510
|
+
* .qoder/agents/ directory. Idempotent — never overwrites existing files.
|
|
511
|
+
*
|
|
512
|
+
* @param {string} srcDir - npm package source directory
|
|
513
|
+
* @param {string} projectRoot - user's project root
|
|
514
|
+
*/
|
|
515
|
+
function configureQoderDelphiAgents(srcDir, projectRoot) {
|
|
516
|
+
const platform = detectPlatform();
|
|
517
|
+
if (platform !== 'qoder') {
|
|
518
|
+
return; // Not Qoder — skip (OpenCode uses .delphi-config.json instead)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const agentSrcDir = path.join(srcDir, 'plugins', 'qoder', 'agents');
|
|
522
|
+
if (!fs.existsSync(agentSrcDir)) {
|
|
523
|
+
console.log(' Qoder Delphi agents: SKIP (templates not bundled)');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const agentsDestDir = path.join(projectRoot, '.qoder', 'agents');
|
|
528
|
+
fs.mkdirSync(agentsDestDir, { recursive: true });
|
|
529
|
+
|
|
530
|
+
const agentFiles = fs.readdirSync(agentSrcDir).filter(f => f.endsWith('.md'));
|
|
531
|
+
let deployed = 0;
|
|
532
|
+
let skipped = 0;
|
|
533
|
+
for (const file of agentFiles) {
|
|
534
|
+
const destFile = path.join(agentsDestDir, file);
|
|
535
|
+
if (fs.existsSync(destFile)) {
|
|
536
|
+
skipped++;
|
|
537
|
+
continue; // Don't overwrite user customizations
|
|
538
|
+
}
|
|
539
|
+
fs.copyFileSync(path.join(agentSrcDir, file), destFile);
|
|
540
|
+
deployed++;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (deployed > 0) {
|
|
544
|
+
console.log(` Qoder Delphi agents: deployed ${deployed} agent(s) to .qoder/agents/`);
|
|
545
|
+
}
|
|
546
|
+
if (skipped > 0) {
|
|
547
|
+
console.log(` Qoder Delphi agents: ${skipped} existing agent(s) preserved`);
|
|
548
|
+
}
|
|
549
|
+
if (deployed === 0 && skipped === 0) {
|
|
550
|
+
console.log(' Qoder Delphi agents: no agent templates found');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
506
554
|
function configureOpenCodePlugin(srcDir, projectRoot) {
|
|
507
555
|
const opencodeJsonPath = path.join(projectRoot, 'opencode.json');
|
|
508
556
|
const pluginSrcPath = path.join(srcDir, 'plugins', 'opencode');
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xp-gate phase-transition — Programmatic sprint phase transition
|
|
3
|
+
*
|
|
4
|
+
* Resolves #338 (dashboard never auto-renders) and #146 (sprint-state enforcement).
|
|
5
|
+
*
|
|
6
|
+
* This CLI command is the programmatic entry point for sprint phase transitions.
|
|
7
|
+
* The sprint-flow SKILL.md MUST instruct the orchestrator to call this command
|
|
8
|
+
* after each phase completion, replacing text-level MUST instructions with
|
|
9
|
+
* a concrete CLI invocation.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* xp-gate phase-transition <phase> <status> [--outputs <json>] [--render]
|
|
13
|
+
*
|
|
14
|
+
* Examples:
|
|
15
|
+
* xp-gate phase-transition 1 completed
|
|
16
|
+
* xp-gate phase-transition 2 in_progress
|
|
17
|
+
* xp-gate phase-transition 3 completed --outputs '{"spec":"path/to/spec.yaml"}'
|
|
18
|
+
* xp-gate phase-transition 3 completed --render (auto-render dashboard)
|
|
19
|
+
*
|
|
20
|
+
* @module phase-transition
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { SprintStateManager } = require('./sprint-state-manager');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render ASCII dashboard from sprint state.
|
|
30
|
+
* Reuses the formatSprintTable logic from sprint-status.
|
|
31
|
+
* @param {object} state - Sprint state object
|
|
32
|
+
* @returns {string} Rendered dashboard
|
|
33
|
+
*/
|
|
34
|
+
function renderDashboard(state) {
|
|
35
|
+
if (!state) return 'No sprint state found';
|
|
36
|
+
|
|
37
|
+
const PHASE_NAMES = {
|
|
38
|
+
'1': 'PREP', '2': 'DESIGN', '3': 'BUILD',
|
|
39
|
+
'4': 'VERIFY', '5': 'SHIP', '6': 'CLOSE',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function statusIcon(status) {
|
|
43
|
+
switch (status) {
|
|
44
|
+
case 'completed': return '✅';
|
|
45
|
+
case 'in_progress': return '🔄';
|
|
46
|
+
case 'skipped': return '⏭️';
|
|
47
|
+
case 'failed': return '❌';
|
|
48
|
+
case 'paused': return '⏸️';
|
|
49
|
+
default: return '⏳';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatDuration(seconds) {
|
|
54
|
+
if (seconds == null || seconds === 0) return '-';
|
|
55
|
+
if (seconds < 60) return `${seconds}s`;
|
|
56
|
+
const mins = Math.floor(seconds / 60);
|
|
57
|
+
if (mins < 60) return `${mins}m`;
|
|
58
|
+
const hrs = Math.floor(mins / 60);
|
|
59
|
+
if (hrs < 24) return `${hrs}h ${mins % 60}m`;
|
|
60
|
+
return `${Math.floor(hrs / 24)}d ${hrs % 24}h`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const branch = state.isolation?.branch || 'unknown';
|
|
64
|
+
const sprintId = state.id || 'unknown';
|
|
65
|
+
const taskDesc = state.task_description || state.issue_title || '-';
|
|
66
|
+
const overallStatus = state.status || 'running';
|
|
67
|
+
const startedAt = state.started_at ? state.started_at.slice(0, 16).replace('T', ' ') : '-';
|
|
68
|
+
|
|
69
|
+
// Build phase history lookup
|
|
70
|
+
const historyLookup = {};
|
|
71
|
+
if (Array.isArray(state.phase_history)) {
|
|
72
|
+
for (const ph of state.phase_history) {
|
|
73
|
+
historyLookup[String(ph.phase)] = ph;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentPhase = state.phase || 1;
|
|
78
|
+
const currentPhaseName = PHASE_NAMES[String(currentPhase)] || `Phase ${currentPhase}`;
|
|
79
|
+
|
|
80
|
+
// Progress bar
|
|
81
|
+
const completedCount = Object.values(historyLookup).filter(h => h.status === 'completed').length;
|
|
82
|
+
const pct = Math.round((completedCount / 6) * 100);
|
|
83
|
+
const filled = '█'.repeat(completedCount);
|
|
84
|
+
const current = currentPhase <= 6 ? '▓' : '';
|
|
85
|
+
const empty = '░'.repeat(Math.max(0, 6 - completedCount - (current ? 1 : 0)));
|
|
86
|
+
const progressBar = `[${filled}${current}${empty}] ${pct}%`;
|
|
87
|
+
|
|
88
|
+
// Build lines
|
|
89
|
+
const lines = [];
|
|
90
|
+
lines.push('+============================================================+');
|
|
91
|
+
lines.push(`| SPRINT PROGRESS ${sprintId.padEnd(20)}|`);
|
|
92
|
+
lines.push('+============================================================+');
|
|
93
|
+
lines.push(`| 需求: ${(taskDesc).slice(0, 54).padEnd(54)}|`);
|
|
94
|
+
lines.push(`| 分支: ${(branch).slice(0, 54).padEnd(54)}|`);
|
|
95
|
+
lines.push(`| 状态: ${(overallStatus).slice(0, 15).padEnd(15)} 启动: ${startedAt.slice(0, 19).padEnd(19)}|`);
|
|
96
|
+
lines.push('+============================================================+');
|
|
97
|
+
lines.push('| |');
|
|
98
|
+
|
|
99
|
+
for (let i = 1; i <= 6; i++) {
|
|
100
|
+
const entry = historyLookup[String(i)];
|
|
101
|
+
const icon = statusIcon(entry?.status || (i < currentPhase ? 'completed' : i === currentPhase ? 'in_progress' : 'pending'));
|
|
102
|
+
const name = (PHASE_NAMES[String(i)] || `Phase ${i}`).padEnd(10);
|
|
103
|
+
const dur = formatDuration(entry?.duration_seconds).padEnd(8);
|
|
104
|
+
lines.push(`| ${icon} Phase ${i}/6 ${name} ${dur} |`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push('| |');
|
|
108
|
+
lines.push(`| ${progressBar.padEnd(58)}|`);
|
|
109
|
+
lines.push('+============================================================+');
|
|
110
|
+
lines.push(`| > 当前: Phase ${currentPhase}/6 ${currentPhaseName}`.padEnd(59) + '|');
|
|
111
|
+
lines.push(`| 状态: ${(entry_status(currentPhase, historyLookup)).padEnd(50)}|`);
|
|
112
|
+
lines.push('+============================================================+');
|
|
113
|
+
|
|
114
|
+
// Outputs section
|
|
115
|
+
if (state.outputs && Object.keys(state.outputs).length > 0) {
|
|
116
|
+
lines.push('| 输出物: |');
|
|
117
|
+
for (const [key, val] of Object.entries(state.outputs)) {
|
|
118
|
+
lines.push(`| ${(`${key}: ${val}`).slice(0, 55).padEnd(55)}|`);
|
|
119
|
+
}
|
|
120
|
+
lines.push('+============================================================+');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
|
|
125
|
+
function entry_status(phaseNum, lookup) {
|
|
126
|
+
const e = lookup[String(phaseNum)];
|
|
127
|
+
return e?.status || (phaseNum < currentPhase ? 'completed' : phaseNum === currentPhase ? 'in_progress' : 'pending');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* CLI entry point for phase-transition command.
|
|
133
|
+
* @param {string[]} args - CLI arguments
|
|
134
|
+
* @returns {Promise<number>} Exit code
|
|
135
|
+
*/
|
|
136
|
+
async function handlePhaseTransition(args = []) {
|
|
137
|
+
if (args.includes('--help')) {
|
|
138
|
+
console.log('Usage: xp-gate phase-transition <phase> <status> [options]');
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('Arguments:');
|
|
141
|
+
console.log(' phase Phase number (1-6)');
|
|
142
|
+
console.log(' status Status: in_progress | completed | skipped | failed | paused');
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('Options:');
|
|
145
|
+
console.log(' --outputs <json> JSON object of outputs to record');
|
|
146
|
+
console.log(' --render Render ASCII dashboard after transition');
|
|
147
|
+
console.log(' --dir <path> Project directory (default: cwd)');
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('Examples:');
|
|
150
|
+
console.log(' xp-gate phase-transition 1 completed');
|
|
151
|
+
console.log(' xp-gate phase-transition 3 completed --render');
|
|
152
|
+
console.log(' xp-gate phase-transition 2 in_progress --outputs \'{"spec":"path.yaml"}\'');
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (args.length < 2) {
|
|
157
|
+
console.error('Error: Missing required arguments. Usage: xp-gate phase-transition <phase> <status>');
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const phase = parseInt(args[0], 10);
|
|
162
|
+
const status = args[1];
|
|
163
|
+
const renderFlag = args.includes('--render');
|
|
164
|
+
const dirIdx = args.indexOf('--dir');
|
|
165
|
+
const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
|
|
166
|
+
|
|
167
|
+
const outputsIdx = args.indexOf('--outputs');
|
|
168
|
+
let outputs = {};
|
|
169
|
+
if (outputsIdx >= 0 && args[outputsIdx + 1]) {
|
|
170
|
+
try {
|
|
171
|
+
outputs = JSON.parse(args[outputsIdx + 1]);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`Error: Invalid JSON for --outputs: ${err.message}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate inputs
|
|
179
|
+
if (phase < 1 || phase > 6) {
|
|
180
|
+
console.error(`Error: Phase must be 1-6, got ${phase}`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const validStatuses = ['in_progress', 'completed', 'skipped', 'failed', 'paused'];
|
|
185
|
+
if (!validStatuses.includes(status)) {
|
|
186
|
+
console.error(`Error: Status must be one of: ${validStatuses.join(', ')}`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Execute transition
|
|
191
|
+
try {
|
|
192
|
+
const manager = new SprintStateManager(projectDir);
|
|
193
|
+
const newState = manager.transitionPhase(phase, status, { outputs });
|
|
194
|
+
|
|
195
|
+
console.log(`✅ Phase ${phase} transitioned to '${status}'`);
|
|
196
|
+
|
|
197
|
+
// Auto-render dashboard if --render flag is set
|
|
198
|
+
if (renderFlag) {
|
|
199
|
+
console.log('');
|
|
200
|
+
const dashboard = renderDashboard(newState);
|
|
201
|
+
console.log(dashboard);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return 0;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error(`Error: Phase transition failed: ${err.message}`);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { handlePhaseTransition, renderDashboard };
|
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-22
|
|
4
|
+
**Commit:** 97dd89a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.25.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.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-22
|
|
4
|
+
**Commit:** 97dd89a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.25.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.
|
|
3
|
+
"version": "0.14.25",
|
|
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"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=18.0.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"version": "0.14.
|
|
42
|
+
"version": "0.14.25",
|
|
43
43
|
"prepack": "node scripts/sync-package-content.js",
|
|
44
44
|
"test": "echo \"Run tests\" && exit 0"
|
|
45
45
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.25",
|
|
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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-22
|
|
4
|
+
**Commit:** 97dd89a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.25.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.
|
|
@@ -294,14 +294,34 @@ Each round MUST output a structured round marker:
|
|
|
294
294
|
| 2 专家(默认) | A(架构) + B(实现) | 代码变更、小型设计 |
|
|
295
295
|
| 3 专家 | A(架构) + B(实现) + C(可行性) | 架构决策、需求文档 |
|
|
296
296
|
|
|
297
|
-
### 模型选择策略(强制 —
|
|
298
|
-
|
|
299
|
-
**MUST 从 `opencode.json` 的 agent 配置中读取模型**,**严禁** hardcode 模型名称。
|
|
297
|
+
### 模型选择策略(强制 — 平台适配)
|
|
300
298
|
|
|
301
299
|
**关键原则**:
|
|
302
300
|
- ✅ 三个专家必须来自 **至少 2 家不同 provider**
|
|
303
|
-
- ❌ 禁止 hardcode
|
|
301
|
+
- ❌ 禁止 hardcode 模型名称
|
|
304
302
|
- ❌ 禁止三个专家全部使用同一 provider 的模型
|
|
303
|
+
- ❌ 禁止使用 Anthropic/OpenAI/Google 等国外模型
|
|
304
|
+
|
|
305
|
+
#### Qoder 平台(推荐 — Custom Agent 模式)
|
|
306
|
+
|
|
307
|
+
Qoder 环境下使用 **Custom Agent** 机制,每个专家是一个独立的 custom agent,配置不同的 Qoder 内置模型:
|
|
308
|
+
|
|
309
|
+
| 专家 | Agent 文件 | 模型 | Credits 费率 |
|
|
310
|
+
|------|-----------|------|-------------|
|
|
311
|
+
| Architecture (A) | `.qoder/agents/delphi-architecture.md` | Qwen3.7-Max | 0.5× |
|
|
312
|
+
| Technical (B) | `.qoder/agents/delphi-technical.md` | GLM-5.2 | 0.6× |
|
|
313
|
+
| Feasibility (C) | `.qoder/agents/delphi-feasibility.md` | DeepSeek-V4-Pro | 0.5× |
|
|
314
|
+
|
|
315
|
+
**执行方式**:通过 Agent tool 并行启动 3 个 subagent(type=GeneralPurpose),每个 agent 使用自己配置的模型独立评审,主 orchestrator 收集结果后计算共识。
|
|
316
|
+
|
|
317
|
+
**优势**:无需外部 API key,直接使用 Qoder Credits,模型由平台统一管理。
|
|
318
|
+
|
|
319
|
+
#### OpenCode 平台(External API 模式)
|
|
320
|
+
|
|
321
|
+
OpenCode 环境下通过 `opencode.json` 的 agent 配置 + `.delphi-config.json` 调用外部 API:
|
|
322
|
+
- **MUST 从 `opencode.json` 的 agent 配置中读取模型**
|
|
323
|
+
- 通过 `scripts/delphi-external-review.cjs` 调用各 provider 的 OpenAI 兼容 API
|
|
324
|
+
- 需要用户自行配置 API key(环境变量注入)
|
|
305
325
|
|
|
306
326
|
### 共识阈值
|
|
307
327
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-07-22
|
|
4
|
+
**Commit:** 97dd89a
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.14.
|
|
6
|
+
**Version:** 0.14.25.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.
|