Skip to content

Commit d5ee132

Browse files
dzianisvengineerclaude
authored
Install reflection on Claude Code (/plugin) + OpenCode (plugin section); macOS keychain auth; cheaper CI eval (#142)
* feat: install reflection on Claude Code (/plugin) and OpenCode (plugin section) Claude Code port (claude/): - Fix Stop hook contract: event name "Stop" (was "stop"), array hook format, read last_assistant_message from hook stdin, emit {decision:"block",reason} to re-prompt; exit 0 to approve. - Mirror reflection-3.ts PREMATURE-STOP ANTIPATTERNS into judge.mjs (PERMISSION-SEEKING / STOPPED-WITH-TODOS / FALSE-COMPLETE). - Auth: ANTHROPIC_API_KEY fallback in addition to OAuth; fail-safe approve-stop on judge error. Add REFLECTION_CC_FAKE_JUDGE test hook. - Add root .claude-plugin/marketplace.json (source ./claude) so `/plugin marketplace add dzianisv/opencode-plugins` + `/plugin install reflection-cc` work. Un-ignore .claude-plugin/ in .gitignore. OpenCode plugin-section install (packages/reflection/): - Publishable npm package `opencode-reflection` mirroring the auto-review packaging pattern: index.ts re-export, prepack/postpack symlink swap, files allowlist. Add to opencode.json "plugin": ["opencode-reflection"] (or a local path) instead of the copy script. - README: document both install paths. Verified: typecheck clean, claude e2e 2/2 (3 need live API creds), npm pack --dry-run = 3 files / 20.8kB, plugin-load test unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(claude): read OAuth credentials from macOS keychain On macOS, Claude Code stores credentials in the login keychain (generic password "Claude Code-credentials"), not ~/.claude/.credentials.json. The judge's OAuth path only read the file, so the in-hook judge could never authenticate on a Mac and silently fell back to no-inject — i.e. the plugin did nothing for most Mac users. loadAuth() now falls back to `security find-generic-password -s "Claude Code-credentials" -w` on darwin. Verified end-to-end: a real classifyStop() call authenticates via the keychain token and returns a verdict from the live Anthropic API. Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs: map reflection plugin to Reflexion (Weng 2023) taxonomy Document that the plugin is a Reflexion-style actor/evaluator/verbal-self- reflection loop (not ReAct/CoH/AD), with a one-to-one mapping: the loop heuristics (PLANNING_LOOP, ACTION_LOOP) mirror Reflexion's inefficient/ hallucinated-trajectory detectors and MAX_ATTEMPTS=3 mirrors its bounded reflection memory. Notes where it differs (fires on stop/idle to catch premature stops; LLM-as-judge rubric mined from 227 real stops). Co-Authored-By: Claude Opus 4.8 <[email protected]> * perf(evals): run judge suite on gpt-5.4-nano with a pass-rate threshold Cut CI judge-eval cost ~25x by switching the provider from gpt-5.1 to the cheapest deployed dev-endpoint model, gpt-5.4-nano. Benchmarked all 34 cases: gpt-5.1 34/34, gpt-5.4 / gpt-5.4-mini / gpt-5.4-nano all 33/34 — the single miss is calibration variance on one borderline case shared by the whole 5.4 family, not a premature-stop-logic failure. run-promptfoo.mjs now honors EVAL_PASS_THRESHOLD: when set, a run that promptfoo failed is re-checked against the suite pass rate (only ever relaxes a fail, never reddens a pass; falls back to native exit on any parse error) and prints the tolerated cases. CI judge step sets 0.97 (tolerate <=1 of 34); a 2nd failure turns CI red. gpt-5.1 remains a one-line swap for full fidelity. Production judge still blocks small models via JUDGE_BLOCKED_PATTERNS — this is CI-eval only. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: engineer <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]>
1 parent 1adac84 commit d5ee132

16 files changed

Lines changed: 473 additions & 67 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "opencode-plugins",
3+
"owner": {
4+
"name": "dzianisv",
5+
"url": "https://github.com/dzianisv"
6+
},
7+
"plugins": [
8+
{
9+
"name": "reflection-cc",
10+
"source": "./claude",
11+
"description": "Re-prompts Claude Code when it stops prematurely — catches PERMISSION-SEEKING, STOPPED-WITH-TODOS, and FALSE-COMPLETE failure modes (78% of real agent stops are premature), and injects targeted recovery instructions via the Stop hook.",
12+
"version": "0.1.0",
13+
"author": {
14+
"name": "dzianisv",
15+
"url": "https://github.com/dzianisv"
16+
}
17+
}
18+
]
19+
}

.github/workflows/evals.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ jobs:
7575
# Must be the base host, e.g. https://vibebrowser-dev.openai.azure.com
7676
AZURE_OPENAI_BASE_URL: ${{ secrets.AZURE_OPENAI_BASE_URL }}
7777
AZURE_OPENAI_API_BASE_URL: ${{ secrets.AZURE_OPENAI_BASE_URL }}
78+
# Judge suite runs the cheap gpt-5.4-nano model (see promptfooconfig.yaml).
79+
# It scores 33/34 — one borderline case differs from the gpt-5.1 baseline.
80+
# Tolerate <=1 of 34 (>=97%); a 2nd failure turns CI red.
81+
EVAL_PASS_THRESHOLD: "0.97"
7882
run: npm run eval:judge -- --no-progress-bar -o evals/results/judge-results.json
7983

8084
- name: Run Stuck Detection Evaluation

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
!.agents/**
44
!.github
55
!.github/**
6+
!.claude-plugin
7+
!.claude-plugin/**
68
!claude/.claude-plugin
79
!claude/.claude-plugin/**
810
.tts

README.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,33 @@ This plugin adds a **judge layer** that automatically evaluates task completion
5757
- **Local TTS** - Hear responses read aloud (Coqui VCTK/VITS, Chatterbox, macOS)
5858
- **Voice-to-text** - Reply to Telegram with voice messages, transcribed by local Whisper
5959

60-
## Quick Install
60+
## Install via opencode.json (preferred)
61+
62+
Add the reflection plugin to the `plugin` array in your `opencode.json` (project-level or `~/.config/opencode/opencode.json` for global):
63+
64+
**Published npm package:**
65+
```json
66+
{
67+
"$schema": "https://opencode.ai/config.json",
68+
"plugin": ["opencode-reflection"]
69+
}
70+
```
71+
72+
**Local path (from a clone of this repo):**
73+
```json
74+
{
75+
"$schema": "https://opencode.ai/config.json",
76+
"plugin": ["/absolute/path/to/opencode-plugins/packages/reflection"]
77+
}
78+
```
79+
80+
OpenCode resolves the entry point from `package.json` `exports`, imports the default export (a `Plugin` function), and calls it automatically at startup. No manual file copying or `bun install` required — OpenCode handles dependency installation.
81+
82+
Restart OpenCode after editing `opencode.json` to activate.
83+
84+
---
85+
86+
## Quick Install (copy-script method)
6187

6288
```bash
6389
curl -fsSL https://raw.githubusercontent.com/dzianisv/opencode-plugins/main/install.sh | bash
@@ -155,6 +181,40 @@ Evaluates task completion after each agent response and provides feedback if wor
155181
4. **Verdict**: PASS → toast notification | FAIL → feedback injected into chat
156182
5. **Continuation**: Agent receives feedback and continues working
157183

184+
### Relation to Reflexion (Weng 2023 / Shinn et al. 2023)
185+
186+
This plugin is, in the taxonomy of Lilian Weng's [*LLM Powered Autonomous Agents*](https://lilianweng.github.io/posts/2023-06-23-agent/),
187+
a **Reflexion**-style self-improvement loop — not ReAct, Chain-of-Hindsight, or
188+
Algorithm Distillation. The mapping is almost one-to-one:
189+
190+
| Reflexion concept (Weng / Shinn et al.) | This plugin |
191+
| --- | --- |
192+
| **Actor** — the policy LLM that acts | The coding agent (OpenCode / Claude Code) itself |
193+
| **Evaluator** — scores the trajectory | The LLM-as-judge self-assessment (`buildSelfAssessmentPrompt` / `classifyStop`), run in an unbiased hidden session |
194+
| **Self-reflection** — verbal feedback added to memory for the next attempt | The feedback string injected back into the chat / the Stop-hook `block` reason — natural-language, not a scalar reward |
195+
| **Heuristic: "inefficient" trajectory (too long without success)** | `PLANNING_LOOP` detector — many tool calls with a near-zero write ratio (`PLANNING_LOOP_MIN_TOOL_CALLS`, `PLANNING_LOOP_WRITE_RATIO_THRESHOLD`) |
196+
| **Heuristic: "hallucination" = consecutive identical actions → same observation** | `ACTION_LOOP` detector — repeated identical commands above `ACTION_LOOP_REPETITION_THRESHOLD` |
197+
| **"Up to three reflections stored in working memory"** | `MAX_ATTEMPTS = 3` — at most three feedback injections per task before giving up |
198+
| **Reset the environment to start a new trial** | Re-prompt the *same* session to continue (no env reset — agentic coding has no episodic reset) |
199+
200+
**Where it differs from textbook Reflexion:**
201+
202+
- **Trigger granularity.** Classic Reflexion evaluates at the end of an episode
203+
/ on a failed trajectory. This plugin fires on the `session.idle` (OpenCode) or
204+
`Stop` (Claude Code) boundary — i.e. *every time the agent thinks it's done*
205+
so its primary job is catching **premature stops**, not just failed runs.
206+
- **Evaluator design.** Reflexion's evaluator is a task-specific heuristic (and
207+
sometimes an LLM). Here the evaluator is primarily an **LLM-as-judge** whose
208+
rubric is **mined from 227 real agent stops** (78% were premature), layered on
209+
top of the two Reflexion-style heuristics above.
210+
- **Verbal, not numeric.** Like Reflexion (and unlike RLHF/CoH), the feedback is
211+
natural language fed straight back into context — no fine-tuning, no reward
212+
model, no gradient updates.
213+
214+
In short: **Reflexion = actor + evaluator + verbal self-reflection with a small
215+
bounded memory of retries**, and that is exactly the shape of this plugin, with
216+
the evaluator specialized toward detecting premature task abandonment.
217+
158218
### State Graph
159219

160220
```

claude/.claude-plugin/plugin.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "reflection-cc",
3+
"displayName": "Reflection (Claude Code)",
34
"version": "0.1.0",
4-
"description": "Re-prompts Claude Code when it stops prematurely due to failure modes like summary-drift-stop or tool-available-punt",
5-
"author": "dzianisv",
5+
"description": "Re-prompts Claude Code when it stops prematurely — catches PERMISSION-SEEKING, STOPPED-WITH-TODOS, and FALSE-COMPLETE failure modes, and injects targeted recovery instructions.",
6+
"author": {
7+
"name": "dzianisv",
8+
"url": "https://github.com/dzianisv"
9+
},
10+
"repository": "https://github.com/dzianisv/opencode-plugins",
611
"license": "MIT",
7-
"hooks": {
8-
"stop": {
9-
"command": "${CLAUDE_PLUGIN_ROOT}/bin/reflect.mjs",
10-
"timeout": 30000
11-
}
12-
}
12+
"hooks": "./hooks/hooks.json"
1313
}

claude/README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,28 @@ Re-prompts Claude Code when it stops prematurely due to failure modes like summa
44

55
## Install
66

7-
**Recommended (works today, CC v2.x):** add the Stop hook directly to `~/.claude/settings.json`:
7+
### Via `/plugin` marketplace (recommended)
8+
9+
```bash
10+
# 1. Register the marketplace (one-time per machine)
11+
/plugin marketplace add dzianisv/opencode-plugins
12+
13+
# 2. Install the plugin
14+
/plugin install reflection-cc
15+
```
16+
17+
Or in one step using the CLI:
18+
19+
```bash
20+
claude plugin marketplace add dzianisv/opencode-plugins
21+
claude plugin install reflection-cc
22+
```
23+
24+
This uses the `marketplace.json` at the repo root (`.claude-plugin/marketplace.json`) which points the `./claude` subdirectory as the plugin source.
25+
26+
### Manual (settings-based install — always works)
27+
28+
Add the Stop hook directly to `~/.claude/settings.json`:
829

930
```json
1031
{
@@ -24,9 +45,9 @@ Re-prompts Claude Code when it stops prematurely due to failure modes like summa
2445
}
2546
```
2647

27-
The plugin manifest under `.claude-plugin/` is included for future marketplace publication, but in CC v2.1.150 `--plugin-dir` and the `enabledPlugins` config path do NOT activate `Stop` hooks for headless `-p` sessions. The settings-based install above is the authoritative path until that gap closes.
48+
**One-session try:** write the JSON above to a file and pass `--settings ./reflect-settings.json`.
2849

29-
**One-session try:** `claude --settings '<json above>'` ... or write the JSON to a file and pass `--settings ./reflect-settings.json`.
50+
> Note: the Stop hook event name is `"Stop"` (capital S) — lowercase `"stop"` is silently ignored by Claude Code.
3051
3152
## Failure Categories
3253

claude/bin/reflect.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,10 @@ export function buildStopContext(stopPayload, transcriptTail) {
357357
}
358358
}
359359

360-
// Derive final assistant text: prefer CC's `response` field (it IS the last turn),
361-
// fall back to the last assistant entry's text content from the tail.
362-
let final_assistant_text = (stopPayload?.response ?? '').trim();
360+
// Derive final assistant text: prefer CC's `last_assistant_message` field (the
361+
// documented Stop hook field name as of CC v2.x — NOT `response`), fall back
362+
// to the last assistant entry's text content from the transcript tail.
363+
let final_assistant_text = (stopPayload?.last_assistant_message ?? stopPayload?.response ?? '').trim();
363364
if (!final_assistant_text) {
364365
// Walk tail in reverse, find last assistant entry with a text block
365366
for (let i = transcriptTail.length - 1; i >= 0; i--) {

claude/hooks/hooks.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
{
22
"hooks": {
3-
"stop": {
4-
"command": "${CLAUDE_PLUGIN_ROOT}/bin/reflect.mjs",
5-
"timeout": 30000
6-
}
3+
"Stop": [
4+
{
5+
"hooks": [
6+
{
7+
"type": "command",
8+
"command": "${CLAUDE_PLUGIN_ROOT}/bin/reflect.mjs",
9+
"timeout": 30
10+
}
11+
]
12+
}
13+
]
714
}
815
}

0 commit comments

Comments
 (0)