[luv-324] fix: enforce Stop hook on OpenCode + cut 0.0.10-beta.8#323
[luv-324] fix: enforce Stop hook on OpenCode + cut 0.0.10-beta.8#323NiveditJain merged 3 commits intomainfrom
Conversation
Stop hooks fired on OpenCode (visible in dashboard activity feed) but the agent stopped without retry — same failure mode Cursor had pre-#318 and Copilot had pre-#299. Root cause: no `cli === "opencode"` branch in policy-evaluator's Stop / SubagentStop handling, so OpenCode fell into the generic exit-2 path. The plugin shim's applyDecision turns exit-2 into `throw new Error(reason)`, but throwing from the `session.idle` event callback is a no-op — OpenCode is already idle by the time the event fires. Fix: emit `{hookSpecificOutput: {additionalContext: <MANDATORY ACTION reasonText>}}` for opencode Stop / SubagentStop in both deny and instruct paths. The shim already routes `additionalContext` through `client.session.prompt(...)` which submits a new user message that re-triggers the agent loop — same model as Cursor's `followup_message` and Copilot's `{decision: "block", reason}`. Promote applyDecision to async and `await client.session.prompt` for Stop/SubagentStop events so the SDK round-trip completes before the plugin context tears down; keep fire-and-forget for tool events to avoid hot-path latency. Sister CLIs verified while in here: - Gemini AfterAgent (canonical Stop) was already correctly emitting `{decision: "block", reason}`; new unit tests pin both deny and instruct shapes to prevent regression. - Pi `agent_end` is observation-only by upstream design — Pi's agent loop has already exited and `AgentEndEventResult` exposes no `block` field. CLAUDE.md already documents this; no code change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update configuration.mdx to reflect the new Stop / SubagentStop force- retry channel: deny on Stop now routes through `client.session.prompt` just like instruct, since `session.idle` is notification-only and throwing from it is silently dropped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR fixes enforcement of ChangesOpenCode Stop/SubagentStop Policy Enforcement
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 5-7: Collapse the long narrative under the "### Fixes" section
into a single short summary line and append the PR number (e.g., "(`#323`)"); for
example, replace the long paragraph describing changes to policy-evaluator.ts,
the OpenCode shim’s session.idle behavior and client.session.prompt awaits, and
new tests (policy-evaluator.test.ts and opencode-plugin-shim.test.ts) with one
concise line referencing the core change (e.g., "Make require-*-before-stop
policies enforce on OpenCode; update policy-evaluator.ts and opencode shim to
await client.session.prompt for Stop/SubagentStop and add tests (`#323`)").
In `@docs/configuration.mdx`:
- Line 199: Update the outdated phrase "Unlike the other four CLIs," in the
sentence that begins with that text to reflect the current CLI count or remove
the numeric reference; locate the sentence in configuration.mdx that reads
"Unlike the other four CLIs," (the larger paragraph describing OpenCode
_(beta)_) and replace it with either "Unlike the other CLIs," or "Unlike the
other six CLIs," depending on whether you want a precise count, ensuring the
rest of the sentence and punctuation remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f0f6c01d-21e0-402a-ab49-a3cc36efe71c
📒 Files selected for processing (6)
CHANGELOG.md__tests__/hooks/opencode-plugin-shim.test.ts__tests__/hooks/policy-evaluator.test.tsdocs/configuration.mdxsrc/hooks/integrations.tssrc/hooks/policy-evaluator.ts
Address PR #323 review: - CHANGELOG.md: append (#323) to the Unreleased entry per repo convention (every entry ends with the PR number). - docs/configuration.mdx:199: "Unlike the other four CLIs" → "Unlike the other six CLIs" — the page now lists six other integrations (Claude Code, Codex, Copilot, Cursor, Pi, Gemini) so the count was stale. Release prep: promote the Unreleased entry to a versioned heading `## 0.0.10-beta.8 — 2026-05-08`. Add a fresh `## Unreleased` heading at the top for the next development cycle. package.json is already at 0.0.10-beta.8 (pre-bumped by chore commit a146ae6 after beta.7 release). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* [luv-323] docs: replace README ASCII-art wordmark with hosted PNG logo Drop the in-text ASCII-art wordmark at the top of README.md and render the branded wordmark (https://d2wq11aau0arks.cloudfront.net/failproof/logo-wordmark.png) inside a centered <p align="center"><img …/></p> wrapper instead, matching the pattern already used for the demo GIF and supported-CLI logo grid right below it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] docs: propagate logo-wordmark image to all 14 i18n READMEs Mirror the README.md change from the previous commit into every docs/i18n/README.<lang>.md so all language editions show the branded PNG wordmark instead of the ASCII-art block. Done in-place rather than waiting on the next auto-translate run so users browsing translated READMEs see the same header today. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] docs: fix PR reference in CHANGELOG entry (#323 → #322) Branch ID and PR number don't match in this repo; the actual PR for luv-323 is #322. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] docs: shorten CHANGELOG bullet to a one-liner per CodeRabbit Per the documented "Each entry should be a single line" convention in CLAUDE.md, condense the four-clause bullet describing the README + i18n logo-wordmark swap into a single short line. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: redesign CLI startup banner to match the PNG wordmark Replace the long-standing slant-figlet "Failproof AI" ASCII art in scripts/launch.ts (the banner shown on every `failproofai` dashboard launch) with a pixel-block "failproof ai" rendered from figlet's "Pagga" font — three lines of ░█▀ block characters in lowercase that echo the chunky-rounded glyph style of the hosted PNG logo (d2wq11aau0arks.cloudfront.net/failproof/logo-wordmark.png). The "i" of "fail" is the only accented glyph: its top dot renders in cyan to mirror the teal flower/rosette in the PNG, and the stem + base render in magenta to mirror the pink "U" hooked around the same i. Everything else stays default terminal color so the banner reads cleanly on both light and dark themes. Version line stays inline at the top-right of the wordmark, same pattern as before. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: use hand-crafted pixel-block wordmark for CLI banner Replace the Pagga-figlet banner from the previous commit with the hand-crafted chunky pixel-block wordmark provided by the user — a much closer visual match to the rounded-lowercase glyph style of the hosted PNG logo than figlet's ░█▀ characters could produce. Drops the cyan/magenta i-accent: the new art is monochrome by design, so the CLI now renders in the user's terminal foreground color instead of forcing a specific palette. Banner is now 19 rows × ~162 cols (up from 3 rows × 46 cols), so it prints inside a wider frame; version line moves to its own row below the wordmark since right-aligning at this width would not fit cleanly on narrower terminals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: halve banner height with half-block ▀▄█ compression Compress the 19-row hand-crafted wordmark from the previous commit to ~10 rows by pairing every two source rows into one row of upper / lower / full half-block characters: top █ + bot █ → █ top █ + bot ' ' → ▀ top ' ' + bot █ → ▄ top ' ' + bot ' ' → ' ' Same pixel grid the user provided, just rendered at one terminal row per two pixel rows. Side benefit: terminal cells are roughly 2:1 (taller than wide) so the compressed wordmark renders closer to the PNG's 1:1 pixel aspect ratio than the full-block 19-row form did. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: add narrow-terminal fallback for the wide CLI banner Per CodeRabbit on PR #322: the new pixel-block wordmark is ~200 cols wide, which wraps badly on standard 80-120 col terminals. Now check process.stdout.columns at startup; if the terminal is narrower than the banner's natural width, print " failproof ai" instead so the output stays clean. Width check only fires when stdout is a TTY — piped / redirected output keeps the full art as-is, since there's no meaningful width to compare against. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] docs: note narrow-terminal banner fallback in CHANGELOG entry Inline addendum to the existing Features bullet — same PR, same one-line form, just adds the new fallback behavior next to the rest of the banner description. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: shrink CLI banner with 2x2 quadrant blocks (~100 cols) User reported that the prior 200-col half-block banner triggered the narrow-terminal fallback on a typical ~120-col window, leaving them with the bare "failproof ai" plain-text version instead of any art. Re-compress the same source pixel grid with Unicode 2x2 quadrant block characters (▖▗▘▙▚▛▜▝▞▟ joining the existing ▀ ▄ █ ▌ ▐) so each terminal cell encodes a 2x2 chunk of source pixels — full art now fits at ~10 rows × ~100 cols, half its prior width. The 2x2 quadrant glyphs also produce naturally rounded letter edges (▟▙▛▜) that look closer to the PNG's rounded-corner pixel font than the rectangular half-block form did. Fallback path is unchanged but now only fires on terminals narrower than ~100 cols, which is rare in practice. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: scale CLI banner 4:3 horizontally to fit 80-col terminals User reported the prior 100-col quadrant-block banner triggered the narrow-terminal fallback on a typical ~82-col window, leaving them with the bare "failproof ai" plain-text version instead of the art. Re-process the same source pixel grid: explode the 2x2-quadrant glyphs back to a 1:1 binary pixel buffer, drop every 4th source column, then re-compress with quadrant blocks. Net effect is a horizontal 4:3 scale (every 4 cols of art rendered in 3 terminal cells) — full wordmark now fits in ~75 cols × ~10 rows, clean on any standard ≥80-col terminal. Slight loss of letter detail vs. the 100-col version but the rounded "failproof ai" silhouette stays recognizable. Plain-text fallback path is unchanged but now only fires below ~75 cols, which is rare in practice. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] revert: drop README + i18n PNG-wordmark swap from this PR Move the README + docs/i18n/README.*.md PNG-wordmark swap out of this PR so it can ship from a separate, dedicated PR. This branch now only covers the CLI startup banner redesign in scripts/launch.ts. Reverts the two README touchpoints (root + 14 i18n) to origin/main and drops the corresponding `### Docs` line from the CHANGELOG entry — keeping only the `### Features` line for the banner. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: drop noisy "Using default .claude projects path" log The default-path log printed unconditionally on every dashboard launch, between the banner / version / links block and Next.js's "Ready in …" output, and added no real signal — the path is fixed and discoverable. The custom-path log stays as-is: that one only fires when the user explicitly opted into a non-default path via --claude-projects-path, and confirming the override is useful. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [luv-323] feat: remove undocumented --projects-path CLI flag The --projects-path / -p flag was parsed in parse-script-args and routed through launch, but never appeared in the failproofai --help output. The same override is already available via the CLAUDE_PROJECTS_PATH environment variable, which lib/paths honors directly. Removes: - The flag-parsing block in parse-script-args (and the claudeProjectsPath field from ParsedScriptArgs). - The "Using custom .claude projects path: <path>" log line + the explicit CLAUDE_PROJECTS_PATH set + the spawn override in launch. An externally-set CLAUDE_PROJECTS_PATH still passes through to the spawned next.js child via the existing process-env spread. - The unused getDefaultClaudeProjectsPath import in launch. - 6 test cases that exercised --projects-path / -p (covering = and space-separated forms, plus the combined-flags case). - The flag row + example from docs/cli/dashboard.mdx, replaced with a short note pointing users at CLAUDE_PROJECTS_PATH instead. - The "--projects-path" entry in the translate-docs keep-as-is list. i18n .mdx files under docs/<lang>/cli/dashboard.mdx still reference the flag; the auto-translate workflow regenerates those from the English source on the next push to main, so the divergence is short-lived. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Summary
require-*-before-stoppolicies fired (visible in dashboard) but the agent stopped without retry — same failure mode as Cursor pre-[luv-319] fix: enforce Stop hook on Cursor Agent CLI + cut 0.0.10-beta.6 #318 / Copilot pre-[luv-299] fix: enforce require-*-before-stop on GitHub Copilot CLI #299. Root cause: nocli === "opencode"branch inpolicy-evaluator.tsforStop/SubagentStop, so OpenCode fell into the generic exit-2 path. The shim'sapplyDecisionturns exit-2 intothrow new Error(reason), but throwing from inside thesession.idleevent callback is a no-op — OpenCode is already idle by the time the event fires. Fix: emit{hookSpecificOutput: {additionalContext: <MANDATORY ACTION reasonText>}}for opencode Stop/SubagentStop in both deny and instruct paths. The shim's existing plumbing routesadditionalContextthroughclient.session.prompt(...)which submits a new user message and re-triggers the agent loop — same model as Cursor'sfollowup_messageand Copilot's{decision: "block", reason}.applyDecisionis nowasyncand awaitsclient.session.promptforStop/SubagentStopevents specifically (fire-and-forget for tool events to avoid hot-path SDK round-trip latency) — without the await, OpenCode could tear down the plugin context before the SDK call lands.AfterAgent → Stopemits{decision: "block", reason}); added unit tests pinning both deny and instruct shapes to prevent regression.agent_endis observation-only by upstream design (Pi's loop has already exited;AgentEndEventResultexposes noblockfield). Documented inCLAUDE.md; no code change.package.jsonwas already at 0.0.10-beta.8 (pre-bumped by chore commit a146ae6 after beta.7 release).Test plan
bun run test:run— 1582 unit tests pass, includes new opencode shim Stop tests + opencode/gemini Stop branch coverage inpolicy-evaluator.test.tsbun run test:e2e— 296 e2e tests passbun run lint— clean (1 pre-existing img warning, unrelated)bunx tsc --noEmit— cleanbun run build:cli— bundles cleanlysession.idlein OpenCode with arequire-*-before-stoppolicy active; confirm the agent receives a follow-up prompt instead of silently stopping🤖 Generated with Claude Code