@boyingliu01/xp-gate 0.8.16 → 0.8.18

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * xp-gate sprint-status — Sprint Flow progress CLI
3
+ *
4
+ * Reads .sprint-state/sprint-state.json (canonical schema per skills/sprint-flow/SKILL.md L893-942)
5
+ * and renders a formatted progress table.
6
+ *
7
+ * @module sprint-status
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const PHASE_NAMES = {
14
+ '-1': 'ISOLATE',
15
+ '-0.5': 'AUTO-ESTIMATE',
16
+ '0': 'THINK',
17
+ '1': 'PLAN',
18
+ '2': 'BUILD',
19
+ '3': 'REVIEW',
20
+ '4': 'USER ACCEPT',
21
+ '5': 'FEEDBACK',
22
+ '6': 'SHIP',
23
+ '7': 'LAND',
24
+ '8': 'CLEANUP',
25
+ };
26
+
27
+ const PHASE_ORDER = ['-1', '-0.5', '0', '1', '2', '3', '4', '5', '6', '7', '8'];
28
+
29
+ /**
30
+ * Read and parse sprint-state.json from a project directory.
31
+ * @param {string} dir - Project root directory
32
+ * @returns {object|null} Parsed sprint state, or null if not found or malformed
33
+ */
34
+ function readSprintState(dir) {
35
+ try {
36
+ const stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
37
+ if (!fs.existsSync(stateFile)) return null;
38
+ const raw = fs.readFileSync(stateFile, 'utf8');
39
+ return JSON.parse(raw);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get a phase's status icon.
47
+ * @param {'completed'|'in_progress'|'pending'|string} status
48
+ * @returns {string} Icon character
49
+ */
50
+ function statusIcon(status) {
51
+ switch (status) {
52
+ case 'completed': return '✅';
53
+ case 'in_progress': return '🔄';
54
+ default: return '⏳';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Format a duration in seconds to a human-readable string.
60
+ * @param {number|null} seconds
61
+ * @returns {string}
62
+ */
63
+ function formatDuration(seconds) {
64
+ if (seconds == null || seconds === 0) return '';
65
+ if (seconds < 60) return `${seconds}s`;
66
+ const mins = Math.floor(seconds / 60);
67
+ if (mins < 60) return `${mins}m`;
68
+ const hrs = Math.floor(mins / 60);
69
+ return `${hrs}h ${mins % 60}m`;
70
+ }
71
+
72
+ /**
73
+ * Check if the state is stale (>1h since last activity).
74
+ * @param {object} state - Sprint state object
75
+ * @returns {boolean}
76
+ */
77
+ function isStale(state) {
78
+ if (!state || !state.started_at) return false;
79
+ const started = new Date(state.started_at).getTime();
80
+ if (isNaN(started)) return false;
81
+ // Check latest phase_history timestamp (started_at or completed_at)
82
+ let latest = started;
83
+ if (Array.isArray(state.phase_history) && state.phase_history.length > 0) {
84
+ for (const ph of state.phase_history) {
85
+ if (ph.completed_at) {
86
+ const t = new Date(ph.completed_at).getTime();
87
+ if (!isNaN(t) && t > latest) latest = t;
88
+ }
89
+ if (ph.started_at) {
90
+ const t = new Date(ph.started_at).getTime();
91
+ if (!isNaN(t) && t > latest) latest = t;
92
+ }
93
+ }
94
+ }
95
+ return Date.now() - latest > 3600000; // 1h
96
+ }
97
+
98
+ /**
99
+ * Render sprint state as a formatted table string.
100
+ * @param {object} state - Sprint state object
101
+ * @returns {string}
102
+ */
103
+ function formatSprintTable(state) {
104
+ if (!state || !state.task_description) {
105
+ return 'No active sprint in this directory';
106
+ }
107
+
108
+ const lines = [];
109
+ const branch = state.isolation?.branch || 'unknown';
110
+ const metrics = state.metrics || {};
111
+
112
+ // Header
113
+ lines.push(`Sprint: ${state.task_description}`);
114
+ lines.push(`ID: ${state.id || 'unknown'} | Branch: ${branch}`);
115
+
116
+ // Metrics line (if available)
117
+ const metricParts = [];
118
+ if (metrics.tests_passed != null) {
119
+ metricParts.push(`Tests: ${metrics.tests_passed} passed${metrics.tests_failed ? `, ${metrics.tests_failed} failed` : ''}`);
120
+ }
121
+ if (metrics.coverage_pct != null) {
122
+ metricParts.push(`Coverage: ${metrics.coverage_pct}%`);
123
+ }
124
+ if (metricParts.length > 0) {
125
+ lines.push(`Metrics: ${metricParts.join(' | ')}`);
126
+ }
127
+
128
+ // Stale warning
129
+ if (isStale(state)) {
130
+ lines.push('⚠️ State may be stale (last updated >1h ago)');
131
+ }
132
+
133
+ lines.push('');
134
+
135
+ // Build phase lookup from phase_history
136
+ const historyByPhase = {};
137
+ if (Array.isArray(state.phase_history)) {
138
+ for (const ph of state.phase_history) {
139
+ historyByPhase[String(ph.phase)] = ph;
140
+ }
141
+ }
142
+
143
+ // Calculate column widths dynamically
144
+ let maxNameLen = 'Phase'.length;
145
+ for (const key of PHASE_ORDER) {
146
+ const history = historyByPhase[key];
147
+ const name = history?.phase_name || PHASE_NAMES[key] || key;
148
+ if (name.length > maxNameLen) maxNameLen = name.length;
149
+ }
150
+
151
+ // Separator line
152
+ const sep = '─'.repeat(maxNameLen + 40);
153
+
154
+ lines.push(sep);
155
+
156
+ for (const key of PHASE_ORDER) {
157
+ const history = historyByPhase[key];
158
+ const name = history?.phase_name || PHASE_NAMES[key] || key;
159
+ const status = history?.status || 'pending';
160
+ const icon = statusIcon(status);
161
+ const dur = formatDuration(history?.duration_seconds);
162
+ const statusLabel = status === 'completed' ? 'Completed' :
163
+ status === 'in_progress' ? 'In Progress' : 'Pending';
164
+
165
+ const line = ` Phase ${key.padStart(4)} ${name.padEnd(maxNameLen + 1)} ${icon} ${dur.padEnd(5)} ${statusLabel}`;
166
+ lines.push(line);
167
+
168
+ // REQ-level progress for BUILD phase
169
+ if (history?.reqs) {
170
+ for (const [reqId, req] of Object.entries(history.reqs)) {
171
+ const reqIcon = statusIcon(req.status);
172
+ lines.push(` ${reqIcon} ${reqId} ${req.name}`);
173
+ }
174
+ }
175
+ }
176
+
177
+ lines.push(sep);
178
+
179
+ return lines.join('\n');
180
+ }
181
+
182
+ /**
183
+ * Convert sprint state to JSON string.
184
+ * @param {object} state - Sprint state object
185
+ * @returns {string} JSON string
186
+ */
187
+ function jsonMode(state) {
188
+ return JSON.stringify(state, null, 2);
189
+ }
190
+
191
+ /**
192
+ * CLI entry point. Parses args and executes the appropriate mode.
193
+ * @param {string[]} args - CLI subargs (without 'sprint-status')
194
+ * @returns {Promise<number>} Exit code
195
+ */
196
+ async function handleSprintStatus(args = []) {
197
+ const jsonFlag = args.includes('--json');
198
+ const watchFlag = args.includes('--watch');
199
+ const dirIdx = args.indexOf('--dir');
200
+ let searchDir = process.cwd();
201
+
202
+ if (dirIdx >= 0 && dirIdx + 1 < args.length) {
203
+ searchDir = path.resolve(args[dirIdx + 1]);
204
+ // Path traversal protection: must be under cwd
205
+ if (!searchDir.startsWith(process.cwd())) {
206
+ console.error('Error: --dir path must be under current working directory');
207
+ return 1;
208
+ }
209
+ }
210
+
211
+ const state = readSprintState(searchDir);
212
+
213
+ if (!state) {
214
+ console.log('No active sprint in this directory');
215
+ return 0;
216
+ }
217
+
218
+ if (jsonFlag) {
219
+ console.log(jsonMode(state));
220
+ return 0;
221
+ }
222
+
223
+ if (watchFlag) {
224
+ const stateFile = path.join(searchDir, '.sprint-state', 'sprint-state.json');
225
+ return watchMode(stateFile);
226
+ }
227
+
228
+ console.log(formatSprintTable(state));
229
+ return 0;
230
+ }
231
+
232
+ /**
233
+ * Watch mode: listen for changes to the sprint state file.
234
+ * Prefers fs.watch(), falls back to fs.watchFile().
235
+ * @param {string} stateFile - Path to sprint-state.json
236
+ * @returns {Promise<number>} This never resolves normally (process.exit on SIGINT)
237
+ */
238
+ function watchMode(stateFile) {
239
+ return new Promise((resolve) => {
240
+ const dir = path.dirname(stateFile);
241
+ let watcher = null;
242
+
243
+ try {
244
+ watcher = fs.watch(dir, (eventType, filename) => {
245
+ if (filename === 'sprint-state.json' || filename === path.basename(stateFile)) {
246
+ const state = readSprintState(path.dirname(dir)); // parent of .sprint-state
247
+ if (state) {
248
+ console.clear();
249
+ console.log(formatSprintTable(state));
250
+ }
251
+ }
252
+ });
253
+ } catch {
254
+ // Fallback to fs.watchFile
255
+ try {
256
+ fs.watchFile(stateFile, { interval: 5000 }, () => {
257
+ const state = readSprintState(path.dirname(dir));
258
+ if (state) {
259
+ console.clear();
260
+ console.log(formatSprintTable(state));
261
+ }
262
+ });
263
+ } catch {
264
+ console.error('Watch mode not available on this platform');
265
+ resolve(1);
266
+ return;
267
+ }
268
+ }
269
+
270
+ // Initial render
271
+ const initialState = readSprintState(path.dirname(dir));
272
+ if (initialState) {
273
+ console.log(formatSprintTable(initialState));
274
+ }
275
+ console.log('Watching for changes... (Ctrl+C to stop)');
276
+
277
+ // Cleanup on exit
278
+ process.on('SIGINT', () => {
279
+ if (watcher) {
280
+ try { watcher.close(); } catch {}
281
+ }
282
+ try { fs.unwatchFile(stateFile); } catch {}
283
+ console.log('\nStopped watching.');
284
+ resolve(0);
285
+ });
286
+ });
287
+ }
288
+
289
+ module.exports = {
290
+ readSprintState,
291
+ formatSprintTable,
292
+ jsonMode,
293
+ isStale,
294
+ handleSprintStatus,
295
+ };
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-06-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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
- **Generated:** 2026-06-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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.8.16",
3
+ "version": "0.8.18",
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.8.16",
3
+ "version": "0.8.18",
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
- **Generated:** 2026-06-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -2,6 +2,9 @@ import { tool } from "@opencode-ai/plugin"
2
2
  import { z } from "zod"
3
3
  import { exec } from "child_process"
4
4
  import { promisify } from "util"
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
6
+ import { join } from "node:path"
7
+ import { homedir } from "node:os"
5
8
 
6
9
  const execAsync = promisify(exec)
7
10
 
@@ -10,10 +13,6 @@ interface OpenCodePluginInput {
10
13
  $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<{ text(): Promise<string> }>
11
14
  }
12
15
 
13
- /**
14
- * Run a shell command via async exec, returning stdout or error message.
15
- * Never throws — returns error string on failure.
16
- */
17
16
  async function runCmd(cmd: string, cwd: string): Promise<string> {
18
17
  try {
19
18
  const { stdout } = await execAsync(cmd, { cwd, timeout: 30000 })
@@ -26,22 +25,15 @@ async function runCmd(cmd: string, cwd: string): Promise<string> {
26
25
  }
27
26
  }
28
27
 
29
- /**
30
- * Check for xp-gate CLI availability and run a command via exec.
31
- */
32
28
  async function runXpGate(subcommand: string, cwd: string): Promise<string> {
33
- // Check if xp-gate is on PATH
34
29
  try {
35
30
  await execAsync("command -v xp-gate", { cwd })
36
31
  } catch {
37
- return "" // CLI not available — caller should fall back
32
+ return ""
38
33
  }
39
34
  return runCmd(`xp-gate ${subcommand}`, cwd)
40
35
  }
41
36
 
42
- /**
43
- * Check for a newer xp-gate version (non-blocking, advisory only).
44
- */
45
37
  async function getUpgradeSuggestion(cwd: string): Promise<string> {
46
38
  try {
47
39
  const result = await runXpGate("upgrade --preview", cwd)
@@ -57,6 +49,79 @@ async function getUpgradeSuggestion(cwd: string): Promise<string> {
57
49
  }
58
50
  }
59
51
 
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
+ // ── Plugin definition ──
124
+
60
125
  export const XpGatePlugin = async (input: OpenCodePluginInput) => {
61
126
  const { directory } = input
62
127
 
@@ -77,7 +142,6 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
77
142
  const upgrade = await getUpgradeSuggestion(cwd)
78
143
  return upgrade ? `${result}\n${upgrade}` : result
79
144
  }
80
- // Fallback: invoke xp-gate source directly via node
81
145
  const cmd = `node ${directory}/src/npm-package/bin/xp-gate.js check "${target}"${gatesFlag}`
82
146
  return runCmd(cmd, cwd)
83
147
  },
@@ -95,7 +159,6 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
95
159
  const upgrade = await getUpgradeSuggestion(cwd)
96
160
  return upgrade ? `${result}\n${upgrade}` : result
97
161
  }
98
- // Fallback: npx tsx on the principles source
99
162
  const cmd = `npx -y tsx ${directory}/src/principles/index.ts --files "${target}" --format console`
100
163
  return runCmd(cmd, cwd)
101
164
  },
@@ -113,12 +176,18 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
113
176
  const upgrade = await getUpgradeSuggestion(cwd)
114
177
  return upgrade ? `${result}\n${upgrade}` : result
115
178
  }
116
- // Fallback: @archlinter/cli directly
117
179
  const cmd = `npx -y @archlinter/cli scan . --config ${config}`
118
180
  return runCmd(cmd, cwd)
119
181
  },
120
182
  }),
121
183
  },
184
+ "chat.message": async (_input: { message: string }) => {
185
+ if (!checked) {
186
+ checked = true
187
+ checkPluginUpdate(directory).catch((_err) => { /* silent: non-critical background check */ })
188
+ }
189
+ return { action: "continue" }
190
+ },
122
191
  }
123
192
  }
124
193
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-06-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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-16
4
- **Commit:** b0448d3
3
+ **Generated:** 2026-06-17
4
+ **Commit:** d1b11ce
5
5
  **Branch:** main
6
- **Version:** 0.8.16.0
6
+ **Version:** 0.8.18.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,68 @@
1
+ #!/bin/bash
2
+ # sprint-flow-guard.sh — Qoder PreToolUse Hook Guard
3
+ #
4
+ # PURPOSE: Physically prevent LLM from editing/writing code before
5
+ # delphi-review is APPROVED. Blocks Edit/Write/ApplyEdit tools.
6
+ #
7
+ # INTEGRATION: Registered in plugins/qoder/hooks/hooks.json
8
+ # as a PreToolUse hook.
9
+ #
10
+ # MECHANISM:
11
+ # - Reads .sprint-state/delphi-reviewed.json
12
+ # - If file missing or verdict != "APPROVED" → deny
13
+ # - If verdict == "APPROVED" → allow
14
+ #
15
+ # GRACEFUL DEGRADATION:
16
+ # - If .sprint-state/ directory doesn't exist → ALLOW (not a sprint project)
17
+ # - If jq not available → ALLOW with warning (zero degradation for existing projects)
18
+
19
+ # Check if this is a sprint project
20
+ SPRINT_STATE_DIR="$(git rev-parse --show-toplevel 2>/dev/null)/.sprint-state"
21
+ APPROVED_FILE="$SPRINT_STATE_DIR/delphi-reviewed.json"
22
+
23
+ # If no .sprint-state directory, this isn't a sprint project → allow
24
+ if [ ! -d "$SPRINT_STATE_DIR" ]; then
25
+ exit 0
26
+ fi
27
+
28
+ # If delphi-reviewed.json doesn't exist, delphi-review hasn't completed → DENY
29
+ if [ ! -f "$APPROVED_FILE" ]; then
30
+ echo '{"decision":"deny","reason":"delphi-review not APPROVED. Complete Phase 1 delphi-review before any code modification. Run: /delphi-review"}'
31
+ exit 1
32
+ fi
33
+
34
+ # Check jq availability
35
+ if ! command -v jq &> /dev/null; then
36
+ # jq not available → warn but allow (degradation for existing project)
37
+ echo '{"decision":"allow","warning":"jq not available, cannot verify delphi-review verdict. Install jq for full protection."}'
38
+ exit 0
39
+ fi
40
+
41
+ # Validate JSON
42
+ if ! jq empty "$APPROVED_FILE" 2>/dev/null; then
43
+ echo '{"decision":"deny","reason":"delphi-reviewed.json is not valid JSON. Re-run: /delphi-review"}'
44
+ exit 1
45
+ fi
46
+
47
+ # Check verdict
48
+ VERDICT=$(jq -r '.verdict' "$APPROVED_FILE" 2>/dev/null)
49
+ MODE=$(jq -r '.mode' "$APPROVED_FILE" 2>/dev/null)
50
+
51
+ if [ "$VERDICT" != "APPROVED" ]; then
52
+ DENY_MSG="{\"decision\":\"deny\",\"reason\":\"delphi-review verdict is '${VERDICT}', not APPROVED. Fix issues and re-run: /delphi-review\"}"
53
+ echo "$DENY_MSG"
54
+ exit 1
55
+ fi
56
+
57
+ # APPROVED → allow
58
+ if [ "$MODE" = "design" ]; then
59
+ SPEC_PATH=$(jq -r '.specification_path // "not found"' "$APPROVED_FILE" 2>/dev/null)
60
+ echo "{\"decision\":\"allow\",\"message\":\"delphi-review design APPROVED. specification: ${SPEC_PATH}\"}"
61
+ elif [ "$MODE" = "code-walkthrough" ]; then
62
+ COMMIT=$(jq -r '.commit // "unknown"' "$APPROVED_FILE" 2>/dev/null)
63
+ echo "{\"decision\":\"allow\",\"message\":\"delphi-review code-walkthrough APPROVED. commit: ${COMMIT}\"}"
64
+ else
65
+ echo '{"decision":"allow","message":"delphi-review APPROVED"}'
66
+ fi
67
+
68
+ exit 0