@boyingliu01/xp-gate 0.10.11 → 0.10.12
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/lib/__tests__/check-version.test.js +19 -5
- package/lib/__tests__/install-skill.test.js +1 -1
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +6 -9
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +8 -6
- package/plugins/claude-code/skills/sprint-flow/references/phase-1-plan.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +3 -1
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +562 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +6 -9
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +8 -6
- package/plugins/opencode/skills/sprint-flow/references/phase-1-plan.md +2 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +3 -1
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/SKILL.md +191 -1088
- package/plugins/qoder/skills/sprint-flow/references/orchestration-rules.md +365 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-1-plan.md +2 -2
- package/plugins/qoder/skills/sprint-flow/references/phase-2-build.md +25 -180
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +3 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-6-ship.md +4 -188
- package/plugins/qoder/skills/sprint-flow/references/phase-7-land.md +4 -135
- package/plugins/qoder/skills/sprint-flow/references/phase-8-cleanup.md +4 -187
- package/plugins/qoder/skills/sprint-flow/references/phase-minus-0-5-auto-estimate.md +6 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-minus-1-isolate.md +58 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +6 -9
- package/skills/sprint-flow/references/orchestration-rules.md +8 -6
- package/skills/sprint-flow/references/phase-1-plan.md +2 -2
- package/skills/sprint-flow/references/phase-3-review.md +3 -1
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XP-Gate E2E Upgrade Test
|
|
3
|
+
*
|
|
4
|
+
* End-to-end tests for the OpenCode plugin's auto-upgrade flow:
|
|
5
|
+
* checkXpGateUpdate → writeUpgradeNotice → chat.message hook
|
|
6
|
+
*
|
|
7
|
+
* These tests validate the FULL code path that was broken before the
|
|
8
|
+
* fire-and-forget fix (commit 9bad3c1):
|
|
9
|
+
* - spawn must be awaited (not discarded when hook returns)
|
|
10
|
+
* - cache must be written with status:current after spawn completes
|
|
11
|
+
* - upgrade-notice.json must be written for TUI display
|
|
12
|
+
* - runBackgroundUpdates must return the upgrade message
|
|
13
|
+
*
|
|
14
|
+
* All tests use REAL `npm install -g` (skipped in CI).
|
|
15
|
+
*
|
|
16
|
+
* Function implementations are copied from plugins/opencode/index.ts
|
|
17
|
+
* because importing from index.ts requires @opencode-ai/plugin runtime.
|
|
18
|
+
* This mirrors the existing pattern in xp-gate-update.test.ts.
|
|
19
|
+
*
|
|
20
|
+
* @test E2E-001 Full runBackgroundUpdates flow
|
|
21
|
+
* @test E2E-002 Await timing verification
|
|
22
|
+
* @test E2E-003 Upgrade notice file content
|
|
23
|
+
* @test E2E-004 Chat.message hook simulation
|
|
24
|
+
* @test E2E-005 Cache integrity after spawn
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, before, after } from "node:test"
|
|
28
|
+
import assert from "node:assert/strict"
|
|
29
|
+
import { randomUUID } from "node:crypto"
|
|
30
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"
|
|
31
|
+
import { join } from "node:path"
|
|
32
|
+
import { tmpdir, homedir } from "node:os"
|
|
33
|
+
import { execSync, spawn } from "node:child_process"
|
|
34
|
+
|
|
35
|
+
// ── Constants (must match index.ts) ──
|
|
36
|
+
|
|
37
|
+
const CACHE_TTL_MS = 86_400_000 // 24h
|
|
38
|
+
const FETCH_TIMEOUT_MS = 5_000
|
|
39
|
+
|
|
40
|
+
const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
|
|
41
|
+
const XP_GATE_CACHE_FILE = join(homedir(), ".xp-gate", "xp-gate-version-check.json")
|
|
42
|
+
const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
|
|
43
|
+
const UPGRADE_NOTICE_FILE = join(homedir(), ".xp-gate", "upgrade-notice.json")
|
|
44
|
+
|
|
45
|
+
// ── Types (must match index.ts) ──
|
|
46
|
+
|
|
47
|
+
type UpgradeResult = {
|
|
48
|
+
action: "noop" | "upgraded" | "error"
|
|
49
|
+
localVersion: string | null
|
|
50
|
+
remoteVersion: string | null
|
|
51
|
+
error?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type UpgradeNotice = {
|
|
55
|
+
kind: "upgraded" | "outdated" | "error"
|
|
56
|
+
localVersion: string | null
|
|
57
|
+
remoteVersion: string | null
|
|
58
|
+
message: string
|
|
59
|
+
ts: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type CacheEntry = {
|
|
63
|
+
ts: number
|
|
64
|
+
remoteVersion: string
|
|
65
|
+
localVersion?: string
|
|
66
|
+
status?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Utilities (must match index.ts) ──
|
|
70
|
+
|
|
71
|
+
function semverLt(a: string, b: string): boolean {
|
|
72
|
+
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
73
|
+
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
74
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
75
|
+
const na = pa[i] ?? 0
|
|
76
|
+
const nb = pb[i] ?? 0
|
|
77
|
+
if (na !== nb) return na < nb
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function fetchNpmLatestVersion(url: string): Promise<string | null> {
|
|
83
|
+
const controller = new AbortController()
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
87
|
+
if (!response.ok) return null
|
|
88
|
+
const data: Record<string, unknown> = await response.json()
|
|
89
|
+
return typeof data.latest === "string" ? data.latest : null
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timer)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readCache(file: string): CacheEntry | null {
|
|
98
|
+
try {
|
|
99
|
+
if (!existsSync(file)) return null
|
|
100
|
+
const raw = readFileSync(file, "utf8")
|
|
101
|
+
const data = JSON.parse(raw)
|
|
102
|
+
if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) return data
|
|
103
|
+
return null
|
|
104
|
+
} catch {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeCache(file: string, data: object): void {
|
|
110
|
+
try {
|
|
111
|
+
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
112
|
+
writeFileSync(file + ".tmp", JSON.stringify(data), "utf8")
|
|
113
|
+
try { rmSync(file) } catch {}
|
|
114
|
+
writeFileSync(file, readFileSync(file + ".tmp", "utf8"), "utf8")
|
|
115
|
+
try { rmSync(file + ".tmp") } catch {}
|
|
116
|
+
} catch {
|
|
117
|
+
// silent
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readXpGateConfig(): { autoUpgrade?: boolean } | null {
|
|
122
|
+
const cfgPath = join(homedir(), ".xp-gate", "config.json")
|
|
123
|
+
try {
|
|
124
|
+
if (!existsSync(cfgPath)) return null
|
|
125
|
+
return JSON.parse(readFileSync(cfgPath, "utf8"))
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Local version override (same pattern as xp-gate-update.test.ts) ──
|
|
132
|
+
|
|
133
|
+
let getLocalVersionOverride: (() => string | null) | null = null
|
|
134
|
+
|
|
135
|
+
function getLocalXpGateVersion(): string | null {
|
|
136
|
+
if (getLocalVersionOverride) return getLocalVersionOverride()
|
|
137
|
+
try {
|
|
138
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf8" }).trim()
|
|
139
|
+
const pkg = JSON.parse(readFileSync(join(globalRoot, XP_GATE_NPM_PKG, "package.json"), "utf8"))
|
|
140
|
+
return pkg.version || null
|
|
141
|
+
} catch {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Core functions (MUST match index.ts EXACTLY) ──
|
|
147
|
+
|
|
148
|
+
async function checkXpGateUpdate(): Promise<UpgradeResult> {
|
|
149
|
+
const cached = readCache(XP_GATE_CACHE_FILE)
|
|
150
|
+
const localVersion = getLocalXpGateVersion()
|
|
151
|
+
|
|
152
|
+
if (cached?.status === "current" && cached.remoteVersion && localVersion && cached.localVersion === localVersion) {
|
|
153
|
+
return { action: "noop", localVersion, remoteVersion: cached.remoteVersion }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
|
|
157
|
+
|
|
158
|
+
const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
|
|
159
|
+
if (!remoteVersion) return { action: "noop", localVersion, remoteVersion: null }
|
|
160
|
+
|
|
161
|
+
if (!semverLt(localVersion, remoteVersion)) {
|
|
162
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
163
|
+
return { action: "noop", localVersion, remoteVersion }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const config = readXpGateConfig()
|
|
167
|
+
if (config?.autoUpgrade === false) {
|
|
168
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
169
|
+
return { action: "noop", localVersion, remoteVersion }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
173
|
+
try {
|
|
174
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
175
|
+
const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
|
|
176
|
+
stdio: "pipe",
|
|
177
|
+
timeout: 120_000,
|
|
178
|
+
})
|
|
179
|
+
child.on("close", (code) => resolve(code))
|
|
180
|
+
child.on("error", (err) => reject(err))
|
|
181
|
+
})
|
|
182
|
+
if (exitCode === 0) {
|
|
183
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
|
|
184
|
+
return { action: "upgraded", localVersion, remoteVersion }
|
|
185
|
+
}
|
|
186
|
+
return { action: "error", localVersion, remoteVersion, error: `npm install exited with code ${exitCode}` }
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
189
|
+
return { action: "error", localVersion, remoteVersion, error: msg }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function writeUpgradeNotice(notice: UpgradeNotice): void {
|
|
194
|
+
writeCache(UPGRADE_NOTICE_FILE, notice)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Full background check — the function called by chat.message hook.
|
|
199
|
+
* MUST match index.ts exactly.
|
|
200
|
+
*/
|
|
201
|
+
async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
|
|
202
|
+
void pluginDir // unused in this function, kept for signature match
|
|
203
|
+
const result = await checkXpGateUpdate()
|
|
204
|
+
// checkPluginUpdate is skipped here — it's not part of the xp-gate upgrade flow
|
|
205
|
+
|
|
206
|
+
if (result.action === "upgraded") {
|
|
207
|
+
const msg = `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
|
|
208
|
+
writeUpgradeNotice({ kind: "upgraded", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
|
|
209
|
+
return msg
|
|
210
|
+
}
|
|
211
|
+
if (result.action === "error") {
|
|
212
|
+
const msg = `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
|
|
213
|
+
writeUpgradeNotice({ kind: "error", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
|
|
214
|
+
return msg
|
|
215
|
+
}
|
|
216
|
+
if (result.remoteVersion && result.localVersion && semverLt(result.localVersion, result.remoteVersion)) {
|
|
217
|
+
const msg = `[XP-Gate] New version v${result.remoteVersion} available (you have v${result.localVersion})`
|
|
218
|
+
writeUpgradeNotice({ kind: "outdated", localVersion: result.localVersion, remoteVersion: result.remoteVersion, message: msg, ts: Date.now() })
|
|
219
|
+
return msg
|
|
220
|
+
}
|
|
221
|
+
return null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Helpers ──
|
|
225
|
+
|
|
226
|
+
function readUpgradeNotice(): UpgradeNotice | null {
|
|
227
|
+
try {
|
|
228
|
+
if (!existsSync(UPGRADE_NOTICE_FILE)) return null
|
|
229
|
+
return JSON.parse(readFileSync(UPGRADE_NOTICE_FILE, "utf8"))
|
|
230
|
+
} catch {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function assertInCI(): boolean {
|
|
236
|
+
if (process.env.CI) {
|
|
237
|
+
console.log(" [SKIP] E2E spawn test disabled in CI (real npm install required)")
|
|
238
|
+
return true
|
|
239
|
+
}
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ═══════════════════════════════════════════════════
|
|
244
|
+
// E2E TESTS
|
|
245
|
+
// ═══════════════════════════════════════════════════
|
|
246
|
+
|
|
247
|
+
// ── Suite 1: Full runBackgroundUpdates E2E ──
|
|
248
|
+
|
|
249
|
+
void describe("E2E: runBackgroundUpdates → upgrade + notice + cache (E2E-001)", () => {
|
|
250
|
+
const fakeHome = join(tmpdir(), "xp-gate-e2e-001-" + randomUUID())
|
|
251
|
+
const origHome = process.env.HOME
|
|
252
|
+
|
|
253
|
+
before(() => {
|
|
254
|
+
process.env.HOME = fakeHome
|
|
255
|
+
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
after(() => {
|
|
259
|
+
process.env.HOME = origHome
|
|
260
|
+
rmSync(fakeHome, { recursive: true, force: true })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
void it("runBackgroundUpdates returns upgrade message when local < remote", async () => {
|
|
264
|
+
if (assertInCI()) return
|
|
265
|
+
|
|
266
|
+
// Seed a stale cache that will force a network check
|
|
267
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
268
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
269
|
+
ts: Date.now() - 86_400_000 - 3600_000, // 25h stale
|
|
270
|
+
localVersion: "0.8.0",
|
|
271
|
+
remoteVersion: "0.8.1",
|
|
272
|
+
}))
|
|
273
|
+
|
|
274
|
+
// Override local version to be older than latest on npm
|
|
275
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
276
|
+
|
|
277
|
+
const msg = await runBackgroundUpdates("/fake/plugin/dir")
|
|
278
|
+
getLocalVersionOverride = null
|
|
279
|
+
|
|
280
|
+
assert.ok(msg !== null, "E2E-001 FAIL: runBackgroundUpdates returned null — expected upgrade message")
|
|
281
|
+
if (msg) {
|
|
282
|
+
assert.ok(msg.includes("Auto-upgraded"), `E2E-001 FAIL: message missing 'Auto-upgraded': ${msg}`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Verify upgrade notice was written
|
|
286
|
+
const notice = readUpgradeNotice()
|
|
287
|
+
assert.ok(notice !== null, "E2E-001 FAIL: upgrade-notice.json was not written")
|
|
288
|
+
if (notice) {
|
|
289
|
+
assert.equal(notice.kind, "upgraded", `E2E-001 FAIL: notice kind=${notice.kind}, expected 'upgraded'`)
|
|
290
|
+
assert.equal(notice.localVersion, "0.8.0")
|
|
291
|
+
assert.ok(notice.remoteVersion !== null)
|
|
292
|
+
assert.ok(notice.message.includes("Auto-upgraded"))
|
|
293
|
+
assert.ok(notice.ts > 0)
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
void it("cache is written with status:current immediately after runBackgroundUpdates returns", async () => {
|
|
298
|
+
if (assertInCI()) return
|
|
299
|
+
|
|
300
|
+
// Re-seed with stale cache to force another upgrade
|
|
301
|
+
rmSync(join(fakeHome, ".xp-gate", "xp-gate-version-check.json"), { force: true })
|
|
302
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
303
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
304
|
+
ts: Date.now() - 86_400_000 - 3600_000,
|
|
305
|
+
localVersion: "0.8.0",
|
|
306
|
+
remoteVersion: "0.8.1",
|
|
307
|
+
}))
|
|
308
|
+
|
|
309
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
310
|
+
|
|
311
|
+
await runBackgroundUpdates("/fake/plugin/dir")
|
|
312
|
+
getLocalVersionOverride = null
|
|
313
|
+
|
|
314
|
+
// After function returns, cache MUST have status:current
|
|
315
|
+
// This is the KEY assertion — fire-and-forget would fail here
|
|
316
|
+
const finalCache = readCache(XP_GATE_CACHE_FILE)
|
|
317
|
+
if (finalCache) {
|
|
318
|
+
assert.equal(finalCache.status, "current",
|
|
319
|
+
"E2E-001 FAIL: cache status ≠ 'current' after runBackgroundUpdates returned. " +
|
|
320
|
+
"If this fails, the spawn is NOT being awaited — the same bug as before the fix.")
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// ── Suite 2: Await timing verification ──
|
|
326
|
+
|
|
327
|
+
void describe("E2E: Await timing — resolve AFTER spawn (E2E-002)", () => {
|
|
328
|
+
const fakeHome = join(tmpdir(), "xp-gate-e2e-002-" + randomUUID())
|
|
329
|
+
const origHome = process.env.HOME
|
|
330
|
+
|
|
331
|
+
before(() => {
|
|
332
|
+
process.env.HOME = fakeHome
|
|
333
|
+
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
after(() => {
|
|
337
|
+
process.env.HOME = origHome
|
|
338
|
+
rmSync(fakeHome, { recursive: true, force: true })
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
void it("runBackgroundUpdates resolve time > 1000ms (spawn is awaited)", async () => {
|
|
342
|
+
if (assertInCI()) return
|
|
343
|
+
|
|
344
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
345
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
346
|
+
ts: Date.now() - 86_400_000 - 3600_000,
|
|
347
|
+
localVersion: "0.8.0",
|
|
348
|
+
remoteVersion: "0.8.1",
|
|
349
|
+
}))
|
|
350
|
+
|
|
351
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
352
|
+
|
|
353
|
+
const startTime = Date.now()
|
|
354
|
+
const msg = await runBackgroundUpdates("/fake/plugin/dir")
|
|
355
|
+
const elapsed = Date.now() - startTime
|
|
356
|
+
getLocalVersionOverride = null
|
|
357
|
+
|
|
358
|
+
console.log(` E2E-002: runBackgroundUpdates() returned in ${elapsed}ms`)
|
|
359
|
+
|
|
360
|
+
if (msg?.includes("Auto-upgraded")) {
|
|
361
|
+
// Only assert timing if upgrade actually happened (network call succeeded)
|
|
362
|
+
assert.ok(elapsed > 1000,
|
|
363
|
+
`E2E-002 FAIL: runBackgroundUpdates resolved in ${elapsed}ms — ` +
|
|
364
|
+
"spawn was NOT awaited. The chat.message hook would lose the upgrade. " +
|
|
365
|
+
"This is the fire-and-forget bug.")
|
|
366
|
+
} else if (elapsed < 200) {
|
|
367
|
+
// If no upgrade happened (network failure, etc.), elapsed should still be
|
|
368
|
+
// reasonable for a network call (5s timeout for npm registry fetch)
|
|
369
|
+
assert.ok(elapsed > 200,
|
|
370
|
+
`E2E-002 FAIL: resolved in ${elapsed}ms with no upgrade — network check too fast?`)
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
// ── Suite 3: Upgrade notice file verification ──
|
|
376
|
+
|
|
377
|
+
void describe("E2E: writeUpgradeNotice file content (E2E-003)", () => {
|
|
378
|
+
const fakeHome = join(tmpdir(), "xp-gate-e2e-003-" + randomUUID())
|
|
379
|
+
const origHome = process.env.HOME
|
|
380
|
+
|
|
381
|
+
before(() => {
|
|
382
|
+
process.env.HOME = fakeHome
|
|
383
|
+
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
after(() => {
|
|
387
|
+
process.env.HOME = origHome
|
|
388
|
+
rmSync(fakeHome, { recursive: true, force: true })
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
void it("notice file has correct schema (kind, localVersion, remoteVersion, message, ts)", async () => {
|
|
392
|
+
if (assertInCI()) return
|
|
393
|
+
|
|
394
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
395
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
396
|
+
ts: Date.now() - 86_400_000 - 3600_000,
|
|
397
|
+
localVersion: "0.8.0",
|
|
398
|
+
remoteVersion: "0.8.1",
|
|
399
|
+
}))
|
|
400
|
+
|
|
401
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
402
|
+
await runBackgroundUpdates("/fake/plugin/dir")
|
|
403
|
+
getLocalVersionOverride = null
|
|
404
|
+
|
|
405
|
+
const notice = readUpgradeNotice()
|
|
406
|
+
assert.ok(notice !== null, "E2E-003 FAIL: notice file not written")
|
|
407
|
+
|
|
408
|
+
if (notice) {
|
|
409
|
+
// Schema validation
|
|
410
|
+
assert.ok(["upgraded", "outdated", "error"].includes(notice.kind),
|
|
411
|
+
`E2E-003 FAIL: invalid kind: ${notice.kind}`)
|
|
412
|
+
assert.ok(typeof notice.localVersion === "string" || notice.localVersion === null,
|
|
413
|
+
"E2E-003 FAIL: localVersion must be string or null")
|
|
414
|
+
assert.ok(typeof notice.remoteVersion === "string" || notice.remoteVersion === null,
|
|
415
|
+
"E2E-003 FAIL: remoteVersion must be string or null")
|
|
416
|
+
assert.ok(typeof notice.message === "string" && notice.message.length > 0,
|
|
417
|
+
"E2E-003 FAIL: message must be non-empty string")
|
|
418
|
+
assert.ok(typeof notice.ts === "number" && notice.ts > 0,
|
|
419
|
+
"E2E-003 FAIL: ts must be positive number")
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
void it("notice file can be read back and parsed (no corruption)", async () => {
|
|
424
|
+
if (assertInCI()) return
|
|
425
|
+
|
|
426
|
+
// Verify the file is valid JSON (not corrupted by tmpfile write)
|
|
427
|
+
const raw = readFileSync(UPGRADE_NOTICE_FILE, "utf8")
|
|
428
|
+
let parsed: unknown = null
|
|
429
|
+
assert.doesNotThrow(() => {
|
|
430
|
+
parsed = JSON.parse(raw)
|
|
431
|
+
}, "E2E-003 FAIL: notice file contains invalid JSON")
|
|
432
|
+
assert.ok(parsed !== null)
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// ── Suite 4: Chat.message hook simulation (E2E-004) ──
|
|
437
|
+
|
|
438
|
+
void describe("E2E: chat.message hook simulation (E2E-004)", () => {
|
|
439
|
+
const fakeHome = join(tmpdir(), "xp-gate-e2e-004-" + randomUUID())
|
|
440
|
+
const origHome = process.env.HOME
|
|
441
|
+
|
|
442
|
+
before(() => {
|
|
443
|
+
process.env.HOME = fakeHome
|
|
444
|
+
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
after(() => {
|
|
448
|
+
process.env.HOME = origHome
|
|
449
|
+
rmSync(fakeHome, { recursive: true, force: true })
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
void it("simulated chat.message hook: upgrade completes before hook returns", async () => {
|
|
453
|
+
if (assertInCI()) return
|
|
454
|
+
|
|
455
|
+
// This simulates the EXACT code path from index.ts lines 321-327:
|
|
456
|
+
// "chat.message": async (_input: { message: string }) => {
|
|
457
|
+
// if (!checked) {
|
|
458
|
+
// checked = true
|
|
459
|
+
// const msg = await runBackgroundUpdates(directory).catch(() => null)
|
|
460
|
+
// if (msg) process.stderr.write(`${msg}\n`)
|
|
461
|
+
// }
|
|
462
|
+
// }
|
|
463
|
+
|
|
464
|
+
let checked = false
|
|
465
|
+
const stderrOutput: string[] = []
|
|
466
|
+
|
|
467
|
+
// Simulate the hook callback (first chat.message invocation)
|
|
468
|
+
const simulateHook = async (pluginDir: string): Promise<void> => {
|
|
469
|
+
if (!checked) {
|
|
470
|
+
checked = true
|
|
471
|
+
const msg = await runBackgroundUpdates(pluginDir).catch(() => null)
|
|
472
|
+
if (msg) stderrOutput.push(msg)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Setup: stale cache forces upgrade
|
|
477
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
478
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
479
|
+
ts: Date.now() - 86_400_000 - 3600_000,
|
|
480
|
+
localVersion: "0.8.0",
|
|
481
|
+
remoteVersion: "0.8.1",
|
|
482
|
+
}))
|
|
483
|
+
|
|
484
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
485
|
+
|
|
486
|
+
const startTime = Date.now()
|
|
487
|
+
await simulateHook("/fake/plugin/dir")
|
|
488
|
+
const elapsed = Date.now() - startTime
|
|
489
|
+
getLocalVersionOverride = null
|
|
490
|
+
|
|
491
|
+
console.log(` E2E-004: simulateHook returned in ${elapsed}ms`)
|
|
492
|
+
|
|
493
|
+
// The hook should have upgrade output written to stderr
|
|
494
|
+
assert.ok(stderrOutput.length > 0,
|
|
495
|
+
"E2E-004 FAIL: no stderr output from upgrade — chat.message hook would be silent")
|
|
496
|
+
|
|
497
|
+
// If upgrade happened, check the content
|
|
498
|
+
if (stderrOutput.length > 0) {
|
|
499
|
+
const output = stderrOutput.join("\n")
|
|
500
|
+
assert.ok(output.includes("Auto-upgraded") || output.includes("New version"),
|
|
501
|
+
`E2E-004 FAIL: unexpected stderr output: ${output}`)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Second hook call should NOT re-run (checked flag)
|
|
505
|
+
const preCount = stderrOutput.length
|
|
506
|
+
await simulateHook("/fake/plugin/dir")
|
|
507
|
+
assert.equal(stderrOutput.length, preCount,
|
|
508
|
+
"E2E-004 FAIL: second hook call re-ran upgrade check (checked flag broken)")
|
|
509
|
+
|
|
510
|
+
// After hook returns, cache must be written
|
|
511
|
+
const finalCache = readCache(XP_GATE_CACHE_FILE)
|
|
512
|
+
if (finalCache && stderrOutput.some(s => s.includes("Auto-upgraded"))) {
|
|
513
|
+
assert.equal(finalCache.status, "current",
|
|
514
|
+
"E2E-004 FAIL: after chat.message hook, cache has no status:current. " +
|
|
515
|
+
"The fire-and-forget bug would cause this.")
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// ── Suite 5: Cache integrity after upgrade (E2E-005) ──
|
|
521
|
+
|
|
522
|
+
void describe("E2E: Cache integrity after real npm install (E2E-005)", () => {
|
|
523
|
+
const fakeHome = join(tmpdir(), "xp-gate-e2e-005-" + randomUUID())
|
|
524
|
+
const origHome = process.env.HOME
|
|
525
|
+
|
|
526
|
+
before(() => {
|
|
527
|
+
process.env.HOME = fakeHome
|
|
528
|
+
mkdirSync(join(fakeHome, ".xp-gate"), { recursive: true })
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
after(() => {
|
|
532
|
+
process.env.HOME = origHome
|
|
533
|
+
rmSync(fakeHome, { recursive: true, force: true })
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
void it("cache is NOT corrupted after spawn (atomic write via tmpfile)", async () => {
|
|
537
|
+
if (assertInCI()) return
|
|
538
|
+
|
|
539
|
+
const cachePath = join(fakeHome, ".xp-gate", "xp-gate-version-check.json")
|
|
540
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
541
|
+
ts: Date.now() - 86_400_000 - 3600_000,
|
|
542
|
+
localVersion: "0.8.0",
|
|
543
|
+
remoteVersion: "0.8.1",
|
|
544
|
+
}))
|
|
545
|
+
|
|
546
|
+
getLocalVersionOverride = () => "0.8.0"
|
|
547
|
+
await runBackgroundUpdates("/fake/plugin/dir")
|
|
548
|
+
getLocalVersionOverride = null
|
|
549
|
+
|
|
550
|
+
// Cache file must be valid JSON
|
|
551
|
+
const raw = readFileSync(XP_GATE_CACHE_FILE, "utf8")
|
|
552
|
+
let cache: unknown = null
|
|
553
|
+
assert.doesNotThrow(() => {
|
|
554
|
+
cache = JSON.parse(raw)
|
|
555
|
+
}, "E2E-005 FAIL: cache file is not valid JSON — tmpfile write may be corrupted")
|
|
556
|
+
|
|
557
|
+
// Cache must have required fields
|
|
558
|
+
const c = cache as Record<string, unknown>
|
|
559
|
+
assert.ok(typeof c.ts === "number", "E2E-005 FAIL: cache missing ts")
|
|
560
|
+
assert.ok(typeof c.remoteVersion === "string", "E2E-005 FAIL: cache missing remoteVersion")
|
|
561
|
+
})
|
|
562
|
+
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-24
|
|
4
|
+
**Commit:** f8338d6
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.12.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
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-24
|
|
4
|
+
**Commit:** f8338d6
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.12.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.
|
|
@@ -237,11 +237,11 @@ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW
|
|
|
237
237
|
- **HARD-GATE**: 设计未批准 → 不可进入实现
|
|
238
238
|
|
|
239
239
|
### Phase 1: PLAN(共识评审)
|
|
240
|
-
- **注意**: `autoplan` 和 `to-issues`
|
|
241
|
-
-
|
|
240
|
+
- **注意**: `autoplan`、`delphi-review` 和 `to-issues` 均为交互式 skill,**必须由 orchestrator 直接执行**(Issue #225, #248, #249)
|
|
241
|
+
- **执行模式(三阶段,全部 orchestrator 直接执行)**:
|
|
242
242
|
1. **Orchestrator 直接执行 autoplan**: `skill(name="autoplan")` → 用户确认 taste_decisions
|
|
243
|
-
2. **Orchestrator 直接执行
|
|
244
|
-
3. **
|
|
243
|
+
2. **Orchestrator 直接执行 delphi-review**: `skill(name="delphi-review")` — 等待 APPROVED(非 APPROVED 时需等待用户确认/处理)
|
|
244
|
+
3. **Orchestrator 直接执行 to-issues**: `skill(name="to-issues")` → 用户确认 Issue 拆分方案
|
|
245
245
|
- 输入: phase-0-summary.md + 设计文档
|
|
246
246
|
- 输出: `specification.yaml`(含 user_stories[])+ `slices-manifest.json`
|
|
247
247
|
|
|
@@ -276,13 +276,10 @@ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW
|
|
|
276
276
|
5. **Mock Minimization**: integration-first, mock 仅限 external services, 密度 > 30% 需 `@mock-justified`
|
|
277
277
|
|
|
278
278
|
### Phase 3: REVIEW + TEST(验证)
|
|
279
|
-
- **
|
|
279
|
+
- **Orchestrator 直接执行**: delphi-review code-walkthrough 需要用户确认 verdict(Issue #249),**必须由 orchestrator 直接调用** `skill(name="delphi-review")`
|
|
280
|
+
- **Orchestrator 按序执行**: `delphi-review --mode code-walkthrough` → 等待 APPROVED → `test-specification-alignment` → `browse` (gstack) → 可选 `qa`/`design-review`/`benchmark` (gstack)
|
|
280
281
|
- 输入: phase-2-summary.md + MVP 代码
|
|
281
282
|
- 输出: 评审报告 + 测试对齐结果
|
|
282
|
-
- `delphi-review --mode code-walkthrough` — 多专家匿名代码走查
|
|
283
|
-
- `test-specification-alignment` — 测试与 Spec 对齐验证
|
|
284
|
-
- `browse` (gstack) — 浏览器自动化测试
|
|
285
|
-
- `k6` / `locust` / `gatling` — 负载/压力测试(可选,后端项目)
|
|
286
283
|
|
|
287
284
|
### 负载/压力测试(可选)
|
|
288
285
|
- **适用项目**:主要用于后端服务的压力测试 (k6/Locust/Gatling),Web 前端已有 `benchmark` 技能覆盖 Core Web Vitals、加载时间和资源大小等性能指标
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
| -1 | ISOLATE | ❌ | Bash(直接执行) | 无 | orchestrator |
|
|
32
32
|
| -0.5 | AUTO-ESTIMATE | ❌ | Bash(直接执行) | 无 | orchestrator |
|
|
33
33
|
| 0 | THINK | ❌ | orchestrator(直接执行) | `["brainstorming"]` | orchestrator |
|
|
34
|
-
| 1 | PLAN |
|
|
34
|
+
| 1 | PLAN | ❌ | orchestrator(直接执行) | `["autoplan", "delphi-review", "to-issues"]` | orchestrator |
|
|
35
35
|
| 2 | BUILD | ✅ | ralph-loop | `["test-driven-development"]` | subagent |
|
|
36
|
-
| 3 | REVIEW |
|
|
36
|
+
| 3 | REVIEW | ❌ | orchestrator(直接执行) | `["delphi-review", "test-specification-alignment"]` | orchestrator |
|
|
37
37
|
| 4 | USER ACCEPT | ❌ | **强制人工** | 无 | 用户 |
|
|
38
38
|
| 5 | FEEDBACK | ✅ | `quick` | `["learn", "retro", "systematic-debugging"]` | subagent |
|
|
39
39
|
| 6 | SHIP | ❌ | orchestrator(直接执行) | `["finishing-a-development-branch", "ship"]` | orchestrator |
|
|
@@ -46,15 +46,17 @@
|
|
|
46
46
|
|-------|-------|---------------------------|
|
|
47
47
|
| 0 | `brainstorming` | 需要与用户对话确认需求、提出澄清问题。Subagent 是 fire-and-forget 模式,无法暂停等待用户输入(Issue #217) |
|
|
48
48
|
| 1 | `autoplan` | taste_decisions 节点暂停等待用户确认。必须由 orchestrator 直接执行(Issue #225) |
|
|
49
|
+
| 1 | `delphi-review` | design 模式需等待 verdict APPROVED;非 APPROVED 时需用户确认是否接受分歧方案(Issue #249) |
|
|
49
50
|
| 1 | `to-issues` | Step 6 "向用户确认" — 展示拆分结果,等待用户批准后才生成 slices-manifest.json |
|
|
51
|
+
| 3 | `delphi-review --mode code-walkthrough` | Code walkthrough 非 APPROVED 时需暂停等待用户处理 Critical Issues(Issue #249) |
|
|
50
52
|
| 6 | `finishing-a-development-branch` | 4 选项菜单 (merge/PR/keep/discard) 需要用户选择;Option 4 (discard) 要求 typed confirmation |
|
|
51
53
|
| 6 | `ship` | PR 创建前需要用户确认;包含 AskUserQuestion STOP 点 |
|
|
52
54
|
| 7 | `land-and-deploy` | Merge 确认、rollback 决策 — 均为用户交互点 |
|
|
53
55
|
|
|
54
|
-
**Phase 1
|
|
56
|
+
**Phase 1 执行模式(全部 orchestrator 直接执行)**:
|
|
55
57
|
1. **Orchestrator 直接执行 autoplan**:`skill(name="autoplan")` → 等待用户确认 taste_decisions
|
|
56
|
-
2. **Orchestrator 直接执行
|
|
57
|
-
3. **
|
|
58
|
+
2. **Orchestrator 直接执行 delphi-review**:`skill(name="delphi-review")` → 等待 APPROVED(非 APPROVED 时暂停等待用户确认)
|
|
59
|
+
3. **Orchestrator 直接执行 to-issues**:`skill(name="to-issues")` → 等待用户确认 Issue 拆分
|
|
58
60
|
|
|
59
61
|
**上下文隔离原则**:
|
|
60
62
|
- 每个 Subagent 在**独立 session** 中启动,不继承 orchestrator 的对话历史
|
|
@@ -71,7 +73,7 @@
|
|
|
71
73
|
| Phase 0 | phase--1-summary(仅路径) | 隔离环境信息(worktree 路径) |
|
|
72
74
|
| Phase 1 | phase-0-summary.md + design-doc | 设计决策 + 结构化规格 |
|
|
73
75
|
| Phase 2 | phase-1-summary.md + specification.yaml | 评审结论 + REQ 列表 |
|
|
74
|
-
| Phase 3 | phase-2-summary.md + MVP 代码 |
|
|
76
|
+
| Phase 3 | phase-2-summary.md + MVP 代码 | 构建结果(orchestrator 直接执行,无 subagent 上下文继承) |
|
|
75
77
|
| Phase 4 | — | **人工验收**。Phase 4 不产生 subagent summary,但用户验收结果记录在 `.sprint-state/phase-outputs/emergent-issues.md`(如有 emergent issues)。Phase 5 加载此文件。 |
|
|
76
78
|
| Phase 5 | phase-4-summary.md + emergent-issues.md | 验收结论 |
|
|
77
79
|
| Phase 6 | phase-5-summary.md + feedback-log.md | 复盘结论 |
|