@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.
Files changed (34) 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 +180 -93
  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 +63 -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/sprint-flow/AGENTS.md +3 -3
  18. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  19. package/plugins/opencode/__tests__/tui-plugin.test.ts +306 -0
  20. package/plugins/opencode/__tests__/xp-gate-update.test.ts +384 -0
  21. package/plugins/opencode/index.ts +183 -75
  22. package/plugins/opencode/package.json +14 -3
  23. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  24. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  25. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  26. package/plugins/opencode/tsconfig.json +1 -1
  27. package/plugins/opencode/tui-plugin.ts +157 -0
  28. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  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/skills/delphi-review/AGENTS.md +3 -3
  33. package/skills/sprint-flow/AGENTS.md +3 -3
  34. 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
- checkPluginUpdate(directory).catch((_err) => { /* silent: non-critical background check */ })
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.2",
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.15.0"
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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.
@@ -10,6 +10,6 @@
10
10
  "declaration": false,
11
11
  "noEmit": true
12
12
  },
13
- "include": ["index.ts"],
13
+ "include": ["index.ts", "tui-plugin.ts"],
14
14
  "exclude": ["node_modules", "dist", "skills"]
15
15
  }
@@ -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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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.
@@ -1,9 +1,9 @@
1
1
  # PRINCIPLES CHECKER MODULE
2
2
 
3
- **Generated:** 2026-06-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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-18
4
- **Commit:** 5ee2fa4
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.0.0
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.