@boyingliu01/opencode-plugin 0.9.3 → 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/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import { z } from "zod"
3
- import { exec, execSync } from "node:child_process"
3
+ import { exec, execSync, spawn } from "node:child_process"
4
4
  import { promisify } from "node:util"
5
5
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"
6
6
  import { join } from "node:path"
@@ -115,10 +115,25 @@ async function checkXpGateUpdate(): Promise<UpgradeResult> {
115
115
  return { action: "noop", localVersion, remoteVersion }
116
116
  }
117
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
+
118
125
  writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion, remoteVersion })
119
126
  try {
120
- execSync(`npm install -g ${XP_GATE_NPM_PKG}@${remoteVersion}`, { stdio: "pipe", timeout: 120_000 })
121
- writeCache(XP_GATE_CACHE_FILE, { ts: Date.now(), localVersion: remoteVersion, remoteVersion, status: "current" })
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 */ })
122
137
  return { action: "upgraded", localVersion, remoteVersion }
123
138
  } catch (err) {
124
139
  const msg = err instanceof Error ? err.message : String(err)
@@ -126,6 +141,16 @@ async function checkXpGateUpdate(): Promise<UpgradeResult> {
126
141
  }
127
142
  }
128
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
+
129
154
  // ── OpenCode plugin version check (notification only) ──
130
155
 
131
156
  async function checkPluginUpdate(pluginDir: string): Promise<void> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -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-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.3.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:** b67fc85
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.3.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:** b67fc85
3
+ **Generated:** 2026-06-19
4
+ **Commit:** b67f7f9
5
5
  **Branch:** main
6
- **Version:** 0.9.3.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.
package/tui-plugin.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  import { existsSync, readFileSync } from "node:fs"
16
16
  import { join } from "node:path"
17
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"
18
19
 
19
20
  // ── Sprint state schema ──
20
21
 
@@ -44,24 +45,6 @@ interface SprintState {
44
45
  phase_history?: SprintPhaseHistory[]
45
46
  }
46
47
 
47
- // ── Constants ──
48
-
49
- const PHASE_NAMES: Record<string, string> = {
50
- "-1": "ISOLATE",
51
- "-0.5": "AUTO-ESTIMATE",
52
- "0": "THINK",
53
- "1": "PLAN",
54
- "2": "BUILD",
55
- "3": "REVIEW",
56
- "4": "USER ACCEPT",
57
- "5": "FEEDBACK",
58
- "6": "SHIP",
59
- "7": "LAND",
60
- "8": "CLEANUP",
61
- }
62
-
63
- const PHASE_ORDER = ["-1", "-0.5", "0", "1", "2", "3", "4", "5", "6", "7", "8"]
64
-
65
48
  // ── Helpers ──
66
49
 
67
50
  function readSprintState(dir: string): SprintState | null {
@@ -74,26 +57,6 @@ function readSprintState(dir: string): SprintState | null {
74
57
  }
75
58
  }
76
59
 
77
- function isStale(state: SprintState): boolean {
78
- if (!state || !state.started_at) return false
79
- const started = new Date(state.started_at).getTime()
80
- if (isNaN(started)) return false
81
- let latest = started
82
- if (Array.isArray(state.phase_history)) {
83
- for (const ph of state.phase_history) {
84
- if (ph.completed_at) {
85
- const t = new Date(ph.completed_at).getTime()
86
- if (!isNaN(t) && t > latest) latest = t
87
- }
88
- if (ph.started_at) {
89
- const t = new Date(ph.started_at).getTime()
90
- if (!isNaN(t) && t > latest) latest = t
91
- }
92
- }
93
- }
94
- return Date.now() - latest > 3_600_000
95
- }
96
-
97
60
  function statusSymbol(status: string | undefined, key: string, currentPhase: string | number | undefined): string {
98
61
  if (status === "completed") return "✓"
99
62
  if (status === "in_progress") return "→"
@@ -109,57 +72,63 @@ function renderPhaseLine(key: string, history: SprintPhaseHistory | undefined, c
109
72
  .replace(/\s+$/, "")
110
73
  }
111
74
 
112
- function renderSprintSidebar(state: SprintState): string {
113
- if (!state || !state.task_description) return ""
114
-
115
- const lines: string[] = []
116
- const metrics = state.metrics || {}
117
- const currentPhase = state.phase
118
-
119
- // Build lookup
120
- const historyByPhase: Record<string, SprintPhaseHistory> = {}
75
+ function buildPhaseLookup(state: SprintState): Record<string, SprintPhaseHistory> {
76
+ const lookup: Record<string, SprintPhaseHistory> = {}
121
77
  if (Array.isArray(state.phase_history)) {
122
78
  for (const ph of state.phase_history) {
123
- historyByPhase[String(ph.phase)] = ph
79
+ lookup[String(ph.phase)] = ph
124
80
  }
125
81
  }
82
+ return lookup
83
+ }
126
84
 
127
- // Title
128
- lines.push(`SPRINT: ${state.task_description}`)
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
+ }
129
91
 
130
- // Metrics
131
- const metricParts: string[] = []
132
- if (metrics.tests_passed != null) {
133
- metricParts.push(`tests:${metrics.tests_passed}`)
134
- }
135
- if (metrics.coverage_pct != null) {
136
- metricParts.push(`cov:${metrics.coverage_pct}%`)
137
- }
138
- if (metricParts.length > 0) {
139
- lines.push(metricParts.join(" "))
140
- }
92
+ function buildStaleWarning(state: SprintState): string | null {
93
+ if (!state || !state.started_at) return null
94
+ return isStale(state) ? "⚠ idle >1h" : null
95
+ }
141
96
 
142
- // Stale warning
143
- if (isStale(state)) {
144
- lines.push("⚠ idle >1h")
145
- }
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
+ }
146
103
 
147
- // Phase progress
104
+ function renderPhaseLines(
105
+ historyByPhase: Record<string, SprintPhaseHistory>,
106
+ currentPhase: string | number | undefined,
107
+ ): string[] {
108
+ const lines: string[] = []
148
109
  for (const key of PHASE_ORDER) {
149
110
  const history = historyByPhase[key]
150
- // Only show phases with activity or current
151
111
  if (!history && String(currentPhase) !== key) continue
152
- const line = renderPhaseLine(key, history, currentPhase)
153
- lines.push(line)
154
-
155
- // REQ-level progress for BUILD phase
112
+ lines.push(renderPhaseLine(key, history, currentPhase))
156
113
  if (key === "2" && history?.reqs) {
157
- const reqNames = Object.entries(history.reqs)
158
- .filter(([, r]) => r.name)
159
- .map(([id, r]) => ` ${statusSymbol(r.status, id, undefined)} ${r.name}`)
160
- if (reqNames.length > 0) lines.push(...reqNames)
114
+ lines.push(...renderBuildReqs(history))
161
115
  }
162
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))
163
132
 
164
133
  return lines.join("\n")
165
134
  }