@boyingliu01/xp-gate 0.12.2 → 0.12.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 (54) hide show
  1. package/adapters/cpp.sh +0 -0
  2. package/adapters/dart.sh +0 -0
  3. package/adapters/flutter.sh +0 -0
  4. package/adapters/go.sh +0 -0
  5. package/adapters/java.sh +0 -0
  6. package/adapters/kotlin.sh +0 -0
  7. package/adapters/objectivec.sh +0 -0
  8. package/adapters/powershell.sh +0 -0
  9. package/adapters/python.sh +0 -0
  10. package/adapters/shell.sh +0 -0
  11. package/adapters/swift.sh +0 -0
  12. package/adapters/typescript.sh +0 -0
  13. package/bin/xp-gate.js +19 -5
  14. package/gate-3.sh +0 -0
  15. package/gate-4.sh +0 -0
  16. package/gate-7.sh +0 -0
  17. package/gate-8.sh +0 -0
  18. package/gate-9.sh +0 -0
  19. package/lib/__tests__/update-hooks.test.js +573 -0
  20. package/lib/init.js +9 -6
  21. package/lib/sprint-status.js +35 -1
  22. package/lib/update-hooks.js +288 -0
  23. package/mock-policy/AGENTS.md +3 -3
  24. package/mutation/AGENTS.md +3 -3
  25. package/package.json +1 -1
  26. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  27. package/plugins/claude-code/skills/delphi-review/AGENTS.md +6 -6
  28. package/plugins/claude-code/skills/delphi-review/SKILL.md +34 -12
  29. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  30. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/opencode/package.json +1 -1
  32. package/plugins/opencode/skills/delphi-review/AGENTS.md +6 -6
  33. package/plugins/opencode/skills/delphi-review/SKILL.md +34 -12
  34. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  35. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  36. package/plugins/qoder/plugin.json +1 -1
  37. package/plugins/qoder/skills/delphi-review/AGENTS.md +6 -6
  38. package/plugins/qoder/skills/delphi-review/SKILL.md +198 -264
  39. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  40. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  41. package/principles/AGENTS.md +3 -3
  42. package/skills/delphi-review/AGENTS.md +6 -6
  43. package/skills/delphi-review/SKILL.md +34 -12
  44. package/skills/sprint-flow/AGENTS.md +3 -3
  45. package/skills/test-specification-alignment/AGENTS.md +3 -3
  46. package/lib/__tests__/next-sprint.test.js +0 -231
  47. package/lib/next-sprint.js +0 -129
  48. package/lib/shared-phase-constants.d.ts +0 -17
  49. package/lib/shared-phase-constants.js +0 -77
  50. package/plugins/opencode/__tests__/tui-plugin.test.ts +0 -543
  51. package/plugins/opencode/__tests__/xp-gate-e2e-upgrade.test.ts +0 -562
  52. package/plugins/opencode/__tests__/xp-gate-update.test.ts +0 -518
  53. package/plugins/opencode/tui-plugin.ts +0 -414
  54. package/plugins/opencode/tui-plugin.tsx +0 -306
@@ -1,306 +0,0 @@
1
- // @no-test-required: TUI rendering plugin — tested via visual verification in OpenCode runtime
2
- /**
3
- * XP-Gate OpenCode TUI Slot Plugin
4
- *
5
- * Registers sidebar_content slot to display Sprint Flow progress
6
- * from active sprint states discovered in .worktrees/sprint/ subdirectories.
7
- *
8
- * Rendered with @opentui/solid JSX — sidebar_content must return JSX.Element,
9
- * NOT string (strings are silently ignored by OpenCode's TUI renderer).
10
- *
11
- * Users register this file in ~/.config/opencode/tui.json as:
12
- * { "plugin": ["@boyingliu01/opencode-plugin/tui"] }
13
- */
14
- /** @jsxImportSource @opentui/solid */
15
-
16
- import { existsSync, readFileSync, readdirSync } from "node:fs"
17
- import { join, dirname, resolve, parse } from "node:path"
18
- import { homedir } from "node:os"
19
- import type { TuiPlugin, TuiSlotPlugin, TuiSlotProps } from "@opencode-ai/plugin/tui"
20
- import { createMemo, Show, For } from "solid-js"
21
-
22
- // ── Phase constants ──
23
-
24
- const PHASE_NAMES: Record<string, string> = {
25
- '-1': 'ISOLATE', '-0.5': 'AUTO-ESTIMATE', '0': 'THINK',
26
- '1': 'PLAN', '2': 'BUILD', '3': 'REVIEW',
27
- '4': 'SHIP', '5': 'LAND', '6': 'USER ACCEPT',
28
- '7': 'FEEDBACK', '8': 'CLEANUP',
29
- };
30
-
31
- const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
32
-
33
- // ── Types ──
34
-
35
- interface SprintReq {
36
- name?: string
37
- status?: "completed" | "in_progress" | "pending"
38
- }
39
- interface SprintPhaseHistory {
40
- phase: number | string
41
- phase_name?: string
42
- status?: "completed" | "in_progress" | "pending"
43
- started_at?: string; completed_at?: string
44
- duration_seconds?: number
45
- reqs?: Record<string, SprintReq>
46
- }
47
- interface SprintState {
48
- id?: string; phase?: number | string; status?: string; started_at?: string
49
- task_description?: string
50
- isolation?: { branch?: string; worktree_path?: string }
51
- metrics?: { tests_passed?: number; tests_failed?: number; coverage_pct?: number }
52
- phase_history?: SprintPhaseHistory[]
53
- }
54
- interface DiscoveredSprint {
55
- state: SprintState; sourcePath: string; worktreeExists: boolean
56
- }
57
-
58
- // ── Discovery ──
59
-
60
- const MAX_DISCOVERY_RESULTS = 5;
61
-
62
- function findGitRoot(startDir: string): string | null {
63
- let current = resolve(startDir);
64
- const root = parse(current).root;
65
- const seen = new Set<string>();
66
- while (current !== root) {
67
- if (seen.has(current)) break;
68
- seen.add(current);
69
- if (existsSync(join(current, '.git'))) return current;
70
- const parent = dirname(current);
71
- if (parent === current) break;
72
- current = parent;
73
- }
74
- if (existsSync(join(root, '.git'))) return root;
75
- return null;
76
- }
77
-
78
- function readSprintState(dir: string): SprintState | null {
79
- try {
80
- const sf = join(dir, '.sprint-state', 'sprint-state.json');
81
- if (!existsSync(sf)) return null;
82
- return JSON.parse(readFileSync(sf, 'utf8')) as SprintState;
83
- } catch { return null; }
84
- }
85
-
86
- function checkWorktreeExists(p: string | undefined): boolean {
87
- if (!p) return false;
88
- try { return existsSync(p); } catch { return false; }
89
- }
90
-
91
- function parseTime(v: unknown): number {
92
- return new Date(v as string).getTime();
93
- }
94
-
95
- function isStaleSprint(s: SprintState | null): boolean {
96
- if (!s?.started_at) return false;
97
- let latest = parseTime(s.started_at);
98
- if (isNaN(latest)) return false;
99
- for (const ph of s.phase_history ?? []) {
100
- if (ph.completed_at) latest = Math.max(latest, parseTime(ph.completed_at));
101
- if (ph.started_at) latest = Math.max(latest, parseTime(ph.started_at));
102
- }
103
- return latest > 0 && Date.now() - latest > 3_600_000;
104
- }
105
-
106
- function discoverActiveSprints(dir: string): DiscoveredSprint[] {
107
- const gitRoot = findGitRoot(dir);
108
- const results: DiscoveredSprint[] = [];
109
-
110
- if (gitRoot) {
111
- const base = join(gitRoot, '.worktrees', 'sprint');
112
- let entries: { name: string; isDirectory: () => boolean }[] = [];
113
- try { if (existsSync(base)) entries = readdirSync(base, { withFileTypes: true }); } catch { /* ok */ }
114
- for (const e of entries) {
115
- if (!e.isDirectory()) continue;
116
- const d = join(base, e.name);
117
- const state = readSprintState(d);
118
- if (!state?.id) continue;
119
- const wt = checkWorktreeExists(d);
120
- if (isStaleSprint(state) && !wt) continue;
121
- results.push({ state, sourcePath: join(d, '.sprint-state', 'sprint-state.json'), worktreeExists: wt });
122
- }
123
- }
124
-
125
- const localState = readSprintState(dir);
126
- if (localState?.id) {
127
- const wp = localState.isolation?.worktree_path;
128
- const hasWp = !!wp;
129
- const wt = hasWp ? checkWorktreeExists(wp) : false;
130
- if (!hasWp || wt) results.push({ state: localState, sourcePath: join(dir, '.sprint-state', 'sprint-state.json'), worktreeExists: wt });
131
- }
132
-
133
- const deduped = new Map<string, DiscoveredSprint>();
134
- for (const e of results) {
135
- const id = e.state.id!;
136
- const existing = deduped.get(id);
137
- if (!existing) { deduped.set(id, e); continue; }
138
- if (e.worktreeExists && !existing.worktreeExists) { deduped.set(id, e); continue; }
139
- if (!e.worktreeExists && existing.worktreeExists) continue;
140
- const eTs = e.state.started_at ? new Date(e.state.started_at).getTime() : 0;
141
- const xTs = existing.state.started_at ? new Date(existing.state.started_at).getTime() : 0;
142
- if (eTs > xTs || (eTs === xTs && e.sourcePath < existing.sourcePath)) deduped.set(id, e);
143
- }
144
-
145
- return Array.from(deduped.values())
146
- .sort((a, b) => {
147
- const aTs = a.state.started_at ? new Date(a.state.started_at).getTime() : 0;
148
- const bTs = b.state.started_at ? new Date(b.state.started_at).getTime() : 0;
149
- return bTs - aTs || String(b.state.id).localeCompare(String(a.state.id));
150
- })
151
- .slice(0, MAX_DISCOVERY_RESULTS);
152
- }
153
-
154
- // ── Cache ──
155
-
156
- let _cache: { data: DiscoveredSprint[]; upgradeNotice: string | null; ts: number; dir: string } | null = null;
157
- const CACHE_TTL_MS = 5_000;
158
-
159
- // ── Render helpers ──
160
-
161
- function statusSymbol(status: string | undefined, currentPhase: string | number | undefined, key: string): string {
162
- if (status === "completed") return "✓"
163
- if (status === "in_progress") return "→"
164
- if (String(currentPhase) === key) return "·"
165
- return "○"
166
- }
167
-
168
- function buildPhaseLookup(state: SprintState): Record<string, SprintPhaseHistory> {
169
- const lookup: Record<string, SprintPhaseHistory> = {}
170
- for (const ph of state.phase_history ?? []) lookup[String(ph.phase)] = ph
171
- return lookup
172
- }
173
-
174
- function phaseLine(key: string, history: SprintPhaseHistory | undefined, currentPhase: string | number | undefined): string {
175
- const name = history?.phase_name || PHASE_NAMES[key] || key
176
- const st = history?.status || (String(currentPhase) === key ? "in_progress" : "pending")
177
- const sym = statusSymbol(st, currentPhase, key)
178
- const suffix = st === "completed" ? "done" : st === "in_progress" ? "active" : ""
179
- return `${sym} ${name.padEnd(14)} ${suffix}`.replace(/\s+$/, "")
180
- }
181
-
182
- function renderSprintTitle(state: SprintState): string {
183
- if (state.task_description) return state.task_description;
184
- if (state.id) {
185
- const m = state.id.match(/sprint-(\d{4}-\d{2}-\d{2})-(\d+)/);
186
- return m ? `Sprint ${m[1]} #${m[2]}` : state.id;
187
- }
188
- return 'Unknown Sprint';
189
- }
190
-
191
- // ── Upgrade notice ──
192
-
193
- const UPGRADE_NOTICE_FILE = join(homedir(), ".xp-gate", "upgrade-notice.json")
194
- const UPGRADE_NOTICE_TTL_MS = 86_400_000
195
-
196
- function readUpgradeNotice(): string | null {
197
- try {
198
- if (!existsSync(UPGRADE_NOTICE_FILE)) return null
199
- const raw = JSON.parse(readFileSync(UPGRADE_NOTICE_FILE, "utf8"))
200
- if (Date.now() - raw.ts < UPGRADE_NOTICE_TTL_MS && raw.message) {
201
- const icon = raw.kind === "upgraded" ? "✓" : raw.kind === "outdated" ? "↑" : "⚠"
202
- return `${icon} ${raw.message}`
203
- }
204
- return null
205
- } catch { return null; }
206
- }
207
-
208
- // ── JSX Components ──
209
-
210
- function SprintCard(props: { sprint: DiscoveredSprint }) {
211
- const { state } = props.sprint
212
- const lookup = buildPhaseLookup(state)
213
-
214
- return (
215
- <box>
216
- <text><b>SPRINT:</b> {renderSprintTitle(state)}</text>
217
- <Show when={state.isolation?.branch}>
218
- {(branch: () => string) => <text fg="#888888"> {branch()}</text>}
219
- </Show>
220
- <Show when={state.metrics?.tests_passed != null || state.metrics?.coverage_pct != null}>
221
- <text fg="#888888">
222
- {state.metrics?.tests_passed != null ? `tests:${state.metrics.tests_passed}` : ""}
223
- {state.metrics?.coverage_pct != null ? ` cov:${state.metrics.coverage_pct}%` : ""}
224
- </text>
225
- </Show>
226
- <Show when={isStaleSprint(state)}>
227
- <text fg="#ffaa00"> ⚠ idle >1h</text>
228
- </Show>
229
- <For each={PHASE_ORDER}>
230
- {(key: string) => {
231
- const hist = lookup[key]
232
- if (!hist && String(state.phase) !== key) return null
233
- return <text>{phaseLine(key, hist, state.phase)}</text>
234
- }}
235
- </For>
236
- </box>
237
- )
238
- }
239
-
240
- function SprintSidebar(props: { sprints: DiscoveredSprint[] }) {
241
- const display = props.sprints.slice(0, 3)
242
-
243
- return (
244
- <box>
245
- <For each={display}>
246
- {(sprint: DiscoveredSprint, i: () => number) => (
247
- <>
248
- <Show when={i() > 0}>
249
- <text>---</text>
250
- </Show>
251
- <SprintCard sprint={sprint} />
252
- </>
253
- )}
254
- </For>
255
- <Show when={props.sprints.length > 3}>
256
- <text>... +{props.sprints.length - 3} more</text>
257
- </Show>
258
- </box>
259
- )
260
- }
261
-
262
- function renderContent(sprints: DiscoveredSprint[], upgradeNotice: string | null, dir: string) {
263
- const components: any[] = []
264
-
265
- if (sprints.length > 0) {
266
- if (upgradeNotice) components.push(<text>{upgradeNotice}</text>)
267
- components.push(<SprintSidebar sprints={sprints} />)
268
- return components.length > 1 ? <box>{components}</box> : components[0]
269
- }
270
-
271
- const hasStateDir = existsSync(join(dir, ".sprint-state"))
272
- const gitRoot = findGitRoot(dir)
273
- const hasWorktreesRoot = gitRoot ? existsSync(join(gitRoot, ".worktrees")) : false
274
-
275
- if (hasStateDir) return <box>{upgradeNotice ? <text>{upgradeNotice}</text> : null}<text><b>SPRINT FLOW</b></text><text> -> Initializing...</text></box>
276
- if (hasWorktreesRoot) return <box>{upgradeNotice ? <text>{upgradeNotice}</text> : null}<text><b>SPRINT FLOW</b></text><text> · Ready...</text></box>
277
- if (upgradeNotice) return <box><text>{upgradeNotice}</text></box>
278
-
279
- return null
280
- }
281
-
282
- // ── TUI Slot Plugin ──
283
-
284
- const tuiPlugin: TuiSlotPlugin = {
285
- slots: {
286
- sidebar_content: (_props: TuiSlotProps) => {
287
- const dir = process.env.XP_GATE_PROJECT_DIR || process.cwd();
288
- const now = Date.now();
289
-
290
- if (_cache && _cache.dir === dir && now - _cache.ts < CACHE_TTL_MS) {
291
- return renderContent(_cache.data, _cache.upgradeNotice, dir);
292
- }
293
-
294
- const sprints = discoverActiveSprints(dir);
295
- const upgradeNotice = readUpgradeNotice()
296
- _cache = { data: sprints, ts: now, dir, upgradeNotice };
297
- return renderContent(sprints, upgradeNotice, dir);
298
- },
299
- },
300
- }
301
-
302
- const plugin: TuiPlugin = async (api, _options, _meta) => {
303
- api.slots.register(tuiPlugin)
304
- }
305
-
306
- export { plugin as tui, readSprintState }