@boyingliu01/xp-gate 0.9.1 → 0.9.3

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 (35) hide show
  1. package/adapters/cpp.sh +3 -3
  2. package/adapters/dart.sh +2 -2
  3. package/adapters/flutter.sh +2 -2
  4. package/adapters/java.sh +4 -4
  5. package/adapters/kotlin.sh +4 -4
  6. package/adapters/objectivec.sh +2 -2
  7. package/adapters/plugins/p3c-java/scripts/install-maven-p3c.sh +3 -3
  8. package/adapters/plugins/whalecloud-java/scripts/install-maven-whalecloud.sh +3 -3
  9. package/adapters/powershell.sh +3 -3
  10. package/adapters/python.sh +2 -2
  11. package/adapters/shell.sh +3 -3
  12. package/adapters/swift.sh +2 -2
  13. package/mock-policy/AGENTS.md +2 -2
  14. package/mutation/AGENTS.md +2 -2
  15. package/package.json +1 -1
  16. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  17. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  18. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  19. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  20. package/plugins/opencode/__tests__/tui-plugin.test.ts +306 -0
  21. package/plugins/opencode/__tests__/xp-gate-update.test.ts +255 -0
  22. package/plugins/opencode/index.ts +158 -75
  23. package/plugins/opencode/package.json +14 -3
  24. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  25. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  26. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  27. package/plugins/opencode/tsconfig.json +1 -1
  28. package/plugins/opencode/tui-plugin.ts +188 -0
  29. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  30. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  31. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  32. package/principles/AGENTS.md +2 -2
  33. package/skills/delphi-review/AGENTS.md +2 -2
  34. package/skills/sprint-flow/AGENTS.md +2 -2
  35. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/adapters/cpp.sh CHANGED
@@ -13,12 +13,12 @@ _detect_cpp_build() {
13
13
  run_static_analysis() {
14
14
  if command -v clang-tidy &>/dev/null; then
15
15
  local files
16
- files=$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" 2>/dev/null | head -20)
16
+ files=$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" 2>/dev/null | sed -n '1,20p; 20q')
17
17
  if [ -n "$files" ]; then
18
- echo "$files" | xargs clang-tidy 2>&1 | head -20
18
+ echo "$files" | xargs clang-tidy 2>&1 | sed -n '1,20p; 20q'
19
19
  fi
20
20
  elif command -v cppcheck &>/dev/null; then
21
- cppcheck --enable=all --inline-suppr . 2>&1 | head -20
21
+ cppcheck --enable=all --inline-suppr . 2>&1 | sed -n '1,20p; 20q'
22
22
  else
23
23
  echo "No C++ analysis tools (clang-tidy/cppcheck)"
24
24
  return 1
package/adapters/dart.sh CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  run_static_analysis() {
4
4
  if command -v dart &>/dev/null; then
5
- dart analyze 2>&1 | head -30
5
+ dart analyze 2>&1 | sed -n '1,30p; 30q'
6
6
  return $?
7
7
  else
8
8
  echo "Dart SDK not available"
@@ -12,7 +12,7 @@ run_static_analysis() {
12
12
 
13
13
  run_lint() {
14
14
  if command -v dart &>/dev/null; then
15
- dart format --output=none --set-exit-if-changed . 2>&1 | head -30
15
+ dart format --output=none --set-exit-if-changed . 2>&1 | sed -n '1,30p; 30q'
16
16
  return $?
17
17
  else
18
18
  echo "Dart SDK not available"
@@ -2,7 +2,7 @@
2
2
 
3
3
  run_static_analysis() {
4
4
  if command -v flutter &>/dev/null; then
5
- flutter analyze 2>&1 | head -30
5
+ flutter analyze 2>&1 | sed -n '1,30p; 30q'
6
6
  return $?
7
7
  else
8
8
  echo "Flutter SDK not available"
@@ -12,7 +12,7 @@ run_static_analysis() {
12
12
 
13
13
  run_lint() {
14
14
  if command -v flutter &>/dev/null; then
15
- flutter format --output=none --set-exit-if-changed . 2>&1 | head -30
15
+ flutter format --output=none --set-exit-if-changed . 2>&1 | sed -n '1,30p; 30q'
16
16
  return $?
17
17
  else
18
18
  echo "Flutter SDK not available"
package/adapters/java.sh CHANGED
@@ -27,13 +27,13 @@ run_static_analysis() {
27
27
  build_system=$(_detect_java_build)
28
28
 
29
29
  if [ "$build_system" = "maven" ]; then
30
- mvn compile -q 2>&1 | grep -i "error\|fail" | head -5
30
+ mvn compile -q 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
31
31
  if [ "${PIPESTATUS[0]}" -ne 0 ]; then
32
32
  echo "❌ Maven compilation failed"
33
33
  return 1
34
34
  fi
35
35
  elif [ "$build_system" = "gradle" ]; then
36
- gradle compileJava --quiet 2>&1 | grep -i "error\|fail" | head -5
36
+ gradle compileJava --quiet 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
37
37
  if [ "${PIPESTATUS[0]}" -ne 0 ]; then
38
38
  echo "❌ Gradle compilation failed"
39
39
  return 1
@@ -42,12 +42,12 @@ run_static_analysis() {
42
42
 
43
43
  # CheckStyle with Google style (legacy fallback)
44
44
  if command -v checkstyle &>/dev/null; then
45
- checkstyle -c /google_checks.xml . 2>&1 | head -20
45
+ checkstyle -c /google_checks.xml . 2>&1 | sed -n '1,20p; 20q'
46
46
  fi
47
47
 
48
48
  # PMD error detection (legacy fallback)
49
49
  if command -v pmd &>/dev/null; then
50
- pmd check -d . -R category/java/errorprone.xml 2>&1 | head -20
50
+ pmd check -d . -R category/java/errorprone.xml 2>&1 | sed -n '1,20p; 20q'
51
51
  fi
52
52
 
53
53
  # p3c-pmd check (Alibaba coding guidelines) — primary Java quality gate
@@ -15,13 +15,13 @@ run_static_analysis() {
15
15
  build_system=$(_detect_kotlin_build)
16
16
 
17
17
  if [ "$build_system" = "maven" ]; then
18
- mvn compile -q 2>&1 | grep -i "error\|fail" | head -5
18
+ mvn compile -q 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
19
19
  if [ ${PIPESTATUS[0]} -ne 0 ]; then
20
20
  echo "❌ Maven compilation failed"
21
21
  return 1
22
22
  fi
23
23
  elif [ "$build_system" = "gradle" ]; then
24
- gradle compileKotlin --quiet 2>&1 | grep -i "error\|fail" | head -5
24
+ gradle compileKotlin --quiet 2>&1 | grep -i "error\|fail" | sed -n '1,5p; 5q'
25
25
  if [ ${PIPESTATUS[0]} -ne 0 ]; then
26
26
  echo "❌ Gradle compilation failed"
27
27
  return 1
@@ -29,10 +29,10 @@ run_static_analysis() {
29
29
  fi
30
30
 
31
31
  if command -v ktlint &>/dev/null; then
32
- ktlint "**/*.kt" 2>&1 | head -20
32
+ ktlint "**/*.kt" 2>&1 | sed -n '1,20p; 20q'
33
33
  return $?
34
34
  elif command -v detekt &>/dev/null; then
35
- detekt --input . 2>&1 | head -20
35
+ detekt --input . 2>&1 | sed -n '1,20p; 20q'
36
36
  return $?
37
37
  else
38
38
  echo "No Kotlin lint tools (ktlint/detekt)"
@@ -2,10 +2,10 @@
2
2
 
3
3
  run_static_analysis() {
4
4
  if command -v oclint &>/dev/null; then
5
- oclint-json-compilation-database 2>&1 | head -30
5
+ oclint-json-compilation-database 2>&1 | sed -n '1,30p; 30q'
6
6
  return $?
7
7
  elif command -v clang-tidy &>/dev/null; then
8
- find . -name "*.m" -o -name "*.mm" -o -name "*.h" | head -20 | xargs clang-tidy 2>&1 | head -30
8
+ find . -name "*.m" -o -name "*.mm" -o -name "*.h" | sed -n '1,20p; 20q' | xargs clang-tidy 2>&1 | sed -n '1,30p; 30q'
9
9
  return $?
10
10
  else
11
11
  echo "No Objective-C analysis tools available (oclint/clang-tidy)"
@@ -46,10 +46,10 @@ if grep -q '<profiles>' pom.xml; then
46
46
 
47
47
  # Use sed to insert before </profiles>
48
48
  # Find the line number of </profiles>
49
- PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | head -1 | cut -d: -f1)
49
+ PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | sed -n '1p; 1q' | cut -d: -f1)
50
50
 
51
51
  if [ -n "$PROFILES_END_LINE" ]; then
52
- head -n $((PROFILES_END_LINE - 1)) pom.xml > pom.xml.tmp
52
+ sed -n "1,$((PROFILES_END_LINE - 1))p; $((PROFILES_END_LINE - 1))q" pom.xml > pom.xml.tmp
53
53
  {
54
54
  echo ""
55
55
  echo " <!-- XP-Gate: Alibaba p3c-pmd quality gate profile -->"
@@ -66,7 +66,7 @@ else
66
66
  PROJECT_END_LINE=$(grep -n '</project>' pom.xml | tail -1 | cut -d: -f1)
67
67
 
68
68
  if [ -n "$PROJECT_END_LINE" ]; then
69
- head -n $((PROJECT_END_LINE - 1)) pom.xml > pom.xml.tmp
69
+ sed -n "1,$((PROJECT_END_LINE - 1))p; $((PROJECT_END_LINE - 1))q" pom.xml > pom.xml.tmp
70
70
  {
71
71
  echo ""
72
72
  echo " <!-- XP-Gate: Alibaba p3c-pmd quality gate profile -->"
@@ -154,9 +154,9 @@ else
154
154
  echo "Step 2: Installing xp-gate-whalecloud-java profile into pom.xml..."
155
155
 
156
156
  if grep -q '<profiles>' pom.xml; then
157
- PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | head -1 | cut -d: -f1)
157
+ PROFILES_END_LINE=$(grep -n '</profiles>' pom.xml | sed -n '1p; 1q' | cut -d: -f1)
158
158
  if [ -n "$PROFILES_END_LINE" ]; then
159
- head -n $((PROFILES_END_LINE - 1)) pom.xml > pom.xml.tmp
159
+ sed -n "1,$((PROFILES_END_LINE - 1))p; $((PROFILES_END_LINE - 1))q" pom.xml > pom.xml.tmp
160
160
  {
161
161
  echo ""
162
162
  echo " <!-- XP-Gate: WhaleCloud Java Coding Standards profile -->"
@@ -169,7 +169,7 @@ else
169
169
  else
170
170
  PROJECT_END_LINE=$(grep -n '</project>' pom.xml | tail -1 | cut -d: -f1)
171
171
  if [ -n "$PROJECT_END_LINE" ]; then
172
- head -n $((PROJECT_END_LINE - 1)) pom.xml > pom.xml.tmp
172
+ sed -n "1,$((PROJECT_END_LINE - 1))p; $((PROJECT_END_LINE - 1))q" pom.xml > pom.xml.tmp
173
173
  {
174
174
  echo ""
175
175
  echo " <!-- XP-Gate: WhaleCloud Java Coding Standards profile -->"
@@ -72,7 +72,7 @@ run_tests() {
72
72
  test_paths="test/"
73
73
  fi
74
74
 
75
- if [ -n "$test_paths" ] && find "$test_paths" -name "*.Tests.ps1" -type f 2>/dev/null | head -1 | grep -q "."; then
75
+ if [ -n "$test_paths" ] && find "$test_paths" -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
76
76
  echo "Running Pester tests..."
77
77
  "$PWSH" -NoProfile -Command "
78
78
  \$results = Invoke-Pester -Path '$test_paths' -PassThru
@@ -84,7 +84,7 @@ run_tests() {
84
84
  exit 0
85
85
  "
86
86
  return $?
87
- elif find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | head -1 | grep -q "."; then
87
+ elif find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
88
88
  echo "Running Pester tests in current directory..."
89
89
  "$PWSH" -NoProfile -Command "
90
90
  \$results = Invoke-Pester -CI -PassThru
@@ -117,7 +117,7 @@ run_coverage() {
117
117
  test_paths="test/"
118
118
  fi
119
119
 
120
- if [ -n "$test_paths" ] || find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | head -1 | grep -q "."; then
120
+ if [ -n "$test_paths" ] || find . -maxdepth 2 -name "*.Tests.ps1" -type f 2>/dev/null | sed -n '1p; 1q' | grep -q "."; then
121
121
  echo "Running Pester with code coverage..."
122
122
  local path_arg="${test_paths:-.}"
123
123
  "$PWSH" -NoProfile -Command "
@@ -63,7 +63,7 @@ run_tests() {
63
63
  fi
64
64
 
65
65
  # Check for errors in summary line (e.g. "3 errors")
66
- ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | head -1)
66
+ ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | sed -n '1p; 1q')
67
67
  if [ -n "$ERROR_COUNT" ] && [ "$ERROR_COUNT" -gt 0 ]; then
68
68
  echo "❌ $ERROR_COUNT test collection/execution errors detected"
69
69
  return 1
@@ -94,7 +94,7 @@ run_coverage() {
94
94
  fi
95
95
 
96
96
  # Check for errors in summary line (e.g. "3 errors")
97
- ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | head -1)
97
+ ERROR_COUNT=$(echo "$PYTEST_OUTPUT" | grep -oP '\d+ error' | grep -oP '\d+' | sed -n '1p; 1q')
98
98
  if [ -n "$ERROR_COUNT" ] && [ "$ERROR_COUNT" -gt 0 ]; then
99
99
  echo "❌ $ERROR_COUNT test collection/execution errors detected"
100
100
  return 1
package/adapters/shell.sh CHANGED
@@ -26,13 +26,13 @@ run_lint() {
26
26
 
27
27
  run_tests() {
28
28
  # Look for and run shell test files
29
- if [[ -d "tests" ]] && [[ -n "$(find tests -name "*.sh" -type f | head -n 1)" ]]; then
29
+ if [ -d "tests" ] && [ -n "$(find tests -name "*.sh" -type f | sed -n '1p; 1q')" ]; then
30
30
  echo "Running Shell tests..."
31
31
  for test_file in tests/*.sh; do
32
- if [[ -x "$test_file" ]]; then
32
+ if [ -x "$test_file" ]; then
33
33
  "$test_file"
34
34
  local exit_code=$?
35
- if [[ $exit_code -ne 0 ]]; then
35
+ if [ "$exit_code" -ne 0 ]; then
36
36
  return $exit_code
37
37
  fi
38
38
  fi
package/adapters/swift.sh CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  run_static_analysis() {
4
4
  if command -v swiftlint &>/dev/null; then
5
- swiftlint lint 2>&1 | head -30
5
+ swiftlint lint 2>&1 | sed -n '1,30p; 30q'
6
6
  return $?
7
7
  else
8
8
  echo "SwiftLint not available"
@@ -12,7 +12,7 @@ run_static_analysis() {
12
12
 
13
13
  run_lint() {
14
14
  if command -v swiftlint &>/dev/null; then
15
- swiftlint lint --strict 2>&1 | head -30
15
+ swiftlint lint --strict 2>&1 | sed -n '1,30p; 30q'
16
16
  return $?
17
17
  else
18
18
  echo "SwiftLint not available"
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-18
4
- **Commit:** 33cf3f2
4
+ **Commit:** b67fc85
5
5
  **Branch:** main
6
- **Version:** 0.9.1.0
6
+ **Version:** 0.9.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-06-18
4
- **Commit:** 33cf3f2
4
+ **Commit:** b67fc85
5
5
  **Branch:** main
6
- **Version:** 0.9.1.0
6
+ **Version:** 0.9.3.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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-18
4
- **Commit:** 33cf3f2
4
+ **Commit:** b67fc85
5
5
  **Branch:** main
6
- **Version:** 0.9.1.0
6
+ **Version:** 0.9.3.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-18
4
- **Commit:** 33cf3f2
4
+ **Commit:** b67fc85
5
5
  **Branch:** main
6
- **Version:** 0.9.1.0
6
+ **Version:** 0.9.3.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-18
4
- **Commit:** 33cf3f2
4
+ **Commit:** b67fc85
5
5
  **Branch:** main
6
- **Version:** 0.9.1.0
6
+ **Version:** 0.9.3.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,306 @@
1
+ /**
2
+ * TUI Slot Plugin tests
3
+ *
4
+ * Tests for pure functions:
5
+ * - readSprintState: file I/O + JSON parse
6
+ * - isStale: staleness detection
7
+ * - statusSymbol: status → icon mapping
8
+ * - renderPhaseLine: single phase → formatted line
9
+ * - renderSprintSidebar: full state → sidebar string
10
+ */
11
+
12
+ import { describe, it, before, after } from "node:test"
13
+ import assert from "node:assert/strict"
14
+ import { randomUUID } from "node:crypto"
15
+ import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs"
16
+ import { join } from "node:path"
17
+ import { tmpdir } from "node:os"
18
+
19
+ // Import the functions under test
20
+ // Since the file uses type-only imports from @opencode-ai/plugin/tui,
21
+ // test runner provides a separate copy of the pure functions
22
+ import { readSprintState, renderSprintSidebar } from "../tui-plugin.ts"
23
+
24
+ // Duplicate helpers here to test in isolation
25
+ function isStale(state: { started_at?: string; phase_history?: Array<{ started_at?: string; completed_at?: string }> }): boolean {
26
+ if (!state || !state.started_at) return false
27
+ const started = new Date(state.started_at).getTime()
28
+ if (isNaN(started)) return false
29
+ let latest = started
30
+ if (Array.isArray(state.phase_history)) {
31
+ for (const ph of state.phase_history) {
32
+ if (ph.completed_at) {
33
+ const t = new Date(ph.completed_at).getTime()
34
+ if (!isNaN(t) && t > latest) latest = t
35
+ }
36
+ if (ph.started_at) {
37
+ const t = new Date(ph.started_at).getTime()
38
+ if (!isNaN(t) && t > latest) latest = t
39
+ }
40
+ }
41
+ }
42
+ return Date.now() - latest > 3_600_000
43
+ }
44
+
45
+ function statusSymbol(status: string | undefined, key: string, currentPhase: string | number | undefined): string {
46
+ if (status === "completed") return "✓"
47
+ if (status === "in_progress") return "→"
48
+ if (String(currentPhase) === key) return "·"
49
+ return "○"
50
+ }
51
+
52
+ const PHASE_NAMES: Record<string, string> = {
53
+ "-1": "ISOLATE",
54
+ "-0.5": "AUTO-ESTIMATE",
55
+ "0": "THINK",
56
+ "1": "PLAN",
57
+ "2": "BUILD",
58
+ "3": "REVIEW",
59
+ "4": "USER ACCEPT",
60
+ "5": "FEEDBACK",
61
+ "6": "SHIP",
62
+ "7": "LAND",
63
+ "8": "CLEANUP",
64
+ }
65
+
66
+ function renderPhaseLine(key: string, history: { status?: string; phase_name?: string } | undefined, currentPhase: string | number | undefined): string {
67
+ const name = history?.phase_name || PHASE_NAMES[key] || key
68
+ const status = history?.status || (String(currentPhase) === key ? "in_progress" : "pending")
69
+ const sym = statusSymbol(status, key, currentPhase)
70
+ return `${sym} ${name.padEnd(14)} ${status === "completed" ? "done" : status === "in_progress" ? "active" : ""}`
71
+ .replace(/\s+$/, "")
72
+ }
73
+
74
+ // ── Test fixtures ──
75
+
76
+ function makeSprintState(overrides: Record<string, unknown> = {}) {
77
+ const base = {
78
+ id: "sprint-001",
79
+ phase: "2",
80
+ status: "in_progress",
81
+ started_at: new Date(Date.now() - 3_600_000).toISOString(),
82
+ task_description: "Implement user login with OAuth2",
83
+ metrics: { tests_passed: 42, coverage_pct: 87 },
84
+ phase_history: [
85
+ { phase: "-1", phase_name: "ISOLATE", status: "completed" as const, started_at: new Date(Date.now() - 86_400_000).toISOString(), completed_at: new Date(Date.now() - 86_000_000).toISOString() },
86
+ { phase: "0", status: "completed" as const, started_at: new Date(Date.now() - 86_000_000).toISOString(), completed_at: new Date(Date.now() - 72_000_000).toISOString() },
87
+ { phase: "1", status: "completed" as const, started_at: new Date(Date.now() - 72_000_000).toISOString(), completed_at: new Date(Date.now() - 36_000_000).toISOString() },
88
+ { 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
+ ],
90
+ }
91
+ return { ...base, ...overrides } as any
92
+ }
93
+
94
+ // ── isStale ──
95
+
96
+ void describe("isStale", () => {
97
+ void it("returns false when state has no started_at", () => {
98
+ assert.equal(isStale({}), false)
99
+ })
100
+
101
+ void it("returns false when started_at is recent", () => {
102
+ const state = { started_at: new Date().toISOString() }
103
+ assert.equal(isStale(state), false)
104
+ })
105
+
106
+ void it("returns true when started_at is >1h ago and no phase_history", () => {
107
+ const state = { started_at: new Date(Date.now() - 7_200_000).toISOString() }
108
+ assert.equal(isStale(state), true)
109
+ })
110
+
111
+ void it("returns false when latest phase_history is recent", () => {
112
+ const state = {
113
+ started_at: new Date(Date.now() - 7_200_000).toISOString(),
114
+ phase_history: [
115
+ { phase: "0", started_at: new Date(Date.now() - 3_600_000).toISOString(), completed_at: new Date(Date.now() - 60_000).toISOString() },
116
+ ],
117
+ }
118
+ assert.equal(isStale(state), false)
119
+ })
120
+
121
+ void it("returns true when all activity is >1h ago", () => {
122
+ const state = {
123
+ started_at: new Date(Date.now() - 86_400_000).toISOString(),
124
+ phase_history: [
125
+ { phase: "-1", started_at: new Date(Date.now() - 86_000_000).toISOString(), completed_at: new Date(Date.now() - 85_000_000).toISOString() },
126
+ ],
127
+ }
128
+ assert.equal(isStale(state), true)
129
+ })
130
+
131
+ void it("returns false when started_at is invalid date", () => {
132
+ const state = { started_at: "not-a-date" }
133
+ assert.equal(isStale(state), false)
134
+ })
135
+ })
136
+
137
+ // ── statusSymbol ──
138
+
139
+ void describe("statusSymbol", () => {
140
+ void it("returns ✓ for completed", () => {
141
+ assert.equal(statusSymbol("completed", "0", undefined), "✓")
142
+ })
143
+
144
+ void it("returns → for in_progress", () => {
145
+ assert.equal(statusSymbol("in_progress", "1", undefined), "→")
146
+ })
147
+
148
+ void it("returns · when key matches currentPhase", () => {
149
+ assert.equal(statusSymbol(undefined, "2", "2"), "·")
150
+ })
151
+
152
+ void it("returns ○ for pending (no match)", () => {
153
+ assert.equal(statusSymbol("pending", "3", "2"), "○")
154
+ })
155
+
156
+ void it("returns ✓ for completed even when key matches", () => {
157
+ // completed takes priority over currentPhase match
158
+ assert.equal(statusSymbol("completed", "2", "2"), "✓")
159
+ })
160
+ })
161
+
162
+ // ── renderPhaseLine ──
163
+
164
+ void describe("renderPhaseLine", () => {
165
+ void it("renders completed phase", () => {
166
+ const line = renderPhaseLine("0", { status: "completed" }, "2")
167
+ assert.ok(line.includes("✓"))
168
+ assert.ok(line.includes("THINK"))
169
+ assert.ok(line.includes("done"))
170
+ })
171
+
172
+ void it("renders in_progress phase", () => {
173
+ const line = renderPhaseLine("2", { status: "in_progress" }, "2")
174
+ assert.ok(line.includes("→"))
175
+ assert.ok(line.includes("BUILD"))
176
+ assert.ok(line.includes("active"))
177
+ })
178
+
179
+ void it("renders pending phase (current phase match)", () => {
180
+ const line = renderPhaseLine("3", undefined, "2")
181
+ assert.ok(line.includes("○"))
182
+ assert.ok(line.includes("REVIEW"))
183
+ })
184
+
185
+ void it("renders current phase with arrow when no history entry (current assigned in_progress)", () => {
186
+ const line = renderPhaseLine("2", undefined, "2")
187
+ assert.ok(line.includes("→"))
188
+ assert.ok(line.includes("BUILD"))
189
+ assert.ok(line.includes("active"))
190
+ })
191
+
192
+ void it("uses custom phase_name from history", () => {
193
+ const line = renderPhaseLine("2", { status: "completed", phase_name: "MY BUILD" }, "2")
194
+ assert.ok(line.includes("MY BUILD"))
195
+ })
196
+ })
197
+
198
+ // ── readSprintState ──
199
+
200
+ void describe("readSprintState", () => {
201
+ const tmpDir = join(tmpdir(), "xp-gate-tui-test-" + randomUUID())
202
+
203
+ before(() => {
204
+ mkdirSync(join(tmpDir, ".sprint-state"), { recursive: true })
205
+ })
206
+
207
+ after(() => {
208
+ rmSync(tmpDir, { recursive: true, force: true })
209
+ })
210
+
211
+ void it("returns null when no sprint-state.json", () => {
212
+ assert.equal(readSprintState(tmpDir), null)
213
+ })
214
+
215
+ void it("returns parsed state when file exists", () => {
216
+ const state = { id: "test", phase: "1", task_description: "Test" }
217
+ writeFileSync(join(tmpDir, ".sprint-state", "sprint-state.json"), JSON.stringify(state))
218
+ const result = readSprintState(tmpDir)
219
+ assert.notEqual(result, null)
220
+ assert.equal(result?.id, "test")
221
+ assert.equal(result?.phase, "1")
222
+ })
223
+
224
+ void it("returns null on malformed JSON", () => {
225
+ writeFileSync(join(tmpDir, ".sprint-state", "sprint-state.json"), "{not-json")
226
+ assert.equal(readSprintState(tmpDir), null)
227
+ })
228
+
229
+ void it("returns null when directory does not exist", () => {
230
+ assert.equal(readSprintState("/nonexistent/path"), null)
231
+ })
232
+ })
233
+
234
+ // ── renderSprintSidebar ──
235
+
236
+ void describe("renderSprintSidebar", () => {
237
+ void it("returns empty string for null state", () => {
238
+ assert.equal(renderSprintSidebar(null as unknown as Parameters<typeof renderSprintSidebar>[0]), "")
239
+ })
240
+
241
+ void it("returns empty string when no task_description", () => {
242
+ assert.equal(renderSprintSidebar({} as Parameters<typeof renderSprintSidebar>[0]), "")
243
+ })
244
+
245
+ void it("includes sprint title on first line", () => {
246
+ const output = renderSprintSidebar(makeSprintState())
247
+ const lines = output.split("\n")
248
+ assert.ok(lines[0].includes("SPRINT:"))
249
+ assert.ok(lines[0].includes("OAuth2"))
250
+ })
251
+
252
+ void it("includes metrics when available", () => {
253
+ const output = renderSprintSidebar(makeSprintState())
254
+ assert.ok(output.includes("tests:42"))
255
+ assert.ok(output.includes("cov:87%"))
256
+ })
257
+
258
+ void it("omits metrics section when none present", () => {
259
+ const output = renderSprintSidebar(makeSprintState({ metrics: {} }))
260
+ assert.ok(!output.includes("tests:"))
261
+ assert.ok(!output.includes("cov:"))
262
+ })
263
+
264
+ void it("shows stale warning when >1h idle", () => {
265
+ const state = makeSprintState({ started_at: new Date(Date.now() - 7_200_000).toISOString(), phase_history: [] })
266
+ const output = renderSprintSidebar(state)
267
+ assert.ok(output.includes("idle"))
268
+ })
269
+
270
+ void it("does not show stale warning when recent activity", () => {
271
+ const state = makeSprintState({
272
+ started_at: new Date(Date.now() - 7_200_000).toISOString(),
273
+ phase_history: [
274
+ { phase: "0", started_at: new Date().toISOString(), completed_at: new Date().toISOString() },
275
+ ],
276
+ })
277
+ const output = renderSprintSidebar(state)
278
+ assert.ok(!output.includes("idle"))
279
+ })
280
+
281
+ void it("shows phases with activity or current", () => {
282
+ const output = renderSprintSidebar(makeSprintState())
283
+ // Should show ISOLATE (completed in history), THINK, PLAN, BUILD (current)
284
+ assert.ok(output.includes("ISOLATE"))
285
+ assert.ok(output.includes("THINK"))
286
+ assert.ok(output.includes("PLAN"))
287
+ assert.ok(output.includes("BUILD"))
288
+ // Should NOT show REVIEW, FEEDBACK, etc. (no history, not current)
289
+ assert.ok(!output.includes("REVIEW"))
290
+ assert.ok(!output.includes("FEEDBACK"))
291
+ })
292
+
293
+ void it("shows REQ-level progress for BUILD phase", () => {
294
+ const output = renderSprintSidebar(makeSprintState())
295
+ assert.ok(output.includes("JWT"))
296
+ assert.ok(output.includes("OAuth2"))
297
+ })
298
+
299
+ void it("renders correct status symbols per phase", () => {
300
+ const output = renderSprintSidebar(makeSprintState())
301
+ // ISOLATE completed → ✓
302
+ const lines = output.split("\n")
303
+ const isolateLine = lines.find((l: string) => l.includes("ISOLATE"))
304
+ assert.ok(isolateLine && isolateLine.startsWith("✓"), `Expected ISOLATE line to start with ✓, got: ${isolateLine}`)
305
+ })
306
+ })