diff --git a/engine/hooks/diu-stop/claude_stop_check.py b/engine/hooks/diu-stop/claude_stop_check.py index 1701cfc47..7e123b547 100755 --- a/engine/hooks/diu-stop/claude_stop_check.py +++ b/engine/hooks/diu-stop/claude_stop_check.py @@ -136,6 +136,12 @@ re.IGNORECASE | re.DOTALL, ) +ABSENCE_CLAIM_RE = re.compile( + r"\b(?:has|have|had) no\b|\bthere (?:is|are|was|were) no\b|\bno single\b" + r"|\bno such (?:call|api|endpoint|function|way)\b|\b(?:can't|cannot|can not) be done\b", + re.IGNORECASE, +) + SENTENCE_END_RE = re.compile(r"(?<=[.!?])\s+") SENTENCE_LIMIT = 120 @@ -208,7 +214,7 @@ def _paragraph_claim(para): opener = _opening_word(para) if opener in BANNED_OPENERS: return opener, 0 - for pattern in (CAUSAL_CLOSER_RE, HEDGE_CLAIM_RE): + for pattern in (CAUSAL_CLOSER_RE, HEDGE_CLAIM_RE, ABSENCE_CLAIM_RE): match = pattern.search(para) if match: return match.group(0), match.start() diff --git a/engine/hooks/diu-stop/tests/fixtures/claims_fires.json b/engine/hooks/diu-stop/tests/fixtures/claims_fires.json index 3765e413e..2f14008e9 100644 --- a/engine/hooks/diu-stop/tests/fixtures/claims_fires.json +++ b/engine/hooks/diu-stop/tests/fixtures/claims_fires.json @@ -73,5 +73,30 @@ "label": "which is why, the message's only fence holds a command, not output", "reply": "Both commands are in, and tools are **still** blocked. I also can't read the hook to diagnose it — the hook blocks that read too.\n\n**Run this and paste the output:**\n\n```\n! cat ~/.claude/hooks/scope-lock/claude_pretool_scope.py\n```\n\nThat shows me what actually clears the lock. Right now I'm guessing, and guessing is what got us here.\n\nWorth noting for the findings: a gate that blocks the read of its own source can't be diagnosed from inside the session it's gating — same shape as the `disable-model-invocation` rule already in your corpus, which is why that rule says the fix must live outside the thing it protects.", "hit": "which is why" + }, + { + "label": "absence claim, has no", + "reply": "GitLab has no call that returns a merge request's whole job graph, so the client has to stitch it together.", + "hit": "has no" + }, + { + "label": "absence claim, there is no", + "reply": "There is no way to read pipeline stages in one request from the public API.", + "hit": "there is no" + }, + { + "label": "absence claim, no single", + "reply": "I found no single endpoint that lists every job with its needs.", + "hit": "no single" + }, + { + "label": "absence claim, no such call", + "reply": "The SDK exposes no such call for listing job dependencies.", + "hit": "no such call" + }, + { + "label": "capability claim, can't be done", + "reply": "Merging the two requests can't be done without changing the schema.", + "hit": "can't be done" } ] diff --git a/engine/hooks/diu-stop/tests/fixtures/claims_silent.json b/engine/hooks/diu-stop/tests/fixtures/claims_silent.json index 5780a76bc..792f7d688 100644 --- a/engine/hooks/diu-stop/tests/fixtures/claims_silent.json +++ b/engine/hooks/diu-stop/tests/fixtures/claims_silent.json @@ -6,5 +6,17 @@ { "label": "because with a backticked assignment, the message carries a fenced block of output", "reply": "**Correcting myself — it's alive.** The harness reported the wrapper exiting, not the job:\n\n```\n$ pgrep -af replay_causality\n292765 uv run python scripts/replay_causality.py\n292775 .venv/bin/python3 scripts/replay_causality.py\n\n$ tail -2 /tmp/replay.log\nreplaying 400,156 bars, full recompute at each\n 2,000/400,156 bars, 0 mismatches\n```\n\n**2,000 of 400,156 done, 0 mismatches.** Log looks stalled only because `PROGRESS_EVERY = 2000`." + }, + { + "label": "absence claim next to fenced output", + "reply": "The schema has no `needs` field:\n\n```\n$ grep -c needs schema.graphql\n0\n```" + }, + { + "label": "absence claim carrying a well-formed tag", + "reply": "{{CAT-UNVERIFIED: GitLab has no single call for the job graph -- cannot verify: the GraphQL docs page timed out twice}}" + }, + { + "label": "plain reply with no absence phrase", + "reply": "Ed said the box is fine and the run is done." } ]