Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions engine/hooks/hooks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ summary = "Stops drift after the user narrowed the task twice."
enabled_by = "CATSTACK_REFLECT_ENFORCEMENT"
rule_modes = { "scope-lock.prompt-instruction" = "warn", "scope-lock.reflection-acknowledged" = "warn" }

[hooks.spreadsheet-completeness-guard]
mode = "stop"
why_mode = "attention"
summary = "Blocks spreadsheet completion claims without source-backed entity coverage."

[hooks.scratchpad-collision]
mode = "stop"
why_mode = "outward"
Expand Down
8 changes: 8 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# spreadsheet-completeness-guard

Stop hook for spreadsheet completion claims. A completion claim must carry a
source-backed `SPREADSHEET_COVERAGE: PASS` receipt from the expected-entity
validator. Missing observations must be explicitly classified; estimates and
zeros are not substitutes for absent competitor data.

Tests: `python3 -m unittest discover -s engine/hooks/spreadsheet-completeness-guard/tests -v`
5 changes: 5 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/claude.hook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"Stop": [{"matcher": "*", "hooks": [{"type": "command", "command": "python3 $HOME/.claude/hooks/spreadsheet-completeness-guard/claude_stop_check.py", "timeout": 10}]}]
}
}
10 changes: 10 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/claude_stop_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))
from detect import detect # noqa: E402
from runtime import run_hook # noqa: E402

if __name__ == "__main__":
run_hook("spreadsheet-completeness-guard", "claude", detect, "Stop", json_error_stderr=False)
5 changes: 5 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/codex.hook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"Stop": [{"matcher": "*", "hooks": [{"type": "command", "command": "python3 $HOME/.codex/hooks/spreadsheet-completeness-guard/codex_stop_check.py", "timeout": 10}]}]
}
}
10 changes: 10 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/codex_stop_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))
from detect import detect # noqa: E402
from runtime import run_hook # noqa: E402

if __name__ == "__main__":
run_hook("spreadsheet-completeness-guard", "codex", detect, "Stop")
5 changes: 5 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/cursor.hook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"stop": [{"command": "python3 $HOME/.cursor/hooks/spreadsheet-completeness-guard/cursor_stop_check.py", "timeout": 10}]
}
}
10 changes: 10 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/cursor_stop_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))
from detect import detect # noqa: E402
from runtime import run_hook # noqa: E402

if __name__ == "__main__":
run_hook("spreadsheet-completeness-guard", "cursor", detect, "stop")
55 changes: 55 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/detect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Stop completion claims for spreadsheet work without a coverage receipt."""
from __future__ import annotations

import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "_sdk"))
from finding import Finding # noqa: E402

RULE_INCOMPLETE = "spreadsheet-completeness-guard.incomplete"
SPREADSHEET_RE = re.compile(r"\b(?:spreadsheet|google sheet|google sheets|workbook|raw tab|aggregation table|chart)\b", re.I)
COMPLETION_RE = re.compile(r"\b(?:complete|completed|done|filled|populated|regenerated|ready|all competitors)\b", re.I)
RECEIPT_RE = re.compile(r"SPREADSHEET_COVERAGE\s*:\s*PASS\b", re.I)
FAIL_RE = re.compile(r"SPREADSHEET_COVERAGE\s*:\s*FAIL\b", re.I)


def claims_completion(message: str) -> bool:
return bool(SPREADSHEET_RE.search(message or "") and COMPLETION_RE.search(message or ""))


def has_receipt(message: str) -> bool:
return bool(RECEIPT_RE.search(message or ""))


def decide(payload: dict) -> Finding | None:
message = str(payload.get("last_assistant_message") or payload.get("message") or "")
if not claims_completion(message) or has_receipt(message):
return None
try:
retry_count = int(payload.get("spreadsheet_retry_count") or 0)
except (TypeError, ValueError):
retry_count = 0
retry_count = max(0, min(retry_count, 3))
if FAIL_RE.search(message):
return Finding(
rule_id=RULE_INCOMPLETE,
subject="spreadsheet coverage failure",
message="Coverage failed after the bounded retry loop. Do not generate or declare the table/chart complete; list the unresolved entity/period keys.",
evidence=f"SPREADSHEET_COVERAGE: FAIL; retry {retry_count}/3",
)
return Finding(
rule_id=RULE_INCOMPLETE,
subject="spreadsheet completion claim",
message=(f"Do not declare spreadsheet data complete until the expected-entity coverage "
f"validator has passed. Run source collection retry {min(retry_count + 1, 3)}/3; "
"add source-backed rows or state unresolved entity/period keys. "
"Only SPREADSHEET_COVERAGE: PASS permits completion."),
evidence=f"completion claim without a coverage receipt; retry {retry_count}/3",
)


def detect(event: dict[str, object]) -> list[Finding]:
finding = decide(event)
return [] if finding is None else [finding]
38 changes: 38 additions & 0 deletions engine/hooks/spreadsheet-completeness-guard/tests/test_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import detect # noqa: E402


class SpreadsheetCompletenessTests(unittest.TestCase):
def test_blocks_completion_without_coverage_receipt(self):
finding = detect.decide({"last_assistant_message": "The spreadsheet is complete and all competitors are populated."})
self.assertIsNotNone(finding)
self.assertEqual(finding.rule_id, "spreadsheet-completeness-guard.incomplete")

def test_stays_silent_with_pass_receipt(self):
finding = detect.decide({"last_assistant_message": "SPREADSHEET_COVERAGE: PASS — the workbook is complete."})
self.assertIsNone(finding)

def test_stays_silent_for_non_spreadsheet_completion(self):
self.assertIsNone(detect.decide({"last_assistant_message": "The README edit is complete."}))

def test_requests_bounded_retry_when_coverage_is_missing(self):
finding = detect.decide({
"last_assistant_message": "The spreadsheet is complete.",
"spreadsheet_retry_count": 1,
})
self.assertIn("retry 2/3", finding.message)

def test_keeps_blocking_after_three_failed_retries(self):
finding = detect.decide({
"last_assistant_message": "SPREADSHEET_COVERAGE: FAIL — the spreadsheet is complete.",
"spreadsheet_retry_count": 3,
})
self.assertIn("after the bounded retry loop", finding.message)


if __name__ == "__main__":
unittest.main()
Loading