Skip to content

[luv-324] fix: enforce Stop hook on OpenCode + cut 0.0.10-beta.8#323

Merged
NiveditJain merged 3 commits intomainfrom
luv-324
May 9, 2026
Merged

[luv-324] fix: enforce Stop hook on OpenCode + cut 0.0.10-beta.8#323
NiveditJain merged 3 commits intomainfrom
luv-324

Conversation

@NiveditJain
Copy link
Copy Markdown
Member

@NiveditJain NiveditJain commented May 9, 2026

Summary

  • OpenCode Stop fix: require-*-before-stop policies 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: no cli === "opencode" branch in policy-evaluator.ts for Stop / SubagentStop, so OpenCode fell into the generic exit-2 path. The shim's applyDecision turns exit-2 into throw new Error(reason), but throwing from inside 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's existing plumbing routes additionalContext through client.session.prompt(...) which submits a new user message and re-triggers the agent loop — same model as Cursor's followup_message and Copilot's {decision: "block", reason}.
  • Async + await for Stop: applyDecision is now async and awaits client.session.prompt for Stop/SubagentStop events 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.
  • Gemini: already correct (AfterAgent → Stop emits {decision: "block", reason}); added unit tests pinning both deny and instruct shapes to prevent regression.
  • Pi: agent_end is observation-only by upstream design (Pi's loop has already exited; AgentEndEventResult exposes no block field). Documented in CLAUDE.md; no code change.
  • Release: cuts 0.0.10-beta.8 by promoting the Unreleased CHANGELOG entry to a versioned heading. package.json was 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 in policy-evaluator.test.ts
  • bun run test:e2e — 296 e2e tests pass
  • bun run lint — clean (1 pre-existing img warning, unrelated)
  • bunx tsc --noEmit — clean
  • bun run build:cli — bundles cleanly
  • CI green on luv-324 (build / docs / quality / test×3 / test-e2e + CodeRabbit)
  • CodeRabbit review feedback addressed (CHANGELOG entry suffix, docs CLI count)
  • Manual smoke: trigger session.idle in OpenCode with a require-*-before-stop policy active; confirm the agent receives a follow-up prompt instead of silently stopping
  • Verify dashboard activity feed still records the deny entry

🤖 Generated with Claude Code

NiveditJain and others added 2 commits May 8, 2026 17:39
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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes enforcement of require-*-before-stop policies on OpenCode CLI for Stop/SubagentStop events. The policy evaluator now returns exitCode: 0 with hookSpecificOutput.additionalContext for deny/instruct decisions, and the plugin shim makes applyDecision async to await client.session.prompt and prevent handler teardown before retry prompts complete.

Changes

OpenCode Stop/SubagentStop Policy Enforcement

Layer / File(s) Summary
Policy Evaluator Logic
src/hooks/policy-evaluator.ts
Adds OpenCode-specific deny and instruct branches for Stop/SubagentStop that return exitCode 0 with hookSpecificOutput.additionalContext, bypassing generic per-event handling.
Plugin Shim Async Handling
src/hooks/integrations.ts
Makes applyDecision async and threads eventName; for Stop/SubagentStop with additionalContext, awaits client.session.prompt and swallows errors; other events continue non-blocking forwarding.
Policy Evaluator Tests
__tests__/hooks/policy-evaluator.test.ts
Validates OpenCode Stop/SubagentStop deny/instruct response shapes (exitCode 0, hookSpecificOutput.additionalContext, MANDATORY ACTION REQUIRED prefix); asserts PreToolUse deny preserves Claude-style permissionDecision without additionalContext; includes Gemini Stop decision block format coverage.
Plugin Shim Tests
__tests__/hooks/opencode-plugin-shim.test.ts
Three new cases: (1) shim awaits client.session.prompt for session.idle with additionalContext and blocks until resolved, (2) rejection swallowed and handler completes, (3) exit code 2 still throws for backward compatibility.
Documentation & Changelog
docs/configuration.mdx, CHANGELOG.md
Updates OpenCode documentation to clarify decision-to-semantics mapping for Stop/SubagentStop deny/instruct using client.session.prompt; adds changelog entry documenting the enforcement fix.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A hop, skip, and await—
the prompt no longer tears down too late!
Stop events now signal their way,
with async shims here to stay. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main fix (enforce Stop hook on OpenCode) and references the version bump, directly aligned with the primary changeset.
Description check ✅ Passed The description provides comprehensive context covering root cause, solution, test plan with checkmarks, and known pending items; however, the template checklist items are missing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a146ae6 and 0587195.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • __tests__/hooks/opencode-plugin-shim.test.ts
  • __tests__/hooks/policy-evaluator.test.ts
  • docs/configuration.mdx
  • src/hooks/integrations.ts
  • src/hooks/policy-evaluator.ts

Comment thread CHANGELOG.md
Comment thread docs/configuration.mdx Outdated
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>
@NiveditJain NiveditJain changed the title [luv-324] fix: enforce Stop hook on OpenCode [luv-324] fix: enforce Stop hook on OpenCode + cut 0.0.10-beta.8 May 9, 2026
@NiveditJain NiveditJain merged commit 19a46e5 into main May 9, 2026
9 checks passed
NiveditJain added a commit that referenced this pull request May 9, 2026
* [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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant