@boyingliu01/xp-gate 0.10.7 → 0.10.9
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__/sprint-discovery.test.ts +364 -0
- package/lib/sprint-discovery.js +220 -0
- package/lib/sprint-status.js +49 -0
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/__tests__/tui-plugin.test.ts +126 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/tui-plugin.ts +197 -42
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -21,6 +21,45 @@ import { tmpdir } from "node:os"
|
|
|
21
21
|
// test runner provides a separate copy of the pure functions
|
|
22
22
|
import { readSprintState, renderSprintSidebar } from "../tui-plugin.ts"
|
|
23
23
|
|
|
24
|
+
// Inline single-sprint rendering logic for multi-sprint comparison
|
|
25
|
+
// (The module-level renderMultiSprintSidebar is not directly importable
|
|
26
|
+
// since it uses module-level state. We test the composition here.)
|
|
27
|
+
function buildMultiSprintBlock(state: Record<string, unknown>, index: number): string {
|
|
28
|
+
const lines: string[] = []
|
|
29
|
+
const id = (state as any).id || `sprint-${index}`
|
|
30
|
+
const desc = (state as any).task_description || id
|
|
31
|
+
lines.push(`SPRINT: ${desc}`)
|
|
32
|
+
if ((state as any).isolation?.branch) {
|
|
33
|
+
lines.push(` ${(state as any).isolation.branch}`)
|
|
34
|
+
}
|
|
35
|
+
const historyByPhase: Record<string, { status?: string; phase_name?: string }> = {}
|
|
36
|
+
if (Array.isArray((state as any).phase_history)) {
|
|
37
|
+
for (const ph of (state as any).phase_history) {
|
|
38
|
+
historyByPhase[String(ph.phase)] = ph
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const key of ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8']) {
|
|
42
|
+
const history = historyByPhase[key]
|
|
43
|
+
if (!history && String((state as any).phase) !== key) continue
|
|
44
|
+
const name = history?.phase_name || ({
|
|
45
|
+
'-1': 'ISOLATE', '-0.5': 'AUTO-ESTIMATE', '0': 'THINK', '1': 'PLAN', '2': 'BUILD',
|
|
46
|
+
'3': 'REVIEW', '4': 'USER ACCEPT', '5': 'FEEDBACK', '6': 'SHIP', '7': 'LAND', '8': 'CLEANUP',
|
|
47
|
+
})[key] || key
|
|
48
|
+
const sym = history?.status === 'completed' ? '✓' :
|
|
49
|
+
history?.status === 'in_progress' ? '→' :
|
|
50
|
+
(String((state as any).phase) === key ? '·' : '○')
|
|
51
|
+
lines.push(`${sym} ${name.padEnd(14)} ${history?.status === 'completed' ? 'done' : history?.status === 'in_progress' ? 'active' : ''}`.replace(/\s+$/, ''))
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildMultiSprintOutput(sprints: Record<string, unknown>[]): string | null {
|
|
57
|
+
if (sprints.length === 0) return null
|
|
58
|
+
const blocks = sprints.slice(0, 3).map((s, i) => buildMultiSprintBlock(s, i))
|
|
59
|
+
if (sprints.length > 3) blocks.push(`… +${sprints.length - 3} more`)
|
|
60
|
+
return blocks.join('\n---\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
24
63
|
// Duplicate helpers here to test in isolation
|
|
25
64
|
function isStale(state: { started_at?: string; phase_history?: Array<{ started_at?: string; completed_at?: string }> }): boolean {
|
|
26
65
|
if (!state || !state.started_at) return false
|
|
@@ -73,7 +112,8 @@ function renderPhaseLine(key: string, history: { status?: string; phase_name?: s
|
|
|
73
112
|
|
|
74
113
|
// ── Test fixtures ──
|
|
75
114
|
|
|
76
|
-
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
function makeSprintState(overrides: Record<string, unknown> = {}): any {
|
|
77
117
|
const base = {
|
|
78
118
|
id: "sprint-001",
|
|
79
119
|
phase: "2",
|
|
@@ -88,7 +128,7 @@ function makeSprintState(overrides: Record<string, unknown> = {}) {
|
|
|
88
128
|
{ phase: "2", status: "in_progress" as const, reqs: { "REQ-001": { name: "JWT auth", status: "completed" as const }, "REQ-002": { name: "OAuth2 flow", status: "in_progress" as const } } },
|
|
89
129
|
],
|
|
90
130
|
}
|
|
91
|
-
return { ...base, ...overrides }
|
|
131
|
+
return { ...base, ...overrides }
|
|
92
132
|
}
|
|
93
133
|
|
|
94
134
|
// ── isStale ──
|
|
@@ -243,20 +283,20 @@ void describe("renderSprintSidebar", () => {
|
|
|
243
283
|
})
|
|
244
284
|
|
|
245
285
|
void it("includes sprint title on first line", () => {
|
|
246
|
-
const output = renderSprintSidebar(makeSprintState())
|
|
286
|
+
const output = renderSprintSidebar(makeSprintState() as unknown as Parameters<typeof renderSprintSidebar>[0])
|
|
247
287
|
const lines = output.split("\n")
|
|
248
288
|
assert.ok(lines[0].includes("SPRINT:"))
|
|
249
289
|
assert.ok(lines[0].includes("OAuth2"))
|
|
250
290
|
})
|
|
251
291
|
|
|
252
292
|
void it("includes metrics when available", () => {
|
|
253
|
-
const output = renderSprintSidebar(makeSprintState())
|
|
293
|
+
const output = renderSprintSidebar(makeSprintState() as unknown as Parameters<typeof renderSprintSidebar>[0])
|
|
254
294
|
assert.ok(output.includes("tests:42"))
|
|
255
295
|
assert.ok(output.includes("cov:87%"))
|
|
256
296
|
})
|
|
257
297
|
|
|
258
298
|
void it("omits metrics section when none present", () => {
|
|
259
|
-
const output = renderSprintSidebar(makeSprintState({ metrics: {} }))
|
|
299
|
+
const output = renderSprintSidebar(makeSprintState({ metrics: {} }) as any)
|
|
260
300
|
assert.ok(!output.includes("tests:"))
|
|
261
301
|
assert.ok(!output.includes("cov:"))
|
|
262
302
|
})
|
|
@@ -279,7 +319,7 @@ void describe("renderSprintSidebar", () => {
|
|
|
279
319
|
})
|
|
280
320
|
|
|
281
321
|
void it("shows phases with activity or current", () => {
|
|
282
|
-
const output = renderSprintSidebar(makeSprintState())
|
|
322
|
+
const output = renderSprintSidebar(makeSprintState() as unknown as Parameters<typeof renderSprintSidebar>[0])
|
|
283
323
|
// Should show ISOLATE (completed in history), THINK, PLAN, BUILD (current)
|
|
284
324
|
assert.ok(output.includes("ISOLATE"))
|
|
285
325
|
assert.ok(output.includes("THINK"))
|
|
@@ -291,16 +331,93 @@ void describe("renderSprintSidebar", () => {
|
|
|
291
331
|
})
|
|
292
332
|
|
|
293
333
|
void it("shows REQ-level progress for BUILD phase", () => {
|
|
294
|
-
const output = renderSprintSidebar(makeSprintState())
|
|
334
|
+
const output = renderSprintSidebar(makeSprintState() as unknown as Parameters<typeof renderSprintSidebar>[0])
|
|
295
335
|
assert.ok(output.includes("JWT"))
|
|
296
336
|
assert.ok(output.includes("OAuth2"))
|
|
297
337
|
})
|
|
298
338
|
|
|
299
339
|
void it("renders correct status symbols per phase", () => {
|
|
300
|
-
const output = renderSprintSidebar(makeSprintState())
|
|
301
|
-
// ISOLATE completed → ✓
|
|
340
|
+
const output = renderSprintSidebar(makeSprintState() as unknown as Parameters<typeof renderSprintSidebar>[0])
|
|
302
341
|
const lines = output.split("\n")
|
|
303
342
|
const isolateLine = lines.find((l: string) => l.includes("ISOLATE"))
|
|
304
343
|
assert.ok(isolateLine && isolateLine.startsWith("✓"), `Expected ISOLATE line to start with ✓, got: ${isolateLine}`)
|
|
305
344
|
})
|
|
306
345
|
})
|
|
346
|
+
|
|
347
|
+
// ── Multi-Sprint Rendering ──
|
|
348
|
+
|
|
349
|
+
void describe("multi-sprint rendering", () => {
|
|
350
|
+
void it("returns null for empty sprints array", () => {
|
|
351
|
+
assert.equal(buildMultiSprintOutput([]), null)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
void it("renders single sprint without separator", () => {
|
|
355
|
+
const output = buildMultiSprintOutput([makeSprintState()])
|
|
356
|
+
assert.ok(output !== null)
|
|
357
|
+
assert.ok(!output!.includes("---"), "Single sprint should not have separator")
|
|
358
|
+
assert.ok(output!.includes("SPRINT:"))
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
void it("renders two sprints separated by ---", () => {
|
|
362
|
+
const sprint2 = makeSprintState({
|
|
363
|
+
id: "sprint-002",
|
|
364
|
+
task_description: "Second sprint",
|
|
365
|
+
started_at: new Date(Date.now() - 7_200_000).toISOString(),
|
|
366
|
+
phase: "1",
|
|
367
|
+
})
|
|
368
|
+
const output = buildMultiSprintOutput([makeSprintState(), sprint2])
|
|
369
|
+
assert.ok(output !== null)
|
|
370
|
+
assert.ok(output!.includes("---"), "Two sprints should be separated by ---")
|
|
371
|
+
assert.ok(output!.includes("OAuth2"))
|
|
372
|
+
assert.ok(output!.includes("Second sprint"))
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
void it("shows only first 3 sprints with overflow message for 4+", () => {
|
|
376
|
+
const sprints = [1, 2, 3, 4, 5].map(i =>
|
|
377
|
+
makeSprintState({
|
|
378
|
+
id: `sprint-00${i}`,
|
|
379
|
+
task_description: `Sprint ${i}`,
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
const output = buildMultiSprintOutput(sprints)
|
|
383
|
+
assert.ok(output !== null)
|
|
384
|
+
assert.ok(output!.includes("+2 more"), "Should show overflow for 5 sprints")
|
|
385
|
+
assert.ok(!output!.includes("Sprint 4"), "4th sprint should be collapsed")
|
|
386
|
+
assert.ok(!output!.includes("Sprint 5"), "5th sprint should be collapsed")
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
void it("renders 3 sprints without overflow", () => {
|
|
390
|
+
const sprints = [1, 2, 3].map(i =>
|
|
391
|
+
makeSprintState({
|
|
392
|
+
id: `sprint-00${i}`,
|
|
393
|
+
task_description: `Sprint ${i}`,
|
|
394
|
+
})
|
|
395
|
+
)
|
|
396
|
+
const output = buildMultiSprintOutput(sprints)
|
|
397
|
+
assert.ok(output !== null)
|
|
398
|
+
assert.ok(!output!.includes("more"), "3 sprints should not show overflow")
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
void it("uses sprint ID as fallback when task_description missing", () => {
|
|
402
|
+
const sprint = makeSprintState({ task_description: undefined, id: "sprint-2026-06-23-01" })
|
|
403
|
+
const output = buildMultiSprintBlock(sprint, 0)
|
|
404
|
+
assert.ok(output.includes("sprint-2026-06-23-01"), "Should use sprint ID as fallback")
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
void it("shows branch info when isolation.branch present", () => {
|
|
408
|
+
const sprint = makeSprintState({
|
|
409
|
+
isolation: { branch: "sprint/my-feature" },
|
|
410
|
+
})
|
|
411
|
+
const output = buildMultiSprintBlock(sprint, 0)
|
|
412
|
+
assert.ok(output.includes("sprint/my-feature"))
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
void it("does not crash on sprint with no started_at or phase_history", () => {
|
|
416
|
+
const sprint = {
|
|
417
|
+
id: "sprint-minimal",
|
|
418
|
+
task_description: "Minimal sprint",
|
|
419
|
+
}
|
|
420
|
+
const output = buildMultiSprintBlock(sprint, 0)
|
|
421
|
+
assert.ok(output.includes("Minimal sprint"))
|
|
422
|
+
})
|
|
423
|
+
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* XP-Gate OpenCode TUI Slot Plugin
|
|
3
3
|
*
|
|
4
4
|
* Registers sidebar_content slot to display Sprint Flow progress
|
|
5
|
-
* from .sprint
|
|
5
|
+
* from active sprint states discovered in .worktrees/sprint/ subdirectories.
|
|
6
6
|
*
|
|
7
7
|
* This is a separate plugin file because SDK 1.x PluginModule does not
|
|
8
8
|
* support server + tui in the same module. Users register this file
|
|
@@ -10,13 +10,16 @@
|
|
|
10
10
|
* { "plugin": ["@boyingliu01/opencode-plugin/tui"] }
|
|
11
11
|
*
|
|
12
12
|
* The npm package exports "./tui" from package.json for this resolution.
|
|
13
|
+
*
|
|
14
|
+
* Discovery logic is inlined here (mirrors src/npm-package/lib/sprint-discovery.js)
|
|
15
|
+
* because the plugin ships separately from the CLI package at runtime.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
16
|
-
import { join } from "node:path"
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
19
|
+
import { join, dirname, resolve, parse } from "node:path"
|
|
17
20
|
import type { TuiPlugin, TuiSlotPlugin, TuiSlotProps } from "@opencode-ai/plugin/tui"
|
|
18
|
-
|
|
19
|
-
//
|
|
21
|
+
|
|
22
|
+
// ── Phase constants (inlined from ../../src/npm-package/lib/shared-phase-constants.js) ──
|
|
20
23
|
|
|
21
24
|
const PHASE_NAMES: Record<string, string> = {
|
|
22
25
|
'-1': 'ISOLATE',
|
|
@@ -34,30 +37,6 @@ const PHASE_NAMES: Record<string, string> = {
|
|
|
34
37
|
|
|
35
38
|
const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
|
|
36
39
|
|
|
37
|
-
function parseTime(value: unknown): number {
|
|
38
|
-
return new Date(value as string).getTime();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isStale(state: SprintState | null): boolean {
|
|
42
|
-
if (!state || !state.started_at) return false;
|
|
43
|
-
const latest = sprintTimestamp(state);
|
|
44
|
-
return latest > 0 && Date.now() - latest > 3600000;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sprintTimestamp(state: SprintState | null): number {
|
|
48
|
-
if (!state || !state.started_at) return 0;
|
|
49
|
-
const started = parseTime(state.started_at);
|
|
50
|
-
if (isNaN(started)) return 0;
|
|
51
|
-
let latest = started;
|
|
52
|
-
if (Array.isArray(state.phase_history)) {
|
|
53
|
-
for (const ph of state.phase_history) {
|
|
54
|
-
latest = ph.completed_at ? Math.max(latest, parseTime(ph.completed_at)) : latest;
|
|
55
|
-
latest = ph.started_at ? Math.max(latest, parseTime(ph.started_at)) : latest;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return latest;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
40
|
// ── Sprint state schema ──
|
|
62
41
|
|
|
63
42
|
interface SprintReq {
|
|
@@ -86,18 +65,144 @@ interface SprintState {
|
|
|
86
65
|
phase_history?: SprintPhaseHistory[]
|
|
87
66
|
}
|
|
88
67
|
|
|
89
|
-
|
|
68
|
+
interface DiscoveredSprint {
|
|
69
|
+
state: SprintState
|
|
70
|
+
sourcePath: string
|
|
71
|
+
worktreeExists: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Discovery: inlined from src/npm-package/lib/sprint-discovery.js ──
|
|
75
|
+
|
|
76
|
+
const MAX_DISCOVERY_RESULTS = 5;
|
|
77
|
+
|
|
78
|
+
function findGitRoot(startDir: string): string | null {
|
|
79
|
+
let current = resolve(startDir);
|
|
80
|
+
const root = parse(current).root;
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
|
|
83
|
+
while (current !== root) {
|
|
84
|
+
if (seen.has(current)) break;
|
|
85
|
+
seen.add(current);
|
|
86
|
+
if (existsSync(join(current, '.git'))) return current;
|
|
87
|
+
const parent = dirname(current);
|
|
88
|
+
if (parent === current) break;
|
|
89
|
+
current = parent;
|
|
90
|
+
}
|
|
91
|
+
if (existsSync(join(root, '.git'))) return root;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
90
94
|
|
|
91
95
|
function readSprintState(dir: string): SprintState | null {
|
|
92
96
|
try {
|
|
93
|
-
const stateFile = join(dir,
|
|
94
|
-
if (!existsSync(stateFile)) return null
|
|
95
|
-
return JSON.parse(readFileSync(stateFile,
|
|
97
|
+
const stateFile = join(dir, '.sprint-state', 'sprint-state.json');
|
|
98
|
+
if (!existsSync(stateFile)) return null;
|
|
99
|
+
return JSON.parse(readFileSync(stateFile, 'utf8')) as SprintState;
|
|
96
100
|
} catch {
|
|
97
|
-
return null
|
|
101
|
+
return null;
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
function checkWorktreeExists(worktreePath: string | undefined): boolean {
|
|
106
|
+
if (!worktreePath) return false;
|
|
107
|
+
try { return existsSync(worktreePath); } catch { return false; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function discoverActiveSprints(dir: string): DiscoveredSprint[] {
|
|
111
|
+
const gitRoot = findGitRoot(dir);
|
|
112
|
+
const results: DiscoveredSprint[] = [];
|
|
113
|
+
|
|
114
|
+
if (gitRoot) {
|
|
115
|
+
const worktreeBase = join(gitRoot, '.worktrees', 'sprint');
|
|
116
|
+
let entries: { name: string; isDirectory: () => boolean }[] = [];
|
|
117
|
+
try {
|
|
118
|
+
if (existsSync(worktreeBase)) {
|
|
119
|
+
entries = readdirSync(worktreeBase, { withFileTypes: true });
|
|
120
|
+
}
|
|
121
|
+
} catch { /* EACCES */ }
|
|
122
|
+
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isDirectory()) continue;
|
|
125
|
+
const sprintDir = join(worktreeBase, entry.name);
|
|
126
|
+
const state = readSprintState(sprintDir);
|
|
127
|
+
if (!state?.id) continue;
|
|
128
|
+
const worktreeExists = checkWorktreeExists(sprintDir);
|
|
129
|
+
if (isStaleSprint(state) && !worktreeExists) continue;
|
|
130
|
+
|
|
131
|
+
results.push({ state, sourcePath: join(sprintDir, '.sprint-state', 'sprint-state.json'), worktreeExists });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fallback: cwd's own .sprint-state/
|
|
136
|
+
const localState = readSprintState(dir);
|
|
137
|
+
if (localState?.id) {
|
|
138
|
+
const localWorktreePath = localState.isolation?.worktree_path;
|
|
139
|
+
const hasExplicitWorktree = !!localWorktreePath;
|
|
140
|
+
const localWorktreeExists = hasExplicitWorktree ? checkWorktreeExists(localWorktreePath) : false;
|
|
141
|
+
if (!hasExplicitWorktree || localWorktreeExists) {
|
|
142
|
+
results.push({
|
|
143
|
+
state: localState,
|
|
144
|
+
sourcePath: join(dir, '.sprint-state', 'sprint-state.json'),
|
|
145
|
+
worktreeExists: localWorktreeExists,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Dedup by state.id
|
|
151
|
+
const deduped = new Map<string, DiscoveredSprint>();
|
|
152
|
+
for (const entry of results) {
|
|
153
|
+
const id = entry.state.id!;
|
|
154
|
+
const existing = deduped.get(id);
|
|
155
|
+
if (!existing) { deduped.set(id, entry); continue; }
|
|
156
|
+
if (entry.worktreeExists && !existing.worktreeExists) { deduped.set(id, entry); continue; }
|
|
157
|
+
if (!entry.worktreeExists && existing.worktreeExists) continue;
|
|
158
|
+
const entryTs = entry.state.started_at ? new Date(entry.state.started_at).getTime() : 0;
|
|
159
|
+
const existingTs = existing.state.started_at ? new Date(existing.state.started_at).getTime() : 0;
|
|
160
|
+
if (entryTs > existingTs || (entryTs === existingTs && entry.sourcePath < existing.sourcePath)) {
|
|
161
|
+
deduped.set(id, entry);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Array.from(deduped.values())
|
|
166
|
+
.sort((a, b) => {
|
|
167
|
+
const aTs = a.state.started_at ? new Date(a.state.started_at).getTime() : 0;
|
|
168
|
+
const bTs = b.state.started_at ? new Date(b.state.started_at).getTime() : 0;
|
|
169
|
+
if (bTs !== aTs) return bTs - aTs;
|
|
170
|
+
return String(b.state.id).localeCompare(String(a.state.id));
|
|
171
|
+
})
|
|
172
|
+
.slice(0, MAX_DISCOVERY_RESULTS);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Cache (module-level, 5s TTL) ──
|
|
176
|
+
|
|
177
|
+
let _cache: { data: DiscoveredSprint[]; ts: number; dir: string } | null = null;
|
|
178
|
+
const CACHE_TTL_MS = 5_000;
|
|
179
|
+
|
|
180
|
+
// ── Helpers ──
|
|
181
|
+
|
|
182
|
+
function parseTime(value: unknown): number {
|
|
183
|
+
return new Date(value as string).getTime();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sprintTimestamp(state: SprintState): number {
|
|
187
|
+
if (!state.started_at) return 0;
|
|
188
|
+
const started = parseTime(state.started_at);
|
|
189
|
+
if (isNaN(started)) return 0;
|
|
190
|
+
let latest = started;
|
|
191
|
+
if (Array.isArray(state.phase_history)) {
|
|
192
|
+
for (const ph of state.phase_history) {
|
|
193
|
+
latest = ph.completed_at ? Math.max(latest, parseTime(ph.completed_at)) : latest;
|
|
194
|
+
latest = ph.started_at ? Math.max(latest, parseTime(ph.started_at)) : latest;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return latest;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isStaleSprint(state: SprintState | null): boolean {
|
|
201
|
+
if (!state?.started_at) return false;
|
|
202
|
+
const latest = sprintTimestamp(state);
|
|
203
|
+
return latest > 0 && Date.now() - latest > 3_600_000;
|
|
204
|
+
}
|
|
205
|
+
|
|
101
206
|
function statusSymbol(status: string | undefined, key: string, currentPhase: string | number | undefined): string {
|
|
102
207
|
if (status === "completed") return "✓"
|
|
103
208
|
if (status === "in_progress") return "→"
|
|
@@ -131,8 +236,8 @@ function buildMetricsLine(metrics: SprintState["metrics"]): string | null {
|
|
|
131
236
|
}
|
|
132
237
|
|
|
133
238
|
function buildStaleWarning(state: SprintState): string | null {
|
|
134
|
-
if (!state
|
|
135
|
-
return
|
|
239
|
+
if (!state?.started_at) return null
|
|
240
|
+
return isStaleSprint(state) ? "⚠ idle >1h" : null
|
|
136
241
|
}
|
|
137
242
|
|
|
138
243
|
function renderBuildReqs(history: SprintPhaseHistory): string[] {
|
|
@@ -178,17 +283,67 @@ function renderSprintSidebar(state: SprintState): string {
|
|
|
178
283
|
return lines.join("\n")
|
|
179
284
|
}
|
|
180
285
|
|
|
286
|
+
function renderSprintTitle(state: SprintState): string {
|
|
287
|
+
if (state.task_description) return state.task_description;
|
|
288
|
+
if (state.id) {
|
|
289
|
+
// Fallback: extract date from sprint ID like "sprint-2026-06-23-01"
|
|
290
|
+
const match = state.id.match(/sprint-(\d{4}-\d{2}-\d{2})-(\d+)/);
|
|
291
|
+
if (match) return `Sprint ${match[1]} #${match[2]}`;
|
|
292
|
+
return state.id;
|
|
293
|
+
}
|
|
294
|
+
return 'Unknown Sprint';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderMultiSprintSidebar(sprints: DiscoveredSprint[]): string | null {
|
|
298
|
+
if (sprints.length === 0) return null;
|
|
299
|
+
|
|
300
|
+
const blocks: string[] = [];
|
|
301
|
+
const displayCount = Math.min(sprints.length, 3);
|
|
302
|
+
|
|
303
|
+
for (let i = 0; i < displayCount; i++) {
|
|
304
|
+
const { state } = sprints[i];
|
|
305
|
+
const title = renderSprintTitle(state);
|
|
306
|
+
const block: string[] = [`SPRINT: ${title}`];
|
|
307
|
+
|
|
308
|
+
if (state.isolation?.branch) {
|
|
309
|
+
block.push(` ${state.isolation.branch}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const metricsLine = buildMetricsLine(state.metrics);
|
|
313
|
+
if (metricsLine) block.push(` ${metricsLine}`);
|
|
314
|
+
|
|
315
|
+
const staleWarning = buildStaleWarning(state);
|
|
316
|
+
if (staleWarning) block.push(` ${staleWarning}`);
|
|
317
|
+
|
|
318
|
+
const historyByPhase = buildPhaseLookup(state);
|
|
319
|
+
block.push(...renderPhaseLines(historyByPhase, state.phase));
|
|
320
|
+
|
|
321
|
+
blocks.push(block.join("\n"));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (sprints.length > 3) {
|
|
325
|
+
blocks.push(`… +${sprints.length - 3} more`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return blocks.join("\n---\n");
|
|
329
|
+
}
|
|
330
|
+
|
|
181
331
|
// ── TUI Slot Plugin ──
|
|
182
332
|
|
|
183
333
|
const tuiPlugin: TuiSlotPlugin = {
|
|
184
334
|
slots: {
|
|
185
335
|
sidebar_content: (_props: TuiSlotProps) => {
|
|
186
|
-
const dir = process.env.XP_GATE_PROJECT_DIR || process.cwd()
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
336
|
+
const dir = process.env.XP_GATE_PROJECT_DIR || process.cwd();
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
|
|
339
|
+
// Use cache if still valid for current directory
|
|
340
|
+
if (_cache && _cache.dir === dir && now - _cache.ts < CACHE_TTL_MS) {
|
|
341
|
+
return renderMultiSprintSidebar(_cache.data);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const sprints = discoverActiveSprints(dir);
|
|
345
|
+
_cache = { data: sprints, ts: now, dir };
|
|
346
|
+
return renderMultiSprintSidebar(sprints);
|
|
192
347
|
},
|
|
193
348
|
},
|
|
194
349
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.9",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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
3
|
**Generated:** 2026-06-23
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 68089a8
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.10.
|
|
6
|
+
**Version:** 0.10.9.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.
|