@boyingliu01/xp-gate 0.14.22 → 0.14.24

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 (40) hide show
  1. package/bin/xp-gate.js +8 -0
  2. package/lib/__tests__/init.test.js +77 -0
  3. package/lib/__tests__/phase-transition.test.js +174 -0
  4. package/lib/init.js +48 -0
  5. package/lib/phase-transition.js +211 -0
  6. package/mock-policy/AGENTS.md +3 -3
  7. package/mutation/AGENTS.md +3 -3
  8. package/package.json +3 -2
  9. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  11. package/plugins/claude-code/skills/delphi-review/SKILL.md +24 -4
  12. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  13. package/plugins/claude-code/skills/sprint-flow/SKILL.md +20 -5
  14. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +11 -16
  15. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  16. package/plugins/opencode/package.json +1 -1
  17. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  18. package/plugins/opencode/skills/delphi-review/SKILL.md +24 -4
  19. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  20. package/plugins/opencode/skills/sprint-flow/SKILL.md +20 -5
  21. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +11 -16
  22. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/qoder/agents/delphi-architecture.md +48 -0
  24. package/plugins/qoder/agents/delphi-feasibility.md +48 -0
  25. package/plugins/qoder/agents/delphi-technical.md +48 -0
  26. package/plugins/qoder/plugin.json +1 -1
  27. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  28. package/plugins/qoder/skills/delphi-review/SKILL.md +276 -167
  29. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/principles/AGENTS.md +3 -3
  32. package/scripts/__tests__/sync-package-content.test.js +491 -0
  33. package/scripts/delphi-external-review.cjs +432 -0
  34. package/scripts/sync-package-content.js +429 -0
  35. package/skills/delphi-review/AGENTS.md +3 -3
  36. package/skills/delphi-review/SKILL.md +24 -4
  37. package/skills/sprint-flow/AGENTS.md +3 -3
  38. package/skills/sprint-flow/SKILL.md +20 -5
  39. package/skills/sprint-flow/references/orchestration-rules.md +11 -16
  40. 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 };
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-21
4
- **Commit:** 03d6f26
3
+ **Generated:** 2026-07-22
4
+ **Commit:** e331f2c
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.24.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-07-21
4
- **Commit:** 03d6f26
3
+ **Generated:** 2026-07-22
4
+ **Commit:** e331f2c
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.24.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.22",
3
+ "version": "0.14.24",
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"
@@ -17,6 +17,7 @@
17
17
  "mutation/",
18
18
  "mock-policy/",
19
19
  "build-integrity/",
20
+ "scripts/",
20
21
  "gate-3.sh",
21
22
  "gate-4.sh",
22
23
  "gate-7.sh",
@@ -38,7 +39,7 @@
38
39
  "node": ">=18.0.0"
39
40
  },
40
41
  "scripts": {
41
- "version": "0.14.22",
42
+ "version": "0.14.24",
42
43
  "prepack": "node scripts/sync-package-content.js",
43
44
  "test": "echo \"Run tests\" && exit 0"
44
45
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.14.22",
3
+ "version": "0.14.24",
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-21
4
- **Commit:** 03d6f26
3
+ **Generated:** 2026-07-22
4
+ **Commit:** e331f2c
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.24.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
- ### 模型选择策略(强制 — 从 opencode.json 读取)
298
-
299
- **MUST 从 `opencode.json` 的 agent 配置中读取模型**,**严禁** hardcode 模型名称。
297
+ ### 模型选择策略(强制 — 平台适配)
300
298
 
301
299
  **关键原则**:
302
300
  - ✅ 三个专家必须来自 **至少 2 家不同 provider**
303
- - ❌ 禁止 hardcode 模型名称(模型列表以 `opencode.json` 为准)
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-21
4
- **Commit:** 03d6f26
3
+ **Generated:** 2026-07-22
4
+ **Commit:** e331f2c
5
5
  **Branch:** main
6
- **Version:** 0.14.22.0
6
+ **Version:** 0.14.24.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.