()
| 191 | } |
| 192 | |
| 193 | async function main(): Promise<never> { |
| 194 | let payload: unknown; |
| 195 | try { |
| 196 | const raw = await readStdin(); |
| 197 | payload = raw.trim() === "" ? {} : JSON.parse(raw); |
| 198 | } catch { |
| 199 | allow("No hook payload parsed; fail open."); |
| 200 | } |
| 201 | |
| 202 | const cwd = process.cwd(); |
| 203 | const spec = findSpec(cwd); |
| 204 | if (spec === null) { |
| 205 | allow("No .specs/next.md found in this project; spec loop not active."); |
| 206 | } |
| 207 | |
| 208 | const root = projectRootFor(spec); |
| 209 | const paths = extractPaths(payload, root); |
| 210 | if (paths.length === 0) { |
| 211 | allow("No file path detected."); |
| 212 | } |
| 213 | |
| 214 | if (!paths.some((path) => isSource(path))) { |
| 215 | allow( |
| 216 | "Specs, tests, docs, config, and workflow files are allowed before approval." |
| 217 | ); |
| 218 | } |
| 219 | |
| 220 | let text: string; |
| 221 | try { |
| 222 | text = readFileSync(spec, "utf8"); |
| 223 | } catch (error: unknown) { |
| 224 | const name = error instanceof Error ? error.constructor.name : "Error"; |
| 225 | allow(`Spec found but unreadable (${name}); failing open.`); |
| 226 | } |
| 227 | |
| 228 | const relSpec = relative(root, spec).split(sep).join(posix.sep); |
| 229 | |
| 230 | if (text.split("\n").length > MAX_SPEC_LINES) { |
| 231 | block( |
| 232 | `Source write blocked: ${relSpec} is over ${String(MAX_SPEC_LINES)} lines. Shrink the slice; don't add ceremony.` |
| 233 | ); |
| 234 | } |
| 235 | |
| 236 | if (!isApproved(text)) { |
| 237 | block( |
| 238 | `Source write blocked. Review ${relSpec}, then run your spec slash command with \`approve\` (Claude Code plugin: \`/solo-spec-loop:spec approve\`) once you have explicitly approved the slice.` |
| 239 | ); |
| 240 | } |
| 241 | |
| 242 | allow("Approved micro-spec found; source writes allowed for this slice."); |
| 243 | } |
| 244 | |
| 245 | void main(); |
no test coverage detected