()
| 142 | |
| 143 | |
| 144 | def main() -> None: |
| 145 | try: |
| 146 | payload = json.load(sys.stdin) |
| 147 | except Exception: |
| 148 | allow("No hook payload parsed; fail open.") |
| 149 | |
| 150 | cwd = Path.cwd() |
| 151 | spec = find_spec(cwd) |
| 152 | if spec is None: |
| 153 | allow("No .specs/next.md found in this project; spec loop not active.") |
| 154 | |
| 155 | root = project_root_for(spec) |
| 156 | paths = extract_paths(payload, root) |
| 157 | if not paths: |
| 158 | allow("No file path detected.") |
| 159 | |
| 160 | if not any(is_source(path) for path in paths): |
| 161 | allow("Specs, tests, docs, config, and workflow files are allowed before approval.") |
| 162 | |
| 163 | try: |
| 164 | text = spec.read_text(encoding="utf-8") |
| 165 | except (OSError, UnicodeDecodeError) as exc: |
| 166 | # Fail open: spec exists but we can't read it cleanly. Better to let |
| 167 | # the human notice the issue than to lock the project out. |
| 168 | allow(f"Spec found but unreadable ({exc.__class__.__name__}); failing open.") |
| 169 | |
| 170 | if len(text.splitlines()) > MAX_SPEC_LINES: |
| 171 | block( |
| 172 | f"Source write blocked: {spec.relative_to(root)} is over " |
| 173 | f"{MAX_SPEC_LINES} lines. Shrink the slice; don't add ceremony." |
| 174 | ) |
| 175 | |
| 176 | if not is_approved(text): |
| 177 | block( |
| 178 | f"Source write blocked. Review {spec.relative_to(root)}, then " |
| 179 | "run your spec slash command with `approve` (Claude Code plugin: `/solo-spec-loop:spec approve`) once you have explicitly approved the slice." |
| 180 | ) |
| 181 | |
| 182 | allow("Approved micro-spec found; source writes allowed for this slice.") |
| 183 | |
| 184 | |
| 185 | if __name__ == "__main__": |
no test coverage detected