You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* [luv-319] fix: enforce Stop hook on Cursor Agent CLI (followup_message + SubagentStop parity)
Cursor's `stop` hook ignores the flat `{permission: "deny"}` shape — that's
honored on tool events only. The only force-retry channel for Stop is
`{followup_message}` on stdout (exit 0), per https://cursor.com/docs/hooks.
The instruct branch already used this shape correctly since #245; the deny
path needed the same treatment, mirroring Copilot's #299 fix.
Without this, the 5 require-*-before-stop builtins were observation-only on
Cursor — the deny was logged but the agent stopped cleanly. User repro:
session 1b510ad4-906c-4f30-9467-ff2e6c581cce at /home/nivedit/dev-purge.
Also subscribes to `subagentStop` (CURSOR_HOOK_EVENT_TYPES + CURSOR_EVENT_MAP)
and widens both deny and instruct branches to match it, for parity with the
Copilot SubagentStop widening from #299.
Cloud Agents caveat: Cursor Cloud Agent VMs do NOT run stop/subagentStop
hooks at all, so this fix only covers local Cursor sessions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: cut 0.0.10-beta.6 release in CHANGELOG
Promote the six entries accumulated under `## Unreleased` to a versioned
heading `## 0.0.10-beta.6 — 2026-05-08`. Add a fresh `## Unreleased`
heading at the top for the next development cycle.
package.json was already at 0.0.10-beta.6 (pre-bumped); no version edit
needed here. The CHANGELOG cut completes the release-prep handshake.
Entries promoted:
- Cursor Stop hook enforcement fix (this PR)
- 5 scripts/translate-docs fixes from #305, #306, #307, #312, #313
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,10 @@
2
2
3
3
## Unreleased
4
4
5
+
## 0.0.10-beta.6 — 2026-05-08
6
+
5
7
### Fixes
8
+
- Make `require-*-before-stop` policies actually enforce on Cursor Agent CLI (and add `SubagentStop` parity). Verified empirically: a `stop` hook emitting Cursor's `{permission: "deny", user_message, agent_message}` flat shape is silently ignored — that shape is honored on tool events only, and on Stop the agent stops cleanly without retry. Per https://cursor.com/docs/hooks the only force-retry channel for `stop` / `subagentStop` is `{followup_message: "<text>"}` on stdout (exit 0), with the text auto-submitted as the next user message (capped at `loop_limit`, default 5). New `cli === "cursor" && eventType in {Stop, SubagentStop}` arm inside the Cursor deny branch in `src/hooks/policy-evaluator.ts` emits that shape ahead of the existing flat-shape return for tool events, mirroring the Cursor Stop instruct branch already at `:336` (which had used `{followup_message}` correctly since #245), the Copilot Stop branch added in #299 at `:279`, and the Gemini AfterAgent branch at `:188`. Without this arm, all 5 `require-*-before-stop` builtins (commit / push / PR / no-conflicts / CI-green) were observation-only on Cursor — exact same failure mode as Copilot pre-#299. Also adds `subagentStop` to `CURSOR_HOOK_EVENT_TYPES` + `CURSOR_EVENT_MAP` so **custom** policies subscribing to `SubagentStop` are reachable from Cursor subagent boundaries (Cursor's `subagentStop` is a sibling of `stop`, same payload + response contract); the instruct branch at `:336` is widened to match both events for parity. The 5 `require-*-before-stop` builtins still match `Stop` only by design — they are session-completion gates (commit / push / PR / conflicts / CI), not subagent-return gates — so the SubagentStop widening does not change builtin behavior. Caveat: Cursor Cloud Agent VMs do NOT run `stop` / `subagentStop` hooks at all (forum-confirmed at <https://forum.cursor.com/t/cursor-cloud-agents-do-not-run-afteragentresponse-or-stop-hooks/159929>) — this fix only covers local Cursor sessions; failproofai cannot enforce Stop policies in Cloud Agent runs. New unit tests pin the Cursor Stop and SubagentStop deny / instruct response shapes (4 tests across `policy-evaluator.test.ts`); new e2e regression in `cursor-integration.e2e.test.ts` confirms `require-commit-before-stop` against a dirty real-git fixture round-trips to the `{followup_message}` shape; existing data-driven `writeHookEntries` and `removeHooksFromFile` tests in `integrations.test.ts` auto-extend to `subagentStop` via iteration over `CURSOR_HOOK_EVENT_TYPES`. Updated `CLAUDE.md` Cursor section with a verified Stop block semantics table and the Cloud Agents caveat.
6
9
- `scripts/translate-docs/mdx-translator.ts`: new `stripStrayTrailingFence` helper, wired into both `translateMdxPage` and `translateReadme`, drops a stray trailing ` ``` ` line that streamed Sonnet runs of long pages sometimes append. The unmatched fence opens a code block that consumes everything to EOF — including the wrapping `</div>` for RTL READMEs — and surfaces in Mintlify as `Failed to parse page content at path i18n/README.he.md: Expected a closing tag for <div> (6:1-6:16)`. Empirically observed on run 25542951106 (post-streaming-switch #307): `docs/i18n/README.he.md` and `docs/i18n/README.tr.md` both ended with 31 fence-line markers (one stray) instead of the canonical 30; a subsequent rebase against main found `docs/i18n/README.ar.md` regenerated by the auto-translate workflow (#312) with the same bug. The helper detects the odd-count case and removes only the last unmatched fence, preserving every balanced pair before it. Also strips the stray trailing fence from all three affected files in this commit so Mintlify can deploy without a re-translate. Six-case unit test covers balanced-unchanged, no-fence-unchanged, stray-trailing-after-balanced-pair, lone-fence, embedded-non-fence-mid-line, and language-tagged pairs (#313).
7
10
- `scripts/translate-docs/translator.ts`: switch `translateContent` from `anthropic.messages.create(...)` to `anthropic.messages.stream(...).finalMessage()` so large Tier-1 (Sonnet) translations don't hit AWS Bedrock's 300 s synchronous `InvokeModel` ceiling. The LiteLLM proxy at `models.aikin.club` routes `claude-sonnet-4-6` weighted 1:1 across `anthropic/claude-sonnet-4-6` and `bedrock/us.anthropic.claude-sonnet-4-6`; under translate-docs load (4 jobs × 4 in-flight = 16 concurrent) any request that lands on Bedrock and runs >300 s is severed by Bedrock and surfaces to the SDK as `APIConnectionError ("Connection error.")` — exactly the symptom that survived #306 (SDK retry bump) and the platform-side `request_timeout: 300 → 600` lift in `exospherehost/platform#345`. Two consecutive matrix runs post-platform-fix ([25540656053](https://github.com/exospherehost/failproofai/actions/runs/25540656053), [25541614351](https://github.com/exospherehost/failproofai/actions/runs/25541614351)) showed the same deterministic failure cohort: the 4 largest pages (`built-in-policies`, `architecture`, `configuration`, `custom-policies`, plus `README`) failing at ~317 s for in-flight slot 1/2 and ~367 s for slots 3/4 — both below the new 600 s ceiling, so the wall isn't ours. `messages.stream(...).finalMessage()` returns the same `Message` shape so the function's public return type is unchanged; Bedrock falls back to `InvokeModelWithResponseStream` (no 300 s wall) and Anthropic-direct supports streaming for the full 10-minute non-streaming budget. SDK `maxRetries: 5`, per-job `MAX_CONCURRENT: 4`, and the platform `request_timeout: 600 s` ceiling all stay as the correct safety bounds; the actual unblock was on the client-side request shape (#307).
8
11
- `scripts/translate-docs`: bump SDK `maxRetries` from the Anthropic default of 2 to 5 in `translator.ts:getClient` and raise per-job `MAX_CONCURRENT` from 2 to 4 in `cli.ts`, both now env-overridable via `TRANSLATE_MAX_RETRIES` and `TRANSLATE_MAX_CONCURRENT`. The LiteLLM proxy behind `ANTHROPIC_BASE_URL` has been horizontally scaled, so the previous cap of 2 (set in #300 to dodge the gateway's connection-drop cliff at ~2 in flight) now leaves capacity on the floor. The errors that *do* still surface are no longer load-induced — they are per-request transient failures (cold replicas, LB hashing landing on an unhealthy pod, idle-socket TCP resets) where the SDK's default 2-retry budget runs out before the LB can route a retry to a healthy replica, and `Anthropic.APIConnectionError ("Connection error.")` bubbles up. Empirically observed: a `--languages zh --force` re-run (Tier-1 Sonnet, 5 uncached MDX pages) returned 2 successes and 2 `Connection error.` lines under the prior 2/2 setting. Bumping to 5 retries (≈0.5+1+2+4+8 ≈ 15 s of jittered backoff per request, 6 connection attempts total per page) absorbs the transient failures; bumping concurrency to 4 takes back the throughput the prior cap forfeited. CI matrix `max-parallel: 4` is unchanged — the new global ceiling of 4×4 = 16 in flight is still half the failure-mode threshold of 28 from #305 even before accounting for the scale-out, so no workflow change needed (#306).
0 commit comments