@boyingliu01/xp-gate 0.9.3 → 0.9.5

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 (38) hide show
  1. package/lib/__tests__/baseline.test.js +89 -0
  2. package/lib/__tests__/doctor-helpers.test.js +139 -0
  3. package/lib/__tests__/upgrade-exec.test.js +42 -33
  4. package/lib/baseline.js +75 -48
  5. package/lib/doctor.js +199 -96
  6. package/lib/install-skill.js +81 -62
  7. package/lib/shared-phase-constants.d.ts +17 -0
  8. package/lib/shared-phase-constants.js +77 -0
  9. package/lib/shared-utils.js +14 -3
  10. package/lib/sprint-status.js +75 -83
  11. package/lib/upgrade.js +128 -85
  12. package/mock-policy/AGENTS.md +3 -3
  13. package/mutation/AGENTS.md +3 -3
  14. package/package.json +1 -1
  15. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  17. package/plugins/claude-code/skills/delphi-review/SKILL.md +34 -0
  18. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  19. package/plugins/claude-code/skills/sprint-flow/SKILL.md +53 -1
  20. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  21. package/plugins/opencode/__tests__/xp-gate-update.test.ts +123 -4
  22. package/plugins/opencode/index.ts +27 -3
  23. package/plugins/opencode/package.json +1 -1
  24. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  25. package/plugins/opencode/skills/delphi-review/SKILL.md +34 -0
  26. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  27. package/plugins/opencode/skills/sprint-flow/SKILL.md +53 -1
  28. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  29. package/plugins/opencode/tui-plugin.ts +49 -76
  30. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  31. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  32. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  33. package/principles/AGENTS.md +3 -3
  34. package/skills/delphi-review/AGENTS.md +3 -3
  35. package/skills/delphi-review/SKILL.md +34 -0
  36. package/skills/sprint-flow/AGENTS.md +3 -3
  37. package/skills/sprint-flow/SKILL.md +53 -1
  38. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -3,6 +3,7 @@
3
3
  * No module-level state — safe for tests that mock fs/path/os.
4
4
  */
5
5
  const fs = require('fs');
6
+ const path = require('path');
6
7
 
7
8
  /**
8
9
  * Recursively copy a directory.
@@ -12,8 +13,8 @@ function copyDirRecursive(src, dest) {
12
13
  fs.mkdirSync(dest, { recursive: true });
13
14
  const entries = fs.readdirSync(src, { withFileTypes: true });
14
15
  for (const entry of entries) {
15
- const srcPath = require('path').join(src, entry.name);
16
- const destPath = require('path').join(dest, entry.name);
16
+ const srcPath = path.join(src, entry.name);
17
+ const destPath = path.join(dest, entry.name);
17
18
  if (entry.isDirectory()) {
18
19
  copyDirRecursive(srcPath, destPath);
19
20
  } else {
@@ -22,4 +23,14 @@ function copyDirRecursive(src, dest) {
22
23
  }
23
24
  }
24
25
 
25
- module.exports = { copyDirRecursive };
26
+ function readXpGateConfig() {
27
+ const cfgPath = path.join(require('os').homedir(), '.xp-gate', 'config.json');
28
+ try {
29
+ if (!fs.existsSync(cfgPath)) return null;
30
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ module.exports = { copyDirRecursive, readXpGateConfig };
@@ -9,22 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
-
13
- const PHASE_NAMES = {
14
- '-1': 'ISOLATE',
15
- '-0.5': 'AUTO-ESTIMATE',
16
- '0': 'THINK',
17
- '1': 'PLAN',
18
- '2': 'BUILD',
19
- '3': 'REVIEW',
20
- '4': 'USER ACCEPT',
21
- '5': 'FEEDBACK',
22
- '6': 'SHIP',
23
- '7': 'LAND',
24
- '8': 'CLEANUP',
25
- };
26
-
27
- const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
12
+ const { PHASE_NAMES, PHASE_ORDER, getLatestTimestamp, isStale } = require('./shared-phase-constants');
28
13
 
29
14
  /**
30
15
  * Read and parse sprint-state.json from a project directory.
@@ -70,29 +55,78 @@ function formatDuration(seconds) {
70
55
  }
71
56
 
72
57
  /**
73
- * Check if the state is stale (>1h since last activity).
58
+ * Format the sprint header lines (task description, ID, branch).
59
+ * @param {object} state - Sprint state object
60
+ * @param {string} branch - Branch name
61
+ * @returns {string[]} Header lines
62
+ */
63
+ function buildHeader(state, branch) {
64
+ return [
65
+ `Sprint: ${state.task_description}`,
66
+ `ID: ${state.id || 'unknown'} | Branch: ${branch}`,
67
+ ];
68
+ }
69
+
70
+ /**
71
+ * Format the metrics line (tests, coverage).
72
+ * @param {object} metrics - Sprint metrics object
73
+ * @returns {string|null} Metrics line or null if no metrics
74
+ */
75
+ function buildMetricsLine(metrics) {
76
+ const parts = [];
77
+ if (metrics.tests_passed != null) {
78
+ parts.push(`Tests: ${metrics.tests_passed} passed${metrics.tests_failed ? `, ${metrics.tests_failed} failed` : ''}`);
79
+ }
80
+ if (metrics.coverage_pct != null) {
81
+ parts.push(`Coverage: ${metrics.coverage_pct}%`);
82
+ }
83
+ return parts.length > 0 ? `Metrics: ${parts.join(' | ')}` : null;
84
+ }
85
+
86
+ /**
87
+ * Build a phase lookup map from phase_history array.
74
88
  * @param {object} state - Sprint state object
75
- * @returns {boolean}
89
+ * @returns {object} Map of phase key → phase history entry
76
90
  */
77
- function isStale(state) {
78
- if (!state || !state.started_at) return false;
79
- const started = new Date(state.started_at).getTime();
80
- if (isNaN(started)) return false;
81
- // Check latest phase_history timestamp (started_at or completed_at)
82
- let latest = started;
83
- if (Array.isArray(state.phase_history) && state.phase_history.length > 0) {
91
+ function buildPhaseLookup(state) {
92
+ const map = {};
93
+ if (Array.isArray(state.phase_history)) {
84
94
  for (const ph of state.phase_history) {
85
- if (ph.completed_at) {
86
- const t = new Date(ph.completed_at).getTime();
87
- if (!isNaN(t) && t > latest) latest = t;
88
- }
89
- if (ph.started_at) {
90
- const t = new Date(ph.started_at).getTime();
91
- if (!isNaN(t) && t > latest) latest = t;
92
- }
95
+ map[String(ph.phase)] = ph;
93
96
  }
94
97
  }
95
- return Date.now() - latest > 3600000; // 1h
98
+ return map;
99
+ }
100
+
101
+ /**
102
+ * Format a single phase row line.
103
+ * @param {string} key - Phase key (e.g. '0', '1')
104
+ * @param {object|undefined} history - Phase history entry
105
+ * @param {number} maxNameLen - Max phase name length for padding
106
+ * @returns {string} Formatted phase line
107
+ */
108
+ function formatPhaseLine(key, history, maxNameLen) {
109
+ const name = history?.phase_name || PHASE_NAMES[key] || key;
110
+ const status = history?.status || 'pending';
111
+ const icon = statusIcon(status);
112
+ const dur = formatDuration(history?.duration_seconds);
113
+ const statusLabel = status === 'completed' ? 'Completed' :
114
+ status === 'in_progress' ? 'In Progress' : 'Pending';
115
+ return ` Phase ${key.padStart(4)} ${name.padEnd(maxNameLen + 1)} ${icon} ${dur.padEnd(5)} ${statusLabel}`;
116
+ }
117
+
118
+ /**
119
+ * Format REQ-level progress sub-lines for a phase (typically BUILD).
120
+ * @param {object|undefined} history - Phase history entry with optional reqs
121
+ * @returns {string[]} Array of REQ progress lines (may be empty)
122
+ */
123
+ function formatReqLines(history) {
124
+ if (!history?.reqs) return [];
125
+ const lines = [];
126
+ for (const [reqId, req] of Object.entries(history.reqs)) {
127
+ lines.push(` ${statusIcon(req.status)} ${reqId} ${req.name}`);
128
+ }
129
+ return lines;
96
130
  }
97
131
 
98
132
  /**
@@ -105,77 +139,35 @@ function formatSprintTable(state) {
105
139
  return 'No active sprint in this directory';
106
140
  }
107
141
 
108
- const lines = [];
109
142
  const branch = state.isolation?.branch || 'unknown';
110
143
  const metrics = state.metrics || {};
144
+ const lines = buildHeader(state, branch);
111
145
 
112
- // Header
113
- lines.push(`Sprint: ${state.task_description}`);
114
- lines.push(`ID: ${state.id || 'unknown'} | Branch: ${branch}`);
115
-
116
- // Metrics line (if available)
117
- const metricParts = [];
118
- if (metrics.tests_passed != null) {
119
- metricParts.push(`Tests: ${metrics.tests_passed} passed${metrics.tests_failed ? `, ${metrics.tests_failed} failed` : ''}`);
120
- }
121
- if (metrics.coverage_pct != null) {
122
- metricParts.push(`Coverage: ${metrics.coverage_pct}%`);
123
- }
124
- if (metricParts.length > 0) {
125
- lines.push(`Metrics: ${metricParts.join(' | ')}`);
126
- }
146
+ const metricsLine = buildMetricsLine(metrics);
147
+ if (metricsLine) lines.push(metricsLine);
127
148
 
128
- // Stale warning
129
149
  if (isStale(state)) {
130
150
  lines.push('⚠️ State may be stale (last updated >1h ago)');
131
151
  }
132
-
133
152
  lines.push('');
134
153
 
135
- // Build phase lookup from phase_history
136
- const historyByPhase = {};
137
- if (Array.isArray(state.phase_history)) {
138
- for (const ph of state.phase_history) {
139
- historyByPhase[String(ph.phase)] = ph;
140
- }
141
- }
154
+ const historyByPhase = buildPhaseLookup(state);
142
155
 
143
- // Calculate column widths dynamically
144
156
  let maxNameLen = 'Phase'.length;
145
157
  for (const key of PHASE_ORDER) {
146
- const history = historyByPhase[key];
147
- const name = history?.phase_name || PHASE_NAMES[key] || key;
158
+ const name = historyByPhase[key]?.phase_name || PHASE_NAMES[key] || key;
148
159
  if (name.length > maxNameLen) maxNameLen = name.length;
149
160
  }
150
161
 
151
- // Separator line
152
162
  const sep = '─'.repeat(maxNameLen + 40);
153
-
154
163
  lines.push(sep);
155
164
 
156
165
  for (const key of PHASE_ORDER) {
157
- const history = historyByPhase[key];
158
- const name = history?.phase_name || PHASE_NAMES[key] || key;
159
- const status = history?.status || 'pending';
160
- const icon = statusIcon(status);
161
- const dur = formatDuration(history?.duration_seconds);
162
- const statusLabel = status === 'completed' ? 'Completed' :
163
- status === 'in_progress' ? 'In Progress' : 'Pending';
164
-
165
- const line = ` Phase ${key.padStart(4)} ${name.padEnd(maxNameLen + 1)} ${icon} ${dur.padEnd(5)} ${statusLabel}`;
166
- lines.push(line);
167
-
168
- // REQ-level progress for BUILD phase
169
- if (history?.reqs) {
170
- for (const [reqId, req] of Object.entries(history.reqs)) {
171
- const reqIcon = statusIcon(req.status);
172
- lines.push(` ${reqIcon} ${reqId} ${req.name}`);
173
- }
174
- }
166
+ lines.push(formatPhaseLine(key, historyByPhase[key], maxNameLen));
167
+ lines.push(...formatReqLines(historyByPhase[key]));
175
168
  }
176
169
 
177
170
  lines.push(sep);
178
-
179
171
  return lines.join('\n');
180
172
  }
181
173
 
package/lib/upgrade.js CHANGED
@@ -1,105 +1,116 @@
1
- const { execSync } = require('child_process');
1
+ const { execSync, spawn } = require('child_process');
2
2
  const { checkUpgrade, formatUpgradeMsg, clearCache, getLocalVersion, getPackageName } = require('./check-version.js');
3
3
 
4
4
  /**
5
- * xp-gate upgrade command handler.
6
- *
7
- * Modes:
8
- * xp-gate upgrade — human-readable output
9
- * xp-gate upgrade --preview — single-line JSON for plugin consumption
10
- * xp-gate upgrade --apply — auto-upgrade via npm install -g
11
- *
12
- * @param {string[]} args
13
- * @returns {Promise<number>} exit code
5
+ * Handle checkUpgrade() failure.
6
+ * @param {Error} err
7
+ * @param {boolean} isPreview
8
+ * @returns {number} exit code (always 1)
14
9
  */
15
- async function upgrade(args) {
16
- const isPreview = args.includes('--preview');
17
- const isApply = args.includes('--apply');
18
- const pkgName = getPackageName();
19
-
20
- // --- check for upgrade ---
21
- let result;
22
- try {
23
- result = await checkUpgrade(pkgName);
24
- } catch (err) {
25
- if (isPreview) {
26
- console.log(JSON.stringify({ error: 'check failed', detail: err.message }));
27
- } else {
28
- console.error('Unable to check for updates (check error).');
29
- }
30
- return 1;
31
- }
32
-
33
- // Guard: if checkUpgrade returned null remote (network issue)
34
- if (!result.remote) {
35
- if (isPreview) {
36
- console.log(JSON.stringify({
37
- local: result.local || 'unknown',
38
- remote: null,
39
- outdated: false,
40
- lagDays: 0,
41
- error: 'Unable to check for updates (network issue)',
42
- }));
43
- } else {
44
- console.error('Unable to check for updates (network issue).');
45
- }
46
- return 0;
10
+ function handleCheckError(err, isPreview) {
11
+ if (isPreview) {
12
+ console.log(JSON.stringify({ error: 'check failed', detail: err.message }));
13
+ } else {
14
+ console.error('Unable to check for updates (check error).');
47
15
  }
16
+ return 1;
17
+ }
48
18
 
49
- // --- --preview: JSON output ---
19
+ /**
20
+ * Handle null remote (network issue) after checkUpgrade().
21
+ * @param {{ local: string|null }} result
22
+ * @param {boolean} isPreview
23
+ * @returns {number} exit code (always 0)
24
+ */
25
+ function handleNullRemote(result, isPreview) {
50
26
  if (isPreview) {
51
- const releaseUrl = result.outdated
52
- ? `https://github.com/boyingliu01/xp-gate/releases/tag/v${result.remote}`
53
- : null;
54
27
  console.log(JSON.stringify({
55
- local: result.local,
56
- remote: result.remote,
57
- outdated: result.outdated,
58
- lagDays: result.lagDays,
59
- releaseUrl,
60
- publishedAt: result.publishedAt || null,
28
+ local: result.local || 'unknown',
29
+ remote: null,
30
+ outdated: false,
31
+ lagDays: 0,
32
+ error: 'Unable to check for updates (network issue)',
61
33
  }));
62
- return 0;
34
+ } else {
35
+ console.error('Unable to check for updates (network issue).');
63
36
  }
37
+ return 0;
38
+ }
39
+
40
+ /**
41
+ * Handle --preview mode: emit single-line JSON.
42
+ * @param {{ local: string, remote: string, outdated: boolean, lagDays: number, publishedAt?: string }} result
43
+ * @returns {number} exit code (always 0)
44
+ */
45
+ function handlePreviewMode(result) {
46
+ const releaseUrl = result.outdated
47
+ ? `https://github.com/boyingliu01/xp-gate/releases/tag/v${result.remote}`
48
+ : null;
49
+ console.log(JSON.stringify({
50
+ local: result.local,
51
+ remote: result.remote,
52
+ outdated: result.outdated,
53
+ lagDays: result.lagDays,
54
+ releaseUrl,
55
+ publishedAt: result.publishedAt || null,
56
+ }));
57
+ return 0;
58
+ }
64
59
 
65
- // --- --apply: auto-upgrade ---
66
- if (isApply) {
67
- if (!result.outdated) {
68
- if (result.local) {
69
- console.log(`\u2713 xp-gate v${result.local} is up to date`);
70
- } else {
71
- console.log('xp-gate is up to date.');
72
- }
73
- return 0;
60
+ /**
61
+ * Handle --apply mode: auto-upgrade via npm install -g.
62
+ * @param {{ local: string, remote: string, outdated: boolean }} result
63
+ * @param {string} pkgName
64
+ * @returns {Promise<number>} exit code
65
+ */
66
+ async function handleApplyMode(result, pkgName) {
67
+ if (!result.outdated) {
68
+ if (result.local) {
69
+ console.log(`\u2713 xp-gate v${result.local} is up to date`);
70
+ } else {
71
+ console.log('xp-gate is up to date.');
74
72
  }
73
+ return 0;
74
+ }
75
75
 
76
- console.log(`Upgrading xp-gate from v${result.local} to v${result.remote}...`);
77
- try {
78
- execSync(`npm install -g ${pkgName}@${result.remote}`, {
79
- stdio: 'inherit',
80
- timeout: 120000, // 2 minute timeout
76
+ console.log(`Upgrading xp-gate from v${result.local} to v${result.remote}...`);
77
+ try {
78
+ const child = spawn('npm', ['install', '-g', `${pkgName}@${result.remote}`], {
79
+ stdio: 'inherit',
80
+ timeout: 120000,
81
+ });
82
+ await new Promise((resolve, reject) => {
83
+ child.on('close', (code) => {
84
+ if (code === 0) resolve();
85
+ else reject(new Error(`npm install exited with code ${code}`));
81
86
  });
82
- // clear cache so next check picks up the new version
83
- clearCache();
84
- console.log(`\u2713 Upgraded to v${result.remote}`);
85
- return 0;
86
- } catch (err) {
87
- const stderr = (err.stderr || '').toString();
88
- if (stderr.includes('EACCES') || stderr.includes('permission') || stderr.includes('EPERM')) {
89
- console.error(`Permission denied. Try: sudo npm install -g ${pkgName}`);
90
- } else if (err.code === 'ETIMEDOUT' || stderr.includes('ETIMEDOUT')) {
91
- console.error('npm install timed out. Check your network and try again.');
92
- console.error(` Retry: npm install -g ${pkgName}@latest`);
93
- } else {
94
- console.error('Upgrade failed:');
95
- console.error(` ${err.message}`);
96
- console.error(` Retry manually: npm install -g ${pkgName}@latest`);
97
- }
98
- return 1;
87
+ child.on('error', reject);
88
+ });
89
+ clearCache();
90
+ console.log(`\u2713 Upgraded to v${result.remote}`);
91
+ return 0;
92
+ } catch (err) {
93
+ const msg = err.message || '';
94
+ if (msg.includes('EACCES') || msg.includes('permission') || msg.includes('EPERM')) {
95
+ console.error(`Permission denied. Try: sudo npm install -g ${pkgName}`);
96
+ } else if (msg.includes('ETIMEDOUT')) {
97
+ console.error('npm install timed out. Check your network and try again.');
98
+ console.error(` Retry: npm install -g ${pkgName}@latest`);
99
+ } else {
100
+ console.error('Upgrade failed:');
101
+ console.error(` ${msg}`);
102
+ console.error(` Retry manually: npm install -g ${pkgName}@latest`);
99
103
  }
104
+ return 1;
100
105
  }
106
+ }
101
107
 
102
- // --- default mode: human-readable ---
108
+ /**
109
+ * Handle default mode: human-readable output.
110
+ * @param {{ local: string, remote: string, outdated: boolean, lagDays: number, publishedAt?: string }} result
111
+ * @returns {number} exit code (always 0)
112
+ */
113
+ function handleDefaultMode(result) {
103
114
  if (!result.outdated) {
104
115
  if (result.local) {
105
116
  console.log(`\u2713 xp-gate v${result.local} is up to date`);
@@ -113,4 +124,36 @@ async function upgrade(args) {
113
124
  return 0;
114
125
  }
115
126
 
127
+ /**
128
+ * xp-gate upgrade command handler.
129
+ *
130
+ * Modes:
131
+ * xp-gate upgrade — human-readable output
132
+ * xp-gate upgrade --preview — single-line JSON for plugin consumption
133
+ * xp-gate upgrade --apply — auto-upgrade via npm install -g
134
+ *
135
+ * @param {string[]} args
136
+ * @returns {Promise<number>} exit code
137
+ */
138
+ async function upgrade(args) {
139
+ const isPreview = args.includes('--preview');
140
+ const isApply = args.includes('--apply');
141
+ const pkgName = getPackageName();
142
+
143
+ let result;
144
+ try {
145
+ result = await checkUpgrade(pkgName);
146
+ } catch (err) {
147
+ return handleCheckError(err, isPreview);
148
+ }
149
+
150
+ if (!result.remote) {
151
+ return handleNullRemote(result, isPreview);
152
+ }
153
+
154
+ if (isPreview) return handlePreviewMode(result);
155
+ if (isApply) return handleApplyMode(result, pkgName);
156
+ return handleDefaultMode(result);
157
+ }
158
+
116
159
  module.exports = { upgrade };
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-06-18
4
- **Commit:** b67fc85
3
+ **Generated:** 2026-06-21
4
+ **Commit:** 1050a30
5
5
  **Branch:** main
6
- **Version:** 0.9.3.0
6
+ **Version:** 0.9.5.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-06-18
4
- **Commit:** b67fc85
3
+ **Generated:** 2026-06-21
4
+ **Commit:** 1050a30
5
5
  **Branch:** main
6
- **Version:** 0.9.3.0
6
+ **Version:** 0.9.5.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.9.3",
3
+ "version": "0.9.5",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
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-06-18
4
- **Commit:** b67fc85
3
+ **Generated:** 2026-06-21
4
+ **Commit:** 1050a30
5
5
  **Branch:** main
6
- **Version:** 0.9.3.0
6
+ **Version:** 0.9.5.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,6 +1,7 @@
1
1
  ---
2
2
  name: delphi-review
3
3
  description: "Use when asked to review a design, plan, or architecture; before implementation starts; or when multi-expert consensus is needed. Triggers: 'review this design', '评审这个需求', 'design review', '多专家评审', 'consensus review', 'code walkthrough', 'push review', 'architecture review', 'PR review', or any request for multi-expert evaluation of requirements, design docs, or PRs."
4
+ auto_continue: true
4
5
  ---
5
6
 
6
7
  # Delphi Consensus Review
@@ -109,6 +110,39 @@ Phase 0: 准备 → Round 1: 匿名独立评审 → 共识检查
109
110
 
110
111
  **Automatic re-review**: 对于常见可控问题(措辞模糊、AC 缺失、格式问题),subagent 应自行修复后自动重评审,无需等待用户。
111
112
 
113
+ ### ⭐ 自动延续规则(MANDATORY — 防止流程卡住)
114
+
115
+ 当 Delphi Review 启动多轮评审(Round 2+)时,**orchestrator 必须自动延续流程,不得等待用户输入**。
116
+
117
+ **触发条件**:
118
+ - Round N 完成,但未达到终止条件(100% approved + ≥90% consensus + 所有 Critical/Major 已处理)
119
+ - 存在待处理的背景任务(subagent dispatched tasks)
120
+
121
+ **自动延续动作**:
122
+ 1. **收集背景任务结果**:等待 `<system-reminder>` 通知后,立即调用 `background_output(task_id="bg_...")` 获取所有 subagent 输出
123
+ 2. **合成 Round N 总结**:汇总专家意见、共识度、待处理问题
124
+ 3. **自动启动 Round N+1**:立即 dispatch 新一轮 subagent 任务,携带上一轮总结作为上下文
125
+ 4. **循环直至终止**:重复步骤 1-3,直到达到终止条件
126
+
127
+ **终止条件**(满足全部):
128
+ - ✅ 所有专家状态 = APPROVED
129
+ - ✅ 共识度 ≥ 90%
130
+ - ✅ 所有 Critical 级别问题已解决
131
+ - ✅ 所有 Major 级别问题已处理(或已记录为已知问题)
132
+
133
+ **例外情况**(直接输出,不进入下一轮):
134
+ - Round 1 即达到 100% approved + 100% consensus → 直接输出最终报告
135
+ - 已达最大轮数(5 轮)→ 输出"未达成共识"报告,标记为 PROCESS_BLOCK
136
+
137
+ **禁止行为**:
138
+ - ❌ 询问用户"要继续吗?"
139
+ - ❌ 等待用户手动触发 Round N+1
140
+ - ❌ 在未达到终止条件时停止流程
141
+
142
+ **错误处理**:
143
+ - 背景任务超时(>10min)→ 标记为 TIMEOUT,输出部分结果并终止
144
+ - 背景任务失败(subagent 错误)→ 重试 1 次,仍失败则输出错误报告并终止
145
+
112
146
  ---
113
147
 
114
148
  ## 修复与重新评审
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-06-18
4
- **Commit:** b67fc85
3
+ **Generated:** 2026-06-21
4
+ **Commit:** 1050a30
5
5
  **Branch:** main
6
- **Version:** 0.9.3.0
6
+ **Version:** 0.9.5.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.
@@ -408,6 +408,58 @@ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW
408
408
 
409
409
  ---
410
410
 
411
+ ### ⭐ Phase State Persistence(阶段状态持久化 — MANDATORY)
412
+
413
+ **编排器必须在每个 Phase 完成后更新 `.sprint-state/sprint-state.json`**:
414
+
415
+ 1. **Phase 完成后立即更新**(每个 Phase 结束前):
416
+ - `phase`: 更新为当前 Phase 编号(如 `0`, `1`, `2`...)
417
+ - `status`: 更新为 `"completed"`(已完成 Phase)
418
+ - `phase_history`: 追加新条目
419
+
420
+ 2. **`phase_history` 数组条目 schema**:
421
+ ```json
422
+ {
423
+ "phase": 0,
424
+ "phase_name": "THINK",
425
+ "status": "completed",
426
+ "timestamp": "2026-06-20T10:30:00Z"
427
+ }
428
+ ```
429
+
430
+ 3. **检查点**:
431
+ - `--status` 参数读取 `sprint-state.json` 并渲染进度看板
432
+ - TUI panel 显示当前 Phase 和历史
433
+ - `--resume-from` 校验 `phase_history` 中的最后完成 Phase
434
+
435
+ 4. **完整 sprint-state.json 示例**:
436
+ ```json
437
+ {
438
+ "id": "sprint-2026-06-20-01",
439
+ "phase": 2,
440
+ "status": "in_progress",
441
+ "phase_history": [
442
+ {"phase": -1, "phase_name": "ISOLATE", "status": "completed", "timestamp": "2026-06-20T10:00:00Z"},
443
+ {"phase": -0.5, "phase_name": "AUTO-ESTIMATE", "status": "completed", "timestamp": "2026-06-20T10:05:00Z"},
444
+ {"phase": 0, "phase_name": "THINK", "status": "completed", "timestamp": "2026-06-20T10:15:00Z"},
445
+ {"phase": 1, "phase_name": "PLAN", "status": "completed", "timestamp": "2026-06-20T10:30:00Z"}
446
+ ],
447
+ "isolation": {
448
+ "worktree_path": "/home/boyingliu01/projects/xp-gate/.worktrees/sprint/sprint-2026-06-20-01",
449
+ "branch": "sprint/2026-06-20-01"
450
+ },
451
+ "outputs": {
452
+ "pain_document": "phase-outputs/phase-0-summary.md",
453
+ "specification": "phase-outputs/specification.yaml"
454
+ },
455
+ "metrics": {
456
+ "coverage_pct": 85.5
457
+ }
458
+ }
459
+ ```
460
+
461
+ ---
462
+
411
463
  ## 使用示例
412
464
 
413
465
  | 场景 | 命令 | 说明 |
@@ -447,7 +499,7 @@ See [Output Contract](#output-contract) below for the canonical machine-readable
447
499
 
448
500
  **Phase Summary** (每个 Phase 必须输出 YAML frontmatter): `phase/N`, `phase_name`, `status`, `outputs[]`, `decisions[]`, `next_phase_context` + markdown body (≤50 lines)
449
501
 
450
- **Sprint State JSON**: `{id, phase, status, isolation {worktree_path, branch}, outputs, metrics}` — 存储于 `.sprint-state/sprint-state.json`
502
+ **Sprint State JSON**: `{id, phase, status, phase_history[], isolation {worktree_path, branch}, outputs, metrics}` — 存储于 `.sprint-state/sprint-state.json`
451
503
 
452
504
  **Final User-Facing Output**: Phase/status, file paths, validation results, next user decision, PR URL or cleanup report
453
505