Deep dive into Sibyl Research System internals. For contributors and advanced users.
The core of Sibyl is a 19-stage state machine in sibyl/orchestrate.py.
| experiment_decision | PIVOT | idea_debate (with alternatives) |
| experiment_decision | PROCEED | writing_outline |
| writing_final_review | Fail (< max rounds) | writing_integrate |
| quality_gate | Score ≥ 8.0 AND iterations ≥ 2 | done |
| quality_gate | Otherwise | literature_search (next iteration) |
| Any stage except init / quality_gate / done | lark_enabled | append background sync trigger after cli_record() |
When lark_enabled: true, Feishu sync is no longer a pipeline stage. Instead, cli_record() appends a trigger to lark_sync/pending_sync.jsonl and returns sync_requested: true. The main Claude session must launch sibyl-lark-sync in the background and continue the research loop without waiting. Sync status is written to lark_sync/sync_status.json.
The experiment supervisor owns local recovery, GPU refresh, dispatch, and lightweight intervention. When it either resolves something material or hits a blocker that needs main-loop judgment, it appends a structured wake event to exp/experiment_supervisor_main_wake.jsonl via experiment-supervisor-notify-main. The main control-plane loop must not sleep for the full experiment poll interval in one chunk; instead it checks wake_cmd every short interval (wake_check_interval_sec, default 90 seconds) and immediately collaborates when an event reports requires_main_system: true.
The orchestrator returns actions that the main session executes:
| skill | Single fork skill | Skill tool |
| skills_parallel | Multiple skills in parallel | Agent tool (parallel) |
| team | Multi-agent team collaboration | TeamCreate → TaskCreate → Agent per teammate |
| agents_parallel | Legacy: parallel agents with dynamic prompts | Agent tool (parallel) |
| bash | Shell command | Bash tool |
| gpu_poll | Poll for free GPUs | SSH MCP → parse → write marker file |
| experiment_wait | Wait for running experiments to complete | Adaptive polling + dynamic dispatch |
| done | Pipeline complete | Report to user |
| stopped | User explicitly halted the project | Resume only after /sibyl-research:resume |
status.json now uses explicit paused / stop_requested booleans, with optional paused_at / stop_requested_at timestamps for diagnostics. Legacy numeric *_at markers are still read for backward compatibility, and transient paused state is auto-cleared by cli_next() so the control plane does not stall. Explicit /sibyl-research:stop remains the only supported manual halt path.
All agent roles are implemented as fork skills in .claude/skills/sibyl-*/SKILL.md.
Defined in .claude/agents/:
| sibyl-heavy.md | Opus 4.6 | synthesizer, supervisor, editor, critic, reflection |
| sibyl-standard.md | Opus 4.6 | literature, planner, experimenter, writing |
| sibyl-light.md | Sonnet 4.6 | optimist, skeptic, strategist, section-critic |
Used for debate stages (idea_debate, result_debate, writing_sections, writing_critique).
Requires: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
Four parallel stages support sub-step checkpoints for crash recovery:
| idea_debate | idea/ |
| result_debate | exp/results/ |
| writing_sections | writing/sections/ |
| writing_critique | writing/critiques/ |
Checkpoints use 4-way validation:
On resume, only incomplete steps are re-executed.
sibyl/gpu_scheduler.py handles parallel experiment execution:
On shared servers, before each experiment batch:
sibyl/evolution.py implements cross-project learning:
SYSTEM, EXPERIMENT, WRITING, ANALYSIS, PLANNING, PIPELINE, IDEATION
Each category maps to specific agents for targeted improvement.
build_digest() aggregates outcomes.jsonl into digest.json with mtime caching.
Effective lessons are injected into agent prompts through the compiled prompt layer: render_skill_prompt(agent, workspace_path=...) builds the final prompt and appends contextual evolution lessons plus any project-specific overrides.
get_self_check_diagnostics() detects:
Written to logs/self_check_diagnostics.json after each reflection.
FarsOrchestrator.__init__ auto-loads project-level config.yaml:
This ensures per-project configuration works without explicit config passing.