jeo
JEO — Integrated AI agent orchestration skill. Plan with ralph+plannotator, execute with team/bmad, verify browser behavior with agent-browser, apply UI feedback with agentation(annotate), auto-cleanup worktrees after completion. Supports Claude, Codex, Gemini CLI, and OpenCode. Install: ralph, omc, omx, ohmg, bmad, plannotator, agent-browser, agentation.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install supercent-io-skills-template-jeo
Repository
Skill path: .agent-skills/jeo
JEO — Integrated AI agent orchestration skill. Plan with ralph+plannotator, execute with team/bmad, verify browser behavior with agent-browser, apply UI feedback with agentation(annotate), auto-cleanup worktrees after completion. Supports Claude, Codex, Gemini CLI, and OpenCode. Install: ralph, omc, omx, ohmg, bmad, plannotator, agent-browser, agentation.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Frontend, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: supercent-io.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install jeo into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/supercent-io/skills-template before adding jeo to shared team environments
- Use jeo for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: jeo
description: "JEO — Integrated AI agent orchestration skill. Plan with ralph+plannotator, execute with team/bmad, verify browser behavior with agent-browser, apply UI feedback with agentation(annotate), auto-cleanup worktrees after completion. Supports Claude, Codex, Gemini CLI, and OpenCode. Install: ralph, omc, omx, ohmg, bmad, plannotator, agent-browser, agentation."
compatibility: "Requires git, node>=18, bash. Optional: bun, docker."
allowed-tools: Read Write Bash Grep Glob Task
metadata:
tags: jeo, orchestration, ralph, plannotator, agentation, annotate, agentui, UI-review, team, bmad, omc, omx, ohmg, agent-browser, multi-agent, workflow, worktree-cleanup, browser-verification, ui-feedback
platforms: Claude, Codex, Gemini, OpenCode
keyword: jeo
version: 1.3.1
source: supercent-io/skills-template
---
# JEO — Integrated Agent Orchestration
> Keyword: `jeo` · `annotate` · `UI-review` · `agentui (deprecated)` | Platforms: Claude Code · Codex CLI · Gemini CLI · OpenCode
>
> A unified skill providing fully automated orchestration flow:
> Plan (ralph+plannotator) → Execute (team/bmad) → UI Feedback (agentation/annotate) → Cleanup (worktree cleanup)
## Control Layers
JEO uses one cross-platform abstraction for orchestration:
- `settings`: platform/runtime configuration such as Claude hooks, Codex `config.toml`, Gemini `settings.json`, MCP registration, and prompt parameters
- `rules`: policy constraints that must hold on every platform
- `hooks`: event callbacks that enforce those rules on each platform
The key JEO rules are:
- do not reopen the PLAN gate when the current plan hash already has a terminal result
- only a revised plan resets `plan_gate_status` to `pending`
- do not process agentation annotations before explicit submit/onSubmit opens the submit gate
The authoritative state is `.omc/state/jeo-state.json`. Hooks may help advance the workflow, but they must obey the state file.
---
## 0. Agent Execution Protocol (follow immediately upon `jeo` keyword detection)
> The following are commands, not descriptions. Execute them in order. Each step only proceeds after the previous one completes.
### STEP 0: State File Bootstrap (required — always first)
```bash
mkdir -p .omc/state .omc/plans .omc/logs
```
If `.omc/state/jeo-state.json` does not exist, create it:
<!-- NOTE: The `worktrees` array was removed from the initial schema as it is not yet implemented.
Add it back when multi-worktree parallel execution tracking is needed.
worktree-cleanup.sh queries git worktree list directly, so it works without this field. -->
```json
{
"phase": "plan",
"task": "<detected task>",
"plan_approved": false,
"plan_gate_status": "pending",
"plan_current_hash": null,
"last_reviewed_plan_hash": null,
"last_reviewed_plan_at": null,
"plan_review_method": null,
"team_available": null,
"retry_count": 0,
"last_error": null,
"checkpoint": null,
"created_at": "<ISO 8601>",
"updated_at": "<ISO 8601>",
"agentation": {
"active": false,
"session_id": null,
"keyword_used": null,
"submit_gate_status": "idle",
"submit_signal": null,
"submit_received_at": null,
"submitted_annotation_count": 0,
"started_at": null,
"timeout_seconds": 120,
"annotations": { "total": 0, "acknowledged": 0, "resolved": 0, "dismissed": 0, "pending": 0 },
"completed_at": null,
"exit_reason": null
}
}
```
Notify the user:
> "JEO activated. Phase: PLAN. Add the `annotate` keyword if a UI feedback loop is needed."
---
### STEP 0.1: Error Recovery Protocol (applies to all STEPs)
**Checkpoint recording — immediately after entering each STEP:**
```python
# Execute immediately at the start of each STEP (agent updates jeo-state.json directly)
python3 -c "
import json, datetime, os, subprocess, tempfile
try:
root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], stderr=subprocess.DEVNULL).decode().strip()
except:
root = os.getcwd()
f = os.path.join(root, '.omc/state/jeo-state.json')
if os.path.exists(f):
import fcntl
with open(f, 'r+') as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
d = json.load(fh)
d['checkpoint']='<current_phase>' # 'plan'|'execute'|'verify'|'cleanup'
d['updated_at']=datetime.datetime.utcnow().isoformat()+'Z'
fh.seek(0)
json.dump(d, fh, ensure_ascii=False, indent=2)
fh.truncate()
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
" 2>/dev/null || true
```
**last_error recording — on pre-flight failure or exception:**
```python
python3 -c "
import json, datetime, os, subprocess, fcntl
try:
root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], stderr=subprocess.DEVNULL).decode().strip()
except:
root = os.getcwd()
f = os.path.join(root, '.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f, 'r+') as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
d = json.load(fh)
d['last_error']='<error message>'
d['retry_count']=d.get('retry_count',0)+1
d['updated_at']=datetime.datetime.utcnow().isoformat()+'Z'
fh.seek(0)
json.dump(d, fh, ensure_ascii=False, indent=2)
fh.truncate()
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
" 2>/dev/null || true
```
**Checkpoint-based resume on restart:**
```python
# If jeo-state.json already exists, resume from checkpoint
python3 -c "
import json, os, subprocess
try:
root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], stderr=subprocess.DEVNULL).decode().strip()
except:
root = os.getcwd()
f = os.path.join(root, '.omc/state/jeo-state.json')
if os.path.exists(f):
d=json.load(open(f))
cp=d.get('checkpoint')
err=d.get('last_error')
if err: print(f'Previous error: {err}')
if cp: print(f'Resuming from: {cp}')
" 2>/dev/null || true
```
> **Rule**: Before `exit 1` in pre-flight, always update `last_error` and increment `retry_count`.
> If `retry_count >= 3`, ask the user whether to abort.
---
### STEP 1: PLAN (never skip)
**Pre-flight (required before entering):**
```bash
# Record checkpoint
python3 -c "
import json,datetime,os,subprocess,fcntl,tempfile
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d.update({'checkpoint':'plan','updated_at':datetime.datetime.utcnow().isoformat()+'Z'})
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
# NOTE: Claude Code — skip this entire bash block.
# plannotator is a hook-only binary; calling it directly always fails.
# For Claude Code: call EnterPlanMode → write plan → call ExitPlanMode.
# The ExitPlanMode PermissionRequest hook fires plannotator automatically.
# The following script is for Codex / Gemini / OpenCode only.
# GUARD: enforce no-repeat PLAN review by plan hash.
# same hash + terminal gate status => skip reopening the plan gate
# revised plan.md content => reset gate to pending and review again
PLAN_GATE_STATUS=$(python3 -c "
import json, os
try:
s = json.load(open('.omc/state/jeo-state.json'))
print(s.get('plan_gate_status', 'pending'))
except Exception:
print('pending')
" 2>/dev/null || echo "pending")
HASH_MATCH=$(python3 -c "
import hashlib, json, os
try:
s = json.load(open('.omc/state/jeo-state.json'))
if not os.path.exists('plan.md'):
print('no-match')
else:
current_hash = hashlib.sha256(open('plan.md', 'rb').read()).hexdigest()
print('match' if current_hash == (s.get('last_reviewed_plan_hash') or '') else 'no-match')
except Exception:
print('no-match')
" 2>/dev/null || echo "no-match")
if [[ "$HASH_MATCH" == "match" && "$PLAN_GATE_STATUS" =~ ^(approved|manual_approved)$ ]]; then
echo "✅ Current plan hash already approved. Skipping re-review."
exit 0
fi
# BUG FIX (v1.3.1): feedback_required + same hash must NOT exit 0 (approved).
# The plan has outstanding feedback and must be revised before re-opening plannotator.
# Exiting 0 here would cause JEO to proceed to EXECUTE with an unapproved plan.
if [[ "$HASH_MATCH" == "match" && "$PLAN_GATE_STATUS" == "feedback_required" ]]; then
echo "❌ Feedback pending for this plan hash — revise plan.md content (new hash required) before re-opening plannotator."
echo " Check .omc/state/jeo-state.json → plannotator_feedback for the recorded feedback."
exit 1
fi
if [[ "$HASH_MATCH" == "match" && "$PLAN_GATE_STATUS" == "infrastructure_blocked" ]]; then
echo "⚠️ Infrastructure blocked for current plan hash. Use manual TTY gate or set plan_gate_status='manual_approved' in jeo-state.json."
exit 32
fi
# plannotator is mandatory for the PLAN step (Codex/Gemini/OpenCode).
# If missing, JEO auto-installs it before opening the PLAN gate.
# Resolve the JEO scripts directory (works from any CWD)
_JEO_SCRIPTS=""
for _candidate in \
"${JEO_SKILL_DIR:-}/scripts" \
"$HOME/.agent-skills/jeo/scripts" \
"$HOME/.codex/skills/jeo/scripts" \
"$(pwd)/.agent-skills/jeo/scripts" \
"scripts" \
; do
if [ -f "${_candidate}/plannotator-plan-loop.sh" ]; then
_JEO_SCRIPTS="$_candidate"
break
fi
done
if [ -z "$_JEO_SCRIPTS" ]; then
echo "❌ JEO scripts not found. Re-run: bash setup-codex.sh (or setup-gemini.sh)"
exit 1
fi
if ! bash "${_JEO_SCRIPTS}/ensure-plannotator.sh"; then
echo "❌ plannotator auto-install failed: cannot proceed with PLAN step."
echo " Retry: bash ${_JEO_SCRIPTS}/../scripts/install.sh --with-plannotator"
exit 1
fi
# Required PLAN gate (Codex / Gemini / OpenCode):
# - Must wait until approve/feedback is received
# - Auto-restart on session exit (up to 3 times)
# - After 3 exits, ask user whether to end PLAN
FEEDBACK_DIR=$(python3 -c "import hashlib,os; h=hashlib.md5(os.getcwd().encode()).hexdigest()[:8]; d=f'/tmp/jeo-{h}'; os.makedirs(d,exist_ok=True); print(d)" 2>/dev/null || echo '/tmp')
FEEDBACK_FILE="${FEEDBACK_DIR}/plannotator_feedback.txt"
bash "${_JEO_SCRIPTS}/plannotator-plan-loop.sh" plan.md "$FEEDBACK_FILE" 3
PLAN_RC=$?
if [ "$PLAN_RC" -eq 0 ]; then
echo "✅ Plan approved"
elif [ "$PLAN_RC" -eq 10 ]; then
echo "❌ Plan not approved — apply feedback, revise plan.md, and retry"
exit 1
elif [ "$PLAN_RC" -eq 32 ]; then
echo "⚠️ plannotator UI unavailable (sandbox/CI). Entering Conversation Approval Mode:"
echo " 1. Output plan.md content to user in conversation"
echo " 2. Ask user: 'approve' to proceed or provide feedback"
echo " 3. DO NOT proceed to EXECUTE until user explicitly approves"
exit 32
elif [ "$PLAN_RC" -eq 30 ] || [ "$PLAN_RC" -eq 31 ]; then
echo "⛔ PLAN exit decision (or awaiting confirmation). Confirm with user before retrying."
exit 1
else
echo "❌ plannotator PLAN gate failed (code=$PLAN_RC)"
exit 1
fi
mkdir -p .omc/plans .omc/logs
```
1. Write `plan.md` (include goal, steps, risks, and completion criteria)
2. **Invoke plannotator** (per platform):
- **Claude Code (hook mode — only supported method)**:
`plannotator` is a hook-only binary. It cannot be called via MCP tool or CLI directly.
Call `EnterPlanMode`, write the plan content in plan mode, then call `ExitPlanMode`.
The `ExitPlanMode` PermissionRequest hook fires the JEO Claude plan-gate wrapper automatically.
That wrapper must skip re-entry when the current plan hash already has a terminal review result.
Wait for the hook to return before proceeding — approved or feedback will arrive via the hook result.
- **Codex / Gemini / OpenCode**: run blocking CLI (never use `&`):
```bash
# _JEO_SCRIPTS must be resolved first via the dynamic path discovery block in the pre-flight above
bash "${_JEO_SCRIPTS}/plannotator-plan-loop.sh" plan.md /tmp/plannotator_feedback.txt 3
```
If `plannotator` is missing, JEO must auto-run `bash "${_JEO_SCRIPTS}/ensure-plannotator.sh"` first and continue only after the CLI is available.
3. Check result:
- `approved: true` (Claude Code: hook returns approved) → update `jeo-state.json` `phase` to `"execute"` and `plan_approved` to `true` → **enter STEP 2**
- Not approved (Claude Code: hook returns feedback; others: `exit 10`) → read feedback, revise `plan.md` → repeat step 2
- Infrastructure blocked (`exit 32`) → localhost bind unavailable (e.g., sandbox/CI). Use manual gate in TTY; confirm with user and retry outside sandbox in non-TTY
- Session exited 3 times (`exit 30/31`) → ask user whether to end PLAN and decide to abort or resume
**NEVER: enter EXECUTE without `approved: true`. NEVER: run with `&` background.**
**NEVER: reopen the same unchanged plan after `approved` or `manual_approved` — it is already approved.**
**NEVER: treat `feedback_required` as approved — it means the plan was REJECTED and must be revised.**
**When `feedback_required` + same hash → exit 1 (not 0), revise plan content to get a new hash, then re-open plannotator.**
---
### STEP 2: EXECUTE
**Pre-flight (auto-detect team availability):**
```bash
# Record checkpoint
python3 -c "
import json,datetime,os,subprocess,fcntl
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d.update({'checkpoint':'execute','updated_at':datetime.datetime.utcnow().isoformat()+'Z'})
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
TEAM_AVAILABLE=false
if [[ "${CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS:-}" =~ ^(1|true|True|yes|YES)$ ]]; then
TEAM_AVAILABLE=true
elif python3 -c "
import json, os, sys
try:
s = json.load(open(os.path.expanduser('~/.claude/settings.json')))
val = s.get('env', {}).get('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS', '')
sys.exit(0 if str(val) in ('1', 'true', 'True', 'yes') else 1)
except Exception:
sys.exit(1)
" 2>/dev/null; then
TEAM_AVAILABLE=true
fi
export TEAM_AVAILABLE_BOOL="$TEAM_AVAILABLE"
python3 -c "
import json,os,subprocess,fcntl
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d['team_available']=os.environ.get('TEAM_AVAILABLE_BOOL','false').lower()=='true'
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
```
1. Update `jeo-state.json` `phase` to `"execute"`
2. **Team available (Claude Code + omc)**:
```
/omc:team 3:executor "<task>"
```
3. **Claude Code but no team**:
```
echo "❌ JEO requires Claude Code team mode. Re-run bash scripts/setup-claude.sh, restart Claude Code, and confirm CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1."
exit 1
```
Never fall back to single-agent execution in Claude Code.
4. **No omc (BMAD fallback — Codex / Gemini / OpenCode only)**:
```
/workflow-init # Initialize BMAD
/workflow-status # Check current step
```
---
### STEP 3: VERIFY
1. Update `jeo-state.json` `phase` to `"verify"`
2. **Basic verification with agent-browser** (when browser UI is present):
```bash
agent-browser snapshot http://localhost:3000
```
3. `annotate` keyword detected → **enter STEP 3.1**
4. Otherwise → **enter STEP 4**
---
### STEP 3.1: VERIFY_UI (only when `annotate` keyword is detected)
1. Pre-flight check (required before entering):
```bash
# Auto-start server, auto-install package, auto-inject component (see §3.3.1 for full script)
bash scripts/ensure-agentation.sh --project-dir "${PROJECT_DIR:-$PWD}" --endpoint "http://localhost:4747" || true
# If agentation-mcp server is still not healthy after pre-flight → graceful skip to STEP 4
if ! curl -sf --connect-timeout 2 http://localhost:4747/health >/dev/null 2>&1; then
python3 -c "
import json,os,subprocess,fcntl,time
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d['last_error']='agentation-mcp not running; VERIFY_UI skipped'
d['updated_at']=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
# Proceed to STEP 4 CLEANUP (no exit 1 — graceful skip)
fi
```
2. Update `jeo-state.json`: `phase = "verify_ui"`, `agentation.active = true`, `agentation.submit_gate_status = "waiting_for_submit"`
3. Wait for explicit human submit:
- **Claude Code**: wait for `UserPromptSubmit` after the user presses **Send Annotations** / `onSubmit`
- **Codex / Gemini / OpenCode**: wait until the human confirms submission and the agent emits `ANNOTATE_READY` (or compatibility alias `AGENTUI_READY`)
4. Before that submit signal arrives, do not read `/pending`, do not acknowledge annotations, and do not start the fix loop
5. After submit arrives, switch `agentation.submit_gate_status = "submitted"` and record `submit_signal`, `submit_received_at`, and `submitted_annotation_count`
6. **Claude Code (MCP)**: blocking call to `agentation_watch_annotations` (`batchWindowSeconds:10`, `timeoutSeconds:120`)
7. **Codex / Gemini / OpenCode (HTTP)**: polling loop via `GET http://localhost:4747/pending`
8. Process each annotation: `acknowledge` → navigate code via `elementPath` → apply fix → `resolve`
9. `count=0` or timeout → reset the submit gate or finish the sub-phase → **enter STEP 4**
**NEVER: process draft annotations before submit/onSubmit.**
---
### STEP 4: CLEANUP
**Pre-flight (check before entering):**
```bash
# Record checkpoint
python3 -c "
import json,datetime,os,subprocess,fcntl
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d.update({'checkpoint':'cleanup','updated_at':datetime.datetime.utcnow().isoformat()+'Z'})
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "⚠️ Not a git repository — skipping worktree cleanup"
else
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
[[ "$UNCOMMITTED" -gt 0 ]] && echo "⚠️ ${UNCOMMITTED} uncommitted change(s) — recommend commit/stash before cleanup"
fi
```
1. Update `jeo-state.json` `phase` to `"cleanup"`
2. Worktree cleanup:
```bash
bash scripts/worktree-cleanup.sh || git worktree prune
```
3. Update `jeo-state.json` `phase` to `"done"`
---
## 1. Quick Start
> **Source of truth**: `https://github.com/supercent-io/skills-template`
> Local paths like `~/.claude/skills/jeo/` are copies installed via `npx skills add`.
> To update to the latest version, reinstall using the command below.
```bash
# Install JEO (npx skills add — recommended)
npx skills add https://github.com/supercent-io/skills-template --skill jeo
# Full install (all AI tools + all components)
bash scripts/install.sh --all
# Check status
bash scripts/check-status.sh
# Individual AI tool setup
bash scripts/setup-claude.sh # Claude Code plugin + hooks
bash scripts/setup-codex.sh # Codex CLI developer_instructions
bash scripts/setup-gemini.sh # Gemini CLI hooks + GEMINI.md
bash scripts/setup-opencode.sh # OpenCode plugin registration
```
---
## 2. Installed Components
Tools that JEO installs and configures:
| Tool | Description | Install Command |
|------|------|-----------|
| **omc** (oh-my-claudecode) | Claude Code multi-agent orchestration | `/plugin marketplace add https://github.com/Yeachan-Heo/oh-my-claudecode` |
| **omx** | Multi-agent orchestration for OpenCode | `bunx oh-my-opencode setup` |
| **ohmg** | Multi-agent framework for Gemini CLI | `bunx oh-my-ag` |
| **bmad** | BMAD workflow orchestration | Included in skills |
| **ralph** | Self-referential completion loop | Included in omc or install separately |
| **plannotator** | Visual plan/diff review | Auto-installed during PLAN via `bash scripts/ensure-plannotator.sh` (or preinstall with `bash scripts/install.sh --with-plannotator`) |
| **agentation** | UI annotation → agent code fix integration (`annotate` keyword, `agentui` compatibility maintained) | `bash scripts/install.sh --with-agentation` |
| **agent-browser** | Headless browser for AI agents — **primary tool for browser behavior verification** | `npm install -g agent-browser` |
| **playwriter** | Playwright-based browser automation (optional) | `npm install -g playwriter` |
---
## 3. JEO Workflow
### Full Flow
```
jeo "<task>"
│
▼
[1] PLAN (ralph + plannotator)
Draft plan with ralph → visual review with plannotator → Approve/Feedback
│
▼
[2] EXECUTE
├─ team available? → /omc:team N:executor "<task>"
│ staged pipeline: plan→prd→exec→verify→fix
└─ no team? → /bmad /workflow-init → run BMAD steps
│
▼
[3] VERIFY (agent-browser — default behavior)
Verify browser behavior with agent-browser
→ capture snapshot → confirm UI/functionality is working
│
├─ with annotate keyword → [3.3.1] VERIFY_UI (agentation watch loop)
│ agentation_watch_annotations blocking → annotation ack→fix→resolve loop
│
▼
[4] CLEANUP
After all work is done → bash scripts/worktree-cleanup.sh
git worktree prune
```
### 3.1 PLAN Step (ralph + plannotator)
> **Platform note**: The `/ralph` slash command is only available in Claude Code (omc).
> Use the "alternative method" below for Codex/Gemini/OpenCode.
**Claude Code (omc):**
```bash
/ralph "jeo-plan: <task>" --completion-promise="PLAN_APPROVED" --max-iterations=5
```
**Codex / Gemini / OpenCode (alternative):**
```bash
# Session-isolated feedback directory (prevents concurrent run conflicts)
FEEDBACK_DIR=$(python3 -c "import hashlib,os; h=hashlib.md5(os.getcwd().encode()).hexdigest()[:8]; d=f'/tmp/jeo-{h}'; os.makedirs(d,exist_ok=True); print(d)" 2>/dev/null || echo '/tmp')
FEEDBACK_FILE="${FEEDBACK_DIR}/plannotator_feedback.txt"
# 1. Write plan.md directly, then review with plannotator (blocking — no &)
PLANNOTATOR_RUNTIME_HOME="${FEEDBACK_DIR}/.plannotator"
mkdir -p "$PLANNOTATOR_RUNTIME_HOME"
touch /tmp/jeo-plannotator-direct.lock && python3 -c "
import json
print(json.dumps({'tool_input': {'plan': open('plan.md').read(), 'permission_mode': 'acceptEdits'}}))
" | env HOME="$PLANNOTATOR_RUNTIME_HOME" PLANNOTATOR_HOME="$PLANNOTATOR_RUNTIME_HOME" plannotator > "$FEEDBACK_FILE" 2>&1
# ↑ Run without &: waits until user clicks Approve/Send Feedback in browser
# 2. Check result and branch
if python3 -c "
import json, sys
try:
d = json.load(open('$FEEDBACK_FILE'))
sys.exit(0 if d.get('approved') is True else 1)
except Exception:
sys.exit(1)
" 2>/dev/null; then
echo "PLAN_APPROVED" # → enter EXECUTE step
else
echo "PLAN_FEEDBACK" # → read \"$FEEDBACK_FILE\", replan, repeat above
fi
```
> **Important**: Do not run with `&` (background). Must run blocking to receive user feedback.
Common flow:
- Generate plan document (`plan.md`)
- Run plannotator blocking → browser UI opens automatically
- Review plan in browser → Approve or Send Feedback
- Approve (`"approved":true`) → enter [2] EXECUTE step
- Feedback → read `/tmp/plannotator_feedback.txt` annotations and replan (loop)
- **exit 32 (sandbox/CI — Conversation Approval Mode)**:
1. Output full `plan.md` content to user
2. Ask: "⚠️ plannotator UI unavailable. Reply 'approve' to proceed or provide feedback."
3. **WAIT for user response — do NOT proceed to EXECUTE**
4. On approval → update `jeo-state.json` `plan_approved=true, plan_gate_status="manual_approved"` → EXECUTE
5. On feedback → revise `plan.md`, retry loop, repeat
**Claude Code manual run:**
```
Shift+Tab×2 → enter plan mode → plannotator runs automatically when plan is complete
```
### 3.2 EXECUTE Step
**When team is available (Claude Code + omc):**
```bash
/omc:team 3:executor "jeo-exec: <task based on approved plan>"
```
- staged pipeline: team-plan → team-prd → team-exec → team-verify → team-fix
- Maximize speed with parallel agent execution
**When Claude Code team mode is unavailable:**
```bash
echo "❌ JEO requires /omc:team in Claude Code. Run bash scripts/setup-claude.sh, restart Claude Code, then retry."
exit 1
```
- Do not degrade to single-agent mode
**When team is unavailable (BMAD fallback — Codex / Gemini / OpenCode):**
```bash
/workflow-init # Initialize BMAD workflow
/workflow-status # Check current step
```
- Proceed in order: Analysis → Planning → Solutioning → Implementation
- Review documents with plannotator after each step completes
### 3.3 VERIFY Step (agent-browser — default behavior)
When browser-based functionality is present, verify behavior with `agent-browser`.
```bash
# Capture snapshot from the URL where the app is running
agent-browser snapshot http://localhost:3000
# Check specific elements (accessibility tree ref method)
agent-browser snapshot http://localhost:3000 -i
# → check element state using @eN ref numbers
# Save screenshot
agent-browser screenshot http://localhost:3000 -o verify.png
```
> **Default behavior**: Automatically runs the agent-browser verification step when browser-related work is complete.
> Backend/CLI tasks without a browser UI skip this step.
### 3.3.1 VERIFY_UI Step (annotate — agentation watch loop)
Runs the agentation watch loop when the `annotate` keyword is detected. (The `agentui` keyword is also supported for backward compatibility.)
This follows the same pattern as plannotator operating in `planui` / `ExitPlanMode`.
**Prerequisites (auto-resolved by pre-flight):**
1. `npx agentation-mcp server` (HTTP :4747) is running — **auto-started if not running**
2. `agentation` npm package installed in the project — **auto-installed if absent**
3. `<Agentation endpoint="http://localhost:4747" />` is mounted in the app — **auto-injected into entry point if absent**
**Pre-flight Check (required before entering — common to all platforms):**
```bash
# ── Step 1: Auto-start agentation-mcp server if not running ─────────────────
JEO_AGENTATION_ENDPOINT="${JEO_AGENTATION_ENDPOINT:-http://localhost:4747}"
JEO_AGENTATION_PORT="${JEO_AGENTATION_PORT:-4747}"
if ! curl -sf --connect-timeout 2 "${JEO_AGENTATION_ENDPOINT}/health" >/dev/null 2>&1; then
echo "[JEO][ANNOTATE] agentation-mcp not running — attempting auto-start on port ${JEO_AGENTATION_PORT}..."
if command -v npx >/dev/null 2>&1; then
npx -y agentation-mcp server --port "${JEO_AGENTATION_PORT}" >/tmp/agentation-mcp.log 2>&1 &
AGENTATION_MCP_PID=$!
echo "[JEO][ANNOTATE] started agentation-mcp (PID ${AGENTATION_MCP_PID})"
# Wait up to 8 seconds for server to become healthy
for i in $(seq 1 8); do
sleep 1
if curl -sf --connect-timeout 1 "${JEO_AGENTATION_ENDPOINT}/health" >/dev/null 2>&1; then
echo "[JEO][ANNOTATE] ✅ agentation-mcp server ready"
break
fi
done
if ! curl -sf --connect-timeout 2 "${JEO_AGENTATION_ENDPOINT}/health" >/dev/null 2>&1; then
echo "[JEO][ANNOTATE] ⚠️ agentation-mcp failed to start — skipping VERIFY_UI"
python3 -c "
import json,os,subprocess,fcntl,time
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d['last_error']='agentation-mcp failed to start; VERIFY_UI skipped'
d['updated_at']=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
# Proceed to STEP 4 CLEANUP (no exit 1 — graceful skip)
fi
else
echo "[JEO][ANNOTATE] ⚠️ npx not found — cannot auto-start agentation-mcp. Skipping VERIFY_UI."
python3 -c "
import json,os,subprocess,fcntl,time
try:
root=subprocess.check_output(['git','rev-parse','--show-toplevel'],stderr=subprocess.DEVNULL).decode().strip()
except:
root=os.getcwd()
f=os.path.join(root,'.omc/state/jeo-state.json')
if os.path.exists(f):
with open(f,'r+') as fh:
fcntl.flock(fh,fcntl.LOCK_EX)
try:
d=json.load(fh)
d['last_error']='npx not found; agentation-mcp auto-start skipped; VERIFY_UI skipped'
d['updated_at']=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
fh.seek(0); json.dump(d,fh,ensure_ascii=False,indent=2); fh.truncate()
finally:
fcntl.flock(fh,fcntl.LOCK_UN)
" 2>/dev/null || true
# Proceed to STEP 4 CLEANUP (no exit 1 — graceful skip)
fi
fi
# ── Step 2: Auto-install agentation package + inject <Agentation> if needed ──
# Only runs when server is confirmed healthy
if curl -sf --connect-timeout 2 "${JEO_AGENTATION_ENDPOINT}/health" >/dev/null 2>&1; then
SESSIONS=$(curl -sf "${JEO_AGENTATION_ENDPOINT}/sessions" 2>/dev/null || echo "[]")
S_COUNT=$(echo "$SESSIONS" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
if [ "$S_COUNT" -eq 0 ]; then
echo "[JEO][ANNOTATE] No active sessions — running ensure-agentation.sh to install package and inject component..."
# Locate ensure-agentation.sh (try common installation paths)
ENSURE_SCRIPT=""
for candidate in \
"$(dirname "${BASH_SOURCE[0]:-$0}")/ensure-agentation.sh" \
"$HOME/.claude/skills/jeo/scripts/ensure-agentation.sh" \
"$HOME/.agent-skills/jeo/scripts/ensure-agentation.sh" \
"$HOME/.codex/skills/jeo/scripts/ensure-agentation.sh"; do
if [[ -f "$candidate" ]]; then
ENSURE_SCRIPT="$candidate"
break
fi
done
if [[ -n "$ENSURE_SCRIPT" ]]; then
ENSURE_EXIT=0
bash "$ENSURE_SCRIPT" \
--project-dir "${PROJECT_DIR:-$PWD}" \
--endpoint "${JEO_AGENTATION_ENDPOINT}" || ENSURE_EXIT=$?
if [ "$ENSURE_EXIT" -eq 0 ]; then
echo "[JEO][ANNOTATE] ensure-agentation completed — waiting up to 15s for browser to reconnect..."
# Wait for dev server hot-reload and browser reconnection
for i in $(seq 1 15); do
sleep 1
NEW_S_COUNT=$(curl -sf "${JEO_AGENTATION_ENDPOINT}/sessions" 2>/dev/null | \
python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
if [ "$NEW_S_COUNT" -gt 0 ]; then
echo "[JEO][ANNOTATE] ✅ Browser session established (${NEW_S_COUNT} session(s))"
S_COUNT=$NEW_S_COUNT
break
fi
done
if [ "$S_COUNT" -eq 0 ]; then
echo "[JEO][ANNOTATE] ⚠️ Component injected but no browser session yet."
echo " → Refresh the browser at your app URL, then re-run annotate."
fi
elif [ "$ENSURE_EXIT" -eq 2 ]; then
echo "[JEO][ANNOTATE] ℹ️ Not a Node.js project — mount <Agentation endpoint='${JEO_AGENTATION_ENDPOINT}' /> manually."
else
echo "[JEO][ANNOTATE] ⚠️ ensure-agentation.sh failed (exit $ENSURE_EXIT) — mount <Agentation endpoint='${JEO_AGENTATION_ENDPOINT}' /> manually."
fi
else
echo "[JEO][ANNOTATE] ⚠️ ensure-agentation.sh not found — mount <Agentation endpoint='${JEO_AGENTATION_ENDPOINT}' /> manually."
fi
fi
echo "[JEO][ANNOTATE] ✅ agentation ready — server OK, ${S_COUNT} session(s)"
fi
```
> After passing pre-flight (`else` branch), update jeo-state.json `phase` to `"verify_ui"`, set `agentation.active` to `true`, and set `agentation.submit_gate_status` to `"waiting_for_submit"`.
> Do not call `/pending` yet. Draft annotations are not actionable until the user explicitly submits them.
**Claude Code (direct MCP tool call):**
```
# annotate keyword detected (or agentui — backward compatible)
# 1. wait for UserPromptSubmit after the user clicks Send Annotations / onSubmit
# 2. the JEO submit-gate hook records submit_gate_status="submitted"
# 3. only then run the blocking agentation watch loop
#
# batchWindowSeconds:10 — receive annotations in 10-second batches
# timeoutSeconds:120 — auto-exit after 120 seconds with no annotations
#
# Per-annotation processing loop:
# 1. agentation_acknowledge_annotation({id}) — show 'processing' in UI
# 2. navigate code via annotation.elementPath (CSS selector) → apply fix
# 3. agentation_resolve_annotation({id, summary}) — mark 'done' + save summary
#
# Loop ends when annotation count=0 or timeout
```
> **Important**: `agentation_watch_annotations` is a blocking call. Do not run with `&` background.
> Same as plannotator's `approved:true` loop: annotation count=0 or timeout = completion signal.
> `annotate` is the primary keyword. `agentui` is a backward-compatible alias and behaves identically.
**Codex / Gemini / OpenCode (HTTP REST API fallback):**
```bash
START_TIME=$(date +%s)
TIMEOUT_SECONDS=120
# Required gate: do not enter the loop until the human has clicked Send Annotations
# and the platform has opened agentation.submit_gate_status="submitted".
while true; do
# Timeout check
NOW=$(date +%s)
ELAPSED=$((NOW - START_TIME))
if [ $ELAPSED -ge $TIMEOUT_SECONDS ]; then
echo "[JEO] agentation polling timeout (${TIMEOUT_SECONDS}s) — some annotations may remain unresolved"
break
fi
SUBMIT_GATE=$(python3 -c "
import json
try:
print(json.load(open('.omc/state/jeo-state.json')).get('agentation', {}).get('submit_gate_status', 'idle'))
except Exception:
print('idle')
" 2>/dev/null || echo "idle")
if [ "$SUBMIT_GATE" != "submitted" ]; then
sleep 2
continue
fi
COUNT=$(curl -sf --connect-timeout 3 --max-time 5 http://localhost:4747/pending 2>/dev/null | python3 -c "import sys,json; data=sys.stdin.read(); d=json.loads(data) if data.strip() else {}; print(d.get('count', len(d.get('annotations', [])) if isinstance(d, dict) else 0))" 2>/dev/null || echo 0)
[ "$COUNT" -eq 0 ] && break
# Process each annotation:
# a) Acknowledge (show as in-progress)
curl -X PATCH http://localhost:4747/annotations/<id> \
-H 'Content-Type: application/json' \
-d '{"status": "acknowledged"}'
# b) Navigate code via elementPath (CSS selector) → apply fix
# c) Resolve (mark done + fix summary)
curl -X PATCH http://localhost:4747/annotations/<id> \
-H 'Content-Type: application/json' \
-d '{"status": "resolved", "resolution": "<fix summary>"}'
sleep 3
done
```
### 3.4 CLEANUP Step (automatic worktree cleanup)
```bash
# Runs automatically after all work is complete
bash scripts/worktree-cleanup.sh
# Individual commands
git worktree list # List current worktrees
git worktree prune # Clean up worktrees for deleted branches
bash scripts/worktree-cleanup.sh --force # Force cleanup including dirty worktrees
```
> Default run removes only clean extra worktrees; worktrees with changes are left with a warning.
> Use `--force` only after review.
---
## 4. Platform Plugin Configuration
### 4.1 Claude Code
```bash
# Automatic setup
bash scripts/setup-claude.sh
# Or manually:
/plugin marketplace add https://github.com/Yeachan-Heo/oh-my-claudecode
/plugin install oh-my-claudecode
/omc:omc-setup
# Add plannotator hook
bash .agent-skills/plannotator/scripts/setup-hook.sh
```
**Config file**: `~/.claude/settings.json`
```json
{
"hooks": {
"PermissionRequest": [{
"matcher": "ExitPlanMode",
"hooks": [{
"type": "command",
"command": "python3 ~/.agent-skills/jeo/scripts/claude-plan-gate.py",
"timeout": 1800
}]
}]
}
}
```
**agentation MCP config** (`~/.claude/settings.json` or `.claude/mcp.json`):
```json
{
"mcpServers": {
"agentation": {
"command": "npx",
"args": ["-y", "agentation-mcp", "server"]
}
},
"hooks": {
"UserPromptSubmit": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "python3 ~/.agent-skills/jeo/scripts/claude-agentation-submit-hook.py",
"timeout": 300
}]
}]
}
}
```
`bash ~/.agent-skills/jeo/scripts/setup-claude.sh` migrates stale `~/.agent-skills/omg/...` hook paths to the active JEO install and installs compatibility shims for legacy Claude setups.
### 4.2 Codex CLI
```bash
# Automatic setup
bash scripts/setup-codex.sh
# What gets configured:
# - developer_instructions: ~/.codex/config.toml
# - prompt file: ~/.codex/prompts/jeo.md
# - notify hook: ~/.codex/hooks/jeo-notify.py
# - [tui] notifications: agent-turn-complete
```
**agentation MCP config** (`~/.codex/config.toml`):
```toml
[mcp_servers.agentation]
command = "npx"
args = ["-y", "agentation-mcp", "server"]
```
**notify hook** (`~/.codex/hooks/jeo-notify.py`):
- Detects `PLAN_READY` signal in `last-assistant-message` when agent turn completes
- Confirms `plan.md` exists, compares the current hash against `last_reviewed_plan_hash`, and skips the gate when the plan was already reviewed
- Saves result to `/tmp/plannotator_feedback.txt`
- Detects `ANNOTATE_READY` signal (or backward-compatible `AGENTUI_READY`) only in `verify_ui`
- Opens `agentation.submit_gate_status="submitted"` first, then polls `http://localhost:4747/pending`
**`~/.codex/config.toml`** config:
```toml
developer_instructions = """
# JEO Orchestration Workflow
# ...
"""
notify = ["python3", "~/.codex/hooks/jeo-notify.py"]
[tui]
notifications = ["agent-turn-complete"]
notification_method = "osc9"
```
> `developer_instructions` must be a **top-level string**.
> Writing it as a `[developer_instructions]` table may cause Codex to fail on startup with `invalid type: map, expected a string`.
> `notify` and `[tui].notifications` must also be set correctly for the PLAN/ANNOTATE follow-up loop to actually work.
Using in Codex:
```bash
/prompts:jeo # Activate JEO workflow
# Agent writes plan.md and outputs "PLAN_READY" → notify hook runs automatically
```
### 4.3 Gemini CLI
```bash
# Automatic setup
bash scripts/setup-gemini.sh
# What gets configured:
# - AfterAgent backup hook: ~/.gemini/hooks/jeo-plannotator.sh
# - Instructions (MANDATORY loop): ~/.gemini/GEMINI.md
```
**Key principle**: The agent must call plannotator **directly in blocking mode** to receive feedback in the same turn.
The AfterAgent hook serves only as a safety net (runs after turn ends → injected in next turn).
**AfterAgent backup hook** (`~/.gemini/settings.json`):
```json
{
"hooks": {
"AfterAgent": [{
"matcher": "",
"hooks": [{
"name": "plannotator-review",
"type": "command",
"command": "bash ~/.gemini/hooks/jeo-plannotator.sh",
"description": "Run plannotator when plan.md is detected (AfterAgent backup)"
}]
}]
}
}
```
**PLAN instructions added to GEMINI.md (mandatory loop)**:
```
1. Write plan.md
2. Run plannotator blocking (no &) → /tmp/plannotator_feedback.txt
3. approved=true → EXECUTE / Not approved → revise and repeat step 2
NEVER proceed to EXECUTE without approved=true.
```
**agentation MCP config** (`~/.gemini/settings.json`):
```json
{
"mcpServers": {
"agentation": {
"command": "npx",
"args": ["-y", "agentation-mcp", "server"]
}
}
}
```
> **Note**: Gemini CLI hook events use `BeforeTool` and `AfterAgent`.
> `ExitPlanMode` is a Claude Code-only hook.
> [Hooks Official Guide](https://developers.googleblog.com/tailor-gemini-cli-to-your-workflow-with-hooks/)
### 4.4 OpenCode
```bash
# Automatic setup
bash scripts/setup-opencode.sh
# Added to opencode.json:
# "@plannotator/opencode@latest" plugin
# "@oh-my-opencode/opencode@latest" plugin (omx)
```
OpenCode slash commands:
- `/jeo-plan` — plan with ralph + plannotator
- `/jeo-exec` — execute with team/bmad
- `/jeo-annotate` — start agentation watch loop (annotate; `/jeo-agentui` is a deprecated alias)
- `/jeo-cleanup` — worktree cleanup
**plannotator integration** (MANDATORY blocking loop):
```bash
# Write plan.md then run PLAN gate (no &) — receive feedback in same turn
bash scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
# - Must wait until approve/feedback is received
# - Auto-restart on session exit (up to 3 times)
# - After 3 exits, confirm with user whether to abort or resume
# - exit 32 if localhost bind unavailable (replace with manual gate in TTY)
# Branch based on result
# approved=true → enter EXECUTE
# not approved → apply feedback, revise plan.md → repeat above
```
**agentation MCP config** (`opencode.json`):
```json
{
"mcp": {
"agentation": {
"type": "local",
"command": ["npx", "-y", "agentation-mcp", "server"]
}
}
}
```
---
## 5. Memory & State
JEO stores state at the following paths:
```
{worktree}/.omc/state/jeo-state.json # JEO execution state
{worktree}/.omc/plans/jeo-plan.md # Approved plan
{worktree}/.omc/logs/jeo-*.log # Execution logs
```
**State file structure:**
```json
{
"mode": "jeo",
"phase": "plan|execute|verify|verify_ui|cleanup|done",
"session_id": "<uuid>",
"task": "current task description",
"plan_approved": true,
"plan_gate_status": "pending|approved|feedback_required|infrastructure_blocked|manual_approved",
"plan_current_hash": "<sha256 or null>",
"last_reviewed_plan_hash": "<sha256 or null>",
"last_reviewed_plan_at": "2026-02-24T00:00:00Z",
"plan_review_method": "plannotator|manual|null",
"team_available": true,
"retry_count": 0,
"last_error": null,
"checkpoint": "plan|execute|verify|verify_ui|cleanup",
"created_at": "2026-02-24T00:00:00Z",
"updated_at": "2026-02-24T00:00:00Z",
"agentation": {
"active": false,
"session_id": null,
"keyword_used": null,
"submit_gate_status": "idle|waiting_for_submit|submitted",
"submit_signal": "claude-user-prompt-submit|codex-notify|gemini-manual|null",
"submit_received_at": "2026-02-24T00:00:00Z",
"submitted_annotation_count": 0,
"started_at": null,
"timeout_seconds": 120,
"annotations": {
"total": 0, "acknowledged": 0, "resolved": 0, "dismissed": 0, "pending": 0
},
"completed_at": null,
"exit_reason": null
}
}
```
> **agentation fields**: `active` — whether the watch loop is running (used as hook guard), `session_id` — for resuming,
> `submit_gate_status` — prevents processing draft annotations before submit/onSubmit, `submit_signal` — which platform opened the gate,
> `submit_received_at` / `submitted_annotation_count` — audit trail for the submitted batch, `exit_reason` — `"all_resolved"` | `"timeout"` | `"user_cancelled"` | `"error"`
>
> **dismissed annotations**: When a user dismisses an annotation in the agentation UI (status becomes `"dismissed"`),
> the agent should skip code changes for that annotation, increment `annotations.dismissed`, and continue to the next pending annotation.
> Dismissed annotations are counted but not acted upon. The watch loop exits normally when `pending == 0` (resolved + dismissed covers all).
>
> **`plan_review_method`**: set to `"plannotator"` when approved via UI, `"manual"` when approved via TTY fallback gate.
>
> **`cleanup_completed`**: set to `true` by `worktree-cleanup.sh` after successful worktree prune.
> **Error recovery fields**:
> - `retry_count` — number of retries after an error. Increments +1 on each pre-flight failure. Ask user to confirm if `>= 3`.
> - `last_error` — most recent error message. Used to identify the cause on restart.
> - `checkpoint` — last phase that was started. Resume from this phase on restart (`plan|execute|verify|cleanup`).
**Checkpoint-based resume flow:**
```bash
# Check checkpoint on restart
python3 -c "
import json, os, subprocess
try:
root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], stderr=subprocess.DEVNULL).decode().strip()
except:
root = os.getcwd()
f = os.path.join(root, '.omc/state/jeo-state.json')
if os.path.exists(f):
d=json.load(open(f))
cp=d.get('checkpoint')
err=d.get('last_error')
rc=d.get('retry_count',0)
print(f'Resume from: {cp or \"beginning\"}')
if err: print(f'Previous error ({rc} time(s)): {err}')
if rc >= 3: print('⚠️ Retry count exceeded 3 — user confirmation required')
"
```
Restore after restart:
```bash
# Check status and resume
bash scripts/check-status.sh --resume
```
---
## 6. Recommended Workflow
```
# Step 1: Install (once)
bash scripts/install.sh --all
bash scripts/check-status.sh
# Step 2: Start work
jeo "<task description>" # Activate with keyword
# Or in Claude: Shift+Tab×2 → plan mode
# Step 3: Review plan with plannotator
# Approve or Send Feedback in browser UI
# Step 4: Automatic execution
# team or bmad handles the work
# Step 5: Cleanup after completion
bash scripts/worktree-cleanup.sh
```
---
## 7. Best Practices
1. **Plan first**: always review the plan with ralph+plannotator before executing (catches wrong approaches early)
2. **Team first**: omc team mode is most efficient in Claude Code
3. **bmad fallback**: use BMAD in environments without team (Codex, Gemini)
4. **Worktree cleanup**: run `worktree-cleanup.sh` immediately after work completes (prevents branch pollution)
5. **State persistence**: use `.omc/state/jeo-state.json` to maintain state across sessions
6. **annotate**: use the `annotate` keyword to run the agentation watch loop for complex UI changes (precise code changes via CSS selector). `agentui` is a backward-compatible alias.
---
## 8. Troubleshooting
| Issue | Solution |
|------|------|
| plannotator not running | JEO first auto-runs `bash scripts/ensure-plannotator.sh`; if it still fails, run `bash .agent-skills/plannotator/scripts/check-status.sh` |
| plannotator not opening in Claude Code | plannotator is hook-only. Do NOT call it via MCP or CLI. Use `EnterPlanMode` → write plan → `ExitPlanMode`; the hook fires automatically. Verify hook is set: `cat ~/.claude/settings.json \| python3 -c "import sys,json;h=json.load(sys.stdin).get('hooks',{});print(h.get('PermissionRequest','missing'))"` |
| plannotator feedback not received | Remove `&` background execution → run blocking, then check `/tmp/plannotator_feedback.txt` (Codex/Gemini/OpenCode only) |
| Same plan is repeatedly re-reviewed in Codex | Compare `last_reviewed_plan_hash` in `jeo-state.json` with the current `plan.md` hash. If they match and `plan_gate_status` is terminal, do not re-run |
| Codex startup failure (`invalid type: map, expected a string`) | Re-run `bash scripts/setup-codex.sh` and confirm `developer_instructions` in `~/.codex/config.toml` is a top-level string |
| Gemini feedback loop missing | Add blocking direct call instruction to `~/.gemini/GEMINI.md` |
| worktree conflict | `git worktree prune && git worktree list` |
| team mode not working | JEO requires team mode in Claude Code. Run `bash scripts/setup-claude.sh`, restart Claude Code, and verify `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` before retrying |
| omc install failed | Run `/omc:omc-doctor` |
| agent-browser error | Check `agent-browser --version` |
| annotate (agentation) not opening | Check `curl http://localhost:4747/health` and `curl http://localhost:4747/sessions`. JEO waits for explicit submit/onSubmit before polling `/pending` |
| annotation not reflected in code | Confirm `summary` field is present when calling `agentation_resolve_annotation` |
| `agentui` keyword not activating | Use the `annotate` keyword (new). `agentui` is a deprecated alias but still works. |
| MCP tool not registered (Codex/Gemini) | Re-run `bash scripts/setup-codex.sh` / `setup-gemini.sh` |
---
## 9. References
- [oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode) — Claude Code multi-agent
- [plannotator](https://plannotator.ai) — visual plan/diff review
- [BMAD Method](https://github.com/bmad-dev/BMAD-METHOD) — structured AI development workflow
- [Agent Skills Spec](https://agentskills.io/specification) — skill format specification
- [agentation](https://github.com/benjitaylor/agentation) — UI annotation → agent code fix integration (`annotate`; `agentui` backward compatible)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/ensure-agentation.sh
```bash
#!/usr/bin/env bash
# JEO helper — ensures agentation npm package is installed and <Agentation> is mounted
# in the project's React entry point before VERIFY_UI runs.
#
# Usage: bash ensure-agentation.sh [--project-dir <path>] [--endpoint <url>] [--quiet] [--dry-run]
# Exit codes:
# 0 — agentation ready (already installed, or just installed+injected)
# 1 — could not install or inject (non-fatal: VERIFY_UI should graceful-skip)
# 2 — package.json not found (not a Node.js project — skip silently)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── defaults ──────────────────────────────────────────────────────────────────
PROJECT_DIR="${PWD}"
ENDPOINT="http://localhost:4747"
QUIET=false
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--project-dir=*) PROJECT_DIR="${arg#*=}" ;;
--endpoint=*) ENDPOINT="${arg#*=}" ;;
--quiet) QUIET=true ;;
--dry-run) DRY_RUN=true ;;
-h|--help)
echo "Usage: bash ensure-agentation.sh [--project-dir <path>] [--endpoint <url>] [--quiet] [--dry-run]"
echo "Ensures agentation npm package is installed and <Agentation> is mounted in the React entry point."
exit 0
;;
esac
done
log() { $QUIET || echo "[JEO][ANNOTATE] $*"; }
warn() { echo "[JEO][ANNOTATE] ⚠️ $*" >&2; }
# ── locate package.json ───────────────────────────────────────────────────────
find_package_json() {
local dir="$1"
# Check project dir and common sub-dirs
for candidate in "$dir" "$dir/src" "$dir/app" "$dir/frontend" "$dir/client" "$dir/web"; do
if [[ -f "$candidate/package.json" ]]; then
echo "$candidate"
return 0
fi
done
return 1
}
PKG_DIR=""
if ! PKG_DIR=$(find_package_json "$PROJECT_DIR"); then
log "No package.json found — not a Node.js project, skipping agentation setup."
exit 2
fi
log "Found package.json at: $PKG_DIR"
# ── check if agentation already in dependencies ───────────────────────────────
is_package_installed() {
python3 - "$PKG_DIR/package.json" <<'EOF'
import sys, json
try:
d = json.load(open(sys.argv[1]))
deps = {**d.get('dependencies', {}), **d.get('devDependencies', {})}
sys.exit(0 if 'agentation' in deps else 1)
except Exception:
sys.exit(1)
EOF
}
# ── detect package manager ────────────────────────────────────────────────────
detect_package_manager() {
if [[ -f "$PKG_DIR/bun.lockb" ]] || [[ -f "$PKG_DIR/bun.lock" ]]; then
echo "bun"
elif [[ -f "$PKG_DIR/pnpm-lock.yaml" ]]; then
echo "pnpm"
elif [[ -f "$PKG_DIR/yarn.lock" ]]; then
echo "yarn"
else
echo "npm"
fi
}
# ── install agentation package ────────────────────────────────────────────────
install_agentation() {
local pm="$1"
log "Installing agentation with $pm..."
if $DRY_RUN; then
log "[dry-run] would run: cd $PKG_DIR && $pm add agentation -D (or equivalent)"
return 0
fi
case "$pm" in
bun) (cd "$PKG_DIR" && bun add agentation -D) ;;
pnpm) (cd "$PKG_DIR" && pnpm add agentation -D) ;;
yarn) (cd "$PKG_DIR" && yarn add agentation --dev) ;;
npm) (cd "$PKG_DIR" && npm install agentation --save-dev --legacy-peer-deps) ;;
esac
}
# ── find React entry point ────────────────────────────────────────────────────
find_entry_point() {
local dir="$PROJECT_DIR"
local candidates=(
# Vite / CRA
"$dir/src/main.tsx"
"$dir/src/main.jsx"
"$dir/main.tsx"
"$dir/main.jsx"
# Next.js App Router
"$dir/app/layout.tsx"
"$dir/app/layout.jsx"
"$dir/src/app/layout.tsx"
"$dir/src/app/layout.jsx"
# Next.js Pages Router
"$dir/pages/_app.tsx"
"$dir/pages/_app.jsx"
"$dir/src/pages/_app.tsx"
"$dir/src/pages/_app.jsx"
)
for f in "${candidates[@]}"; do
if [[ -f "$f" ]]; then
echo "$f"
return 0
fi
done
return 1
}
# ── inject <Agentation> into entry point ─────────────────────────────────────
inject_agentation() {
local entry="$1"
local endpoint="$2"
# Already injected?
if grep -q 'from.*agentation' "$entry" 2>/dev/null; then
log "<Agentation> already imported in $entry"
return 0
fi
log "Injecting <Agentation endpoint=\"$endpoint\" /> into $entry"
if $DRY_RUN; then
log "[dry-run] would inject into $entry"
return 0
fi
# Detect framework pattern from filename
local filename
filename="$(basename "$entry")"
local framework="vite"
[[ "$filename" == "layout.tsx" || "$filename" == "layout.jsx" ]] && framework="next-app"
[[ "$filename" == "_app.tsx" || "$filename" == "_app.jsx" ]] && framework="next-pages"
python3 - "$entry" "$endpoint" "$framework" <<'PYEOF'
import sys, re
entry_path = sys.argv[1]
endpoint = sys.argv[2]
framework = sys.argv[3]
with open(entry_path, 'r', encoding='utf-8') as f:
content = f.read()
IMPORT_LINE = "import { Agentation } from 'agentation';"
COMPONENT_DEV = f"{{process.env.NODE_ENV === 'development' && <Agentation endpoint=\"{endpoint}\" />}}"
# Already injected guard (double-check)
if 'agentation' in content.lower():
print(f"[JEO][ANNOTATE] agentation already present in {entry_path}", file=sys.stderr)
sys.exit(0)
# --- Add import line ---
# Insert after the last existing import statement
import_match = list(re.finditer(r'^import\s.+;?\s*$', content, re.MULTILINE))
if import_match:
last_import = import_match[-1]
insert_pos = last_import.end()
content = content[:insert_pos] + '\n' + IMPORT_LINE + content[insert_pos:]
else:
# No imports found; prepend
content = IMPORT_LINE + '\n' + content
# --- Inject component ---
if framework == 'next-app':
# Next.js App Router layout.tsx: inject before closing </body> or </html>, or before return closing
if '</body>' in content:
content = content.replace('</body>', f' {COMPONENT_DEV}\n </body>', 1)
elif 'return (' in content:
# Inject before last closing paren of return block
last_paren = content.rfind('\n)')
if last_paren >= 0:
content = content[:last_paren] + f'\n {COMPONENT_DEV}' + content[last_paren:]
elif framework == 'next-pages':
# Next.js Pages _app: inject inside Component rendering
if '<Component' in content:
content = re.sub(
r'(<Component\s[^/]*/?>)',
r'\1\n ' + COMPONENT_DEV,
content,
count=1
)
elif 'return (' in content:
last_paren = content.rfind('\n )')
if last_paren >= 0:
content = content[:last_paren] + f'\n {COMPONENT_DEV}' + content[last_paren:]
else:
# Vite / CRA: inject inside the root render call
# Strategy 1: inject before ReactDOM.createRoot closing render call's last closing tag
# Look for </StrictMode>, </React.StrictMode>, or the outermost JSX closing tag
strict_match = re.search(r'(</(?:React\.)?StrictMode>)', content)
if strict_match:
pos = strict_match.start()
content = content[:pos] + f' {COMPONENT_DEV}\n ' + content[pos:]
else:
# Strategy 2: find render(<...>) and inject inside
render_match = re.search(r'(\.render\s*\(\s*<)([^)]+?)(\s*>\s*\))', content, re.DOTALL)
if render_match:
inner = render_match.group(2)
new_inner = inner + f'\n {COMPONENT_DEV}'
content = content[:render_match.start(2)] + new_inner + content[render_match.end(2):]
with open(entry_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"[JEO][ANNOTATE] ✅ Injected <Agentation endpoint=\"{endpoint}\" /> into {entry_path}")
PYEOF
}
# ── main ──────────────────────────────────────────────────────────────────────
main() {
local pm
pm=$(detect_package_manager)
log "Detected package manager: $pm"
# 1. Install package if needed
if is_package_installed; then
log "agentation package already in $PKG_DIR/package.json"
else
log "agentation package not found — installing..."
if ! install_agentation "$pm"; then
warn "Failed to install agentation package. Run manually: cd $PKG_DIR && $pm add agentation -D"
exit 1
fi
log "agentation package installed successfully."
fi
# 2. Find entry point and inject component
local entry=""
if ! entry=$(find_entry_point); then
warn "Could not find React entry point (main.jsx, app/layout.tsx, pages/_app.tsx). Mount <Agentation endpoint=\"$ENDPOINT\" /> manually."
exit 1
fi
inject_agentation "$entry" "$ENDPOINT"
log "Entry point: $entry — injection complete."
exit 0
}
main
```
### scripts/worktree-cleanup.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Worktree Cleanup Script
# Removes stale git worktrees after task completion
# Usage: bash worktree-cleanup.sh [--force] [--dry-run] [--list]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
err() { echo -e "${RED}✗${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false; FORCE=false; LIST_ONLY=false
for arg in "$@"; do
case $arg in
--dry-run) DRY_RUN=true ;;
--force)
FORCE=true
warn "WARNING: --force removes ALL non-main worktrees."
warn " Active feature branches in separate worktrees will also be removed."
warn " Press Ctrl+C within 5 seconds to cancel..."
sleep 5
;;
--list) LIST_ONLY=true ;;
esac
done
echo ""
echo "JEO — Worktree Cleanup"
echo "====================="
# ── Check git repo ─────────────────────────────────────────────────────────────
if ! git rev-parse --git-dir >/dev/null 2>&1; then
err "Not a git repository. Run from project root."
exit 1
fi
GIT_ROOT=$(git rev-parse --show-toplevel)
# ── List all worktrees ─────────────────────────────────────────────────────────
info "Current worktrees:"
git worktree list
echo ""
# ── Identify stale worktrees ──────────────────────────────────────────────────
MAIN_WORKTREE=$(git worktree list | head -1 | awk '{print $1}')
WORKTREES_TO_REMOVE=()
DIRTY_WORKTREES=()
while IFS= read -r line; do
WORKTREE_PATH=$(echo "$line" | awk '{print $1}')
# Skip main worktree
[[ "$WORKTREE_PATH" == "$MAIN_WORKTREE" ]] && continue
if $FORCE; then
WORKTREES_TO_REMOVE+=("$WORKTREE_PATH")
elif [[ -z "$(git -C "$WORKTREE_PATH" status --porcelain 2>/dev/null)" ]]; then
WORKTREES_TO_REMOVE+=("$WORKTREE_PATH")
else
DIRTY_WORKTREES+=("$WORKTREE_PATH")
fi
done < <(git worktree list | tail -n +2)
# ── List mode ─────────────────────────────────────────────────────────────────
if $LIST_ONLY; then
if [[ ${#WORKTREES_TO_REMOVE[@]} -eq 0 ]] && [[ ${#DIRTY_WORKTREES[@]} -eq 0 ]]; then
ok "No extra worktrees found"
else
if [[ ${#WORKTREES_TO_REMOVE[@]} -gt 0 ]]; then
echo "Worktrees ready to remove:"
for wt in "${WORKTREES_TO_REMOVE[@]}"; do
echo " - $wt"
done
fi
if [[ ${#DIRTY_WORKTREES[@]} -gt 0 ]]; then
echo "Dirty worktrees skipped by default:"
for wt in "${DIRTY_WORKTREES[@]}"; do
echo " - $wt"
done
echo "Use --force to remove dirty worktrees after reviewing them."
fi
fi
exit 0
fi
# ── Remove identified worktrees ───────────────────────────────────────────────
if [[ ${#WORKTREES_TO_REMOVE[@]} -eq 0 ]]; then
ok "No extra worktrees to remove"
else
info "Removing ${#WORKTREES_TO_REMOVE[@]} worktree(s)..."
for WORKTREE_PATH in "${WORKTREES_TO_REMOVE[@]}"; do
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would remove: $WORKTREE_PATH"
else
info "Removing: $WORKTREE_PATH"
if $FORCE; then
if git worktree remove "$WORKTREE_PATH" --force 2>/dev/null; then
ok "Removed: $WORKTREE_PATH"
else
warn "Could not remove $WORKTREE_PATH with --force"
fi
elif git worktree remove "$WORKTREE_PATH" 2>/dev/null; then
ok "Removed: $WORKTREE_PATH"
else
warn "Could not remove $WORKTREE_PATH (dirty or already gone)"
fi
fi
done
fi
if [[ ${#DIRTY_WORKTREES[@]} -gt 0 ]] && ! $FORCE; then
warn "Skipped ${#DIRTY_WORKTREES[@]} dirty worktree(s). Re-run with --force after review."
fi
# ── Prune stale worktree entries ──────────────────────────────────────────────
info "Pruning stale worktree references..."
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would run: git worktree prune"
else
git worktree prune
ok "Stale worktree references pruned"
fi
# ── Deactivate ralphmode (revert project permissionMode) ─────────────────────
_PLAN_GATE_SCRIPT=""
for _candidate in \
"$(dirname "${BASH_SOURCE[0]}")/claude-plan-gate.py" \
"$HOME/.agent-skills/jeo/scripts/claude-plan-gate.py" \
"$HOME/.claude/skills/jeo/scripts/claude-plan-gate.py"; do
if [[ -f "$_candidate" ]]; then
_PLAN_GATE_SCRIPT="$_candidate"
break
fi
done
if [[ -n "$_PLAN_GATE_SCRIPT" ]] && command -v python3 >/dev/null 2>&1; then
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would deactivate ralphmode (restore project permissionMode)"
else
python3 "$_PLAN_GATE_SCRIPT" --deactivate 2>&1 || true
fi
fi
# ── Update JEO state ──────────────────────────────────────────────────────────
STATE_FILE="$GIT_ROOT/.omc/state/jeo-state.json"
if [[ -f "$STATE_FILE" ]] && command -v python3 >/dev/null 2>&1; then
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would update JEO state: phase=cleanup, cleanup_completed=true"
else
STATE_FILE="$STATE_FILE" python3 - <<'PYEOF'
import json, os
state_path = os.environ["STATE_FILE"]
try:
with open(state_path) as f:
state = json.load(f)
state["phase"] = "done"
# state["worktrees"] = [] # 미구현 필드 — SKILL.md 초기 스키마에서도 제거됨. 향후 멀티-worktree 추적 구현 시 복원.
state["cleanup_completed"] = True
with open(state_path, "w") as f:
json.dump(state, f, indent=2)
print("✓ JEO state updated: cleanup complete")
except Exception as e:
print(f"⚠ Could not update state: {e}")
PYEOF
fi
fi
# ── Final summary ─────────────────────────────────────────────────────────────
echo ""
echo "Final worktree list:"
git worktree list
echo ""
ok "Worktree cleanup complete"
echo ""
```
### scripts/install.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Master Installation Script
# Installs and configures: ralph, omc, omx, ohmg, bmad, agent-browser, playwriter, plannotator, agentation
# Usage: bash install.sh [--all] [--with-omc] [--with-plannotator] [--with-browser] [--with-agentation] [--dry-run]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
SKILLS_ROOT="$(dirname "$SKILL_DIR")"
# Colors
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
err() { echo -e "${RED}✗${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false
INSTALL_ALL=false
INSTALL_OMC=false
INSTALL_PLANNOTATOR=false
INSTALL_AGENTATION=false
INSTALL_BROWSER=false
INSTALL_BMAD=false
INSTALL_OMX=false
INSTALL_OHMG=false
for arg in "$@"; do
case $arg in
--all) INSTALL_ALL=true ;;
--with-omc) INSTALL_OMC=true ;;
--with-plannotator) INSTALL_PLANNOTATOR=true ;;
--with-browser) INSTALL_BROWSER=true ;;
--with-bmad) INSTALL_BMAD=true ;;
--with-omx) INSTALL_OMX=true ;;
--with-ohmg) INSTALL_OHMG=true ;;
--with-agentation) INSTALL_AGENTATION=true ;;
--dry-run) DRY_RUN=true ;;
-h|--help)
echo "JEO Master Installer"
echo "Usage: bash install.sh [options]"
echo "Options:"
echo " --all Install all components"
echo " --with-omc Install oh-my-claudecode (Claude Code)"
echo " --with-plannotator Install plannotator CLI"
echo " --with-browser Install agent-browser + playwriter"
echo " --with-bmad Install BMAD orchestrator"
echo " --with-omx Install omx (OpenCode multi-agent)"
echo " --with-ohmg Install ohmg (Gemini multi-agent)"
echo " --with-agentation Install agentation MCP (UI annotation \u2194 agent)"
echo " --dry-run Preview without executing"
exit 0
;;
esac
done
if $INSTALL_ALL; then
INSTALL_OMC=true; INSTALL_PLANNOTATOR=true
INSTALL_BROWSER=true; INSTALL_BMAD=true; INSTALL_OMX=true; INSTALL_OHMG=true; INSTALL_AGENTATION=true
fi
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ JEO Skill — Integrated Orchestration ║"
echo "║ Version 1.1.0 ║"
echo "╚══════════════════════════════════════════╝"
echo ""
run() {
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} $*"
else
eval "$@"
fi
}
# ── Detect OS ─────────────────────────────────────────────────────────────────
OS="unknown"
if [[ "$OSTYPE" == "darwin"* ]]; then OS="macos"
elif [[ "$OSTYPE" == "linux"* ]]; then OS="linux"
elif [[ "$OSTYPE" == "msys"* ]] || [[ "$OSTYPE" == "cygwin"* ]]; then OS="windows"
fi
info "Detected OS: $OS"
# ── Check prerequisites ────────────────────────────────────────────────────────
info "Checking prerequisites..."
MISSING_DEPS=()
if command -v node >/dev/null 2>&1; then
NODE_VER=$(node --version 2>/dev/null | grep -oE '[0-9]+' | head -1)
if [[ -z "$NODE_VER" ]] || [[ "$NODE_VER" -lt 18 ]]; then
MISSING_DEPS+=("node >=18 (현재: $(node --version 2>/dev/null || echo 'unknown'))")
fi
else
MISSING_DEPS+=("node >=18")
fi
command -v npm >/dev/null 2>&1 || MISSING_DEPS+=("npm")
command -v git >/dev/null 2>&1 || MISSING_DEPS+=("git")
command -v bash >/dev/null 2>&1 || MISSING_DEPS+=("bash")
if [[ ${#MISSING_DEPS[@]} -gt 0 ]]; then
err "Missing required dependencies: ${MISSING_DEPS[*]}"
echo "Install them first, then re-run this script."
exit 1
fi
ok "Prerequisites satisfied"
# ── 1. omc (oh-my-claudecode) ──────────────────────────────────────────────────
if $INSTALL_OMC; then
echo ""
info "Installing omc (oh-my-claudecode)..."
if command -v claude >/dev/null 2>&1; then
echo " Run these commands inside Claude Code:"
echo " /plugin marketplace add https://github.com/Yeachan-Heo/oh-my-claudecode"
echo " /plugin install oh-my-claudecode"
echo " /omc:omc-setup"
ok "omc install instructions shown (Claude Code required)"
else
warn "claude CLI not found — install Claude Code first, then run omc setup manually"
fi
fi
# ── 2. omx (oh-my-opencode) ───────────────────────────────────────────────────
if $INSTALL_OMX; then
echo ""
info "Installing omx (oh-my-opencode)..."
if command -v bun >/dev/null 2>&1; then
run "bunx oh-my-opencode setup 2>/dev/null || true"
ok "omx (oh-my-opencode) configured"
elif command -v npx >/dev/null 2>&1; then
run "npx oh-my-opencode setup 2>/dev/null || true"
ok "omx configured via npx"
else
warn "bun/npx not found — install bun first: curl -fsSL https://bun.sh/install | bash"
fi
fi
# ── 3. ohmg (oh-my-ag / Gemini) ───────────────────────────────────────────────
if $INSTALL_OHMG; then
echo ""
info "Installing ohmg (oh-my-ag for Gemini CLI)..."
if command -v bun >/dev/null 2>&1; then
run "bunx oh-my-ag 2>/dev/null || true"
ok "ohmg configured"
else
warn "bun not found — install bun first: curl -fsSL https://bun.sh/install | bash"
fi
fi
# ── 4. plannotator ─────────────────────────────────────────────────────────────
if $INSTALL_PLANNOTATOR; then
echo ""
info "Installing plannotator..."
PLANNOTATOR_INSTALL="$SKILLS_ROOT/plannotator/scripts/install.sh"
PLANNOTATOR_INSTALLED=false
if [[ -f "$PLANNOTATOR_INSTALL" ]]; then
if run "bash '$PLANNOTATOR_INSTALL' --all"; then
PLANNOTATOR_INSTALLED=true
ok "plannotator installed via skills script"
else
err "plannotator install script failed: $PLANNOTATOR_INSTALL"
fi
else
# Fallback: direct install
if run "curl -fsSL https://plannotator.ai/install.sh | bash"; then
PLANNOTATOR_INSTALLED=true
ok "plannotator installed"
else
err "plannotator install failed — check https://plannotator.ai"
fi
fi
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
if ! $PLANNOTATOR_INSTALLED || ! command -v plannotator >/dev/null 2>&1; then
err "plannotator is still unavailable after install attempt"
exit 1
fi
fi
# ── 5. agent-browser ──────────────────────────────────────────────────────────
if $INSTALL_BROWSER; then
echo ""
info "Installing agent-browser..."
if command -v npm >/dev/null 2>&1; then
run "npm install -g agent-browser 2>/dev/null || npx agent-browser --version 2>/dev/null || true"
ok "agent-browser installed"
else
warn "npm not found"
fi
echo ""
info "Installing playwriter..."
if command -v npm >/dev/null 2>&1; then
run "npm install -g playwriter 2>/dev/null || true"
ok "playwriter installed"
else
warn "npm not found"
fi
fi
# ── 7. bmad ───────────────────────────────────────────────────────────────────
if $INSTALL_BMAD; then
echo ""
info "Configuring BMAD orchestrator..."
BMAD_SKILL="$SKILLS_ROOT/bmad-orchestrator/SKILL.md"
if [[ -f "$BMAD_SKILL" ]]; then
ok "BMAD skill available at: $BMAD_SKILL"
else
warn "BMAD skill not found — ensure skills-template is properly installed"
fi
fi
# ── 8. agentation MCP ────────────────────────────────────────────────────────────────────────────
if $INSTALL_AGENTATION; then
echo ""
info "Installing agentation MCP..."
if command -v npx >/dev/null 2>&1; then
run "npx -y agentation-mcp doctor 2>/dev/null || npx -y agentation-mcp --version 2>/dev/null || true"
ok "agentation-mcp (server) available via npx"
info "Start server: npx agentation-mcp server"
info "React component: auto-installed + injected by ensure-agentation.sh at VERIFY_UI time"
info "Manual install: cd <your-app> && npm install agentation --save-dev"
info "Manual inject: import { Agentation } from 'agentation'; <Agentation endpoint=\"http://localhost:4747\" />"
else
warn "npx not found — install Node.js first"
fi
fi
# ── 9. Setup platform integrations ────────────────────────────────────────────────────────────────────────────
if $INSTALL_ALL; then
echo ""
info "Setting up platform integrations..."
run "bash '$SCRIPT_DIR/setup-claude.sh' 2>/dev/null || true"
run "bash '$SCRIPT_DIR/setup-codex.sh' 2>/dev/null || true"
run "bash '$SCRIPT_DIR/setup-gemini.sh' 2>/dev/null || true"
run "bash '$SCRIPT_DIR/setup-opencode.sh' 2>/dev/null || true"
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ JEO Installation Complete! ║"
echo "╚══════════════════════════════════════════╝"
echo ""
ok "JEO skill installed successfully"
echo ""
echo "Next steps:"
echo " 1. bash scripts/check-status.sh — Verify all integrations"
echo " 2. Restart your AI tools (Claude Code, Gemini CLI, OpenCode, Codex)"
echo " 3. Use keyword 'jeo' to activate the orchestration workflow"
echo " 4. Use keyword 'annotate' inside jeo to start agentation watch loop (agentui is a deprecated alias)"
echo ""
```
### scripts/check-status.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Status Check
# Verifies all JEO components and integrations
# Usage: bash check-status.sh [--resume]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
ok() { echo -e " ${GREEN}✓${NC} $*"; }
warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
err() { echo -e " ${RED}✗${NC} $*"; }
info() { echo -e "${BLUE}${BOLD}$*${NC}"; }
RESUME=false
[[ "${1:-}" == "--resume" ]] && RESUME=true
PASS=0; WARN=0; FAIL=0
check() {
local label="$1"; local cmd="$2"
if eval "$cmd" >/dev/null 2>&1; then
ok "$label"; ((PASS++)) || true
else
err "$label (not found)"; ((FAIL++)) || true
fi
}
check_opt() {
local label="$1"; local cmd="$2"
if eval "$cmd" >/dev/null 2>&1; then
ok "$label"; ((PASS++)) || true
else
warn "$label (optional — not configured)"; ((WARN++)) || true
fi
}
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ JEO Skill — Status Check ║"
echo "╚══════════════════════════════════════════╝"
echo ""
# ── Prerequisites ─────────────────────────────────────────────────────────────
info "Prerequisites"
check "node >=18" "node --version | grep -E 'v(1[89]|[2-9][0-9])'"
check "npm" "command -v npm"
check "git" "command -v git"
check_opt "bun" "command -v bun"
check_opt "python3" "command -v python3"
echo ""
# ── Core Tools ────────────────────────────────────────────────────────────────
info "Core Tools"
check_opt "plannotator CLI" "command -v plannotator"
check_opt "agent-browser" "command -v agent-browser || npx agent-browser --version"
check_opt "playwriter" "command -v playwriter"
echo ""
# ── AI Tool Integrations ──────────────────────────────────────────────────────
info "AI Tool Integrations"
# Claude Code
if [[ -f "${HOME}/.claude/settings.json" ]]; then
if grep -Eq "claude-plan-gate.py|plannotator" "${HOME}/.claude/settings.json" 2>/dev/null; then
ok "Claude Code — plannotator hook configured"; ((PASS++)) || true
else
warn "Claude Code — plannotator hook not found in settings.json"; ((WARN++)) || true
fi
TEAMS_ENABLED=false
if [[ "${CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS:-}" =~ ^(1|true|True|yes|YES)$ ]]; then
TEAMS_ENABLED=true
elif python3 -c "
import json, os, sys
try:
s = json.load(open(os.path.expanduser('~/.claude/settings.json')))
val = s.get('env', {}).get('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS', '')
sys.exit(0 if str(val) in ('1', 'true', 'True', 'yes') else 1)
except Exception:
sys.exit(1)
" 2>/dev/null; then
TEAMS_ENABLED=true
fi
if $TEAMS_ENABLED; then
ok "Claude Code — experimental agent teams enabled"; ((PASS++)) || true
else
err "Claude Code — experimental agent teams not enabled (required for JEO team execution)"; ((FAIL++)) || true
fi
if grep -q '"agentation"' "${HOME}/.claude/settings.json" 2>/dev/null; then
ok "Claude Code — agentation MCP configured"; ((PASS++)) || true
else
warn "Claude Code — agentation MCP not configured"; ((WARN++)) || true
fi
if grep -Eq "claude-agentation-submit-hook.py|localhost:4747/pending" "${HOME}/.claude/settings.json" 2>/dev/null; then
ok "Claude Code — agentation prompt hook configured"; ((PASS++)) || true
else
warn "Claude Code — agentation prompt hook not configured"; ((WARN++)) || true
fi
if grep -Eq '\.agent-skills/omg/scripts/(claude-agentation-submit-hook\.py|claude-plan-gate\.py)' "${HOME}/.claude/settings.json" 2>/dev/null; then
warn "Claude Code — legacy omg hook path detected; rerun ~/.agent-skills/jeo/scripts/setup-claude.sh"; ((WARN++)) || true
fi
else
warn "Claude Code — ~/.claude/settings.json not found"; ((WARN++)) || true
fi
# Codex CLI
if [[ -f "${HOME}/.codex/config.toml" ]]; then
if python3 - <<'PYEOF' >/dev/null 2>&1
import pathlib, re
p = pathlib.Path.home() / '.codex' / 'config.toml'
text = p.read_text(encoding='utf-8')
m1 = re.search(r'(?ms)^developer_instructions\s*=\s*"""\n?(.*?)\n?"""\s*$', text)
if m1 and 'Keyword: jeo | Platforms: Codex, Claude, Gemini, OpenCode' in m1.group(1):
raise SystemExit(0)
m2 = re.search(r'(?m)^developer_instructions\s*=\s*"(.*)"\s*$', text)
if m2 and 'Keyword: jeo | Platforms: Codex, Claude, Gemini, OpenCode' in bytes(m2.group(1), 'utf-8').decode('unicode_escape'):
raise SystemExit(0)
raise SystemExit(1)
PYEOF
then
ok "Codex CLI — JEO developer_instructions configured"; ((PASS++)) || true
else
warn "Codex CLI — JEO developer_instructions missing/invalid in config.toml"; ((WARN++)) || true
fi
if [[ -f "${HOME}/.codex/prompts/jeo.md" ]]; then
ok "Codex CLI — /prompts:jeo available"; ((PASS++)) || true
else
warn "Codex CLI — /prompts:jeo not found"; ((WARN++)) || true
fi
if grep -q "jeo-notify.py" "${HOME}/.codex/config.toml" 2>/dev/null; then
ok "Codex CLI — notify hook configured"; ((PASS++)) || true
else
warn "Codex CLI — notify hook missing"; ((WARN++)) || true
fi
if grep -q 'agent-turn-complete' "${HOME}/.codex/config.toml" 2>/dev/null; then
ok "Codex CLI — tui notifications include agent-turn-complete"; ((PASS++)) || true
else
warn "Codex CLI — tui notifications missing agent-turn-complete"; ((WARN++)) || true
fi
else
warn "Codex CLI — ~/.codex/config.toml not found"; ((WARN++)) || true
fi
# Gemini CLI
if [[ -f "${HOME}/.gemini/settings.json" ]]; then
if grep -q "plannotator" "${HOME}/.gemini/settings.json" 2>/dev/null; then
ok "Gemini CLI — plannotator hook configured"; ((PASS++)) || true
else
warn "Gemini CLI — plannotator hook not found"; ((WARN++)) || true
fi
else
warn "Gemini CLI — ~/.gemini/settings.json not found"; ((WARN++)) || true
fi
if [[ -f "${HOME}/.gemini/GEMINI.md" ]]; then
if grep -q "JEO Orchestration" "${HOME}/.gemini/GEMINI.md" 2>/dev/null; then
ok "Gemini CLI — JEO instructions in GEMINI.md"; ((PASS++)) || true
else
warn "Gemini CLI — JEO not in GEMINI.md"; ((WARN++)) || true
fi
fi
# OpenCode
for candidate in "./opencode.json" "${HOME}/opencode.json" "${HOME}/.config/opencode/opencode.json"; do
if [[ -f "$candidate" ]]; then
if grep -q "plannotator" "$candidate" 2>/dev/null; then
ok "OpenCode — plannotator plugin configured ($candidate)"; ((PASS++)) || true
else
warn "OpenCode — plannotator not in $candidate"; ((WARN++)) || true
fi
break
fi
done
echo ""
# ── JEO State ─────────────────────────────────────────────────────────────────
info "JEO State"
GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
STATE_FILE="$GIT_ROOT/.omc/state/jeo-state.json"
PHASE="unknown"
if [[ -f "$STATE_FILE" ]]; then
ok "State file found: $STATE_FILE"
if command -v python3 >/dev/null 2>&1; then
PHASE=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('phase','unknown'))" 2>/dev/null || echo "unknown")
TASK=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('task','(none)'))" 2>/dev/null || echo "(none)")
RETRY=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('retry_count',0))" 2>/dev/null || echo "0")
LAST_ERR=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); e=d.get('last_error'); print(e if e else '(none)')" 2>/dev/null || echo "(none)")
PLAN_GATE=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('plan_gate_status','(none)'))" 2>/dev/null || echo "(none)")
PLAN_HASH=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('last_reviewed_plan_hash') or '(none)')" 2>/dev/null || echo "(none)")
SUBMIT_GATE=$(python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('agentation',{}).get('submit_gate_status','(none)'))" 2>/dev/null || echo "(none)")
echo " Current phase: $PHASE"
echo " Task: $TASK"
echo " Plan gate: $PLAN_GATE"
[[ "$PLAN_HASH" != "(none)" ]] && echo " Last reviewed hash: ${PLAN_HASH:0:12}..."
echo " Agentation submit gate: $SUBMIT_GATE"
[[ "$RETRY" -gt 0 ]] && echo " Retry count: $RETRY"
[[ "$LAST_ERR" != "(none)" ]] && echo -e " ${RED}Last error: $LAST_ERR${NC}"
fi
if $RESUME; then
echo ""
echo " Resume instructions:"
echo " The JEO workflow was in phase: $PHASE"
echo " Check .omc/plans/jeo-plan.md for the approved plan"
fi
else
warn "No active JEO state (no workflow in progress)"
if $RESUME && command -v python3 >/dev/null 2>&1; then
info "Initializing fresh JEO state file for new session..."
mkdir -p "$GIT_ROOT/.omc/state" "$GIT_ROOT/.omc/plans"
GIT_ROOT="$GIT_ROOT" python3 - <<'PYEOF'
import json, datetime, os, uuid
now = datetime.datetime.utcnow().isoformat() + "Z"
git_root = os.environ["GIT_ROOT"]
state = {
"mode": "jeo",
"phase": "plan",
"session_id": str(uuid.uuid4()),
"task": "resumed session",
"plan_approved": False,
"plan_gate_status": "pending",
"plan_current_hash": None,
"last_reviewed_plan_hash": None,
"last_reviewed_plan_at": None,
"plan_review_method": None,
"checkpoint": None,
"last_error": None,
"retry_count": 0,
"team_available": False,
"agentation": {
"active": False,
"session_id": None,
"keyword_used": None,
"submit_gate_status": "idle",
"submit_signal": None,
"submit_received_at": None,
"submitted_annotation_count": 0,
"started_at": None,
"timeout_seconds": 120,
"annotations": {"total": 0, "pending": 0, "acknowledged": 0, "resolved": 0, "dismissed": 0},
"completed_at": None,
"exit_reason": None
},
"created_at": now,
"updated_at": now
}
os.makedirs(os.path.join(git_root, ".omc", "state"), exist_ok=True)
os.makedirs(os.path.join(git_root, ".omc", "plans"), exist_ok=True)
state_path = os.path.join(git_root, ".omc", "state", "jeo-state.json")
with open(state_path, "w") as f:
json.dump(state, f, indent=2)
print(f"✓ Fresh JEO state initialized at {state_path} (phase: plan)")
PYEOF
elif [[ -z "${1:-}" ]]; then
echo " Tip: Run with --resume to initialize state for a new session"
fi
fi
# Worktrees
WORKTREE_COUNT=$(git worktree list 2>/dev/null | wc -l | tr -d ' ') || WORKTREE_COUNT=0
if [[ "$WORKTREE_COUNT" -gt 1 ]]; then
warn "Active worktrees: $WORKTREE_COUNT (run worktree-cleanup.sh when done)"
else
ok "No extra worktrees active"
fi
echo ""
# ── Summary ────────────────────────────────────────────────────────────────────
echo "══════════════════════════════════════════"
echo -e " ${GREEN}✓${NC} Passed: $PASS ${YELLOW}⚠${NC} Warnings: $WARN ${RED}✗${NC} Failed: $FAIL"
echo "══════════════════════════════════════════"
echo ""
if [[ $FAIL -gt 0 ]]; then
echo "Fix failures by running: bash scripts/install.sh --all"
elif [[ $WARN -gt 0 ]]; then
echo "Run platform setup scripts to resolve warnings:"
echo " bash scripts/setup-claude.sh"
echo " bash scripts/setup-codex.sh"
echo " bash scripts/setup-gemini.sh"
echo " bash scripts/setup-opencode.sh"
else
echo -e "${GREEN}All checks passed! JEO is fully configured.${NC}"
fi
echo ""
```
### scripts/setup-claude.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Claude Code Plugin & Hook Setup
# Configures: omc plugin, plannotator hook, agentation MCP, jeo workflow in ~/.claude/settings.json
# Usage: bash setup-claude.sh [--dry-run]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
err() { echo -e "${RED}✗${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
# Resolve JEO_SKILL_DIR: prefer stable canonical paths over script location
# to ensure hooks survive reinstalls regardless of where this script is run from.
_SCRIPT_JEO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
_CANONICAL_JEO="${HOME}/.agent-skills/jeo" # canonical (setup-all-skills Step 4 rsync target)
_GLOBAL_JEO="${HOME}/.agents/skills/jeo" # npx skills add -g actual install path
if [[ -d "${_CANONICAL_JEO}/scripts" ]]; then
JEO_SKILL_DIR="${_CANONICAL_JEO}"
elif [[ -d "${_GLOBAL_JEO}/scripts" ]]; then
JEO_SKILL_DIR="${_GLOBAL_JEO}"
warn "Using global install path: ${JEO_SKILL_DIR}"
warn "Run Step 4 rsync from setup-all-skills-prompt.md to sync to canonical path."
else
JEO_SKILL_DIR="${_SCRIPT_JEO}"
warn "jeo not at canonical path. Using script location: ${JEO_SKILL_DIR}"
warn "Hooks will break if this location changes. Run setup-all-skills-prompt.md Step 4 to fix."
fi
CLAUDE_SETTINGS="${HOME}/.claude/settings.json"
LEGACY_OMG_DIR="${HOME}/.agent-skills/omg/scripts"
sync_legacy_python_compat() {
local source_script="$1"
local legacy_target="$2"
mkdir -p "$(dirname "$legacy_target")"
if ln -sfn "$source_script" "$legacy_target" 2>/dev/null; then
ok "Legacy omg shim linked: $legacy_target"
return
fi
cp "$source_script" "$legacy_target"
chmod +x "$legacy_target"
ok "Legacy omg shim copied: $legacy_target"
}
echo ""
echo "JEO — Claude Code Setup"
echo "========================"
# ── 1. Check Claude Code ──────────────────────────────────────────────────────
if ! command -v claude >/dev/null 2>&1; then
warn "claude CLI not found. Install Claude Code first."
echo ""
echo "Plugin installation (run inside Claude Code session):"
echo " /plugin marketplace add https://github.com/Yeachan-Heo/oh-my-claudecode"
echo " /plugin install oh-my-claudecode"
echo " /omc:omc-setup"
echo ""
echo "plannotator plugin:"
echo " /plugin marketplace add backnotprop/plannotator"
echo " /plugin install plannotator@plannotator"
else
ok "claude CLI found"
fi
# ── 2. Configure ~/.claude/settings.json ─────────────────────────────────────
info "Configuring ~/.claude/settings.json..."
mkdir -p "$(dirname "$CLAUDE_SETTINGS")"
if [[ -f "$CLAUDE_SETTINGS" ]]; then
if ! $DRY_RUN; then
cp "$CLAUDE_SETTINGS" "${CLAUDE_SETTINGS}.jeo.bak"
ok "Backup created: ${CLAUDE_SETTINGS}.jeo.bak"
fi
fi
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would sync plannotator hook, agent teams, agentation MCP, and UserPromptSubmit hook in $CLAUDE_SETTINGS"
else
JEO_SKILL_DIR="$JEO_SKILL_DIR" python3 - <<'PYEOF'
import json
import os
settings_path = os.path.expanduser("~/.claude/settings.json")
jeo_skill_dir = os.environ["JEO_SKILL_DIR"]
plan_gate_cmd = f'python3 "{jeo_skill_dir}/scripts/claude-plan-gate.py"'
agentation_cmd = f'python3 "{jeo_skill_dir}/scripts/claude-agentation-submit-hook.py"'
try:
with open(settings_path) as f:
settings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
settings = {}
changed = False
messages = []
hooks = settings.setdefault("hooks", {})
perm_req = hooks.setdefault("PermissionRequest", [])
plannotator_entry = next((entry for entry in perm_req if entry.get("matcher") == "ExitPlanMode"), None)
if plannotator_entry is None:
plannotator_entry = {"matcher": "ExitPlanMode", "hooks": []}
perm_req.append(plannotator_entry)
changed = True
plan_hooks = plannotator_entry.setdefault("hooks", [])
# Remove raw `plannotator` entries — claude-plan-gate.py calls plannotator
# internally, so keeping both causes plannotator to launch twice.
raw_plannotator = [
h for h in plan_hooks
if h.get("command", "").strip() == "plannotator"
]
if raw_plannotator:
plan_hooks[:] = [h for h in plan_hooks if h not in raw_plannotator]
changed = True
messages.append(f"✓ Removed {len(raw_plannotator)} duplicate raw plannotator hook(s) from ExitPlanMode")
existing_plan_hook = next(
(
h for h in plan_hooks
if "claude-plan-gate.py" in h.get("command", "")
),
None,
)
duplicate_plan_hooks = [
h for h in plan_hooks
if h is not existing_plan_hook and "claude-plan-gate.py" in h.get("command", "")
]
if duplicate_plan_hooks:
plan_hooks[:] = [h for h in plan_hooks if h not in duplicate_plan_hooks]
changed = True
messages.append(f"✓ Removed {len(duplicate_plan_hooks)} duplicate plan gate hook(s)")
if existing_plan_hook is None:
plan_hooks.append({
"type": "command",
"command": plan_gate_cmd,
"timeout": 1800,
})
changed = True
messages.append("✓ JEO plan gate wrapper added to ExitPlanMode")
else:
if existing_plan_hook.get("command") != plan_gate_cmd:
existing_plan_hook["command"] = plan_gate_cmd
changed = True
if existing_plan_hook.get("timeout") != 1800:
existing_plan_hook["timeout"] = 1800
changed = True
messages.append("✓ JEO plan gate wrapper synced")
env = settings.setdefault("env", {})
if env.get("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS") != "1":
env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
changed = True
messages.append("✓ Experimental agent teams enabled")
else:
messages.append("✓ Experimental agent teams already enabled")
mcp_servers = settings.setdefault("mcpServers", {})
if "agentation" not in mcp_servers:
mcp_servers["agentation"] = {
"command": "npx",
"args": ["-y", "agentation-mcp", "server"],
}
changed = True
messages.append("✓ agentation MCP server registered")
else:
messages.append("✓ agentation MCP already registered")
user_prompt = hooks.setdefault("UserPromptSubmit", [])
# Migrate old-format entries (flat {"type":"command",...}) to new matcher format
migrated = False
new_user_prompt = []
for entry in user_prompt:
if "matcher" in entry and "hooks" in entry:
new_user_prompt.append(entry)
elif entry.get("type") == "command":
# Old format → wrap in new matcher format
new_user_prompt.append({"matcher": "*", "hooks": [entry]})
migrated = True
else:
new_user_prompt.append(entry)
if migrated:
hooks["UserPromptSubmit"] = new_user_prompt
user_prompt = new_user_prompt
changed = True
messages.append("✓ UserPromptSubmit hooks migrated to new matcher format")
def is_agentation_submit_hook(command):
return (
"claude-agentation-submit-hook.py" in command
or command.startswith("curl -sf --connect-timeout 1 http://localhost:4747")
)
agentation_matches = []
for entry in user_prompt:
if not isinstance(entry, dict):
continue
hook_list = entry.setdefault("hooks", [])
for hook in list(hook_list):
if is_agentation_submit_hook(hook.get("command", "")):
agentation_matches.append((entry, hook_list, hook))
if not agentation_matches:
user_prompt.append({
"matcher": "*",
"hooks": [{"type": "command", "command": agentation_cmd, "timeout": 300}],
})
changed = True
messages.append("✓ agentation submit-gate hook added")
else:
agentation_entry, hook_list, target_hook = agentation_matches[0]
removed_duplicates = 0
for duplicate_entry, duplicate_hook_list, duplicate_hook in agentation_matches[1:]:
duplicate_hook_list.remove(duplicate_hook)
removed_duplicates += 1
changed = True
if removed_duplicates:
messages.append(f"✓ Removed {removed_duplicates} duplicate legacy agentation hook(s)")
if target_hook.get("command") != agentation_cmd:
target_hook["command"] = agentation_cmd
changed = True
if target_hook.get("timeout") != 300:
target_hook["timeout"] = 300
changed = True
if agentation_entry.get("matcher") != "*":
agentation_entry["matcher"] = "*"
changed = True
pruned_user_prompt = []
for entry in user_prompt:
if not isinstance(entry, dict):
pruned_user_prompt.append(entry)
continue
if entry.get("hooks"):
pruned_user_prompt.append(entry)
continue
changed = True
if pruned_user_prompt != user_prompt:
hooks["UserPromptSubmit"] = pruned_user_prompt
user_prompt = pruned_user_prompt
else:
hooks["UserPromptSubmit"] = user_prompt
messages.append("✓ agentation submit-gate hook synced")
if changed or not os.path.exists(settings_path):
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
for message in messages:
print(message)
PYEOF
ok "Claude Code settings synced"
fi
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would create legacy omg compatibility shims in ${LEGACY_OMG_DIR}"
else
sync_legacy_python_compat \
"${JEO_SKILL_DIR}/scripts/claude-agentation-submit-hook.py" \
"${LEGACY_OMG_DIR}/claude-agentation-submit-hook.py"
sync_legacy_python_compat \
"${JEO_SKILL_DIR}/scripts/claude-plan-gate.py" \
"${LEGACY_OMG_DIR}/claude-plan-gate.py"
fi
# ── 3. Instructions ───────────────────────────────────────────────────────────
echo ""
echo "Manual plugin installation (run inside Claude Code):"
echo ""
echo " # Install oh-my-claudecode (omc)"
echo " /plugin marketplace add https://github.com/Yeachan-Heo/oh-my-claudecode"
echo " /plugin install oh-my-claudecode"
echo " /omc:omc-setup"
echo ""
echo " # Install plannotator"
echo " /plugin marketplace add backnotprop/plannotator"
echo " /plugin install plannotator@plannotator"
echo ""
echo " # Then restart Claude Code"
echo ""
ok "Claude Code setup complete"
echo " IMPORTANT: Restart Claude Code to activate all hooks and plugins"
echo " JEO requires /omc:team execution in Claude Code. Verify CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 after restart."
echo ""
```
### scripts/setup-codex.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Codex CLI Setup
# Configures: developer_instructions + agentation MCP in ~/.codex/config.toml + /prompts:jeo
# Usage: bash setup-codex.sh [--dry-run]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
JEO_SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CODEX_CONFIG="${HOME}/.codex/config.toml"
CODEX_PROMPTS_DIR="${HOME}/.codex/prompts"
JEO_PROMPT_FILE="${CODEX_PROMPTS_DIR}/jeo.md"
echo ""
echo "JEO — Codex CLI Setup"
echo "======================"
# ── 1. Check Codex CLI ────────────────────────────────────────────────────────
if ! command -v codex >/dev/null 2>&1; then
warn "codex CLI not found. Install via: npm install -g @openai/codex"
fi
# ── 2. Configure ~/.codex/config.toml ────────────────────────────────────────
info "Configuring ~/.codex/config.toml..."
HOOK_DIR="${HOME}/.codex/hooks"
HOOK_FILE="${HOOK_DIR}/jeo-notify.py"
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would create/update $CODEX_CONFIG"
echo -e "${YELLOW}[DRY-RUN]${NC} Would create $JEO_PROMPT_FILE"
else
mkdir -p "$(dirname "$CODEX_CONFIG")" "$CODEX_PROMPTS_DIR"
# Backup existing config
[[ -f "$CODEX_CONFIG" ]] && cp "$CODEX_CONFIG" "${CODEX_CONFIG}.jeo.bak"
JEO_INSTRUCTION=$(cat <<JEOEOF
# JEO Orchestration Workflow
# Keyword: jeo | Platforms: Codex, Claude, Gemini, OpenCode
#
# JEO provides integrated AI orchestration:
# 1. PLAN: ralph+plannotator for visual plan review
# 2. EXECUTE: team (if available) or bmad workflow
# 3. VERIFY: agent-browser snapshot for UI verification
# 4. CLEANUP: auto worktree cleanup after completion
#
# Trigger with: jeo "<task description>"
# Use /prompts:jeo for full workflow activation
#
# PLAN phase protocol (Codex):
# 1. Write plan to plan.md
# 2. Run mandatory PLAN gate (auto-installs plannotator if missing, blocks for feedback/approve, retries dead sessions up to 3):
# bash ${JEO_SKILL_DIR}/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
# Rule: if the current plan hash already has a terminal gate result
# (approved/manual_approved/feedback_required/infrastructure_blocked), do not reopen plannotator.
# Only a revised plan.md resets the gate to pending.
# 3. Output "PLAN_READY" to trigger notify hook as backup signal
# 4. Check result:
# - approved=true -> EXECUTE
# - approved=false -> re-plan
# - exit 32 -> localhost bind blocked (sandbox/CI). run PLAN gate in local TTY
# 5. Never emit PLAN_READY in execute/verify phases.
#
# VERIFY_UI submit gate protocol:
# 1. Enter verify_ui and set agentation.submit_gate_status="waiting_for_submit"
# 2. Wait until the user clicks Send Annotations / onSubmit in agentation
# 3. Only then emit "ANNOTATE_READY" (or "AGENTUI_READY" for compatibility)
# 4. notify hook may fetch /pending only after that explicit submit signal
#
# BMAD commands (fallback when team unavailable):
# /workflow-init — initialize BMAD workflow
# /workflow-status — check current BMAD phase
#
# Tools: agent-browser, playwriter, plannotator
JEOEOF
)
python3 - <<PYEOF
import re, os
config_path = os.path.expanduser("~/.codex/config.toml")
jeo_instruction = """${JEO_INSTRUCTION}"""
try:
content = open(config_path).read() if os.path.exists(config_path) else ""
except Exception:
content = ""
content = re.sub(r'(?ms)^\[developer_instructions\]\s*\n.*?(?=^\[|\Z)', '', content).strip() + "\n"
# Only strip legacy bare JEO block if it is NOT inside a developer_instructions value
if not re.search(r'(?ms)^developer_instructions\s*=\s*"""', content):
content = re.sub(r'(?ms)^# JEO Orchestration Workflow\n.*?^"""\s*\n', '', content)
def parse_existing_instructions(text: str) -> str:
m = re.search(r'(?ms)^developer_instructions\s*=\s*"""\n?(.*?)\n?"""\s*$', text)
if m:
return m.group(1)
m = re.search(r'(?m)^developer_instructions\s*=\s*"(.*)"\s*$', text)
if m:
return bytes(m.group(1), "utf-8").decode("unicode_escape")
return ""
existing = parse_existing_instructions(content)
jeo_pattern = re.compile(
r'(?ms)^# JEO Orchestration Workflow\n.*?^# Tools: agent-browser, playwriter, plannotator\s*$'
)
if jeo_pattern.search(existing):
merged = jeo_pattern.sub(jeo_instruction.strip(), existing).strip()
else:
merged = (existing.rstrip() + "\n\n" if existing.strip() else "") + jeo_instruction.strip()
new_assignment = 'developer_instructions = """\n' + merged + '\n"""\n'
if re.search(r'(?m)^developer_instructions\s*=', content):
content = re.sub(r'(?ms)^developer_instructions\s*=\s*(""".*?"""|".*?")\s*$', new_assignment, content, count=1)
else:
first_table = re.search(r'(?m)^\[', content)
if first_table:
content = content[:first_table.start()] + new_assignment + "\n" + content[first_table.start():]
else:
content = new_assignment + "\n" + content
with open(config_path, "w") as f:
f.write(content)
# Post-write: validate TOML and auto-fix stray standalone " lines
try:
import tomllib as _toml
with open(config_path, "rb") as f:
_toml.load(f)
except Exception:
with open(config_path) as f:
raw = f.read()
cleaned = re.sub(r'(?m)^"$\n', '', raw)
with open(config_path, "w") as f:
f.write(cleaned)
print("✓ JEO developer_instructions synced (top-level string)")
PYEOF
ok "JEO developer_instructions synced in ~/.codex/config.toml"
# ── 3. Create /prompts:jeo prompt file ──────────────────────────────────────
cat > "$JEO_PROMPT_FILE" <<'PROMPTEOF'
# JEO — Integrated Agent Orchestration Prompt
You are now operating in **JEO mode** — Integrated AI Agent Orchestration.
## Your Workflow
### Step 1: PLAN (plannotator — blocking loop)
Before writing any code, create and review a plan:
1. Write a detailed implementation plan in \`plan.md\` (objectives, steps, risks, acceptance criteria)
2. Run plannotator PLAN gate (blocking, mandatory; auto-installs plannotator if missing):
\`\`\`bash
bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
echo "PLAN_READY"
\`\`\`
Same-hash guard:
- if \`plan_gate_status\` is already terminal for the current plan hash, do not reopen plannotator
- only a modified plan resets the gate to \`pending\`
3. Read /tmp/plannotator_feedback.txt
4. If \`"approved":true\` → proceed to EXECUTE
5. If NOT approved → read annotations, revise plan.md, repeat from step 2
6. **If PLAN gate exits with 32 → CONVERSATION APPROVAL MODE (MANDATORY)**:
- Output the full contents of \`plan.md\` to the user in the conversation
- Ask explicitly: "⚠️ plannotator UI unavailable in this environment. Please review the plan above and reply 'approve' to proceed, or provide feedback."
- **STOP and wait for user response. Do NOT proceed to EXECUTE until user approves.**
- On 'approve'/'yes'/'ok': update \`jeo-state.json\` \`plan_approved=true, plan_gate_status="manual_approved"\` → EXECUTE
- On feedback text: revise \`plan.md\`, retry \`plannotator-plan-loop.sh\`, repeat
NEVER skip plannotator. NEVER proceed to EXECUTE without approved=true or explicit user approval in conversation.
### Step 2: EXECUTE (BMAD workflow for Codex)
Use BMAD structured phases:
- \`/workflow-init\` — Initialize BMAD for this project
- Analysis phase: understand requirements fully
- Planning phase: detailed technical plan
- Solutioning phase: architecture decisions
- Implementation phase: write code
### Step 3: VERIFY (agent-browser)
If the task has browser UI:
- Run: \`agent-browser snapshot http://localhost:3000\`
- Check UI elements via accessibility tree (-i flag)
- Save screenshot: \`agent-browser screenshot <url> -o verify.png\`
### Step 3.1: VERIFY_UI (agentation submit gate)
If \`annotate\` / \`agentui\` is requested:
1. Update \`.omc/state/jeo-state.json\` to \`phase="verify_ui"\`
2. Set \`agentation.submit_gate_status="waiting_for_submit"\`
3. Wait until the human actually clicks **Send Annotations** / triggers \`onSubmit\`
4. Only then emit \`ANNOTATE_READY\`
5. Process agentation annotations after the notify hook reports submitted items
Never read \`/pending\` before the submit gate opens.
### Step 4: CLEANUP (worktree)
After all tasks complete:
- Run: git worktree prune
- Run: bash ${JEO_SKILL_DIR}/scripts/worktree-cleanup.sh
## Key Commands
- Plan review — run plannotator BLOCKING (no &), then output PLAN_READY:
Mandatory behavior:
- If plannotator is missing, the PLAN gate auto-runs \`ensure-plannotator.sh\` first
- Wait for approve/feedback every time
- If session dies, restart up to 3 times
- After 3 dead sessions, stop and ask whether PLAN should be terminated
- If loop exits 32: localhost bind blocked. use local TTY/manual PLAN gate
\`\`\`bash
bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
# Output PLAN_READY to trigger notify hook as backup signal
echo "PLAN_READY"
# Check result
python3 -c "
import json, sys
try:
d = json.load(open('/tmp/plannotator_feedback.txt'))
sys.exit(0 if d.get('approved') is True else 1)
except Exception:
sys.exit(1)
" && echo "PLAN_APPROVED — proceed to EXECUTE" || cat /tmp/plannotator_feedback.txt
\`\`\`
- Browser verify: \`agent-browser snapshot http://localhost:3000\`
- BMAD init: \`/workflow-init\`
- Worktree cleanup: \`bash ${JEO_SKILL_DIR}/scripts/worktree-cleanup.sh\`
## State File
Save progress to: \`.omc/state/jeo-state.json\`
\`\`\`json
{
"phase": "plan|execute|verify|verify_ui|cleanup|done",
"task": "current task description",
"plan_approved": false,
"plan_gate_status": "pending",
"plan_current_hash": null,
"last_reviewed_plan_hash": null,
"plan_review_method": null,
"team_available": false,
"retry_count": 0,
"last_error": null,
"checkpoint": null,
"agentation": {
"submit_gate_status": "idle",
"submit_signal": null,
"submit_received_at": null,
"submitted_annotation_count": 0
}
}
\`\`\`
Always check state file on resume to continue from last phase.
PROMPTEOF
# Fix absolute paths in prompt file (replace relative .agent-skills paths with absolute JEO_SKILL_DIR)
sed -i.bak \
-e "s|bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh|bash ${JEO_SKILL_DIR}/scripts/plannotator-plan-loop.sh|g" \
-e "s|\\\${JEO_SKILL_DIR}|${JEO_SKILL_DIR}|g" \
"$JEO_PROMPT_FILE"
rm -f "${JEO_PROMPT_FILE}.bak"
ok "JEO prompt file created: $JEO_PROMPT_FILE"
# ── 4. Create plannotator notify hook ────────────────────────────────────────
info "Setting up plannotator notify hook..."
mkdir -p "$HOOK_DIR"
cat > "$HOOK_FILE" << 'HOOKEOF'
#!/usr/bin/env python3
"""JEO Codex notify hook — detects PLAN_READY / ANNOTATE_READY and triggers plannotator / agentation."""
import hashlib, json, os, re, subprocess, sys, urllib.request, urllib.error, time
# Exact signal strings (matched as standalone lines, allowing surrounding whitespace)
PLAN_SIGNALS = ["PLAN_READY"]
ANNOTATE_SIGNALS = ["ANNOTATE_READY", "AGENTUI_READY"]
PLAN_TERMINAL_STATUSES = {"approved", "manual_approved", "feedback_required", "infrastructure_blocked"}
def get_jeo_phase(cwd: str) -> str:
"""Read current JEO phase from state file. Returns empty string if not available."""
state_path = os.path.join(cwd, ".omc", "state", "jeo-state.json")
try:
with open(state_path) as f:
return json.load(f).get("phase", "")
except (FileNotFoundError, json.JSONDecodeError, KeyError):
return ""
def get_state_path(cwd: str) -> str:
return os.path.join(cwd, ".omc", "state", "jeo-state.json")
def load_state(cwd: str) -> dict:
state_path = get_state_path(cwd)
try:
with open(state_path) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def update_state(cwd: str, mutator) -> None:
state_path = get_state_path(cwd)
if not os.path.exists(state_path):
return
try:
import fcntl, datetime
with open(state_path, "r+") as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
try:
data = json.load(fh)
except Exception:
data = {}
mutator(data)
data["updated_at"] = datetime.datetime.utcnow().isoformat() + "Z"
fh.seek(0)
json.dump(data, fh, indent=2)
fh.truncate()
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
except Exception:
pass
def get_feedback_file(cwd: str) -> str:
"""Return session-isolated feedback file path based on cwd MD5."""
_session_key = hashlib.md5(cwd.encode()).hexdigest()[:8]
feedback_dir = f"/tmp/jeo-{_session_key}"
os.makedirs(feedback_dir, exist_ok=True)
return os.path.join(feedback_dir, "plannotator_feedback.txt")
def get_plannotator_env(cwd: str) -> dict:
_session_key = hashlib.md5(cwd.encode()).hexdigest()[:8]
runtime_home = f"/tmp/jeo-{_session_key}/.plannotator"
os.makedirs(runtime_home, exist_ok=True)
env = os.environ.copy()
env["HOME"] = runtime_home
env["PLANNOTATOR_HOME"] = runtime_home
return env
def get_plan_loop_script(cwd: str):
candidates = [
os.path.join(cwd, ".agent-skills", "jeo", "scripts", "plannotator-plan-loop.sh"),
os.path.expanduser("~/.codex/skills/jeo/scripts/plannotator-plan-loop.sh"),
os.path.expanduser("~/.agent-skills/jeo/scripts/plannotator-plan-loop.sh"),
]
for p in candidates:
if os.path.exists(p):
return p
return None
def compute_plan_hash(plan_path: str) -> str:
try:
with open(plan_path, "rb") as fh:
return hashlib.sha256(fh.read()).hexdigest()
except Exception:
return ""
def should_skip_plan_gate(cwd: str, plan_path: str) -> bool:
state = load_state(cwd)
current_hash = compute_plan_hash(plan_path)
if not current_hash:
return False
return (
state.get("phase") == "plan"
and state.get("plan_gate_status") in PLAN_TERMINAL_STATUSES
and state.get("last_reviewed_plan_hash") == current_hash
)
def write_plan_gate_result(cwd: str, rc: int, feedback_file: str) -> None:
"""Write plannotator gate result to jeo-state.json."""
state_path = os.path.join(cwd, '.omc', 'state', 'jeo-state.json')
if not os.path.exists(state_path):
return
try:
import fcntl, datetime
with open(state_path, 'r+') as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
d = json.load(fh)
if rc == 0:
d['plan_approved'] = True
d['phase'] = 'execute'
d['plan_gate_status'] = 'approved'
elif rc == 10:
d['plan_approved'] = False
d['plan_gate_status'] = 'feedback_required'
try:
d['plannotator_feedback'] = json.load(open(feedback_file))
except Exception:
pass
elif rc == 32:
d['plan_gate_status'] = 'infrastructure_blocked'
d['last_error'] = 'localhost bind unavailable (sandbox/CI)'
d['updated_at'] = datetime.datetime.utcnow().isoformat() + 'Z'
fh.seek(0); json.dump(d, fh, indent=2); fh.truncate()
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
except Exception:
pass
def main() -> int:
try:
notification = json.loads(sys.argv[1])
except (IndexError, json.JSONDecodeError):
return 0
if notification.get("type") != "agent-turn-complete":
return 0
msg = notification.get("last-assistant-message", "").strip()
cwd = notification.get("cwd", os.getcwd())
phase = get_jeo_phase(cwd)
# PLAN_READY: trigger plannotator (only during plan phase)
if phase in ("plan",):
if any(re.search(rf'(?m)^{re.escape(sig)}\s*$', msg or '') for sig in PLAN_SIGNALS):
plan_candidates = ["plan.md", ".omc/plans/jeo-plan.md", "docs/plan.md"]
plan_path = None
for candidate in plan_candidates:
p = os.path.join(cwd, candidate)
if os.path.exists(p):
plan_path = p
break
if plan_path is None:
print("[JEO] plan.md not found in known locations")
return 0
if should_skip_plan_gate(cwd, plan_path):
state = load_state(cwd)
print(
f"[JEO] plan gate skipped: current hash already reviewed ({state.get('plan_gate_status', 'unknown')})"
)
return 0
feedback_file = get_feedback_file(cwd)
loop_script = get_plan_loop_script(cwd)
if loop_script:
result = subprocess.run(
["bash", loop_script, plan_path, feedback_file, "3"],
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=get_plannotator_env(cwd),
)
if result.stdout:
print(result.stdout.strip())
write_plan_gate_result(cwd, result.returncode, feedback_file)
if result.returncode == 32:
print("[JEO] plannotator unavailable: localhost bind blocked (sandbox/CI).")
print("[JEO] CONVERSATION APPROVAL MODE: agent must output plan.md and request explicit user approval.")
print(f"[JEO] plannotator loop result code={result.returncode} feedback={feedback_file}")
else:
plan_content = open(plan_path).read()
payload = json.dumps({"tool_input": {"plan": plan_content, "permission_mode": "acceptEdits"}})
try:
with open(feedback_file, "w") as f:
subprocess.run(["plannotator"], input=payload, stdout=f, stderr=f, text=True, env=get_plannotator_env(cwd))
print(f"[JEO] plannotator feedback \u2192 {feedback_file}")
except FileNotFoundError:
print("[JEO] plannotator not found \u2014 skipping")
return 0
# ANNOTATE_READY: poll agentation HTTP API only after explicit submit signal in VERIFY_UI
if phase == "verify_ui":
if any(re.search(rf'(?m)^{re.escape(sig)}\s*$', msg or '') for sig in ANNOTATE_SIGNALS):
state = load_state(cwd)
agentation = state.get("agentation", {})
if agentation.get("submit_gate_status") == "submitted":
print("[JEO] agentation submit gate already opened for this turn")
else:
def mark_submitted(data):
agentation_state = data.setdefault("agentation", {})
agentation_state["submit_gate_status"] = "submitted"
agentation_state["submit_signal"] = "codex-notify"
agentation_state["submit_received_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
update_state(cwd, mark_submitted)
base_url = "http://localhost:4747"
try:
with urllib.request.urlopen(f"{base_url}/pending", timeout=2) as r:
data = json.loads(r.read())
count = data.get("count", 0)
annotations = data.get("annotations", [])
def record_count(state_data):
agentation_state = state_data.setdefault("agentation", {})
agentation_state["submitted_annotation_count"] = count
if count == 0:
agentation_state["submit_gate_status"] = "waiting_for_submit"
update_state(cwd, record_count)
if count == 0:
print("[JEO] agentation: no pending annotations")
else:
print(f"[JEO] agentation: {count} pending annotations")
for ann in annotations:
sev = ann.get("severity", "suggestion")
print(f" [{sev}] {ann.get('element','?')} | {ann.get('comment','')[:80]}")
print(f" elementPath: {ann.get('elementPath','?')}")
except (urllib.error.URLError, Exception) as e:
print(f"[JEO] agentation server not reachable ({base_url}): {e}")
return 0
return 0
if __name__ == "__main__":
sys.exit(main())
HOOKEOF
chmod +x "$HOOK_FILE"
# Inject absolute JEO script path as first candidate in get_plan_loop_script()
sed -i.bak \
"s|os.path.join(cwd, \".agent-skills\", \"jeo\", \"scripts\", \"plannotator-plan-loop.sh\")|\"${JEO_SKILL_DIR}/scripts/plannotator-plan-loop.sh\",\n os.path.join(cwd, \".agent-skills\", \"jeo\", \"scripts\", \"plannotator-plan-loop.sh\")|" \
"$HOOK_FILE"
rm -f "${HOOK_FILE}.bak"
ok "JEO notify hook created: $HOOK_FILE"
# Add notify + tui to config.toml
python3 - <<PYEOF
import re, os
config_path = os.path.expanduser("~/.codex/config.toml")
hook_path = os.path.expanduser("~/.codex/hooks/jeo-notify.py")
try:
content = open(config_path).read() if os.path.exists(config_path) else ""
except Exception:
content = ""
notify_line = f'notify = ["python3", "{hook_path}"]\n'
if re.search(r'(?m)^notify\s*=', content):
content = re.sub(r'(?m)^notify\s*=.*$', notify_line.rstrip(), content, count=1)
print("✓ notify hook synced in config.toml")
else:
first_table = re.search(r'^\[', content, re.MULTILINE)
if first_table:
content = content[:first_table.start()] + notify_line + "\n" + content[first_table.start():]
else:
content = notify_line + content
print("✓ notify hook registered in config.toml")
# Add agentation [[mcp_servers]] if missing (check both array-of-tables and table formats)
if not re.search(r'(?ms)^\[\[mcp_servers\]\]\s*\nname\s*=\s*"agentation"\s*\n', content) and \
not re.search(r'(?m)^\[mcp_servers\.agentation\]', content):
agentation_block = '\n[mcp_servers.agentation]\ncommand = "npx"\nargs = ["-y", "agentation-mcp", "server"]\n'
content = content.rstrip() + agentation_block
print("\u2713 agentation MCP server added to config.toml")
else:
print("\u2713 agentation MCP already in config.toml")
# Add or sync [tui] section
tui_match = re.search(r'(?ms)^\[tui\]\s*\n(.*?)(?=^\[|\Z)', content)
if not tui_match:
content = content.rstrip() + '\n\n[tui]\nnotifications = ["agent-turn-complete"]\nnotification_method = "osc9"\n'
print("✓ [tui] section added")
else:
tui_body = tui_match.group(1)
notif_match = re.search(r'(?m)^notifications\s*=\s*\[(.*?)\]\s*$', tui_body)
notifications = []
if notif_match:
notifications = re.findall(r'"([^"]+)"', notif_match.group(1))
if "agent-turn-complete" not in notifications:
notifications.append("agent-turn-complete")
notifications_line = 'notifications = [' + ', '.join(f'"{item}"' for item in notifications) + ']'
if notif_match:
tui_body = re.sub(r'(?m)^notifications\s*=\s*\[(.*?)\]\s*$', notifications_line, tui_body, count=1)
else:
tui_body = notifications_line + '\n' + tui_body
if re.search(r'(?m)^notification_method\s*=', tui_body):
tui_body = re.sub(r'(?m)^notification_method\s*=.*$', 'notification_method = "osc9"', tui_body, count=1)
else:
tui_body = tui_body.rstrip() + '\nnotification_method = "osc9"\n'
content = content[:tui_match.start(1)] + tui_body + content[tui_match.end(1):]
print("✓ [tui] notifications synced")
with open(config_path, "w") as f:
f.write(content)
PYEOF
ok "Codex config.toml updated (notify hook + agentation MCP + tui)"
fi
echo ""
echo "Codex CLI usage after setup:"
echo " /prompts:jeo ← Activate JEO orchestration workflow"
echo " notify hook: ~/.codex/hooks/jeo-notify.py"
echo " fires on: PLAN_READY / ANNOTATE_READY signals in agent output (AGENTUI_READY also accepted)"
echo " writes to: /tmp/plannotator_feedback.txt"
echo ""
ok "Codex CLI setup complete"
echo ""
```
### scripts/setup-gemini.sh
```bash
#!/usr/bin/env bash
# JEO Skill — Gemini CLI Hook & GEMINI.md Setup
# Configures: ExitPlanMode hook in ~/.gemini/settings.json + JEO instructions in GEMINI.md
# Usage: bash setup-gemini.sh [--dry-run] [--hook-only] [--md-only]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false; HOOK_ONLY=false; MD_ONLY=false
for arg in "$@"; do
case $arg in --dry-run) DRY_RUN=true ;; --hook-only) HOOK_ONLY=true ;; --md-only) MD_ONLY=true ;; esac
done
JEO_SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GEMINI_SETTINGS="${HOME}/.gemini/settings.json"
GEMINI_MD="${HOME}/.gemini/GEMINI.md"
echo ""
echo "JEO — Gemini CLI Setup"
echo "======================"
# ── 1. Check Gemini CLI ───────────────────────────────────────────────────────
if ! command -v gemini >/dev/null 2>&1; then
warn "gemini CLI not found. Install via: npm install -g @google/gemini-cli"
fi
# NOTE: Gemini CLI uses AfterAgent hook (not ExitPlanMode, which is Claude Code-only).
# The primary method is agent direct blocking call — do NOT use & (background).
# Manual blocking call (same-turn feedback, auto-installs plannotator if missing):
# bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
# ── 2. Configure ~/.gemini/settings.json ─────────────────────────────────────
if ! $MD_ONLY; then
info "Configuring ~/.gemini/settings.json..."
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would add AfterAgent hook to $GEMINI_SETTINGS"
else
mkdir -p "$(dirname "$GEMINI_SETTINGS")"
[[ -f "$GEMINI_SETTINGS" ]] && cp "$GEMINI_SETTINGS" "${GEMINI_SETTINGS}.jeo.bak"
# Create hook helper script (avoids plannotator plan - hanging on empty stdin)
GEMINI_HOOK_DIR="${HOME}/.gemini/hooks"
mkdir -p "$GEMINI_HOOK_DIR"
cat > "${GEMINI_HOOK_DIR}/jeo-plannotator.sh" << 'HOOKEOF'
#!/usr/bin/env bash
# JEO AfterAgent backup hook — runs plannotator if plan.md exists in cwd
# Phase guard: only fire during PLAN phase to prevent conflict with agentation.
# Repeat guard: same plan hash + terminal gate status must not reopen plannotator.
JEO_STATE="${PWD}/.omc/state/jeo-state.json"
if [[ ! -f "$JEO_STATE" ]]; then
exit 0 # JEO is not active — no state file
fi
PHASE=$(python3 -c "
import json, sys
try:
d = json.load(open('$JEO_STATE'))
print(d.get('phase', 'unknown'))
except Exception:
print('unknown')
" 2>/dev/null || echo "unknown")
# 화이트리스트: "plan"일 때만 실행, 그 외(unknown, done, execute 등) 모두 종료
if [[ "$PHASE" != "plan" ]]; then
exit 0
fi
# AfterAgent 이중 실행 방지: 에이전트가 직접 호출한 턴이면 건너뜀
LOCK_FILE="/tmp/jeo-plannotator-direct.lock"
if [[ -f "$LOCK_FILE" ]]; then
rm -f "$LOCK_FILE"
exit 0
fi
PLAN_FILE="$(pwd)/plan.md"
test -f "$PLAN_FILE" || exit 0
LOOP_SCRIPT_CANDIDATES=(
"$(pwd)/.agent-skills/jeo/scripts/plannotator-plan-loop.sh"
"$HOME/.codex/skills/jeo/scripts/plannotator-plan-loop.sh"
"$HOME/.agent-skills/jeo/scripts/plannotator-plan-loop.sh"
)
LOOP_SCRIPT=""
for candidate in "${LOOP_SCRIPT_CANDIDATES[@]}"; do
if [[ -f "$candidate" ]]; then
LOOP_SCRIPT="$candidate"
break
fi
done
if [[ -n "$LOOP_SCRIPT" ]]; then
set +e
bash "$LOOP_SCRIPT" "$PLAN_FILE" /tmp/plannotator_feedback.txt 3
LOOP_RC=$?
set -e
if [[ "$LOOP_RC" -eq 0 ]]; then
echo "[JEO] plannotator approved=true (written to jeo-state.json)"
elif [[ "$LOOP_RC" -eq 10 ]]; then
echo "[JEO] plannotator approved=false — feedback written to jeo-state.json"
elif [[ "$LOOP_RC" -eq 32 ]]; then
echo "[JEO] plannotator unavailable: localhost bind blocked (sandbox/CI)." >&2
echo "[JEO] run PLAN gate in local TTY to use manual fallback approve/feedback." >&2
fi
else
PLANNOTATOR_RUNTIME_HOME="/tmp/jeo-$(python3 -c "import hashlib,os; print(f'/tmp/jeo-{hashlib.md5(os.getcwd().encode()).hexdigest()[:8]}')")/.plannotator"
mkdir -p "$PLANNOTATOR_RUNTIME_HOME"
python3 -c "
import json, sys
plan = open(sys.argv[1]).read()
sys.stdout.write(json.dumps({'tool_input': {'plan': plan, 'permission_mode': 'acceptEdits'}}))
" "$PLAN_FILE" | env HOME="$PLANNOTATOR_RUNTIME_HOME" PLANNOTATOR_HOME="$PLANNOTATOR_RUNTIME_HOME" plannotator > /tmp/plannotator_feedback.txt 2>&1 || true
fi
HOOKEOF
chmod +x "${GEMINI_HOOK_DIR}/jeo-plannotator.sh"
# Create agentation AfterAgent hook (phase-guarded + submit-gated)
cat > "${GEMINI_HOOK_DIR}/jeo-agentation.sh" << 'AGENTHOOKEOF'
#!/usr/bin/env bash
# JEO AfterAgent hook — check pending agentation annotations during VERIFY_UI phase
# Submit gate: only process annotations after explicit Send Annotations / onSubmit confirmation.
# Phase guard: only fire during verify_ui phase
JEO_STATE="${PWD}/.omc/state/jeo-state.json"
if [[ -f "$JEO_STATE" ]]; then
PHASE=$(python3 -c "import json; print(json.load(open('$JEO_STATE')).get('phase',''))" 2>/dev/null || echo "")
SUBMIT_GATE=$(python3 -c "import json; print(json.load(open('$JEO_STATE')).get('agentation',{}).get('submit_gate_status',''))" 2>/dev/null || echo "")
if [[ "$PHASE" != "verify_ui" ]]; then
exit 0
fi
if [[ "$SUBMIT_GATE" != "submitted" ]]; then
exit 0
fi
else
exit 0 # No state file means JEO is not active
fi
# Check agentation server and report pending annotations
PENDING=$(curl -sf --connect-timeout 2 http://localhost:4747/pending 2>/dev/null) || exit 0
COUNT=$(echo "$PENDING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo 0)
if [ "$COUNT" -gt 0 ]; then
echo "=== AGENTATION: ${COUNT} annotations pending ==="
echo "$PENDING" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for i, a in enumerate(d.get('annotations', [])):
sev = a.get('severity', 'suggestion')
print(f' [{i+1}] [{sev}] {a.get(\"element\",\"?\")} ({a.get(\"elementPath\",\"?\")[:60]})')
print(f' {a.get(\"comment\",\"\")[:80]}')
" 2>/dev/null
echo "=== END ==="
fi
AGENTHOOKEOF
chmod +x "${GEMINI_HOOK_DIR}/jeo-agentation.sh"
python3 - <<PYEOF
import json, os
settings_path = os.path.expanduser("~/.gemini/settings.json")
hook_path = os.path.expanduser("~/.gemini/hooks/jeo-plannotator.sh")
try:
with open(settings_path) as f:
settings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
settings = {}
hooks = settings.setdefault("hooks", {})
# AfterAgent is the correct Gemini CLI hook (ExitPlanMode is Claude Code-only)
after_agent = hooks.setdefault("AfterAgent", [])
# Migrate old-format entries (flat {"type":"command",...}) to new matcher format
migrated = False
new_after_agent = []
for entry in after_agent:
if "matcher" in entry and "hooks" in entry:
# Ensure timeout on plannotator hooks (1800s for blocking UI wait)
for h in entry.get("hooks", []):
if "plannotator" in h.get("command", "") and "timeout" not in h:
h["timeout"] = 1800
migrated = True
new_after_agent.append(entry)
elif entry.get("type") == "command":
# Old format -> wrap in new matcher format
if "timeout" not in entry:
entry["timeout"] = 300
new_after_agent.append({"matcher": "", "hooks": [entry]})
migrated = True
else:
new_after_agent.append(entry)
if migrated:
hooks["AfterAgent"] = new_after_agent
after_agent = new_after_agent
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("\\u2713 AfterAgent hooks migrated to new matcher format with timeouts")
# Check if jeo plannotator hook already exists (old or new form)
planno_exists = any(
any(
h.get("command", "").startswith("plannotator") or "jeo-plannotator" in h.get("command", "")
for h in entry.get("hooks", [])
)
for entry in after_agent
)
if not planno_exists:
after_agent.append({
"matcher": "",
"hooks": [{
"name": "plannotator-review",
"type": "command",
"command": f"bash {hook_path}",
"timeout": 1800,
"description": "PLAN phase backup gate with no-repeat hash guard"
}]
})
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("✓ plannotator AfterAgent hook added to ~/.gemini/settings.json")
else:
print("\u2713 plannotator hook already present")
# Add agentation AfterAgent hook (phase-guarded + submit-gated)
agentation_hook_path = os.path.expanduser("~/.gemini/hooks/jeo-agentation.sh")
agentation_exists = any(
any(
"jeo-agentation" in h.get("command", "")
for h in entry.get("hooks", [])
)
for entry in after_agent
)
if not agentation_exists:
after_agent.append({
"matcher": "",
"hooks": [{
"name": "agentation-check",
"type": "command",
"command": f"bash {agentation_hook_path}",
"timeout": 300,
"description": "VERIFY_UI submit gate opened: check pending agentation annotations"
}]
})
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("\u2713 agentation AfterAgent hook added to ~/.gemini/settings.json")
else:
print("\u2713 agentation hook already present")
# Add agentation MCP server if missing
mcp_servers = settings.setdefault("mcpServers", {})
if "agentation" not in mcp_servers:
mcp_servers["agentation"] = {
"command": "npx",
"args": ["-y", "agentation-mcp", "server"]
}
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
print("\u2713 agentation MCP server added to ~/.gemini/settings.json")
else:
print("\u2713 agentation MCP already present")
PYEOF
ok "Gemini CLI settings updated"
fi
fi
# ── 3. Update GEMINI.md ───────────────────────────────────────────────────────
if ! $HOOK_ONLY; then
info "Updating ~/.gemini/GEMINI.md..."
JEO_SECTION='
## JEO Orchestration Workflow
Keyword: `jeo` | Tool: Gemini CLI
JEO provides integrated AI agent orchestration across all AI tools.
### Workflow Phases
**PLAN** (plannotator — 직접 blocking 호출 필수):
1. `plan.md` 작성 (목표, 단계, 리스크, 완료 기준 포함)
2. PLAN gate 실행 (& 절대 금지, plannotator 없으면 자동 설치 후 계속 진행):
bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3
# 동작 보장:
# - approve/feedback 입력까지 반드시 대기
# - 같은 plan hash 에 이미 approved/feedback/infrastructure_blocked가 기록돼 있으면 재실행 금지
# - plan.md 내용이 바뀔 때만 gate_status를 pending으로 리셋
# - 세션 종료 시 자동 재시작 (최대 3회)
# - 3회 종료 시 PLAN 종료 여부를 사용자에게 확인
# - exit 32 시 localhost bind 차단(sandbox/CI): local TTY에서 수동 PLAN gate 실행
3. /tmp/plannotator_feedback.txt 읽기
4. "approved":true → EXECUTE 진입 / 미승인 → 피드백 반영 후 plan.md 수정 후 2번 반복
5. PLAN gate exit 32면 인프라 차단이므로 local TTY에서 PLAN gate 재실행
NEVER skip plannotator. NEVER proceed to EXECUTE without approved=true.
**EXECUTE** (BMAD for Gemini):
- BMAD is the primary orchestration fallback when omc team is unavailable
- `/workflow-init` — Initialize BMAD structured workflow
- `/workflow-status` — Check current phase
- Phases: Analysis → Planning → Solutioning → Implementation
**VERIFY** (agent-browser):
- `agent-browser snapshot http://localhost:3000`
- UI/기능 정상 여부 확인
**CLEANUP** (worktree):
- After all work: `bash '"${JEO_SKILL_DIR}"'/scripts/worktree-cleanup.sh`
**ANNOTATE** (agentation watch loop — HTTP API 폴백):
When user says "annotate" or "agentui" (deprecated alias) or asks to process UI annotations:
1. Set `.omc/state/jeo-state.json` → `phase="verify_ui"` and `agentation.submit_gate_status="waiting_for_submit"`
2. Wait for the human to click **Send Annotations** / trigger `onSubmit`
3. Only after that explicit submit signal, reply `ANNOTATE_READY` and update `agentation.submit_gate_status="submitted"`
4. Then GET http://localhost:4747/pending — check count
5. For each annotation: PATCH status:acknowledged, fix code via elementPath, PATCH status:resolved + resolution
6. Repeat until count=0. Emit `AGENTUI_READY` when done.
NEVER poll `/pending` before submit gate opens. NEVER treat draft annotations as actionable.
### ohmg Integration
For Gemini multi-agent orchestration:
```bash
bunx oh-my-ag # Initialize ohmg
/coordinate "<task>" # Coordinate multi-agent task
```
'
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would append JEO section to $GEMINI_MD"
else
mkdir -p "$(dirname "$GEMINI_MD")"
[[ -f "$GEMINI_MD" ]] && cp "$GEMINI_MD" "${GEMINI_MD}.jeo.bak"
if [[ -f "$GEMINI_MD" ]] && grep -q "^## JEO Orchestration Workflow" "$GEMINI_MD"; then
GEMINI_MD="$GEMINI_MD" JEO_SECTION="$JEO_SECTION" python3 - <<'PYEOF'
import os
import re
path = os.environ["GEMINI_MD"]
section = os.environ["JEO_SECTION"].strip()
text = open(path, encoding="utf-8").read()
pattern = re.compile(r"\n## JEO Orchestration Workflow\n.*?\Z", re.S)
updated = pattern.sub("\n" + section + "\n", text.rstrip() + "\n")
with open(path, "w", encoding="utf-8") as f:
f.write(updated)
PYEOF
ok "JEO section synced in ~/.gemini/GEMINI.md"
else
echo "$JEO_SECTION" >> "$GEMINI_MD"
ok "JEO instructions added to ~/.gemini/GEMINI.md"
fi
fi
fi
echo ""
echo "Gemini CLI usage after setup:"
echo " gemini --approval-mode plan ← Plan mode (plannotator fires on exit)"
echo " /workflow-init ← BMAD orchestration"
echo " bunx oh-my-ag ← ohmg multi-agent (Gemini)"
echo ""
ok "Gemini CLI setup complete"
echo ""
```
### scripts/setup-opencode.sh
```bash
#!/usr/bin/env bash
# JEO Skill — OpenCode Plugin Registration
# Configures: opencode.json plugin entry + agentation MCP + slash commands
# Usage: bash setup-opencode.sh [--dry-run]
set -euo pipefail
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
info() { echo -e "${BLUE}→${NC} $*"; }
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
# Resolve opencode.json priority:
# 1) cwd (project-level config), then
# 2) ~/.config/opencode/opencode.json, then
# 3) legacy ~/.opencode.json
OPENCODE_JSON=""
for candidate in "./opencode.json" "${HOME}/.config/opencode/opencode.json" "${HOME}/opencode.json"; do
[[ -f "$candidate" ]] && OPENCODE_JSON="$candidate" && break
done
echo ""
echo "JEO — OpenCode Plugin Setup"
echo "==========================="
# ── 1. Check OpenCode ────────────────────────────────────────────────────────
if ! command -v opencode >/dev/null 2>&1; then
warn "opencode CLI not found. Install via: npm install -g opencode-ai"
fi
# Optional runtime check: OpenCode writes SQLite/cache/state under XDG dirs.
# If HOME-backed defaults are not writable, suggest tmp-based launcher.
if command -v opencode >/dev/null 2>&1; then
OPENCODE_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"
if ! (mkdir -p "$OPENCODE_DATA_DIR" 2>/dev/null && [[ -w "$OPENCODE_DATA_DIR" ]]); then
warn "OpenCode data dir is not writable: $OPENCODE_DATA_DIR"
warn "Use: bash $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/run-opencode-safe.sh"
fi
fi
# ── 2. Configure opencode.json ────────────────────────────────────────────────
info "Configuring opencode.json..."
if [[ -z "$OPENCODE_JSON" ]]; then
OPENCODE_JSON="${HOME}/.config/opencode/opencode.json"
warn "No opencode.json found — will create at $OPENCODE_JSON"
fi
if $DRY_RUN; then
echo -e "${YELLOW}[DRY-RUN]${NC} Would configure $OPENCODE_JSON with JEO plugin"
else
# Backup
mkdir -p "$(dirname "$OPENCODE_JSON")"
[[ -f "$OPENCODE_JSON" ]] && cp "$OPENCODE_JSON" "${OPENCODE_JSON}.jeo.bak"
OPENCODE_JSON_PATH="$OPENCODE_JSON" python3 - <<'PYEOF'
import json, os
config_path = os.environ["OPENCODE_JSON_PATH"]
try:
with open(config_path) as f:
config = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
config = {}
# Set schema (normalize old escaped key if present)
legacy_schema_key = "\\$schema"
if legacy_schema_key in config:
if "$schema" not in config:
config["$schema"] = config[legacy_schema_key]
config.pop(legacy_schema_key)
config.setdefault("$schema", "https://opencode.ai/config.json")
# Add plugins
plugins = config.setdefault("plugin", [])
# Add plannotator if not present
if "@plannotator/opencode@latest" not in plugins:
plugins.append("@plannotator/opencode@latest")
print("✓ plannotator plugin added")
# Add omx if not present
if "@oh-my-opencode/opencode@latest" not in plugins:
plugins.append("@oh-my-opencode/opencode@latest")
print("\u2713 omx (oh-my-opencode) plugin added")
# Add agentation MCP if not present
mcp_config = config.setdefault("mcp", {})
if "agentation" not in mcp_config:
mcp_config["agentation"] = {
"type": "local",
"command": ["npx", "-y", "agentation-mcp", "server"]
}
print("\u2713 agentation MCP added to opencode.json")
else:
print("\u2713 agentation MCP already present")
# Migrate legacy instructions to valid type (OpenCode expects array)
legacy_instructions = config.get("instructions")
if isinstance(legacy_instructions, str):
text = legacy_instructions.strip()
config["instructions"] = [text] if text else []
elif legacy_instructions is not None and not isinstance(legacy_instructions, list):
config["instructions"] = [str(legacy_instructions)]
# Register JEO slash commands in OpenCode's "command" table
commands = config.setdefault("command", {})
jeo_commands = {
"jeo-plan": {
"description": "JEO planning workflow (ralph + plannotator)",
"template": (
"Write plan.md, then run mandatory PLAN gate: "
"bash .agent-skills/jeo/scripts/plannotator-plan-loop.sh plan.md /tmp/plannotator_feedback.txt 3. "
"If plannotator is missing, the PLAN gate auto-installs it first. "
"This waits for approve/feedback, restarts dead sessions up to 3 times, "
"and asks whether to stop PLAN after repeated failures."
),
},
"jeo-exec": {
"description": "JEO execute workflow (team/bmad)",
"template": (
"Execute approved plan using team agents if available; otherwise use BMAD workflow phases."
),
},
"jeo-annotate": {
"description": "Process agentation annotations (VERIFY_UI loop)",
"template": (
"Run agentation watch loop: acknowledge, implement fix, resolve, and repeat until pending count is 0."
),
},
"jeo-verify": {
"description": "Verify browser behavior with agent-browser",
"template": "Run agent-browser snapshot and verify the UI/flow for the current task.",
},
"jeo-cleanup": {
"description": "Cleanup worktrees after JEO completion",
"template": "Run: bash .agent-skills/jeo/scripts/worktree-cleanup.sh",
},
}
added = 0
for name, spec in jeo_commands.items():
if name not in commands:
commands[name] = spec
added += 1
if added:
print(f"\u2713 Added {added} JEO command(s) to opencode.json")
else:
print("\u2713 JEO commands already present")
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print(f"✓ opencode.json saved: {config_path}")
PYEOF
ok "OpenCode configuration updated"
fi
echo ""
echo "OpenCode slash commands after setup:"
echo " /jeo-plan ← Start planning workflow"
echo " /jeo-exec \u2190 Execute task"
echo " /jeo-annotate \u2190 agentation watch loop (VERIFY_UI); /jeo-agentui is deprecated alias"
echo " /jeo-verify \u2190 Verify UI with agent-browser"
echo " /jeo-cleanup ← Clean worktrees"
echo " /plannotator-review ← Code review UI"
echo ""
echo "If OpenCode shows 'readonly database', run:"
echo " bash $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/run-opencode-safe.sh"
echo ""
echo " Restart OpenCode to activate plugins."
echo ""
ok "OpenCode setup complete"
echo ""
```
### scripts/plannotator-plan-loop.sh
```bash
#!/usr/bin/env bash
# JEO PLAN gate for plannotator.
# Guarantees blocking review, retries dead sessions, and requires explicit stop decision.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Cross-platform temp dir: respects TMPDIR (macOS/Linux) TMP/TEMP (Windows Git Bash)
_TMPDIR="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}"
PLAN_FILE="${1:-plan.md}"
FEEDBACK_FILE="${2:-}"
MAX_RESTARTS="${3:-3}"
# Dedicated port for plannotator (IANA unassigned — rarely conflicts with other services).
# Override with: PLANNOTATOR_PORT=XXXXX bash plannotator-plan-loop.sh ...
PLANNOTATOR_PORT="${PLANNOTATOR_PORT:-47291}"
# Seconds to wait for plannotator to bind its port after launch (startup detection).
PLANNOTATOR_START_TIMEOUT="${PLANNOTATOR_START_TIMEOUT:-15}"
PORT_ERROR_REGEX='Failed to start server\. Is port .* in use|EADDRINUSE|EPERM|operation not permitted|Failed to listen'
if ! command -v plannotator >/dev/null 2>&1; then
if ! bash "$SCRIPT_DIR/ensure-plannotator.sh" --quiet; then
echo "[JEO][PLAN] plannotator is required in PLAN phase." >&2
exit 127
fi
fi
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
if [[ ! -f "$PLAN_FILE" ]]; then
echo "[JEO][PLAN] plan file not found: $PLAN_FILE" >&2
exit 2
fi
if ! [[ "$MAX_RESTARTS" =~ ^[0-9]+$ ]] || [[ "$MAX_RESTARTS" -lt 1 ]]; then
echo "[JEO][PLAN] invalid MAX_RESTARTS: $MAX_RESTARTS" >&2
exit 2
fi
PLAN_HASH="$(python3 - "$PLAN_FILE" <<'PYEOF'
import hashlib, pathlib, sys
path = pathlib.Path(sys.argv[1])
try:
data = path.read_text(encoding="utf-8")
except Exception:
data = ""
print(hashlib.sha256(data.encode("utf-8")).hexdigest() if data else "")
PYEOF
)"
SESSION_KEY="$(python3 -c "import hashlib,os; print(hashlib.md5(os.getcwd().encode()).hexdigest()[:8])" 2>/dev/null || echo "default")"
FEEDBACK_DIR="${_TMPDIR}/jeo-${SESSION_KEY}"
RUNTIME_HOME="${FEEDBACK_DIR}/.plannotator"
mkdir -p "$FEEDBACK_DIR" "$RUNTIME_HOME"
if [[ -z "$FEEDBACK_FILE" ]]; then
FEEDBACK_FILE="${FEEDBACK_DIR}/plannotator_feedback.txt"
else
mkdir -p "$(dirname "$FEEDBACK_FILE")"
fi
write_manual_feedback_json() {
local approved="$1"
local note="${2:-}"
python3 - "$FEEDBACK_FILE" "$approved" "$note" <<'PYEOF'
import json, sys
path, approved_raw, note = sys.argv[1], sys.argv[2], sys.argv[3]
approved = approved_raw.lower() == "true"
payload = {
"approved": approved,
"source": "jeo-manual-fallback",
"note": note,
}
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
PYEOF
}
write_state_gate_status() {
local status="$1"
JEO_GATE_STATUS="$status" python3 -c "
import json, os, subprocess, datetime
try:
root = subprocess.check_output(['git','rev-parse','--show-toplevel'], stderr=subprocess.DEVNULL).decode().strip()
except Exception:
root = os.getcwd()
f = os.path.join(root, '.omc/state/jeo-state.json')
if os.path.exists(f):
try:
import fcntl
with open(f, 'r+') as fh:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
d = json.load(fh)
d['plan_gate_status'] = os.environ['JEO_GATE_STATUS']
d['updated_at'] = datetime.datetime.utcnow().isoformat() + 'Z'
fh.seek(0); json.dump(d, fh, indent=2); fh.truncate()
finally:
fcntl.flock(fh, fcntl.LOCK_UN)
except Exception:
pass
" 2>/dev/null || true
}
persist_plan_state() {
local gate_status="$1"
local approved="$2"
local review_method="${3:-plannotator}"
local feedback_path="${4:-}"
JEO_GATE_STATUS="$gate_status" \
JEO_APPROVED="$approved" \
JEO_REVIEW_METHOD="$review_method" \
JEO_PLAN_HASH="$PLAN_HASH" \
JEO_FEEDBACK_FILE="$feedback_path" \
python3 - <<'PYEOF'
import datetime
import json
import os
import subprocess
try:
root = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except Exception:
root = os.getcwd()
state_path = os.path.join(root, ".omc", "state", "jeo-state.json")
if not os.path.exists(state_path):
raise SystemExit(0)
try:
import fcntl
except Exception:
fcntl = None
feedback_payload = None
feedback_file = os.environ.get("JEO_FEEDBACK_FILE", "")
if feedback_file and os.path.exists(feedback_file):
try:
feedback_payload = json.load(open(feedback_file))
except Exception:
feedback_payload = None
with open(state_path, "r+", encoding="utf-8") as fh:
if fcntl:
fcntl.flock(fh, fcntl.LOCK_EX)
try:
state = json.load(fh)
gate_status = os.environ.get("JEO_GATE_STATUS", "pending")
approved = os.environ.get("JEO_APPROVED", "false").lower() == "true"
review_method = os.environ.get("JEO_REVIEW_METHOD", "plannotator")
plan_hash = os.environ.get("JEO_PLAN_HASH", "")
state["plan_gate_status"] = gate_status
state["plan_approved"] = approved
state["plan_review_method"] = review_method
state["plan_current_hash"] = plan_hash
state["last_reviewed_plan_hash"] = plan_hash
state["last_reviewed_plan_at"] = datetime.datetime.utcnow().isoformat() + "Z"
if approved:
state["phase"] = "execute"
elif gate_status == "feedback_required" and feedback_payload is not None:
state["plannotator_feedback"] = feedback_payload
state["updated_at"] = datetime.datetime.utcnow().isoformat() + "Z"
fh.seek(0)
json.dump(state, fh, ensure_ascii=False, indent=2)
fh.truncate()
finally:
if fcntl:
fcntl.flock(fh, fcntl.LOCK_UN)
PYEOF
}
prepare_state_for_plan_hash() {
JEO_PLAN_HASH="$PLAN_HASH" python3 - <<'PYEOF'
import datetime
import json
import os
import subprocess
import sys
try:
root = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
except Exception:
root = os.getcwd()
state_path = os.path.join(root, ".omc", "state", "jeo-state.json")
if not os.path.exists(state_path):
raise SystemExit(0)
state = json.load(open(state_path, encoding="utf-8"))
current_hash = os.environ.get("JEO_PLAN_HASH", "")
last_hash = state.get("last_reviewed_plan_hash")
gate_status = state.get("plan_gate_status", "pending")
if current_hash and last_hash == current_hash and gate_status in {"approved", "manual_approved"}:
print("SKIP_APPROVED")
raise SystemExit(0)
if current_hash and last_hash == current_hash and gate_status == "feedback_required":
print("SKIP_FEEDBACK")
raise SystemExit(0)
if current_hash and last_hash == current_hash and gate_status == "infrastructure_blocked":
print("SKIP_BLOCKED")
raise SystemExit(0)
state["plan_current_hash"] = current_hash
if current_hash and last_hash and current_hash != last_hash and gate_status != "pending":
state["plan_gate_status"] = "pending"
state["plan_approved"] = False
state["updated_at"] = datetime.datetime.utcnow().isoformat() + "Z"
with open(state_path, "w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
print("RESET_FOR_NEW_HASH")
PYEOF
}
STATE_PLAN_GUARD="$(prepare_state_for_plan_hash 2>/dev/null || true)"
case "$STATE_PLAN_GUARD" in
SKIP_APPROVED)
echo "[JEO][PLAN] plan gate already approved for current plan hash — skipping re-entry." >&2
exit 0
;;
SKIP_FEEDBACK)
echo "[JEO][PLAN] feedback already recorded for current plan hash — revise the plan before re-opening plannotator." >&2
exit 10
;;
SKIP_BLOCKED)
echo "[JEO][PLAN] infrastructure-blocked state already recorded for current plan hash — waiting for manual approval path." >&2
exit 32
;;
RESET_FOR_NEW_HASH)
echo "[JEO][PLAN] detected revised plan content — resetting gate status to pending." >&2
;;
esac
manual_fallback_gate() {
if [[ ! -t 0 || ! -t 1 ]]; then
return 32
fi
echo "[JEO][PLAN] plannotator UI를 열 수 없는 환경입니다. 수동 PLAN gate로 전환합니다." >&2
echo "[JEO][PLAN] 선택: [a]pprove / [f]eedback / [s]top" >&2
read -r -p "선택하세요 [a/f/s]: " choice
case "${choice,,}" in
a|approve)
write_manual_feedback_json "true" "manual-approve (fallback gate)"
persist_plan_state "manual_approved" "true" "manual" "$FEEDBACK_FILE"
echo "[JEO][PLAN] manual approved=true" >&2
return 0
;;
f|feedback)
read -r -p "피드백 내용을 입력하세요: " fb
write_manual_feedback_json "false" "${fb:-manual-feedback (fallback gate)}"
persist_plan_state "feedback_required" "false" "manual" "$FEEDBACK_FILE"
echo "[JEO][PLAN] manual approved=false (feedback)" >&2
return 10
;;
s|stop|n|no)
echo "[JEO][PLAN] user requested PLAN stop." >&2
return 30
;;
*)
echo "[JEO][PLAN] invalid choice. stopping PLAN." >&2
return 31
;;
esac
}
# probe_plannotator_port PORT
# Returns:
# 0 — port is free and localhost bind is permitted (plannotator can start)
# 1 — port is already in use (conflict — another instance may be running)
# 2 — localhost bind is not permitted (sandbox/CI restriction)
probe_plannotator_port() {
local port="${1:-$PLANNOTATOR_PORT}"
# Primary: Node.js probe on the exact plannotator port
if command -v node >/dev/null 2>&1; then
node -e "
const net=require('net');
const s=net.createServer();
s.on('error',(e)=>{
process.exitCode = e.code==='EADDRINUSE' ? 1 : 2;
process.exit();
});
s.listen({host:'127.0.0.1',port:${port}},()=>s.close(()=>process.exit(0)));
" >/dev/null 2>&1
return $?
fi
# Fallback: Python3 socket probe on the exact port
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import socket, sys
port = int(sys.argv[1])
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0)
try:
s.bind(('127.0.0.1', port))
s.close()
sys.exit(0)
except OSError as e:
import errno
sys.exit(1 if e.errno in (errno.EADDRINUSE,) else 2)
" "$port" 2>/dev/null
return $?
fi
# Neither available — assume free (conservative default)
return 0
}
# wait_for_listen PORT PID [TIMEOUT_SECS]
# Polls until plannotator binds PORT, the process dies, or timeout is reached.
# Returns:
# 0 — plannotator is listening (browser UI ready)
# 1 — process exited before binding
# 2 — timeout reached (process still alive but port not yet bound)
wait_for_listen() {
local port="$1" pid="$2" timeout="${3:-$PLANNOTATOR_START_TIMEOUT}"
local elapsed=0
while [[ $elapsed -lt $timeout ]]; do
kill -0 "$pid" 2>/dev/null || return 1
# Primary: bash built-in /dev/tcp (no subprocess, available on Linux/macOS)
if ( exec 3<>/dev/tcp/127.0.0.1/"$port" ) 2>/dev/null; then
return 0
fi
# Fallback: Python3 socket connect (Windows Git Bash, systems without /dev/tcp)
if command -v python3 >/dev/null 2>&1; then
python3 -c "
import socket, sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(0.5)
try:
s.connect(('127.0.0.1', int(sys.argv[1])))
s.close()
sys.exit(0)
except Exception:
sys.exit(1)
" "$port" 2>/dev/null && return 0
fi
sleep 1
(( elapsed++ )) || true
done
return 2
}
# Pre-launch probe: verify the dedicated port is available before starting plannotator.
if [[ "${JEO_SKIP_LISTEN_PROBE:-0}" != "1" ]]; then
set +e
probe_plannotator_port "$PLANNOTATOR_PORT"
probe_port_rc=$?
set -e
if [[ "$probe_port_rc" -eq 2 ]]; then
echo "[JEO][PLAN] localhost bind probe failed — listen not permitted (sandbox/CI)." >&2
set +e
manual_fallback_gate
probe_rc=$?
set -e
if [[ "$probe_rc" -eq 32 ]]; then
write_state_gate_status "infrastructure_blocked"
fi
exit "$probe_rc"
elif [[ "$probe_port_rc" -eq 1 ]]; then
echo "[JEO][PLAN] port ${PLANNOTATOR_PORT} already in use — another plannotator instance may be running." >&2
echo "[JEO][PLAN] override with: PLANNOTATOR_PORT=<free-port> or kill the existing process." >&2
exit 32
fi
echo "[JEO][PLAN] port ${PLANNOTATOR_PORT} is available — starting plannotator." >&2
fi
attempt=1
while (( attempt <= MAX_RESTARTS )); do
: > "$FEEDBACK_FILE"
# Write plan JSON to a temp file so plannotator can be backgrounded (enables PID tracking)
PLAN_JSON_FILE="${FEEDBACK_DIR}/plan_input.json"
python3 -c "
import json, sys
plan = open(sys.argv[1]).read()
sys.stdout.write(json.dumps({'tool_input': {'plan': plan, 'permission_mode': 'acceptEdits'}}))
" "$PLAN_FILE" > "$PLAN_JSON_FILE"
# Launch plannotator with the dedicated port so we can monitor its binding state.
PORT="$PLANNOTATOR_PORT" env HOME="$RUNTIME_HOME" PLANNOTATOR_HOME="$RUNTIME_HOME" \
plannotator < "$PLAN_JSON_FILE" > "$FEEDBACK_FILE" 2>&1 &
PLANNOTATOR_PID=$!
# Phase 1: STARTING — wait for plannotator to bind the port (browser UI ready).
set +e
wait_for_listen "$PLANNOTATOR_PORT" "$PLANNOTATOR_PID" "$PLANNOTATOR_START_TIMEOUT"
listen_rc=$?
set -e
case "$listen_rc" in
0)
echo "[JEO][PLAN] plannotator listening on port ${PLANNOTATOR_PORT} — waiting for user input." >&2
;;
1)
echo "[JEO][PLAN] plannotator exited during startup (attempt ${attempt}/${MAX_RESTARTS})." >&2
wait "$PLANNOTATOR_PID" 2>/dev/null || true
((attempt++)) || true
continue
;;
2)
echo "[JEO][PLAN] plannotator startup timeout (${PLANNOTATOR_START_TIMEOUT}s) — port ${PLANNOTATOR_PORT} not bound yet; continuing to wait." >&2
;;
esac
# Phase 2: LISTENING / RUNNING — block while browser session is active.
while kill -0 "$PLANNOTATOR_PID" 2>/dev/null; do
sleep 1
done
wait "$PLANNOTATOR_PID" 2>/dev/null || true
set +e
python3 - "$FEEDBACK_FILE" <<'PYEOF'
import json, sys
path = sys.argv[1]
try:
payload = json.load(open(path))
except Exception:
sys.exit(20)
approved = payload.get("approved")
if approved is True:
sys.exit(0)
if approved is False:
sys.exit(10)
sys.exit(20)
PYEOF
rc=$?
set -e
if [[ "$rc" -eq 0 ]]; then
echo "[JEO][PLAN] approved=true"
persist_plan_state "approved" "true" "plannotator" "$FEEDBACK_FILE"
exit 0
fi
if [[ "$rc" -eq 10 ]]; then
echo "[JEO][PLAN] approved=false (feedback)"
persist_plan_state "feedback_required" "false" "plannotator" "$FEEDBACK_FILE"
exit 10
fi
if grep -Eiq "$PORT_ERROR_REGEX" "$FEEDBACK_FILE"; then
echo "[JEO][PLAN] plannotator server bind failure detected (EADDRINUSE/EPERM)." >&2
set +e
manual_fallback_gate
fallback_rc=$?
set -e
if [[ "$fallback_rc" -eq 32 ]]; then
write_state_gate_status "infrastructure_blocked"
fi
exit "$fallback_rc"
fi
# Classify crash type for clearer diagnostics before retrying
if [[ -s "$FEEDBACK_FILE" ]]; then
echo "[JEO][PLAN] browser crash detected (non-JSON output, attempt ${attempt}/${MAX_RESTARTS}). restarting..." >&2
else
echo "[JEO][PLAN] plannotator exited without output (attempt ${attempt}/${MAX_RESTARTS}). restarting..." >&2
fi
((attempt++))
done
echo "[JEO][PLAN] plannotator session ended ${MAX_RESTARTS} times." >&2
set +e
manual_fallback_gate
fallback_rc=$?
set -e
if [[ "$fallback_rc" -eq 32 ]]; then
echo "[JEO][PLAN] confirmation required. stop and ask user whether to continue PLAN." >&2
write_state_gate_status "infrastructure_blocked"
# Write a structured blocked-state file so agent frameworks can parse the situation
python3 - "$FEEDBACK_DIR/jeo-blocked.json" "$PLAN_FILE" <<'PYEOF'
import json, sys
out_path, plan_file = sys.argv[1], sys.argv[2]
try:
with open(out_path, "w", encoding="utf-8") as f:
json.dump({
"status": "infrastructure_blocked",
"reason": "plannotator_session_exhausted",
"max_restarts_reached": True,
"action_required": "manual_approval",
"plan_file": plan_file,
"instruction": (
"Review the plan file and manually approve or reject: "
"run the hook script again in a TTY, or set plan_gate_status "
"in .omc/state/jeo-state.json to 'manual_approved'."
),
}, f, ensure_ascii=False, indent=2)
except Exception:
pass
PYEOF
fi
exit "$fallback_rc"
```
### scripts/ensure-plannotator.sh
```bash
#!/usr/bin/env bash
# JEO helper — guarantees plannotator is available before PLAN.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
SKILLS_ROOT="$(dirname "$SKILL_DIR")"
QUIET=false
for arg in "$@"; do
case "$arg" in
--quiet) QUIET=true ;;
-h|--help)
echo "Usage: bash ensure-plannotator.sh [--quiet]"
echo "Ensures the plannotator CLI is installed before JEO PLAN runs."
exit 0
;;
esac
done
log() {
if ! $QUIET; then
echo "$@"
fi
}
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
if command -v plannotator >/dev/null 2>&1; then
log "[JEO][PLAN] plannotator already available: $(command -v plannotator)"
exit 0
fi
if [[ "${JEO_SKIP_PLANNOTATOR_AUTO_INSTALL:-0}" == "1" ]]; then
echo "[JEO][PLAN] plannotator not found and auto-install is disabled." >&2
exit 127
fi
INSTALL_CANDIDATES=(
"$SKILLS_ROOT/plannotator/scripts/install.sh"
"$HOME/.agent-skills/plannotator/scripts/install.sh"
"$HOME/.codex/skills/plannotator/scripts/install.sh"
)
for candidate in "${INSTALL_CANDIDATES[@]}"; do
if [[ ! -f "$candidate" ]]; then
continue
fi
log "[JEO][PLAN] plannotator missing. Installing via $candidate"
if bash "$candidate"; then
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
if command -v plannotator >/dev/null 2>&1; then
log "[JEO][PLAN] plannotator installation completed: $(command -v plannotator)"
exit 0
fi
fi
echo "[JEO][PLAN] installer finished but plannotator is still unavailable: $candidate" >&2
done
echo "[JEO][PLAN] plannotator auto-install failed. Run: bash scripts/install.sh --with-plannotator" >&2
exit 127
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### references/FLOW.md
```markdown
# JEO Workflow — Detailed Reference
## Complete Execution Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ JEO WORKFLOW │
│ │
│ [START] User activates "jeo" keyword with task description │
│ │ │
│ ┌───────────────▼──────────────────┐ │
│ │ PHASE 1: PLAN │ │
│ │ ralph creates plan.md │ │
│ │ plannotator reviews visually │ │
│ │ same hash never reopens gate │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Approve → continue │ │ │
│ │ │ Feedback → re-plan │ │ │
│ │ └──────────────────────────┘ │ │
│ └───────────────┬──────────────────┘ │
│ │ │
│ ┌───────────────▼──────────────────┐ │
│ │ PHASE 2: EXECUTE │ │
│ │ │ │
│ │ team available? │ │
│ │ ├─ YES: /omc:team N:executor │ │
│ │ │ staged pipeline │ │
│ │ └─ NO: /bmad /workflow-init │ │
│ │ Analysis→Planning→ │ │
│ │ Solutioning→Implementation│ │
│ └───────────────┬──────────────────┘ │
│ │ │
│ │ PHASE 3: VERIFY │ │
│ │ agent-browser snapshot <url> │ │
│ │ UI/기능 동작 확인 │ │
│ │ │ │
│ │ [annotate keyword? (agentui alias)]│ │
│ │ └─ YES: VERIFY_UI (agentation) │ │
│ │ wait for onSubmit first │ │
│ │ ack → fix → resolve │ │
│ └───────────────┬──────────────────┘ │
│ │ │
│ ┌───────────────▼──────────────────┐ │
│ │ PHASE 4: CLEANUP │ │
│ │ bash scripts/worktree-cleanup.sh│ │
│ │ clean worktrees only by default │ │
│ │ git worktree prune │ │
│ └───────────────┬──────────────────┘ │
│ │ │
│ [DONE] │
└─────────────────────────────────────────────────────────────────┘
```
---
## annotate — agentation Watch Loop (VERIFY_UI Sub-Phase)
```
annotate keyword detected (or agentui alias — or user requests UI annotation review)
│
▼
[PREFLIGHT] 3-step check before entering watch loop:
│ 1. GET /health → server running?
│ 2. GET /sessions → <Agentation> component mounted?
│ 3. GET /pending → baseline annotation count
│
├─ FAIL (server down) → retry 3x, 5s interval → ERROR (exit with message)
│
▼ OK → update jeo-state.json: phase="verify_ui", agentation.active=true
[SUBMIT GATE] wait for explicit Send Annotations / onSubmit
state: agentation.submit_gate_status="waiting_for_submit"
│
├─ no submit yet → stay idle, do not poll /pending
│
▼ submit observed → set submit_gate_status="submitted"
[WATCH] agentation_watch_annotations({batchWindowSeconds:10, timeoutSeconds:120})
blocking — waits for submitted annotations or timeout
│
├─ Annotations received (sorted by severity: blocking → important → suggestion):
│ │
│ ▼
│ [ACK] agentation_acknowledge_annotation({id})
│ → status: 'acknowledged' → spinner shown in toolbar
│ │
│ ▼
│ [FIND] grep/AST search using annotation.elementPath (CSS selector)
│ annotation.comment → understand desired change
│ │
│ ▼
│ [FIX] apply code change to matched component/file
│ │
│ ▼
│ [RESOLVE] agentation_resolve_annotation({id, summary})
│ → status: 'resolved' → green checkmark in toolbar
│ │
│ ▼
│ [RE-SNAPSHOT] agent-browser snapshot <url> ← verify fix visually
│ → compare before/after
│ │
│ └─ Next annotation → repeat ACK→FIND→FIX→RESOLVE→RE-SNAPSHOT
│
├─ count=0 (all resolved)
│ └─ update jeo-state.json: agentation.exit_reason="all_resolved"
│ └─ VERIFY_UI complete → proceed to CLEANUP
│
└─ timeout (120s)
└─ update jeo-state.json: agentation.exit_reason="timeout"
└─ summarize what was/wasn't addressed → proceed to CLEANUP
```
**HTTP REST API (Codex / Gemini / OpenCode fallback — no MCP):**
```
LOOP:
GET http://localhost:4747/pending → {count, annotations[]}
if count == 0 → break (done)
for each annotation:
PATCH .../annotations/:id {status:"acknowledged"}
[search & fix code via elementPath]
PATCH .../annotations/:id {status:"resolved", resolution:"<summary>"}
sleep 5 → repeat
```
### VERIFY_UI Internal State Machine
```
IDLE ──(annotate keyword)──► PREFLIGHT
│
┌────────────┤
│ │
FAIL OK
│ │
▼ ▼
RECOVER WAIT_FOR_SUBMIT
(3x retry) │
│ ┌─────┼───────────────┐
│ │ │ │
ERROR submit no submit timeout
│ │ │ │
▼ ▼ ▼ ▼
FAIL WATCHING IDLE TIMEOUT
│ │
ACK→FIX→RESOLVE report
│
RE-SNAPSHOT
│
issues? ─Y─► WATCHING
│
N
▼
DONE
```
### plannotator-agentation Phase Separation
```
Phase Guard: hooks check jeo-state.json phase before executing
PLAN phase:
plannotator ✅ (allowed)
agentation ❌ (blocked by phase guard)
rule same hash + approved/manual_approved => skip re-entry (exit 0)
rule same hash + feedback_required => skip re-entry (exit 1, NOT exit 0)
rule same hash + infrastructure_blocked => skip re-entry (exit 32)
⚠️ feedback_required is NOT terminal — it means plan was REJECTED.
Same hash + feedback_required must exit 1 (not approved), not exit 0.
Only a revised plan (new hash) resets gate to pending and reopens plannotator.
EXECUTE phase:
plannotator ❌ (blocked by phase guard)
agentation ❌ (blocked by phase guard)
VERIFY / VERIFY_UI phase:
plannotator ❌ (blocked by phase guard)
agentation ✅ (allowed only after submit gate opens)
```
---
## Platform-Specific Execution Paths
### Claude Code (Primary)
```
jeo keyword detected
│
├─ omc available? → /omc:team N:executor (team orchestration)
│ ├─ team-plan: explore + planner agents
│ ├─ team-prd: analyst agent
│ ├─ team-exec: executor agents (parallel)
│ ├─ team-verify: verifier + reviewers
│ └─ team-fix: debugger/executor (loop until done)
│
└─ plannotator hook: ExitPlanMode → JEO plan-gate wrapper
└─ skips same reviewed hash, otherwise opens browser UI
```
**State file**: `{worktree}/.omc/state/jeo-state.json`
### Codex CLI
```
/prompts:jeo activated
│
├─ Plan: Write plan.md manually or via ralph prompt
├─ Execute: BMAD /workflow-init (no native team support)
├─ Verify: agent-browser snapshot <url>
└─ Cleanup: bash .agent-skills/jeo/scripts/worktree-cleanup.sh
```
**Config**: `~/.codex/config.toml` (developer_instructions)
**Prompt**: `~/.codex/prompts/jeo.md`
### Gemini CLI
```
gemini --approval-mode plan
│
├─ Plan mode: write plan → exit → plannotator fires
├─ Execute: ohmg (bunx oh-my-ag) or BMAD /workflow-init
├─ Verify: agent-browser snapshot <url>
└─ Cleanup: bash .agent-skills/jeo/scripts/worktree-cleanup.sh
```
**Config**: `~/.gemini/settings.json` (AfterAgent hook)
**Instructions**: `~/.gemini/GEMINI.md`
### OpenCode
```
/jeo-plan → /jeo-exec → /jeo-status → /jeo-cleanup
│
├─ omx (oh-my-opencode): /omx:team N:executor "<task>"
├─ BMAD fallback: /workflow-init
├─ plannotator: /plannotator-review (code review)
└─ Slash commands registered via opencode.json
```
**Config**: `opencode.json` (plugins + instructions)
---
## State Machine
```
States: plan → execute → verify → verify_ui? → cleanup → done
Transitions:
plan → execute (plan approved — approved=true or manual_approved)
plan → plan (feedback received, re-plan: gate=feedback_required, agent must revise plan.md)
plan → plan (same hash + approved/manual_approved => skip re-entry, stay at plan→execute)
plan → plan (same hash + feedback_required => exit 1, NOT exit 0 — plan rejected, revise required)
plan → plan (same hash + infrastructure_blocked => exit 32 — manual gate needed)
plan → plan (new hash after feedback => RESET_FOR_NEW_HASH, gate resets to pending, reopens plannotator)
⚠️ v1.3.1 fix: same hash + feedback_required used to incorrectly exit 0 (approved), causing JEO
to enter EXECUTE without proper approval. Now correctly exits 1 (not approved).
execute → verify (tasks complete, browser UI present)
verify → verify_ui (annotate keyword detected — or agentui alias — submit gate waiting)
verify → cleanup (no annotate/agentui, verification passed)
verify_ui → verify_ui (waiting_for_submit until explicit onSubmit/ANNOTATE_READY)
verify_ui → cleanup (annotations all resolved or timeout)
cleanup → done (worktrees removed, prune complete)
```
State persisted in: `.omc/state/jeo-state.json`
```json
{
"phase": "plan",
"task": "Implement user authentication",
"plan_approved": false,
"plan_gate_status": "pending",
"plan_current_hash": null,
"last_reviewed_plan_hash": null,
"last_reviewed_plan_at": null,
"plan_review_method": null,
"team_available": true,
"retry_count": 0,
"last_error": null,
"checkpoint": null,
"created_at": "2026-02-24T00:00:00Z",
"updated_at": "2026-02-24T00:00:00Z",
"agentation": {
"active": false,
"session_id": null,
"keyword_used": null,
"submit_gate_status": "idle",
"submit_signal": null,
"submit_received_at": null,
"submitted_annotation_count": 0,
"started_at": null,
"timeout_seconds": 120,
"annotations": {
"total": 0,
"acknowledged": 0,
"resolved": 0,
"dismissed": 0,
"pending": 0
},
"completed_at": null,
"exit_reason": null
}
}
```
**Error recovery fields:**
- `retry_count`: incremented on each Pre-flight failure; ≥ 3 triggers user confirmation
- `last_error`: most recent error message; cleared on successful step entry
- `checkpoint`: last successfully entered phase (`"plan"` | `"execute"` | `"verify"` | `"cleanup"`); used to resume after interruption
**agentation fields:**
- `active`: whether VERIFY_UI watch loop is currently running (used as guard by hooks)
- `session_id`: agentation session ID for resume via `agentation_get_session`
- `keyword_used`: `"annotate"` or `"agentui"` (tracks which keyword triggered entry)
- `submit_gate_status`: `"idle"` | `"waiting_for_submit"` | `"submitted"`; draft annotations stay blocked until submit
- `submit_signal`: which platform opened the submit gate
- `submit_received_at` / `submitted_annotation_count`: audit trail for the submitted batch
- `started_at`: ISO-8601 timestamp when VERIFY_UI watch loop started
- `timeout_seconds`: poll timeout in seconds (default: 120)
- `annotations.*`: cumulative counts by lifecycle status (`total`, `acknowledged`, `resolved`, `dismissed`, `pending`)
- `exit_reason`: `"all_resolved"` | `"timeout"` | `"user_cancelled"` | `"error"`
---
## Team vs BMAD Decision Matrix
| Condition | Executor | Notes |
|-----------|----------|-------|
| Claude Code + omc + AGENT_TEAMS=1 | **team** | Best option — parallel staged pipeline |
| Claude Code + omc (no teams) | **blocked** | Re-run setup and enable experimental teams; JEO should not silently downgrade |
| Codex CLI | **BMAD** | Structured phases, no native team |
| Gemini CLI + ohmg | **ohmg** | Multi-agent via oh-my-ag |
| Gemini CLI (basic) | **BMAD** | Fallback structured workflow |
| OpenCode + omx | **omx team** | oh-my-opencode team orchestration |
| OpenCode (basic) | **BMAD** | Fallback structured workflow |
---
## agent-browser Verify Pattern
```bash
# 앱 실행 중인 URL에서 스냅샷 캡처
agent-browser snapshot http://localhost:3000
# 특정 요소 확인 (accessibility tree ref 방식)
agent-browser snapshot http://localhost:3000 -i
# → @eN ref 번호로 요소 상태 확인
# 스크린샷 저장
agent-browser screenshot http://localhost:3000 -o verify.png
```
---
## Worktree Manual Cleanup
```bash
# List all worktrees
git worktree list
# Remove specific worktree
git worktree remove /path/to/worktree --force
# Prune stale references
git worktree prune
```
---
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` | Enable native team orchestration | `1` |
| `PLANNOTATOR_REMOTE` | Remote mode (no auto browser open) | unset |
| `PLANNOTATOR_PORT` | Fixed plannotator port | auto |
| `JEO_MAX_ITERATIONS` | Max ralph loop iterations | `20` |
| `AGENTATION_PORT` | agentation MCP server port | `4747` |
| `AGENTATION_TIMEOUT` | annotate watch loop timeout (seconds) | `120` |
---
## Troubleshooting
### plannotator feedback loop: accept not registering / subsequent feedback skipped
**Root cause (v1.3.1 fix)**: When plan hash matched and `plan_gate_status == "feedback_required"`,
the pre-flight guard in SKILL.md incorrectly exited 0 (approved), causing JEO to enter EXECUTE
without a real approval. This affected Codex / Gemini / OpenCode platforms.
**Symptom**: After giving feedback, the plan appears "accepted" but feedback items are missing
on the next round, or plannotator skips re-opening for subsequent feedback rounds.
**Fix applied in v1.3.1**: The guard now exits 1 (not approved) for `feedback_required` + same hash.
**Manual recovery** (if stuck in wrong state):
```bash
# Check current gate state
python3 -c "import json; s=json.load(open('.omc/state/jeo-state.json')); print(s.get('plan_gate_status'), s.get('last_reviewed_plan_hash','')[:8])"
# Force reset gate to pending (allows re-review of same plan)
python3 -c "
import json, datetime
f='.omc/state/jeo-state.json'
s=json.load(open(f))
s['plan_gate_status']='pending'
s['last_reviewed_plan_hash']=None
s['updated_at']=datetime.datetime.utcnow().isoformat()+'Z'
open(f,'w').write(json.dumps(s,indent=2))
print('reset done')
"
```
**Best practice**: Always make substantive content changes to `plan.md` when incorporating feedback.
The gate uses SHA-256 content hash — even a single character change produces a new hash.
### plannotator not opening on plan exit
```bash
# Check hook is configured
bash scripts/check-status.sh
# Re-run hook setup
bash scripts/setup-claude.sh # or setup-gemini.sh
# Verify plannotator CLI is installed
which plannotator || plannotator --version
```
### team mode not working
```bash
# Ensure env variable is set
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
# Or add to ~/.claude/settings.json:
# "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" }
# Fall back to ralph:
/ralph "<task>" --max-iterations=20
```
### worktree removal fails
```bash
# Force remove
git worktree remove /path/to/wt --force
# If git objects missing
git worktree prune --verbose
# Manual directory removal
rm -rf /path/to/worktree
git worktree prune
```
### annotate (agentation) watch loop not triggering
```bash
# Verify agentation-mcp server is running
curl -sf http://localhost:4747/pending
# Should return: {"count": N, "annotations": [...]}
# Check MCP registration (Claude Code)
cat ~/.claude/settings.json | python3 -c "import sys,json; s=json.load(sys.stdin); print(s.get('mcpServers', {}))"
# Restart agentation-mcp server
npx agentation-mcp server
```
```
### references/PLANNOTATOR_RELIABILITY_CONSENSUS_2026-03-06.md
```markdown
# plannotator 신뢰성 이슈 합의서 (2026-03-06)
## 문제 정의
- 이슈: `jeo` PLAN 단계에서 `plannotator`가 시작되지 않아 승인 게이트가 막힘.
- 관측 에러:
- `Failed to start server. Is port 0 in use?`
- `code: "EADDRINUSE"`
- 영향: `approved=true`를 만들 수 없어 PLAN→EXECUTE 전이가 중단됨.
---
## 근거 기반 진단
### 1) 로컬 재현 (Codex sandbox)
- `bun -e "Bun.serve({port:0,...})"` 실행 시 동일 에러 재현.
- `node`로 localhost bind probe 시 `EPERM listen 127.0.0.1`.
- 결론: 일부 실행환경(특히 sandbox/CI/headless)은 localhost listen 자체가 차단됨.
### 2) 외부 근거
- Bun 공식 문서: `Bun.serve`는 실제로 포트를 bind하며 `port: 0`은 랜덤 포트에 listen하도록 설계됨.
- https://bun.com/docs/api/http
- Claude Code 이슈에서도 동일한 plannotator/Bun 에러가 보고됨.
- https://github.com/anthropics/claude-code/issues/11469
- Claude Code hooks는 `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Notification` 등 이벤트 기반 자동화를 지원.
- https://docs.anthropic.com/en/docs/claude-code/hooks
- Gemini CLI hooks도 동기 이벤트 기반으로 동작 (`BeforeTool`, `AfterTool`, `UserPromptSubmit` 등)하며 설정 파일에서 관리.
- https://developers.googleblog.com/tailor-gemini-cli-to-your-workflow-with-hooks/
- Codex는 `notify` 훅/이벤트를 지원하며 config에서 hook command를 연결 가능.
- https://developers.openai.com/codex/config/
---
## 역할별 토론 결과
### QA 관점
- 현재 실패는 일회성 플루크가 아니라 환경 의존 재현 버그.
- 승인 게이트는 “실패 이유를 분류”해야 하며, 인프라 실패와 사용자 피드백 실패를 구분해야 함.
- 수용 기준:
1. localhost bind 불가 시 즉시 감지
2. 불필요한 3회 재시도 제거
3. 대체 승인 루프 제공
### 시스템 엔지니어 관점
- 본질은 `plannotator` UI 서버 의존성 + 실행환경의 listen 제한.
- `EADDRINUSE`는 실제 포트 충돌이 아니라 listen 실패(권한/격리) 래핑일 수 있음.
- 안정화 전략:
- 사전 probe (localhost listen)
- 인프라 차단 전용 종료 코드
- TTY/비TTY 분기 fallback
### Codex 관점
- notify hook 기반 자동 루프는 가능하지만 sandbox 정책에 따라 UI 서버가 실패할 수 있음.
- 따라서 PLAN gate는 “UI-only hard fail”이 아니라 “fallback-aware state machine”이어야 함.
### Claude 관점
- hook 체인은 강력하지만, 실행환경 제약(권한/네트워크) 문제는 hooks만으로 해결 불가.
- 실패를 빠르게 surfacing하고 수동 gate로 전환하는 것이 실용적.
### Gemini CLI 관점
- hook는 안전망으로 유효하지만, PLAN 단계는 blocking gate가 본질.
- UI 서버 실패 시에도 다음 턴으로 피드백 전달 가능한 deterministic fallback이 필요.
---
## 합의안 (최종)
1. PLAN gate를 2레인으로 분리
- Primary: `plannotator` hook mode (UI approve/feedback)
- Fallback: 수동 PLAN gate (TTY에서 approve/feedback/stop)
2. 인프라 실패 전용 exit code 도입
- `exit 32`: localhost bind 불가 또는 plannotator server bind 실패
- 기존 코드 유지:
- `0` approved
- `10` feedback
- `30/31` stop/confirm
3. 재시도 정책 개선
- bind 불가 시 재시도 반복 대신 즉시 fallback 전환.
- 비TTY에서는 `exit 32`로 빠르게 사용자 확인 유도.
4. 문서/운영 규칙 업데이트
- JEO 문서에 `exit 32` 분기와 운영 가이드 추가.
- 플랫폼 설정 스크립트(setup-codex/setup-gemini 등)에서도 안내 문구 통일.
---
## 적용 내용
### 코드
- 수정: `.agent-skills/jeo/scripts/plannotator-plan-loop.sh`
- localhost listen probe 추가
- bind 실패 패턴 감지 (`EADDRINUSE/EPERM/...`)
- TTY 수동 gate 추가 (`approve/feedback/stop`)
- `exit 32` 표준화
### 문서
- 수정: `.agent-skills/jeo/SKILL.md`
- PLAN 결과 분기에 `exit 32` 추가
- 인프라 차단 시 운영 절차 명시
---
## 운영 권고
1. 로컬 개발(권장)
- sandbox 없는 로컬 터미널에서 PLAN gate 수행
- UI 승인 완료 후 EXECUTE 단계로 진입
2. 제한 환경(CI/강샌드박스)
- PLAN 단계는 수동 gate 또는 외부 승인 채널로 분리
- `exit 32`를 “환경 제약”으로 취급하고 실패 집계에서 분리
3. 장기 개선
- plannotator upstream에 Node HTTP fallback 또는 no-listen 승인 모드 제안
- 훅 체인에서 인프라 실패 telemetry를 수집해 자동 진단 메시지 제공
```
### scripts/claude-agentation-submit-hook.py
```python
#!/usr/bin/env python3
"""JEO Claude UserPromptSubmit wrapper for agentation.
Only exposes pending annotations after an explicit submit-style prompt arrives
during the VERIFY_UI gate.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
READY_TOKENS = (
"annotate",
"annotate_ready",
"agentui",
"ui검토",
"send annotations",
"submitted annotations",
"elementpath",
)
def git_root() -> Path:
try:
return Path(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
)
except Exception:
return Path.cwd()
def state_path(root: Path) -> Path:
return root / ".omc" / "state" / "jeo-state.json"
def load_state(root: Path) -> dict[str, Any]:
path = state_path(root)
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
def save_state(root: Path, state: dict[str, Any]) -> None:
path = state_path(root)
if not path.parent.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def flatten_strings(node: Any) -> list[str]:
parts: list[str] = []
if isinstance(node, str):
parts.append(node)
elif isinstance(node, dict):
for value in node.values():
parts.extend(flatten_strings(value))
elif isinstance(node, list):
for value in node:
parts.extend(flatten_strings(value))
return parts
def submitted_prompt(payload: str) -> bool:
try:
data = json.loads(payload)
except Exception:
return False
combined = " ".join(flatten_strings(data)).lower()
return any(token in combined for token in READY_TOKENS)
def fetch_pending() -> dict[str, Any] | None:
try:
with urllib.request.urlopen("http://localhost:4747/pending", timeout=2) as response:
return json.loads(response.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError, ValueError, json.JSONDecodeError):
return None
def update_submit_gate(root: Path, state: dict[str, Any], count: int) -> None:
agentation = state.setdefault("agentation", {})
agentation["submit_gate_status"] = "submitted"
agentation["submit_signal"] = "claude-user-prompt-submit"
agentation["submit_received_at"] = subprocess.check_output(
["python3", "-c", "import datetime;print(datetime.datetime.utcnow().isoformat()+\"Z\")"],
text=True,
).strip()
agentation["submitted_annotation_count"] = count
save_state(root, state)
def main() -> int:
payload = sys.stdin.read()
root = git_root()
state = load_state(root)
if state.get("phase") != "verify_ui":
return 0
if not submitted_prompt(payload):
return 0
pending = fetch_pending()
if not pending:
return 0
count = int(pending.get("count", 0) or 0)
if count <= 0:
return 0
update_submit_gate(root, state, count)
print(f"=== AGENTATION: {count} submitted UI annotations ===")
for index, annotation in enumerate(pending.get("annotations", []), start=1):
element = annotation.get("element", "?")
path = annotation.get("elementPath", "?")
comment = annotation.get("comment", "")
print(f"[{index}] {element} ({path})")
if comment:
print(f" {comment}")
print("=== END ===")
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/claude-plan-gate.py
```python
#!/usr/bin/env python3
"""JEO Claude plan gate wrapper.
Wraps the Claude Code ExitPlanMode hook so JEO can skip redundant plannotator
launches when the current plan content has already been reviewed.
On approval (plannotator exit code 0), ralphmode is automatically activated:
- jeo-state.json: ralphmode_active=true, plan_gate_status="approved"
- project .claude/settings.json: permissionMode="acceptEdits"
This allows the EXECUTE phase (/omc:team) to run with minimal approval prompts.
Ralphmode is deactivated at CLEANUP (worktree-cleanup.sh resets permissionMode).
"""
from __future__ import annotations
import datetime
import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
SKIP_STATUSES = {"approved", "manual_approved", "feedback_required", "infrastructure_blocked"}
RALPHMODE_PERMISSION = "acceptEdits"
def git_root() -> Path:
try:
return Path(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
)
except Exception:
return Path.cwd()
def state_path(root: Path) -> Path:
return root / ".omc" / "state" / "jeo-state.json"
def load_state(root: Path) -> dict[str, Any]:
path = state_path(root)
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
def save_state(root: Path, state: dict[str, Any]) -> None:
path = state_path(root)
if not path.parent.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def find_plan_text(root: Path, payload: str) -> str:
for candidate in (
root / ".omc" / "plans" / "jeo-plan.md",
root / "plan.md",
root / "docs" / "plan.md",
):
if candidate.exists():
try:
return candidate.read_text(encoding="utf-8")
except Exception:
continue
try:
data = json.loads(payload)
except Exception:
return ""
tool_input = data.get("tool_input", {})
if isinstance(tool_input, dict):
plan = tool_input.get("plan")
if isinstance(plan, str):
return plan
return ""
def plan_hash(plan_text: str) -> str:
if not plan_text:
return ""
return hashlib.sha256(plan_text.encode("utf-8")).hexdigest()
def should_skip(state: dict[str, Any], current_hash: str) -> bool:
if state.get("phase") != "plan":
return False
gate_status = state.get("plan_gate_status")
last_hash = state.get("last_reviewed_plan_hash")
return bool(current_hash and gate_status in SKIP_STATUSES and last_hash == current_hash)
def reset_for_revised_plan(root: Path, state: dict[str, Any], current_hash: str) -> None:
last_hash = state.get("last_reviewed_plan_hash")
if not current_hash or current_hash == last_hash:
return
if state.get("plan_gate_status") in SKIP_STATUSES:
state["plan_gate_status"] = "pending"
state["plan_approved"] = False
state["plan_current_hash"] = current_hash
state["updated_at"] = datetime.datetime.utcnow().isoformat() + "Z"
try:
save_state(root, state)
except Exception as exc:
print(f"[JEO] ⚠️ Failed to reset plan state: {exc}", file=sys.stderr)
def run_plannotator(payload: str, plan_text: str = "") -> int:
"""Run plannotator with the hook payload, injecting plan_text if tool_input.plan is missing.
plannotator expects {"tool_input": {"plan": "...", "permission_mode": "..."}} on stdin.
If the ExitPlanMode hook payload does not include tool_input.plan (e.g. Claude Code does
not embed it), we inject the plan text found by find_plan_text() so plannotator can
render the plan UI correctly. Without this injection the browser page has no content
and clicking "Approve" causes a page error.
"""
try:
data = json.loads(payload)
except Exception:
data = {}
if plan_text:
tool_input = data.get("tool_input")
if not isinstance(tool_input, dict):
data["tool_input"] = {"plan": plan_text, "permission_mode": "acceptEdits"}
else:
if not tool_input.get("plan"):
tool_input["plan"] = plan_text
if not tool_input.get("permission_mode"):
tool_input["permission_mode"] = "acceptEdits"
enriched_payload = json.dumps(data)
proc = subprocess.run(["plannotator"], input=enriched_payload, text=True)
return proc.returncode
def activate_ralphmode(root: Path, state: dict[str, Any], current_hash: str = "") -> None:
"""Activate ralphmode after plan approval.
- Sets ralphmode_active=true in jeo-state.json
- Records last_reviewed_plan_hash so should_skip() can skip re-reviews of the same plan
- Writes permissionMode=acceptEdits to project .claude/settings.json
so the EXECUTE phase (/omc:team) runs with minimal approval prompts.
"""
now = datetime.datetime.utcnow().isoformat() + "Z"
state["plan_approved"] = True
state["plan_gate_status"] = "approved"
state["ralphmode_active"] = True
state["ralphmode_activated_at"] = now
state["updated_at"] = now
if current_hash:
state["last_reviewed_plan_hash"] = current_hash
state["last_reviewed_plan_at"] = now
state["plan_review_method"] = "plannotator"
try:
save_state(root, state)
except Exception as exc:
print(f"[JEO] ⚠️ Failed to save jeo-state.json: {exc}", file=sys.stderr)
# Write project-local .claude/settings.json with acceptEdits
project_settings_path = root / ".claude" / "settings.json"
try:
project_settings_path.parent.mkdir(parents=True, exist_ok=True)
ps: dict[str, Any] = {}
if project_settings_path.exists():
try:
ps = json.loads(project_settings_path.read_text(encoding="utf-8"))
except Exception:
ps = {}
previous = ps.get("permissionMode", "default")
ps["permissionMode"] = RALPHMODE_PERMISSION
ps["_ralphmode_previous_permission"] = previous
project_settings_path.write_text(json.dumps(ps, ensure_ascii=False, indent=2), encoding="utf-8")
print(
f"[JEO] ✅ Plan approved → ralphmode activated"
f" (permissionMode: {previous!r} → {RALPHMODE_PERMISSION!r})."
f" EXECUTE phase ready.",
file=sys.stderr,
)
except Exception as exc:
print(f"[JEO] ⚠️ ralphmode profile write failed: {exc}", file=sys.stderr)
def deactivate_ralphmode(root: Path) -> None:
"""Revert permissionMode to the previous value saved before ralphmode activation.
Called by worktree-cleanup.sh (or manually) at CLEANUP phase.
"""
project_settings_path = root / ".claude" / "settings.json"
if not project_settings_path.exists():
return
try:
ps = json.loads(project_settings_path.read_text(encoding="utf-8"))
if ps.get("permissionMode") != RALPHMODE_PERMISSION:
return # Already reverted or never set
previous = ps.pop("_ralphmode_previous_permission", "default")
if previous == "default":
ps.pop("permissionMode", None)
else:
ps["permissionMode"] = previous
project_settings_path.write_text(json.dumps(ps, ensure_ascii=False, indent=2), encoding="utf-8")
# Update jeo-state.json
state = load_state(root)
if state:
state["ralphmode_active"] = False
state["updated_at"] = datetime.datetime.utcnow().isoformat() + "Z"
save_state(root, state)
print(f"[JEO] ralphmode deactivated (permissionMode restored to {previous!r}).", file=sys.stderr)
except Exception as exc:
print(f"[JEO] ⚠️ ralphmode deactivation failed: {exc}", file=sys.stderr)
def main() -> int:
# Support --deactivate flag for use in CLEANUP (worktree-cleanup.sh)
if "--deactivate" in sys.argv:
deactivate_ralphmode(git_root())
return 0
payload = sys.stdin.read()
root = git_root()
state = load_state(root)
plan_text = find_plan_text(root, payload)
current_hash = plan_hash(plan_text)
if should_skip(state, current_hash):
status = state.get("plan_gate_status", "unknown")
print(
f"[JEO][PLAN] Claude hook skipped: plan gate already recorded for current hash ({status}).",
file=sys.stderr,
)
return 0
reset_for_revised_plan(root, state, current_hash)
rc = run_plannotator(payload, plan_text)
if rc == 0:
# Plan approved — activate ralphmode for the EXECUTE phase
activate_ralphmode(root, load_state(root), current_hash)
return rc
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/run-opencode-safe.sh
```bash
#!/usr/bin/env bash
# Launch OpenCode with writable runtime directories.
# Useful when default HOME/XDG paths are readonly in sandboxed environments.
set -euo pipefail
if ! command -v opencode >/dev/null 2>&1; then
echo "opencode CLI not found. Install via: npm install -g opencode-ai" >&2
exit 1
fi
SESSION_KEY="$(python3 -c "import hashlib,os; print(hashlib.md5(os.getcwd().encode()).hexdigest()[:8])" 2>/dev/null || echo "default")"
RUNTIME_ROOT="/tmp/jeo-opencode-${SESSION_KEY}"
export XDG_DATA_HOME="${RUNTIME_ROOT}/data"
export XDG_STATE_HOME="${RUNTIME_ROOT}/state"
export XDG_CACHE_HOME="${RUNTIME_ROOT}/cache"
mkdir -p "${XDG_DATA_HOME}" "${XDG_STATE_HOME}" "${XDG_CACHE_HOME}"
exec opencode "$@"
```