@boyingliu01/xp-gate 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/baseline.test.js +89 -0
- package/lib/__tests__/doctor-helpers.test.js +139 -0
- package/lib/__tests__/upgrade-exec.test.js +42 -33
- package/lib/baseline.js +75 -48
- package/lib/doctor.js +180 -93
- package/lib/install-skill.js +81 -62
- package/lib/shared-phase-constants.d.ts +17 -0
- package/lib/shared-phase-constants.js +63 -0
- package/lib/shared-utils.js +14 -3
- package/lib/sprint-status.js +75 -83
- package/lib/upgrade.js +128 -85
- 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/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/__tests__/tui-plugin.test.ts +306 -0
- package/plugins/opencode/__tests__/xp-gate-update.test.ts +384 -0
- package/plugins/opencode/index.ts +183 -75
- package/plugins/opencode/package.json +14 -3
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/tsconfig.json +1 -1
- package/plugins/opencode/tui-plugin.ts +157 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- 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/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { z } from "zod"
|
|
3
|
-
import { exec } from "child_process"
|
|
4
|
-
import { promisify } from "util"
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
|
|
3
|
+
import { exec, execSync, spawn } from "node:child_process"
|
|
4
|
+
import { promisify } from "node:util"
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"
|
|
6
6
|
import { join } from "node:path"
|
|
7
7
|
import { homedir } from "node:os"
|
|
8
8
|
|
|
@@ -13,6 +13,183 @@ interface OpenCodePluginInput {
|
|
|
13
13
|
$: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<{ text(): Promise<string> }>
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// ── Constants ──
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_MS = 86_400_000 // 24h
|
|
19
|
+
const FETCH_TIMEOUT_MS = 5_000
|
|
20
|
+
|
|
21
|
+
const XP_GATE_NPM_PKG = "@boyingliu01/xp-gate"
|
|
22
|
+
const XP_GATE_CACHE_FILE = join(homedir(), ".xp-gate", "xp-gate-version-check.json")
|
|
23
|
+
const XP_GATE_REGISTRY_URL = `https://registry.npmjs.org/-/package/${encodeURIComponent(XP_GATE_NPM_PKG)}/dist-tags`
|
|
24
|
+
|
|
25
|
+
const OPENCODE_PLUGIN_REGISTRY = "https://registry.npmjs.org/-/package/@boyingliu01%2Fopencode-plugin/dist-tags"
|
|
26
|
+
const OPENCODE_CACHE_FILE = join(homedir(), ".xp-gate", "opencode-plugin-version-check.json")
|
|
27
|
+
|
|
28
|
+
let checked = false
|
|
29
|
+
|
|
30
|
+
// ── Utilities ──
|
|
31
|
+
|
|
32
|
+
function semverLt(a: string, b: string): boolean {
|
|
33
|
+
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
34
|
+
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
35
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
36
|
+
const na = pa[i] ?? 0
|
|
37
|
+
const nb = pb[i] ?? 0
|
|
38
|
+
if (na !== nb) return na < nb
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchNpmLatestVersion(url: string): Promise<string | null> {
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
48
|
+
if (!response.ok) return null
|
|
49
|
+
const data: Record<string, unknown> = await response.json()
|
|
50
|
+
return typeof data.latest === "string" ? data.latest : null
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readCache(file: string): { ts: number; remoteVersion: string; status?: string } | null {
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(file)) return null
|
|
61
|
+
const raw = readFileSync(file, "utf8")
|
|
62
|
+
const data = JSON.parse(raw)
|
|
63
|
+
if (Date.now() - data.ts < CACHE_TTL_MS && data.remoteVersion) return data
|
|
64
|
+
return null
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeCache(file: string, data: object): void {
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
73
|
+
writeFileSync(file + ".tmp", JSON.stringify(data), "utf8")
|
|
74
|
+
try { rmSync(file) } catch {}
|
|
75
|
+
writeFileSync(file, readFileSync(file + ".tmp", "utf8"), "utf8")
|
|
76
|
+
try { rmSync(file + ".tmp") } catch {}
|
|
77
|
+
} catch {
|
|
78
|
+
// silent
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── XP-Gate npm package auto-update ──
|
|
83
|
+
|
|
84
|
+
type UpgradeResult = {
|
|
85
|
+
action: "noop" | "upgraded" | "error"
|
|
86
|
+
localVersion: string | null
|
|
87
|
+
remoteVersion: string | null
|
|
88
|
+
error?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getLocalXpGateVersion(): string | null {
|
|
92
|
+
try {
|
|
93
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf8" }).trim()
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(globalRoot, XP_GATE_NPM_PKG, "package.json"), "utf8"))
|
|
95
|
+
return pkg.version || null
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkXpGateUpdate(): Promise<UpgradeResult> {
|
|
102
|
+
const cached = readCache(XP_GATE_CACHE_FILE)
|
|
103
|
+
if (cached?.status === "current" && cached.remoteVersion) {
|
|
104
|
+
return { action: "noop", localVersion: cached.remoteVersion, remoteVersion: cached.remoteVersion }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const localVersion = getLocalXpGateVersion()
|
|
108
|
+
if (!localVersion) return { action: "noop", localVersion: null, remoteVersion: null }
|
|
109
|
+
|
|
110
|
+
const remoteVersion = await fetchNpmLatestVersion(XP_GATE_REGISTRY_URL)
|
|
111
|
+
if (!remoteVersion) return { action: "noop", localVersion, remoteVersion: null }
|
|
112
|
+
|
|
113
|
+
if (!semverLt(localVersion, remoteVersion)) {
|
|
114
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
115
|
+
return { action: "noop", localVersion, remoteVersion }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check opt-out config
|
|
119
|
+
const config = readXpGateConfig()
|
|
120
|
+
if (config?.autoUpgrade === false) {
|
|
121
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
122
|
+
return { action: "noop", localVersion, remoteVersion }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
126
|
+
try {
|
|
127
|
+
const child = spawn("npm", ["install", "-g", `${XP_GATE_NPM_PKG}@${remoteVersion}`], {
|
|
128
|
+
stdio: "pipe",
|
|
129
|
+
timeout: 120_000,
|
|
130
|
+
})
|
|
131
|
+
child.on("close", (code) => {
|
|
132
|
+
if (code === 0) {
|
|
133
|
+
writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
child.on("error", () => { /* empty — cache won't get status:current, so next check retries */ })
|
|
137
|
+
return { action: "upgraded", localVersion, remoteVersion }
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
140
|
+
return { action: "error", localVersion, remoteVersion, error: msg }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readXpGateConfig(): { autoUpgrade?: boolean } | null {
|
|
145
|
+
const cfgPath = join(homedir(), ".xp-gate", "config.json")
|
|
146
|
+
try {
|
|
147
|
+
if (!existsSync(cfgPath)) return null
|
|
148
|
+
return JSON.parse(readFileSync(cfgPath, "utf8"))
|
|
149
|
+
} catch {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── OpenCode plugin version check (notification only) ──
|
|
155
|
+
|
|
156
|
+
async function checkPluginUpdate(pluginDir: string): Promise<void> {
|
|
157
|
+
const cached = readCache(OPENCODE_CACHE_FILE)
|
|
158
|
+
if (cached?.status === "current" && cached.remoteVersion) return
|
|
159
|
+
|
|
160
|
+
let localVersion = ""
|
|
161
|
+
try {
|
|
162
|
+
const pkg = JSON.parse(readFileSync(join(pluginDir, "package.json"), "utf8"))
|
|
163
|
+
localVersion = pkg.version || ""
|
|
164
|
+
} catch {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const remoteVersion = await fetchNpmLatestVersion(OPENCODE_PLUGIN_REGISTRY)
|
|
169
|
+
if (!remoteVersion) return
|
|
170
|
+
|
|
171
|
+
if (remoteVersion && localVersion && semverLt(localVersion, remoteVersion)) {
|
|
172
|
+
writeCache(OPENCODE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
|
|
173
|
+
} else if (remoteVersion && localVersion) {
|
|
174
|
+
writeCache(OPENCODE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion, status: "current" })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Combined background check (runs once on first chat.message) ──
|
|
179
|
+
|
|
180
|
+
async function runBackgroundUpdates(pluginDir: string): Promise<string | null> {
|
|
181
|
+
const result = await checkXpGateUpdate()
|
|
182
|
+
await checkPluginUpdate(pluginDir)
|
|
183
|
+
|
|
184
|
+
if (result.action === "upgraded") {
|
|
185
|
+
return `[XP-Gate] Auto-upgraded from v${result.localVersion} to v${result.remoteVersion}`
|
|
186
|
+
}
|
|
187
|
+
if (result.action === "error") {
|
|
188
|
+
return `[XP-Gate] Upgrade check: v${result.remoteVersion} available (auto-upgrade failed: ${result.error})`
|
|
189
|
+
}
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
16
193
|
async function runCmd(cmd: string, cwd: string): Promise<string> {
|
|
17
194
|
try {
|
|
18
195
|
const { stdout } = await execAsync(cmd, { cwd, timeout: 30000 })
|
|
@@ -49,77 +226,6 @@ async function getUpgradeSuggestion(cwd: string): Promise<string> {
|
|
|
49
226
|
}
|
|
50
227
|
}
|
|
51
228
|
|
|
52
|
-
// ── Auto-update check for opencode-plugin ──
|
|
53
|
-
|
|
54
|
-
const CACHE_TTL_MS = 86_400_000
|
|
55
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/@boyingliu01%2Fopencode-plugin/dist-tags"
|
|
56
|
-
const FETCH_TIMEOUT_MS = 5_000
|
|
57
|
-
const CACHE_FILE = join(homedir(), ".xp-gate", "opencode-plugin-version-check.json")
|
|
58
|
-
|
|
59
|
-
let checked = false
|
|
60
|
-
let checkInFlight: Promise<void> | null = null
|
|
61
|
-
|
|
62
|
-
function semverLt(a: string, b: string): boolean {
|
|
63
|
-
const pa = a.replace(/^v/, "").split(".").map(Number)
|
|
64
|
-
const pb = b.replace(/^v/, "").split(".").map(Number)
|
|
65
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
66
|
-
const na = pa[i] ?? 0
|
|
67
|
-
const nb = pb[i] ?? 0
|
|
68
|
-
if (na !== nb) return na < nb
|
|
69
|
-
}
|
|
70
|
-
return false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function checkPluginUpdate(pluginDir: string): Promise<void> {
|
|
74
|
-
if (checkInFlight) return
|
|
75
|
-
|
|
76
|
-
checkInFlight = (async () => {
|
|
77
|
-
try {
|
|
78
|
-
mkdirSync(join(homedir(), ".xp-gate"), { recursive: true })
|
|
79
|
-
|
|
80
|
-
if (existsSync(CACHE_FILE)) {
|
|
81
|
-
const cached = JSON.parse(readFileSync(CACHE_FILE, "utf8"))
|
|
82
|
-
if (Date.now() - cached.ts < CACHE_TTL_MS) return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let localVersion = ""
|
|
86
|
-
try {
|
|
87
|
-
const pkg = JSON.parse(readFileSync(join(pluginDir, "package.json"), "utf8"))
|
|
88
|
-
localVersion = pkg.version || ""
|
|
89
|
-
} catch {
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const controller = new AbortController()
|
|
94
|
-
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
|
|
95
|
-
try {
|
|
96
|
-
const response = await fetch(NPM_REGISTRY_URL, { signal: controller.signal })
|
|
97
|
-
if (!response.ok) return
|
|
98
|
-
const data: Record<string, unknown> = await response.json()
|
|
99
|
-
const remoteVersion = String(data.latest || "")
|
|
100
|
-
|
|
101
|
-
if (remoteVersion && localVersion && semverLt(localVersion, remoteVersion)) {
|
|
102
|
-
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), localVersion, remoteVersion }))
|
|
103
|
-
process.stderr.write(
|
|
104
|
-
`[XP-Gate] New opencode-plugin version v${remoteVersion} available (you have v${localVersion})\n` +
|
|
105
|
-
`[XP-Gate] Update with: cd ~/.config/opencode && npm update @boyingliu01/opencode-plugin\n`
|
|
106
|
-
)
|
|
107
|
-
} else if (remoteVersion && localVersion) {
|
|
108
|
-
// Cache "up to date" to avoid re-fetching every session
|
|
109
|
-
writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), localVersion, remoteVersion, status: "current" }))
|
|
110
|
-
}
|
|
111
|
-
} finally {
|
|
112
|
-
clearTimeout(timer)
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// All errors silently ignored
|
|
116
|
-
}
|
|
117
|
-
})()
|
|
118
|
-
|
|
119
|
-
await checkInFlight
|
|
120
|
-
checkInFlight = null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
229
|
// ── Plugin definition ──
|
|
124
230
|
|
|
125
231
|
export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
@@ -184,7 +290,9 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
|
184
290
|
"chat.message": async (_input: { message: string }) => {
|
|
185
291
|
if (!checked) {
|
|
186
292
|
checked = true
|
|
187
|
-
|
|
293
|
+
runBackgroundUpdates(directory).then((msg) => {
|
|
294
|
+
if (msg) process.stderr.write(`${msg}\n`)
|
|
295
|
+
})
|
|
188
296
|
}
|
|
189
297
|
return { action: "continue" }
|
|
190
298
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/opencode-plugin",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"description": "XP-Gate quality gates + AI workflow skills for OpenCode",
|
|
6
|
+
"description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/boyingliu01/xp-gate",
|
|
@@ -13,8 +13,19 @@
|
|
|
13
13
|
"registry": "https://registry.npmjs.org",
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./index.ts",
|
|
19
|
+
"types": "./index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./tui": {
|
|
22
|
+
"import": "./tui-plugin.ts",
|
|
23
|
+
"types": "./tui-plugin.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
16
26
|
"files": [
|
|
17
27
|
"index.ts",
|
|
28
|
+
"tui-plugin.ts",
|
|
18
29
|
"skills/",
|
|
19
30
|
"tsconfig.json",
|
|
20
31
|
"README.md"
|
|
@@ -24,7 +35,7 @@
|
|
|
24
35
|
"check": "tsc --noEmit"
|
|
25
36
|
},
|
|
26
37
|
"dependencies": {
|
|
27
|
-
"@opencode-ai/plugin": "^1.
|
|
38
|
+
"@opencode-ai/plugin": "^1.17.8"
|
|
28
39
|
},
|
|
29
40
|
"devDependencies": {
|
|
30
41
|
"typescript": "^5.4.0"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.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-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XP-Gate OpenCode TUI Slot Plugin
|
|
3
|
+
*
|
|
4
|
+
* Registers sidebar_content slot to display Sprint Flow progress
|
|
5
|
+
* from .sprint-state/sprint-state.json.
|
|
6
|
+
*
|
|
7
|
+
* This is a separate plugin file because SDK 1.x PluginModule does not
|
|
8
|
+
* support server + tui in the same module. Users register this file
|
|
9
|
+
* in ~/.config/opencode/tui.json as:
|
|
10
|
+
* { "plugin": ["@boyingliu01/opencode-plugin/tui"] }
|
|
11
|
+
*
|
|
12
|
+
* The npm package exports "./tui" from package.json for this resolution.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
16
|
+
import { join } from "node:path"
|
|
17
|
+
import type { TuiPlugin, TuiSlotPlugin, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
|
18
|
+
import { PHASE_NAMES, PHASE_ORDER, getLatestTimestamp, isStale } from "../../src/npm-package/lib/shared-phase-constants.js"
|
|
19
|
+
|
|
20
|
+
// ── Sprint state schema ──
|
|
21
|
+
|
|
22
|
+
interface SprintReq {
|
|
23
|
+
name?: string
|
|
24
|
+
status?: "completed" | "in_progress" | "pending"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SprintPhaseHistory {
|
|
28
|
+
phase: number | string
|
|
29
|
+
phase_name?: string
|
|
30
|
+
status?: "completed" | "in_progress" | "pending"
|
|
31
|
+
started_at?: string
|
|
32
|
+
completed_at?: string
|
|
33
|
+
duration_seconds?: number
|
|
34
|
+
reqs?: Record<string, SprintReq>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SprintState {
|
|
38
|
+
id?: string
|
|
39
|
+
phase?: number | string
|
|
40
|
+
status?: string
|
|
41
|
+
started_at?: string
|
|
42
|
+
task_description?: string
|
|
43
|
+
isolation?: { branch?: string; worktree_path?: string }
|
|
44
|
+
metrics?: { tests_passed?: number; tests_failed?: number; coverage_pct?: number }
|
|
45
|
+
phase_history?: SprintPhaseHistory[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Helpers ──
|
|
49
|
+
|
|
50
|
+
function readSprintState(dir: string): SprintState | null {
|
|
51
|
+
try {
|
|
52
|
+
const stateFile = join(dir, ".sprint-state", "sprint-state.json")
|
|
53
|
+
if (!existsSync(stateFile)) return null
|
|
54
|
+
return JSON.parse(readFileSync(stateFile, "utf8"))
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function statusSymbol(status: string | undefined, key: string, currentPhase: string | number | undefined): string {
|
|
61
|
+
if (status === "completed") return "✓"
|
|
62
|
+
if (status === "in_progress") return "→"
|
|
63
|
+
if (String(currentPhase) === key) return "·"
|
|
64
|
+
return "○"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderPhaseLine(key: string, history: SprintPhaseHistory | undefined, currentPhase: string | number | undefined): string {
|
|
68
|
+
const name = history?.phase_name || PHASE_NAMES[key] || key
|
|
69
|
+
const status = history?.status || (String(currentPhase) === key ? "in_progress" : "pending")
|
|
70
|
+
const sym = statusSymbol(status, key, currentPhase)
|
|
71
|
+
return `${sym} ${name.padEnd(14)} ${status === "completed" ? "done" : status === "in_progress" ? "active" : ""}`
|
|
72
|
+
.replace(/\s+$/, "")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildPhaseLookup(state: SprintState): Record<string, SprintPhaseHistory> {
|
|
76
|
+
const lookup: Record<string, SprintPhaseHistory> = {}
|
|
77
|
+
if (Array.isArray(state.phase_history)) {
|
|
78
|
+
for (const ph of state.phase_history) {
|
|
79
|
+
lookup[String(ph.phase)] = ph
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return lookup
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildMetricsLine(metrics: SprintState["metrics"]): string | null {
|
|
86
|
+
const parts: string[] = []
|
|
87
|
+
if (metrics?.tests_passed != null) parts.push(`tests:${metrics.tests_passed}`)
|
|
88
|
+
if (metrics?.coverage_pct != null) parts.push(`cov:${metrics.coverage_pct}%`)
|
|
89
|
+
return parts.length > 0 ? parts.join(" ") : null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildStaleWarning(state: SprintState): string | null {
|
|
93
|
+
if (!state || !state.started_at) return null
|
|
94
|
+
return isStale(state) ? "⚠ idle >1h" : null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderBuildReqs(history: SprintPhaseHistory): string[] {
|
|
98
|
+
if (!history.reqs) return []
|
|
99
|
+
return Object.entries(history.reqs)
|
|
100
|
+
.filter(([, r]) => r.name)
|
|
101
|
+
.map(([id, r]) => ` ${statusSymbol(r.status, id, undefined)} ${r.name}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderPhaseLines(
|
|
105
|
+
historyByPhase: Record<string, SprintPhaseHistory>,
|
|
106
|
+
currentPhase: string | number | undefined,
|
|
107
|
+
): string[] {
|
|
108
|
+
const lines: string[] = []
|
|
109
|
+
for (const key of PHASE_ORDER) {
|
|
110
|
+
const history = historyByPhase[key]
|
|
111
|
+
if (!history && String(currentPhase) !== key) continue
|
|
112
|
+
lines.push(renderPhaseLine(key, history, currentPhase))
|
|
113
|
+
if (key === "2" && history?.reqs) {
|
|
114
|
+
lines.push(...renderBuildReqs(history))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return lines
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderSprintSidebar(state: SprintState): string {
|
|
121
|
+
if (!state || !state.task_description) return ""
|
|
122
|
+
|
|
123
|
+
const lines: string[] = [`SPRINT: ${state.task_description}`]
|
|
124
|
+
const metricsLine = buildMetricsLine(state.metrics)
|
|
125
|
+
if (metricsLine) lines.push(metricsLine)
|
|
126
|
+
|
|
127
|
+
const staleWarning = buildStaleWarning(state)
|
|
128
|
+
if (staleWarning) lines.push(staleWarning)
|
|
129
|
+
|
|
130
|
+
const historyByPhase = buildPhaseLookup(state)
|
|
131
|
+
lines.push(...renderPhaseLines(historyByPhase, state.phase))
|
|
132
|
+
|
|
133
|
+
return lines.join("\n")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── TUI Slot Plugin ──
|
|
137
|
+
|
|
138
|
+
const tuiPlugin: TuiSlotPlugin = {
|
|
139
|
+
slots: {
|
|
140
|
+
sidebar_content: (_props: TuiSlotProps) => {
|
|
141
|
+
const dir = process.env.XP_GATE_PROJECT_DIR || process.cwd()
|
|
142
|
+
const state = readSprintState(dir)
|
|
143
|
+
if (!state) return null
|
|
144
|
+
const text = renderSprintSidebar(state)
|
|
145
|
+
if (!text) return null
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
+
return text as any
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Wrap as TuiPlugin (async factory)
|
|
153
|
+
const plugin: TuiPlugin = async (api, _options, _meta) => {
|
|
154
|
+
api.slots.register(tuiPlugin)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { plugin as tui, readSprintState, renderSprintSidebar }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.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-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.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-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**11-phase** development pipeline: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP. Phase 2 default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE in Phase 1: design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-06-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-06-19
|
|
4
|
+
**Commit:** b67f7f9
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.9.
|
|
6
|
+
**Version:** 0.9.4.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|