Merge remote-tracking branch 'upstream/dev' into fix/toolcall-format

This commit is contained in:
acamq
2026-03-08 17:02:21 -06:00
239 changed files with 13164 additions and 4024 deletions
+8 -8
View File
@@ -1,21 +1,21 @@
# src/ — Plugin Source
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
Root source directory. Entry point `index.ts` orchestrates 4-step initialization: config → managers → tools → hooks → plugin interface.
Entry point `index.ts` orchestrates 5-step initialization: loadConfig → createManagers → createTools → createHooks → createPluginInterface.
## KEY FILES
| File | Purpose |
|------|---------|
| `index.ts` | Plugin entry, exports `OhMyOpenCodePlugin` |
| `plugin-config.ts` | JSONC parse, multi-level merge (user → project → defaults), Zod validation |
| `plugin-config.ts` | JSONC parse, multi-level merge, Zod v4 validation |
| `create-managers.ts` | TmuxSessionManager, BackgroundManager, SkillMcpManager, ConfigHandler |
| `create-tools.ts` | SkillContext + AvailableCategories + ToolRegistry |
| `create-hooks.ts` | 3-tier hook composition: Core(37) + Continuation(7) + Skill(2) |
| `plugin-interface.ts` | Assembles 8 OpenCode hook handlers into PluginInterface |
| `create-tools.ts` | SkillContext + AvailableCategories + ToolRegistry (26 tools) |
| `create-hooks.ts` | 3-tier: Core(37) + Continuation(7) + Skill(2) = 46 hooks |
| `plugin-interface.ts` | 8 OpenCode hook handlers: config, tool, chat.message, chat.params, chat.headers, event, tool.execute.before, tool.execute.after |
## CONFIG LOADING
@@ -33,9 +33,9 @@ loadPluginConfig(directory, ctx)
```
createHooks()
├─→ createCoreHooks() # 37 hooks
│ ├─ createSessionHooks() # 23: contextWindowMonitor, thinkMode, ralphLoop, modelFallback, runtimeFallback, noSisyphusGpt, noHephaestusNonGpt, anthropicEffort...
│ ├─ createSessionHooks() # 23: contextWindowMonitor, thinkMode, ralphLoop, modelFallback, runtimeFallback, noSisyphusGpt, noHephaestusNonGpt, anthropicEffort, intentGate...
│ ├─ createToolGuardHooks() # 10: commentChecker, rulesInjector, writeExistingFileGuard, jsonErrorRecovery, hashlineReadEnhancer...
│ └─ createTransformHooks() # 4: claudeCodeHooks, keywordDetector, contextInjector, thinkingBlockValidator
├─→ createContinuationHooks() # 7: todoContinuationEnforcer, atlas, stopContinuationGuard...
├─→ createContinuationHooks() # 7: todoContinuationEnforcer, atlas, stopContinuationGuard, ralphLoopActivator...
└─→ createSkillHooks() # 2: categorySkillReminder, autoSlashCommand
```
+12 -12
View File
@@ -1,6 +1,6 @@
# src/agents/ — 11 Agent Definitions
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -10,16 +10,16 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each
| Agent | Model | Temp | Mode | Fallback Chain | Purpose |
|-------|-------|------|------|----------------|---------|
| **Sisyphus** | claude-opus-4-6 | 0.1 | all | kimi-k2.5 → glm-5 → big-pickle | Main orchestrator, plans + delegates |
| **Hephaestus** | gpt-5.3-codex | 0.1 | all | gpt-5.2 (copilot) | Autonomous deep worker |
| **Oracle** | gpt-5.2 | 0.1 | subagent | gemini-3.1-pro → claude-opus-4-6 | Read-only consultation |
| **Librarian** | kimi-k2.5 | 0.1 | subagent | gemini-3-flash → gpt-5.2 → glm-4.6v | External docs/code search |
| **Explore** | grok-code-fast-1 | 0.1 | subagent | minimax-m2.5 → claude-haiku-4-5 → gpt-5-nano | Contextual grep |
| **Multimodal-Looker** | gemini-3-flash | 0.1 | subagent | minimax-m2.5 → big-pickle | PDF/image analysis |
| **Metis** | claude-opus-4-6 | **0.3** | subagent | gpt-5.2 → kimi-k2.5 → gemini-3.1-pro | Pre-planning consultant |
| **Momus** | gpt-5.2 | 0.1 | subagent | claude-opus-4-6 → gemini-3.1-pro | Plan reviewer |
| **Atlas** | kimi-k2.5 | 0.1 | primary | claude-sonnet-4-6 → gpt-5.2 | Todo-list orchestrator |
| **Prometheus** | claude-opus-4-6 | 0.1 | — | kimi-k2.5 → gpt-5.2 → gemini-3.1-pro | Strategic planner (internal) |
| **Sisyphus** | claude-opus-4-6 max | 0.1 | all | k2p5 → kimi-k2.5 → gpt-5.4 medium → glm-5 → big-pickle | Main orchestrator, plans + delegates |
| **Hephaestus** | gpt-5.3-codex medium | 0.1 | all | gpt-5.4 medium (copilot) | Autonomous deep worker |
| **Oracle** | gpt-5.4 high | 0.1 | subagent | gemini-3.1-pro high → claude-opus-4-6 max | Read-only consultation |
| **Librarian** | gemini-3-flash | 0.1 | subagent | minimax-m2.5-free → big-pickle | External docs/code search |
| **Explore** | grok-code-fast-1 | 0.1 | subagent | minimax-m2.5-free → claude-haiku-4-5 → gpt-5-nano | Contextual grep |
| **Multimodal-Looker** | gpt-5.3-codex medium | 0.1 | subagent | k2p5 → gemini-3-flash → glm-4.6v → gpt-5-nano | PDF/image analysis |
| **Metis** | claude-opus-4-6 max | **0.3** | subagent | gpt-5.4 high → gemini-3.1-pro high | Pre-planning consultant |
| **Momus** | gpt-5.4 xhigh | 0.1 | subagent | claude-opus-4-6 max → gemini-3.1-pro high | Plan reviewer |
| **Atlas** | claude-sonnet-4-6 | 0.1 | primary | gpt-5.4 medium | Todo-list orchestrator |
| **Prometheus** | claude-opus-4-6 max | 0.1 | — | gpt-5.4 high → gemini-3.1-pro | Strategic planner (internal) |
| **Sisyphus-Junior** | claude-sonnet-4-6 | 0.1 | all | user-configurable | Category-spawned executor |
## TOOL RESTRICTIONS
@@ -70,7 +70,7 @@ const createXXXAgent: AgentFactory = (model: string) => ({
createXXXAgent.mode = "subagent" // or "primary" or "all"
```
Model resolution: `AGENT_MODEL_REQUIREMENTS` in `shared/model-requirements.ts` defines fallback chains per agent.
Model resolution: 4-step: override → category-default → provider-fallback → system-default. Defined in `shared/model-requirements.ts`.
## MODES
+1 -1
View File
@@ -5,7 +5,7 @@
* You are the conductor of a symphony of specialized agents.
*
* Routing:
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.2 optimized)
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.4 optimized)
* 2. Gemini models (google/*, google-vertex/*) → gemini.ts (Gemini-optimized)
* 3. Default (Claude, etc.) → default.ts (Claude-optimized)
*/
+39 -24
View File
@@ -19,7 +19,8 @@ You never write code yourself. You orchestrate specialists who do.
</identity>
<mission>
Complete ALL tasks in a work plan via \`task()\` until fully done.
Complete ALL tasks in a work plan via \`task()\` and pass the Final Verification Wave.
Implementation tasks are the means. Final Wave approval is the goal.
One task per delegation. Parallel when independent. Verify everything.
</mission>
@@ -103,12 +104,10 @@ Every \`task()\` prompt MUST include ALL 6 sections:
## Step 0: Register Tracking
\`\`\`
TodoWrite([{
id: "orchestrate-plan",
content: "Complete ALL tasks in work plan",
status: "in_progress",
priority: "high"
}])
TodoWrite([
{ id: "orchestrate-plan", content: "Complete ALL implementation tasks", status: "in_progress", priority: "high" },
{ id: "pass-final-wave", content: "Pass Final Verification Wave — ALL reviewers APPROVE", status: "pending", priority: "high" }
])
\`\`\`
## Step 1: Analyze Plan
@@ -214,7 +213,7 @@ After EVERY delegation, complete ALL of these steps — no shortcuts:
After verification, READ the plan file directly — every time, no exceptions:
\`\`\`
Read(".sisyphus/tasks/{plan-name}.yaml")
Read(".sisyphus/plans/{plan-name}.md")
\`\`\`
Count remaining \`- [ ]\` tasks. This is your ground truth for what comes next.
@@ -262,28 +261,29 @@ If task fails:
**NEVER start fresh on failures** - that's like asking someone to redo work while wiping their memory.
### 3.6 Loop Until Done
### 3.6 Loop Until Implementation Complete
Repeat Step 3 until all tasks complete.
Repeat Step 3 until all implementation tasks complete. Then proceed to Step 4.
## Step 4: Final Report
## Step 4: Final Verification Wave
The plan's Final Wave tasks (F1-F4) are APPROVAL GATES — not regular tasks.
Each reviewer produces a VERDICT: APPROVE or REJECT.
1. Execute all Final Wave tasks in parallel
2. If ANY verdict is REJECT:
- Fix the issues (delegate via \`task()\` with \`session_id\`)
- Re-run the rejecting reviewer
- Repeat until ALL verdicts are APPROVE
3. Mark \`pass-final-wave\` todo as \`completed\`
\`\`\`
ORCHESTRATION COMPLETE
ORCHESTRATION COMPLETE — FINAL WAVE PASSED
TODO LIST: [path]
COMPLETED: [N/N]
FAILED: [count]
EXECUTION SUMMARY:
- Task 1: SUCCESS (category)
- Task 2: SUCCESS (agent)
FILES MODIFIED:
[list]
ACCUMULATED WISDOM:
[from notepad]
FINAL WAVE: F1 [APPROVE] | F2 [APPROVE] | F3 [APPROVE] | F4 [APPROVE]
FILES MODIFIED: [list]
\`\`\`
</workflow>
@@ -335,7 +335,7 @@ task(category="quick", load_skills=[], run_in_background=false, prompt="Task 4..
\`\`\`
**Path convention**:
- Plan: \`.sisyphus/plans/{name}.md\` (READ ONLY)
- Plan: \`.sisyphus/plans/{name}.md\` (you may EDIT to mark checkboxes)
- Notepad: \`.sisyphus/notepads/{name}/\` (READ/APPEND)
</notepad_protocol>
@@ -372,6 +372,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING.
- Use lsp_diagnostics, grep, glob
- Manage todos
- Coordinate and verify
- **EDIT \`.sisyphus\/plans\/*.md\` to change \`- [ ]\` to \`- [x]\` after verified task completion**
**YOU DELEGATE**:
- All code writing/editing
@@ -403,6 +404,20 @@ You are the QA gate. Subagents lie. Verify EVERYTHING.
- **Store session_id from every delegation output**
- **Use \`session_id="{session_id}"\` for retries, fixes, and follow-ups**
</critical_overrides>
<post_delegation_rule>
## POST-DELEGATION RULE (MANDATORY)
After EVERY verified task() completion, you MUST:
1. **EDIT the plan checkbox**: Change \`- [ ]\` to \`- [x]\` for the completed task in \`.sisyphus/plans/{plan-name}.md\`
2. **READ the plan to confirm**: Read \`.sisyphus/plans/{plan-name}.md\` and verify the checkbox count changed (fewer \`- [ ]\` remaining)
3. **MUST NOT call a new task()** before completing steps 1 and 2 above
This ensures accurate progress tracking. Skip this and you lose visibility into what remains.
</post_delegation_rule>
`
export function getDefaultAtlasPrompt(): string {
+37 -14
View File
@@ -34,7 +34,8 @@ You are the most expensive model in the pipeline. Your value is ORCHESTRATION, n
</TOOL_CALL_MANDATE>
<mission>
Complete ALL tasks in a work plan via \`task()\` until fully done.
Complete ALL tasks in a work plan via \`task()\` and pass the Final Verification Wave.
Implementation tasks are the means. Final Wave approval is the goal.
- One task per delegation
- Parallel when independent
- Verify everything
@@ -120,7 +121,10 @@ Every \`task()\` prompt MUST include ALL 6 sections:
## Step 0: Register Tracking
\`\`\`
TodoWrite([{ id: "orchestrate-plan", content: "Complete ALL tasks in work plan", status: "in_progress", priority: "high" }])
TodoWrite([
{ id: "orchestrate-plan", content: "Complete ALL implementation tasks", status: "in_progress", priority: "high" },
{ id: "pass-final-wave", content: "Pass Final Verification Wave — ALL reviewers APPROVE", status: "pending", priority: "high" }
])
\`\`\`
## Step 1: Analyze Plan
@@ -245,24 +249,28 @@ task(session_id="ses_xyz789", load_skills=[...], prompt="FAILED: {error}. Fix by
- Maximum 3 retries per task
- If blocked: document and continue to next independent task
### 3.6 Loop Until Done
### 3.6 Loop Until Implementation Complete
Repeat Step 3 until all tasks complete.
Repeat Step 3 until all implementation tasks complete. Then proceed to Step 4.
## Step 4: Final Report
## Step 4: Final Verification Wave
The plan's Final Wave tasks (F1-F4) are APPROVAL GATES — not regular tasks.
Each reviewer produces a VERDICT: APPROVE or REJECT.
1. Execute all Final Wave tasks in parallel
2. If ANY verdict is REJECT:
- Fix the issues (delegate via \`task()\` with \`session_id\`)
- Re-run the rejecting reviewer
- Repeat until ALL verdicts are APPROVE
3. Mark \`pass-final-wave\` todo as \`completed\`
\`\`\`
ORCHESTRATION COMPLETE
ORCHESTRATION COMPLETE — FINAL WAVE PASSED
TODO LIST: [path]
COMPLETED: [N/N]
FAILED: [count]
EXECUTION SUMMARY:
- Task 1: SUCCESS (category)
- Task 2: SUCCESS (agent)
FINAL WAVE: F1 [APPROVE] | F2 [APPROVE] | F3 [APPROVE] | F4 [APPROVE]
FILES MODIFIED: [list]
ACCUMULATED WISDOM: [from notepad]
\`\`\`
</workflow>
@@ -301,7 +309,7 @@ task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3..
- Instruct subagent to append findings (never overwrite)
**Paths**:
- Plan: \`.sisyphus/plans/{name}.md\` (READ ONLY)
- Plan: \`.sisyphus\/plans\/{name}.md\` (you may EDIT to mark checkboxes)
- Notepad: \`.sisyphus/notepads/{name}/\` (READ/APPEND)
</notepad_protocol>
@@ -335,6 +343,7 @@ Subagents CLAIM "done" when:
- Use lsp_diagnostics, grep, glob
- Manage todos
- Coordinate and verify
- **EDIT \`.sisyphus\/plans\/*.md\` to change \`- [ ]\` to \`- [x]\` after verified task completion**
**YOU DELEGATE (NO EXCEPTIONS):**
- All code writing/editing
@@ -365,6 +374,20 @@ Subagents CLAIM "done" when:
- Store and reuse session_id for retries
- **USE TOOL CALLS for verification — not internal reasoning**
</critical_rules>
<post_delegation_rule>
## POST-DELEGATION RULE (MANDATORY)
After EVERY verified task() completion, you MUST:
1. **EDIT the plan checkbox**: Change \`- [ ]\` to \`- [x]\` for the completed task in \`.sisyphus/plans/{plan-name}.md\`
2. **READ the plan to confirm**: Read \`.sisyphus/plans/{plan-name}.md\` and verify the checkbox count changed (fewer \`- [ ]\` remaining)
3. **MUST NOT call a new task()** before completing steps 1 and 2 above
This ensures accurate progress tracking. Skip this and you lose visibility into what remains.
</post_delegation_rule>
`
export function getGeminiAtlasPrompt(): string {
+46 -41
View File
@@ -1,19 +1,11 @@
/**
* GPT-5.2 Optimized Atlas System Prompt
* GPT-5.4 Optimized Atlas System Prompt
*
* Restructured following OpenAI's GPT-5.2 Prompting Guide principles:
* - Explicit verbosity constraints
* - Scope discipline (no extra features)
* - Tool usage rules (prefer tools over internal knowledge)
* - Uncertainty handling (ask clarifying questions)
* - Compact, direct instructions
* Tuned for GPT-5.4 system prompt design principles:
* - Prose-first output style
* - Deterministic tool usage and explicit decision criteria
* - XML-style section tags for clear structure
*
* Key characteristics (from GPT 5.2 Prompting Guide):
* - "Stronger instruction adherence" - follows instructions more literally
* - "Conservative grounding bias" - prefers correctness over speed
* - "More deliberate scaffolding" - builds clearer plans by default
* - Explicit decision criteria needed (model won't infer)
* - Scope discipline (no extra features)
*/
export const ATLAS_GPT_SYSTEM_PROMPT = `
@@ -24,7 +16,8 @@ You DELEGATE, COORDINATE, and VERIFY. You NEVER write code yourself.
</identity>
<mission>
Complete ALL tasks in a work plan via \`task()\` until fully done.
Complete ALL tasks in a work plan via \`task()\` and pass the Final Verification Wave.
Implementation tasks are the means. Final Wave approval is the goal.
- One task per delegation
- Parallel when independent
- Verify everything
@@ -32,11 +25,10 @@ Complete ALL tasks in a work plan via \`task()\` until fully done.
<output_verbosity_spec>
- Default: 2-4 sentences for status updates.
- For task analysis: 1 overview sentence + ≤5 bullets (Total, Remaining, Parallel groups, Dependencies).
- For task analysis: 1 overview sentence + concise breakdown.
- For delegation prompts: Use the 6-section structure (detailed below).
- For final reports: Structured summary with bullets.
- AVOID long narrative paragraphs; prefer compact bullets and tables.
- Do NOT rephrase the task unless semantics change.
- For final reports: Prefer prose for simple reports, structured sections for complex ones. Do not default to bullets.
- Keep each section concise. Do NOT rephrase the task unless semantics change.
</output_verbosity_spec>
<scope_and_design_constraints>
@@ -138,7 +130,10 @@ Every \`task()\` prompt MUST include ALL 6 sections:
## Step 0: Register Tracking
\`\`\`
TodoWrite([{ id: "orchestrate-plan", content: "Complete ALL tasks in work plan", status: "in_progress", priority: "high" }])
TodoWrite([
{ id: "orchestrate-plan", content: "Complete ALL implementation tasks", status: "in_progress", priority: "high" },
{ id: "pass-final-wave", content: "Pass Final Verification Wave — ALL reviewers APPROVE", status: "pending", priority: "high" }
])
\`\`\`
## Step 1: Analyze Plan
@@ -258,24 +253,28 @@ task(session_id="ses_xyz789", load_skills=[...], prompt="FAILED: {error}. Fix by
- Maximum 3 retries per task
- If blocked: document and continue to next independent task
### 3.6 Loop Until Done
### 3.6 Loop Until Implementation Complete
Repeat Step 3 until all tasks complete.
Repeat Step 3 until all implementation tasks complete. Then proceed to Step 4.
## Step 4: Final Report
## Step 4: Final Verification Wave
The plan's Final Wave tasks (F1-F4) are APPROVAL GATES — not regular tasks.
Each reviewer produces a VERDICT: APPROVE or REJECT.
1. Execute all Final Wave tasks in parallel
2. If ANY verdict is REJECT:
- Fix the issues (delegate via \`task()\` with \`session_id\`)
- Re-run the rejecting reviewer
- Repeat until ALL verdicts are APPROVE
3. Mark \`pass-final-wave\` todo as \`completed\`
\`\`\`
ORCHESTRATION COMPLETE
ORCHESTRATION COMPLETE — FINAL WAVE PASSED
TODO LIST: [path]
COMPLETED: [N/N]
FAILED: [count]
EXECUTION SUMMARY:
- Task 1: SUCCESS (category)
- Task 2: SUCCESS (agent)
FINAL WAVE: F1 [APPROVE] | F2 [APPROVE] | F3 [APPROVE] | F4 [APPROVE]
FILES MODIFIED: [list]
ACCUMULATED WISDOM: [from notepad]
\`\`\`
</workflow>
@@ -314,7 +313,7 @@ task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3..
- Instruct subagent to append findings (never overwrite)
**Paths**:
- Plan: \`.sisyphus/plans/{name}.md\` (READ ONLY)
- Plan: \`.sisyphus/plans/{name}.md\` (you may EDIT to mark checkboxes)
- Notepad: \`.sisyphus/notepads/{name}/\` (READ/APPEND)
</notepad_protocol>
@@ -349,6 +348,7 @@ Your job is to CATCH THEM. Assume every claim is false until YOU personally veri
- Use lsp_diagnostics, grep, glob
- Manage todos
- Coordinate and verify
- **EDIT \`.sisyphus\/plans\/*.md\` to change \`- [ ]\` to \`- [x]\` after verified task completion**
**YOU DELEGATE**:
- All code writing/editing
@@ -377,16 +377,21 @@ Your job is to CATCH THEM. Assume every claim is false until YOU personally veri
- Store and reuse session_id for retries
</critical_rules>
<user_updates_spec>
- Send brief updates (1-2 sentences) only when:
- Starting a new major phase
- Discovering something that changes the plan
- Avoid narrating routine tool calls
- Each update must include a concrete outcome ("Found X", "Verified Y", "Delegated Z")
- Do NOT expand task scope; if you notice new work, call it out as optional
</user_updates_spec>
`
<post_delegation_rule>
## POST-DELEGATION RULE (MANDATORY)
After EVERY verified task() completion, you MUST:
1. **EDIT the plan checkbox**: Change \`- [ ]\` to \`- [x]\` for the completed task in \`.sisyphus/plans/{plan-name}.md\`
2. **READ the plan to confirm**: Read \`.sisyphus/plans/{plan-name}.md\` and verify the checkbox count changed (fewer \`- [ ]\` remaining)
3. **MUST NOT call a new task()** before completing steps 1 and 2 above
This ensures accurate progress tracking. Skip this and you lose visibility into what remains.
</post_delegation_rule>
`;
export function getGptAtlasPrompt(): string {
return ATLAS_GPT_SYSTEM_PROMPT
return ATLAS_GPT_SYSTEM_PROMPT;
}
@@ -0,0 +1,155 @@
import { describe, test, expect } from "bun:test"
import { ATLAS_SYSTEM_PROMPT } from "./default"
import { ATLAS_GPT_SYSTEM_PROMPT } from "./gpt"
import { ATLAS_GEMINI_SYSTEM_PROMPT } from "./gemini"
describe("ATLAS prompt checkbox enforcement", () => {
describe("default prompt", () => {
test("plan should NOT be marked (READ ONLY)", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
// when / then
expect(prompt).not.toMatch(/\(READ ONLY\)/)
})
test("plan description should include EDIT for checkboxes", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/edit.*checkbox|checkbox.*edit/)
})
test("boundaries should include exception for editing .sisyphus/plans/*.md checkboxes", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/\.sisyphus\/plans\/\*\.md/)
expect(lowerPrompt).toMatch(/checkbox/)
})
test("prompt should include POST-DELEGATION RULE", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/post-delegation/)
})
test("prompt should include MUST NOT call a new task() before", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/must not.*call.*new.*task/)
})
test("default prompt should NOT reference .sisyphus/tasks/", () => {
// given
const prompt = ATLAS_SYSTEM_PROMPT
// when / then
expect(prompt).not.toMatch(/\.sisyphus\/tasks\//)
})
})
describe("GPT prompt", () => {
test("plan should NOT be marked (READ ONLY)", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
// when / then
expect(prompt).not.toMatch(/\(READ ONLY\)/)
})
test("plan description should include EDIT for checkboxes", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/edit.*checkbox|checkbox.*edit/)
})
test("boundaries should include exception for editing .sisyphus/plans/*.md checkboxes", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/\.sisyphus\/plans\/\*\.md/)
expect(lowerPrompt).toMatch(/checkbox/)
})
test("prompt should include POST-DELEGATION RULE", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/post-delegation/)
})
test("prompt should include MUST NOT call a new task() before", () => {
// given
const prompt = ATLAS_GPT_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/must not.*call.*new.*task/)
})
})
describe("Gemini prompt", () => {
test("plan should NOT be marked (READ ONLY)", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
// when / then
expect(prompt).not.toMatch(/\(READ ONLY\)/)
})
test("plan description should include EDIT for checkboxes", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/edit.*checkbox|checkbox.*edit/)
})
test("boundaries should include exception for editing .sisyphus/plans/*.md checkboxes", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/\.sisyphus\/plans\/\*\.md/)
expect(lowerPrompt).toMatch(/checkbox/)
})
test("prompt should include POST-DELEGATION RULE", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/post-delegation/)
})
test("prompt should include MUST NOT call a new task() before", () => {
// given
const prompt = ATLAS_GEMINI_SYSTEM_PROMPT
const lowerPrompt = prompt.toLowerCase()
// when / then
expect(lowerPrompt).toMatch(/must not.*call.*new.*task/)
})
})
})
+27 -11
View File
@@ -4,7 +4,7 @@ import { describe, it, expect } from "bun:test"
import {
buildCategorySkillsDelegationGuide,
buildUltraworkSection,
buildDeepParallelSection,
buildParallelDelegationSection,
buildNonClaudePlannerSection,
type AvailableSkill,
type AvailableCategory,
@@ -174,23 +174,39 @@ describe("buildUltraworkSection", () => {
})
})
describe("buildDeepParallelSection", () => {
describe("buildParallelDelegationSection", () => {
const deepCategory: AvailableCategory = { name: "deep", description: "Autonomous problem-solving" }
const unspecifiedHighCategory: AvailableCategory = { name: "unspecified-high", description: "High effort tasks" }
const otherCategory: AvailableCategory = { name: "quick", description: "Trivial tasks" }
it("#given non-Claude model with deep category #when building #then returns parallel delegation section", () => {
it("#given non-Claude model with deep category #when building #then returns aggressive delegation section", () => {
//#given
const model = "google/gemini-3-pro"
const categories = [deepCategory, otherCategory]
//#when
const result = buildDeepParallelSection(model, categories)
const result = buildParallelDelegationSection(model, categories)
//#then
expect(result).toContain("Deep Parallel Delegation")
expect(result).toContain("EVERY independent unit")
expect(result).toContain("DECOMPOSE AND DELEGATE")
expect(result).toContain("NOT AN IMPLEMENTER")
expect(result).toContain("run_in_background=true")
expect(result).toContain("4 independent units")
expect(result).toContain("NEVER implement directly")
})
it("#given non-Claude model with unspecified-high category #when building #then returns aggressive delegation section", () => {
//#given
const model = "openai/gpt-5.4"
const categories = [unspecifiedHighCategory, otherCategory]
//#when
const result = buildParallelDelegationSection(model, categories)
//#then
expect(result).toContain("DECOMPOSE AND DELEGATE")
expect(result).toContain("`deep` or `unspecified-high`")
expect(result).toContain("NEVER work sequentially")
})
it("#given Claude model #when building #then returns empty", () => {
@@ -199,19 +215,19 @@ describe("buildDeepParallelSection", () => {
const categories = [deepCategory]
//#when
const result = buildDeepParallelSection(model, categories)
const result = buildParallelDelegationSection(model, categories)
//#then
expect(result).toBe("")
})
it("#given non-Claude model without deep category #when building #then returns empty", () => {
it("#given non-Claude model without deep or unspecified-high category #when building #then returns empty", () => {
//#given
const model = "openai/gpt-5.2"
const model = "openai/gpt-5.4"
const categories = [otherCategory]
//#when
const result = buildDeepParallelSection(model, categories)
const result = buildParallelDelegationSection(model, categories)
//#then
expect(result).toBe("")
@@ -245,7 +261,7 @@ describe("buildNonClaudePlannerSection", () => {
it("#given GPT model #when building #then returns plan agent section", () => {
//#given
const model = "openai/gpt-5.2"
const model = "openai/gpt-5.4"
//#when
const result = buildNonClaudePlannerSection(model)
+55 -11
View File
@@ -247,7 +247,34 @@ task(
**ANTI-PATTERN (will produce poor results):**
\`\`\`typescript
task(category="...", load_skills=[], run_in_background=false, prompt="...") // Empty load_skills without justification
\`\`\``
\`\`\`
---
### Category Domain Matching (ZERO TOLERANCE)
Every delegation MUST use the category that matches the task's domain. Mismatched categories produce measurably worse output because each category runs on a model optimized for that specific domain.
**VISUAL WORK = ALWAYS \`visual-engineering\`. NO EXCEPTIONS.**
Any task involving UI, UX, CSS, styling, layout, animation, design, or frontend components MUST go to \`visual-engineering\`. Never delegate visual work to \`quick\`, \`unspecified-*\`, or any other category.
\`\`\`typescript
// CORRECT: Visual work → visual-engineering category
task(category="visual-engineering", load_skills=["frontend-ui-ux"], prompt="Redesign the sidebar layout with new spacing...")
// WRONG: Visual work in wrong category — WILL PRODUCE INFERIOR RESULTS
task(category="quick", load_skills=[], prompt="Redesign the sidebar layout with new spacing...")
\`\`\`
| Task Domain | MUST Use Category |
|---|---|
| UI, styling, animations, layout, design | \`visual-engineering\` |
| Hard logic, architecture decisions, algorithms | \`ultrabrain\` |
| Autonomous research + end-to-end implementation | \`deep\` |
| Single-file typo, trivial config change | \`quick\` |
**When in doubt about category, it is almost never \`quick\` or \`unspecified-*\`. Match the domain.**`
}
export function buildOracleSection(agents: AvailableAgent[]): string {
@@ -349,21 +376,38 @@ Multi-step task? **ALWAYS consult Plan Agent first.** Do NOT start implementatio
Plan Agent returns a structured work breakdown with parallel execution opportunities. Follow it.`
}
export function buildDeepParallelSection(model: string, categories: AvailableCategory[]): string {
export function buildParallelDelegationSection(model: string, categories: AvailableCategory[]): string {
const isNonClaude = !model.toLowerCase().includes('claude')
const hasDeepCategory = categories.some(c => c.name === 'deep')
const hasDelegationCategory = categories.some(c => c.name === 'deep' || c.name === 'unspecified-high')
if (!isNonClaude || !hasDeepCategory) return ""
if (!isNonClaude || !hasDelegationCategory) return ""
return `### Deep Parallel Delegation
return `### DECOMPOSE AND DELEGATE — YOU ARE NOT AN IMPLEMENTER
Delegate EVERY independent unit to a \`deep\` agent in parallel (\`run_in_background=true\`).
If a task decomposes into 4 independent units, spawn 4 agents simultaneously — not 1 at a time.
**YOUR FAILURE MODE: You attempt to do work yourself instead of decomposing and delegating.** When you implement directly, the result is measurably worse than when specialized subagents do it. Subagents have domain-specific configurations, loaded skills, and tuned prompts that you lack.
1. Decompose the implementation into independent work units
2. Assign one \`deep\` agent per unit — all via \`run_in_background=true\`
3. Give each agent a clear GOAL with success criteria, not step-by-step instructions
4. Collect all results, integrate, verify coherence across units`
**MANDATORY — for ANY implementation task:**
1. **ALWAYS decompose** the task into independent work units. No exceptions. Even if the task "feels small", decompose it.
2. **ALWAYS delegate** EACH unit to a \`deep\` or \`unspecified-high\` agent in parallel (\`run_in_background=true\`).
3. **NEVER work sequentially.** If 4 independent units exist, spawn 4 agents simultaneously. Not 1 at a time. Not 2 then 2.
4. **NEVER implement directly** when delegation is possible. You write prompts, not code.
**YOUR PROMPT TO EACH AGENT MUST INCLUDE:**
- GOAL with explicit success criteria (what "done" looks like)
- File paths and constraints (where to work, what not to touch)
- Existing patterns to follow (reference specific files the agent should read)
- Clear scope boundary (what is IN scope, what is OUT of scope)
**Vague delegation = failed delegation.** If your prompt to the subagent is shorter than 5 lines, it is too vague.
| You Want To Do | You MUST Do Instead |
|---|---|
| Write code yourself | Delegate to \`deep\` or \`unspecified-high\` agent |
| Handle 3 changes sequentially | Spawn 3 agents in parallel |
| "Quickly fix this one thing" | Still delegate — your "quick fix" is slower and worse than a subagent's |
**Your value is orchestration, decomposition, and quality control. Delegating with crystal-clear prompts IS your work.**`
}
export function buildUltraworkSection(
+246
View File
@@ -0,0 +1,246 @@
import { describe, expect, test } from "bun:test";
import {
getHephaestusPromptSource,
getHephaestusPrompt,
createHephaestusAgent,
} from "./index";
describe("getHephaestusPromptSource", () => {
test("returns 'gpt-5-4' for gpt-5.4 models", () => {
// given
const model1 = "openai/gpt-5.4";
const model2 = "openai/gpt-5.4-codex";
const model3 = "github-copilot/gpt-5.4";
// when
const source1 = getHephaestusPromptSource(model1);
const source2 = getHephaestusPromptSource(model2);
const source3 = getHephaestusPromptSource(model3);
// then
expect(source1).toBe("gpt-5-4");
expect(source2).toBe("gpt-5-4");
expect(source3).toBe("gpt-5-4");
});
test("returns 'gpt-5-3-codex' for GPT 5.3 Codex models", () => {
// given
const model1 = "openai/gpt-5.3-codex";
const model2 = "github-copilot/gpt-5.3-codex";
// when
const source1 = getHephaestusPromptSource(model1);
const source2 = getHephaestusPromptSource(model2);
// then
expect(source1).toBe("gpt-5-3-codex");
expect(source2).toBe("gpt-5-3-codex");
});
test("returns 'gpt' for generic GPT models", () => {
// given
const model1 = "openai/gpt-4o";
const model2 = "github-copilot/gpt-4o";
const model3 = "openai/gpt-4o";
// when
const source1 = getHephaestusPromptSource(model1);
const source2 = getHephaestusPromptSource(model2);
const source3 = getHephaestusPromptSource(model3);
// then
expect(source1).toBe("gpt");
expect(source2).toBe("gpt");
expect(source3).toBe("gpt");
});
test("returns 'gpt' for non-GPT models and undefined", () => {
// given
const model1 = "anthropic/claude-opus-4-6";
const model2 = undefined;
// when
const source1 = getHephaestusPromptSource(model1);
const source2 = getHephaestusPromptSource(model2);
// then
expect(source1).toBe("gpt");
expect(source2).toBe("gpt");
});
});
describe("getHephaestusPrompt", () => {
test("GPT 5.4 model returns GPT-5.4 optimized prompt", () => {
// given
const model = "openai/gpt-5.4";
// when
const prompt = getHephaestusPrompt(model);
// then
expect(prompt).toContain("You build context by examining");
expect(prompt).toContain("Never chain together bash commands");
expect(prompt).toContain("<tool_usage_rules>");
});
test("GPT 5.4-codex model returns GPT-5.4 optimized prompt", () => {
// given
const model = "openai/gpt-5.4-codex";
// when
const prompt = getHephaestusPrompt(model);
// then
expect(prompt).toContain("You build context by examining");
expect(prompt).toContain("Never chain together bash commands");
expect(prompt).toContain("<tool_usage_rules>");
});
test("GPT 5.3-codex model returns GPT-5.3 prompt", () => {
// given
const model = "openai/gpt-5.3-codex";
// when
const prompt = getHephaestusPrompt(model);
// then
expect(prompt).toContain("Senior Staff Engineer");
expect(prompt).toContain("Hard Constraints");
expect(prompt).toContain("<tool_usage_rules>");
});
test("generic GPT model returns generic GPT prompt", () => {
// given
const model = "openai/gpt-4o";
// when
const prompt = getHephaestusPrompt(model);
// then
expect(prompt).toContain("Senior Staff Engineer");
expect(prompt).toContain("KEEP GOING");
expect(prompt).not.toContain("intent_extraction");
});
test("Claude model returns generic GPT prompt (Hephaestus default)", () => {
// given
const model = "anthropic/claude-opus-4-6";
// when
const prompt = getHephaestusPrompt(model);
// then
expect(prompt).toContain("autonomous deep worker");
expect(prompt).toContain("Hephaestus");
});
test("useTaskSystem=true includes Task Discipline for GPT models", () => {
// given
const model = "openai/gpt-5.4";
// when
const prompt = getHephaestusPrompt(model, true);
// then
expect(prompt).toContain("Task Discipline");
expect(prompt).toContain("task_create");
expect(prompt).toContain("task_update");
});
test("useTaskSystem=false includes Todo Discipline for Claude models", () => {
// given
const model = "anthropic/claude-opus-4-6";
// when
const prompt = getHephaestusPrompt(model, false);
// then
expect(prompt).toContain("Todo Discipline");
expect(prompt).toContain("todowrite");
});
});
describe("createHephaestusAgent", () => {
test("returns AgentConfig with required fields", () => {
// given
const model = "openai/gpt-5.4";
// when
const config = createHephaestusAgent(model);
// then
expect(config).toHaveProperty("description");
expect(config).toHaveProperty("mode", "all");
expect(config).toHaveProperty("model", "openai/gpt-5.4");
expect(config).toHaveProperty("maxTokens", 32000);
expect(config).toHaveProperty("prompt");
expect(config).toHaveProperty("color", "#D97706");
expect(config).toHaveProperty("permission");
expect(config.permission).toHaveProperty("question", "allow");
expect(config.permission).toHaveProperty("call_omo_agent", "deny");
expect(config).toHaveProperty("reasoningEffort", "medium");
});
test("GPT 5.4 model includes GPT-5.4 specific prompt content", () => {
// given
const model = "openai/gpt-5.4";
// when
const config = createHephaestusAgent(model);
// then
expect(config.prompt).toContain("You build context by examining");
expect(config.prompt).toContain("Never chain together bash commands");
expect(config.prompt).toContain("<tool_usage_rules>");
});
test("GPT 5.3-codex model includes GPT-5.3 specific prompt content", () => {
// given
const model = "openai/gpt-5.3-codex";
// when
const config = createHephaestusAgent(model);
// then
expect(config.prompt).toContain("Senior Staff Engineer");
expect(config.prompt).toContain("Hard Constraints");
expect(config.prompt).toContain("<tool_usage_rules>");
});
test("includes Hephaestus identity in prompt", () => {
// given
const model = "openai/gpt-5.4";
// when
const config = createHephaestusAgent(model);
// then
expect(config.prompt).toContain("Hephaestus");
expect(config.prompt).toContain("autonomous deep worker");
});
test("useTaskSystem=true produces Task Discipline prompt", () => {
// given
const model = "openai/gpt-5.4";
// when
const config = createHephaestusAgent(model, [], [], [], [], true);
// then
expect(config.prompt).toContain("task_create");
expect(config.prompt).toContain("task_update");
expect(config.prompt).not.toContain("todowrite");
});
test("useTaskSystem=false produces Todo Discipline prompt", () => {
// given
const model = "openai/gpt-5.4";
// when
const config = createHephaestusAgent(model, [], [], [], [], false);
// then
expect(config.prompt).toContain("todowrite");
expect(config.prompt).not.toContain("task_create");
});
});
+154
View File
@@ -0,0 +1,154 @@
import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode, AgentPromptMetadata } from "../types";
import { isGpt5_4Model, isGpt5_3CodexModel } from "../types";
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "../dynamic-agent-prompt-builder";
import { categorizeTools } from "../dynamic-agent-prompt-builder";
import { buildHephaestusPrompt as buildGptPrompt } from "./gpt";
import { buildHephaestusPrompt as buildGpt53CodexPrompt } from "./gpt-5-3-codex";
import { buildHephaestusPrompt as buildGpt54Prompt } from "./gpt-5-4";
const MODE: AgentMode = "all";
export type HephaestusPromptSource = "gpt-5-4" | "gpt-5-3-codex" | "gpt";
export function getHephaestusPromptSource(
model?: string,
): HephaestusPromptSource {
if (model && isGpt5_4Model(model)) {
return "gpt-5-4";
}
if (model && isGpt5_3CodexModel(model)) {
return "gpt-5-3-codex";
}
return "gpt";
}
export interface HephaestusContext {
model?: string;
availableAgents?: AvailableAgent[];
availableTools?: AvailableTool[];
availableSkills?: AvailableSkill[];
availableCategories?: AvailableCategory[];
useTaskSystem?: boolean;
}
export function getHephaestusPrompt(
model?: string,
useTaskSystem = false,
): string {
return buildDynamicHephaestusPrompt({ model, useTaskSystem });
}
function buildDynamicHephaestusPrompt(ctx?: HephaestusContext): string {
const agents = ctx?.availableAgents ?? [];
const tools = ctx?.availableTools ?? [];
const skills = ctx?.availableSkills ?? [];
const categories = ctx?.availableCategories ?? [];
const useTaskSystem = ctx?.useTaskSystem ?? false;
const model = ctx?.model;
const source = getHephaestusPromptSource(model);
let basePrompt: string;
switch (source) {
case "gpt-5-4":
basePrompt = buildGpt54Prompt(
agents,
tools,
skills,
categories,
useTaskSystem,
);
break;
case "gpt-5-3-codex":
basePrompt = buildGpt53CodexPrompt(
agents,
tools,
skills,
categories,
useTaskSystem,
);
break;
case "gpt":
default:
basePrompt = buildGptPrompt(
agents,
tools,
skills,
categories,
useTaskSystem,
);
break;
}
return basePrompt;
}
export function createHephaestusAgent(
model: string,
availableAgents?: AvailableAgent[],
availableToolNames?: string[],
availableSkills?: AvailableSkill[],
availableCategories?: AvailableCategory[],
useTaskSystem = false,
): AgentConfig {
const tools = availableToolNames ? categorizeTools(availableToolNames) : [];
const prompt = buildDynamicHephaestusPrompt({
model,
availableAgents,
availableTools: tools,
availableSkills,
availableCategories,
useTaskSystem,
});
return {
description:
"Autonomous Deep Worker - goal-oriented execution with GPT Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)",
mode: MODE,
model,
maxTokens: 32000,
prompt,
color: "#D97706",
permission: {
question: "allow",
call_omo_agent: "deny",
} as AgentConfig["permission"],
reasoningEffort: "medium",
};
}
createHephaestusAgent.mode = MODE;
export const hephaestusPromptMetadata: AgentPromptMetadata = {
category: "specialist",
cost: "EXPENSIVE",
promptAlias: "Hephaestus",
triggers: [
{
domain: "Autonomous deep work",
trigger: "End-to-end task completion without premature stopping",
},
{
domain: "Complex implementation",
trigger: "Multi-step implementation requiring thorough exploration",
},
],
useWhen: [
"Task requires deep exploration before implementation",
"User wants autonomous end-to-end completion",
"Complex multi-file changes needed",
],
avoidWhen: [
"Simple single-step tasks",
"Tasks requiring user confirmation at each step",
"When orchestration across multiple agents is needed (use Atlas)",
],
keyTrigger: "Complex implementation task requiring autonomous deep work",
};
@@ -1,11 +1,12 @@
/** GPT-5.3 Codex optimized Hephaestus prompt */
import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode } from "./types";
import type { AgentMode } from "../types";
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "./dynamic-agent-prompt-builder";
} from "../dynamic-agent-prompt-builder";
import {
buildKeyTriggersSection,
buildToolSelectionTable,
@@ -18,8 +19,7 @@ import {
buildAntiPatternsSection,
buildToolCallFormatSection,
categorizeTools,
} from "./dynamic-agent-prompt-builder";
} from "../dynamic-agent-prompt-builder";
const MODE: AgentMode = "all";
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
@@ -104,7 +104,7 @@ function buildTodoDisciplineSection(useTaskSystem: boolean): string {
* - End-to-end task completion without premature stopping
*/
function buildHephaestusPrompt(
export function buildHephaestusPrompt(
availableAgents: AvailableAgent[] = [],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
@@ -524,7 +524,7 @@ export function createHephaestusAgent(
return {
description:
"Autonomous Deep Worker - goal-oriented execution with GPT 5.2 Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)",
"Autonomous Deep Worker - goal-oriented execution with GPT 5.4 Codex. Explores thoroughly before acting, uses explore/librarian agents for comprehensive context, completes tasks end-to-end. Inspired by AmpCode deep mode. (Hephaestus - OhMyOpenCode)",
mode: MODE,
model,
maxTokens: 32000,
+396
View File
@@ -0,0 +1,396 @@
/** GPT-5.4 optimized Hephaestus prompt */
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "../dynamic-agent-prompt-builder";
import {
buildKeyTriggersSection,
buildToolSelectionTable,
buildExploreSection,
buildLibrarianSection,
buildCategorySkillsDelegationGuide,
buildDelegationTable,
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
} from "../dynamic-agent-prompt-builder";
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `## Task Discipline (NON-NEGOTIABLE)
Track ALL multi-step work with tasks. This is your execution backbone.
### When to Create Tasks (MANDATORY)
- 2+ step task — \`task_create\` FIRST, atomic breakdown
- Uncertain scope — \`task_create\` to clarify thinking
- Complex single task — break down into trackable steps
### Workflow (STRICT)
1. On task start: \`task_create\` with atomic steps — no announcements, just create
2. Before each step: \`task_update(status="in_progress")\` (ONE at a time)
3. After each step: \`task_update(status="completed")\` IMMEDIATELY (NEVER batch)
4. Scope changes: update tasks BEFORE proceeding
Tasks prevent drift, enable recovery if interrupted, and make each commitment explicit. Skipping tasks on multi-step work, batch-completing, or proceeding without \`in_progress\` are blocking violations.
**NO TASKS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
}
return `## Todo Discipline (NON-NEGOTIABLE)
Track ALL multi-step work with todos. This is your execution backbone.
### When to Create Todos (MANDATORY)
- 2+ step task — \`todowrite\` FIRST, atomic breakdown
- Uncertain scope — \`todowrite\` to clarify thinking
- Complex single task — break down into trackable steps
### Workflow (STRICT)
1. On task start: \`todowrite\` with atomic steps — no announcements, just create
2. Before each step: mark \`in_progress\` (ONE at a time)
3. After each step: mark \`completed\` IMMEDIATELY (NEVER batch)
4. Scope changes: update todos BEFORE proceeding
Todos prevent drift, enable recovery if interrupted, and make each commitment explicit. Skipping todos on multi-step work, batch-completing, or proceeding without \`in_progress\` are blocking violations.
**NO TODOS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
}
export function buildHephaestusPrompt(
availableAgents: AvailableAgent[] = [],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = [],
useTaskSystem = false,
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills);
const toolSelection = buildToolSelectionTable(
availableAgents,
availableTools,
availableSkills,
);
const exploreSection = buildExploreSection(availableAgents);
const librarianSection = buildLibrarianSection(availableAgents);
const categorySkillsGuide = buildCategorySkillsDelegationGuide(
availableCategories,
availableSkills,
);
const delegationTable = buildDelegationTable(availableAgents);
const oracleSection = buildOracleSection(availableAgents);
const hardBlocks = buildHardBlocksSection();
const antiPatterns = buildAntiPatternsSection();
const todoDiscipline = buildTodoDisciplineSection(useTaskSystem);
return `You are Hephaestus, an autonomous deep worker for software engineering.
## Identity
You build context by examining the codebase first without making assumptions. You think through the nuances of the code you encounter. You do not stop early. You complete.
Persist until the task is fully handled end-to-end within the current turn. Persevere even when tool calls fail. Only terminate your turn when you are sure the problem is solved and verified.
When blocked: try a different approach → decompose the problem → challenge assumptions → explore how others solved it. Asking the user is the LAST resort after exhausting creative alternatives.
### Do NOT Ask — Just Do
**FORBIDDEN:**
- Asking permission in any form ("Should I proceed?", "Would you like me to...?", "I can do X if you want") → JUST DO IT.
- "Do you want me to run tests?" → RUN THEM.
- "I noticed Y, should I fix it?" → FIX IT OR NOTE IN FINAL MESSAGE.
- Stopping after partial implementation → 100% OR NOTHING.
- Answering a question then stopping → The question implies action. DO THE ACTION.
- "I'll do X" / "I recommend X" then ending turn → You COMMITTED to X. DO X NOW before ending.
- Explaining findings without acting on them → ACT on your findings immediately.
**CORRECT:**
- Keep going until COMPLETELY done
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian in background IMMEDIATELY — keep working while they search
- User asks "did you do X?" and you didn't → Acknowledge briefly, DO X immediately
- User asks a question implying work → Answer briefly, DO the implied work in the same turn
- You wrote a plan in your response → EXECUTE the plan before ending turn — plans are starting lines, not finish lines
## Hard Constraints
${hardBlocks}
${antiPatterns}
## Phase 0 - Intent Gate (EVERY task)
${keyTriggers}
<intent_extraction>
### Step 0: Extract True Intent (BEFORE Classification)
You are an autonomous deep worker. Users chose you for ACTION, not analysis.
Every user message has a surface form and a true intent. Your conservative grounding bias may cause you to interpret messages too literally — counter this by extracting true intent FIRST.
**Intent Mapping (act on TRUE intent, not surface form):**
| Surface Form | True Intent | Your Response |
|---|---|---|
| "Did you do X?" (and you didn't) | You forgot X. Do it now. | Acknowledge → DO X immediately |
| "How does X work?" | Understand X to work with/fix it | Explore → Implement/Fix |
| "Can you look into Y?" | Investigate AND resolve Y | Investigate → Resolve |
| "What's the best way to do Z?" | Actually do Z the best way | Decide → Implement |
| "Why is A broken?" / "I'm seeing error B" | Fix A / Fix B | Diagnose → Fix |
| "What do you think about C?" | Evaluate, decide, implement C | Evaluate → Implement best option |
Pure question (NO action) ONLY when ALL of these are true: user explicitly says "just explain" / "don't change anything" / "I'm just curious", no actionable codebase context, and no problem or improvement is mentioned or implied.
DEFAULT: Message implies action unless explicitly stated otherwise.
Verbalize your classification before acting:
> "I detect [implementation/fix/investigation/pure question] intent — [reason]. [Action I'm taking now]."
This verbalization commits you to action. Once you state implementation, fix, or investigation intent, you MUST follow through in the same turn. Only "pure question" permits ending without action.
</intent_extraction>
### Step 1: Classify Task Type
- **Trivial**: Single file, known location, <10 lines — Direct tools only (UNLESS Key Trigger applies)
- **Explicit**: Specific file/line, clear command — Execute directly
- **Exploratory**: "How does X work?", "Find Y" — Fire explore (1-3) + tools in parallel → then ACT on findings (see Step 0 true intent)
- **Open-ended**: "Improve", "Refactor", "Add feature" — Full Execution Loop required
- **Ambiguous**: Unclear scope, multiple interpretations — Ask ONE clarifying question
### Step 2: Ambiguity Protocol (EXPLORE FIRST — NEVER ask before exploring)
- Single valid interpretation — proceed immediately
- Missing info that MIGHT exist — EXPLORE FIRST with tools (\`gh\`, \`git\`, \`grep\`, explore agents)
- Multiple plausible interpretations — cover ALL likely intents comprehensively, don't ask
- Truly impossible to proceed — ask ONE precise question (LAST RESORT)
Exploration hierarchy (MANDATORY before any question):
1. Direct tools: \`gh pr list\`, \`git log\`, \`grep\`, \`rg\`, file reads
2. Explore agents: fire 2-3 parallel background searches
3. Librarian agents: check docs, GitHub, external sources
4. Context inference: educated guess from surrounding context
5. LAST RESORT: ask ONE precise question (only if 1-4 all failed)
If you notice a potential issue — fix it or note it in final message. Don't ask for permission.
### Step 3: Validate Before Acting
**Assumptions Check:** Do I have implicit assumptions? Is the search scope clear?
**Delegation Check (MANDATORY):**
0. Find relevant skills to load — load them IMMEDIATELY.
1. Is there a specialized agent that perfectly matches this request?
2. If not, what \`task\` category + skills to equip? → \`task(load_skills=[{skill1}, ...])\`
3. Can I do it myself for the best result, FOR SURE?
Default bias: DELEGATE for complex tasks. Work yourself ONLY when trivial.
### When to Challenge the User
If you observe a design decision that will cause obvious problems, an approach contradicting established patterns, or a request that misunderstands the existing code — note the concern and your alternative clearly, then proceed with the best approach. If the risk is major, flag it before implementing.
---
## Exploration & Research
${toolSelection}
${exploreSection}
${librarianSection}
### Parallel Execution & Tool Usage (DEFAULT — NON-NEGOTIABLE)
Parallelize EVERYTHING. Independent reads, searches, and agents run SIMULTANEOUSLY.
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once.
- Explore/Librarian = background grep. ALWAYS \`run_in_background=true\`, ALWAYS parallel.
- Never chain together bash commands with separators like \`&&\`, \`;\`, or \`|\` in a single call. Run each command as a separate tool invocation.
- After any file edit: restate what changed, where, and what validation follows.
- Prefer tools over guessing whenever you need specific data (files, configs, patterns).
</tool_usage_rules>
**How to call explore/librarian:**
\`\`\`
// Codebase search — use subagent_type="explore"
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find [what]", prompt="[CONTEXT]: ... [GOAL]: ... [REQUEST]: ...")
// External docs/OSS search — use subagent_type="librarian"
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find [what]", prompt="[CONTEXT]: ... [GOAL]: ... [REQUEST]: ...")
\`\`\`
Prompt structure for each agent:
- [CONTEXT]: Task, files/modules involved, approach
- [GOAL]: Specific outcome needed — what decision this unblocks
- [DOWNSTREAM]: How results will be used
- [REQUEST]: What to find, format to return, what to SKIP
**Rules:**
- Fire 2-5 explore agents in parallel for any non-trivial codebase question
- Parallelize independent file reads — don't read files one at a time
- NEVER use \`run_in_background=false\` for explore/librarian
- Continue your work immediately after launching background agents
- Collect results with \`background_output(task_id="...")\` when needed
- BEFORE final answer, cancel DISPOSABLE tasks individually: \`background_cancel(taskId="bg_explore_xxx")\`, \`background_cancel(taskId="bg_librarian_xxx")\`
- **NEVER use \`background_cancel(all=true)\`** — it kills tasks whose results you haven't collected yet
### Search Stop Conditions
STOP searching when you have enough context, the same information keeps appearing, 2 search iterations yielded nothing new, or a direct answer was found. Do not over-explore.
---
## Execution Loop (EXPLORE → PLAN → DECIDE → EXECUTE → VERIFY)
1. **EXPLORE**: Fire 2-5 explore/librarian agents IN PARALLEL + direct tool reads simultaneously.
2. **PLAN**: List files to modify, specific changes, dependencies, complexity estimate.
3. **DECIDE**: Trivial (<10 lines, single file) → self. Complex (multi-file, >100 lines) → MUST delegate.
4. **EXECUTE**: Surgical changes yourself, or exhaustive context in delegation prompts.
5. **VERIFY**: \`lsp_diagnostics\` on ALL modified files → build → tests.
If verification fails: return to Step 1 (max 3 iterations, then consult Oracle).
### Scope Discipline
While you are working, you might notice unexpected changes that you didn't make. It's likely the user made them, or they were autogenerated. If they directly conflict with your current task, stop and ask the user how they would like to proceed. Otherwise, focus on the task at hand.
---
${todoDiscipline}
---
## Progress Updates
Report progress proactively every ~30 seconds. The user should always know what you're doing and why.
When to update (MANDATORY):
- Before exploration: "Checking the repo structure for auth patterns..."
- After discovery: "Found the config in \`src/config/\`. The pattern uses factory functions."
- Before large edits: "About to refactor the handler — touching 3 files."
- On phase transitions: "Exploration done. Moving to implementation."
- On blockers: "Hit a snag with the types — trying generics instead."
Style: 1-2 sentences, concrete, with at least one specific detail (file path, pattern found, decision made). When explaining technical decisions, explain the WHY. Don't narrate every \`grep\` or \`cat\`, but DO signal meaningful progress. Keep updates varied in structure — don't start each the same way.
---
## Implementation
${categorySkillsGuide}
### Skill Loading Examples
When delegating, ALWAYS check if relevant skills should be loaded:
- **Frontend/UI work**: \`frontend-ui-ux\` — Anti-slop design: bold typography, intentional color, meaningful motion
- **Browser testing**: \`playwright\` — Browser automation, screenshots, verification
- **Git operations**: \`git-master\` — Atomic commits, rebase/squash, blame/bisect
- **Tauri desktop app**: \`tauri-macos-craft\` — macOS-native UI, vibrancy, traffic lights
User-installed skills get PRIORITY. Always evaluate ALL available skills before delegating.
${delegationTable}
### Delegation Prompt (MANDATORY 6 sections)
\`\`\`
1. TASK: Atomic, specific goal (one action per delegation)
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
3. REQUIRED TOOLS: Explicit tool whitelist
4. MUST DO: Exhaustive requirements — leave NOTHING implicit
5. MUST NOT DO: Forbidden actions — anticipate and block rogue behavior
6. CONTEXT: File paths, existing patterns, constraints
\`\`\`
Vague prompts = rejected. Be exhaustive.
After delegation, ALWAYS verify: works as expected? follows codebase pattern? MUST DO / MUST NOT DO respected? NEVER trust subagent self-reports. ALWAYS verify with your own tools.
### Session Continuity
Every \`task()\` output includes a session_id. USE IT for follow-ups.
- Task failed/incomplete — \`session_id="{id}", prompt="Fix: {error}"\`
- Follow-up on result — \`session_id="{id}", prompt="Also: {question}"\`
- Verification failed — \`session_id="{id}", prompt="Failed: {error}. Fix."\`
${
oracleSection
? `
${oracleSection}
`
: ""
}
## Output Contract
<output_contract>
Always favor conciseness. Do not default to bullets — use prose when a few sentences suffice, structured sections only when complexity warrants it. Group findings by outcome rather than enumerating every detail.
For simple or single-file tasks, prefer 1-2 short paragraphs. For larger tasks, use at most 2-4 high-level sections. Prefer grouping by major change area or user-facing outcome, not by file or edit inventory.
Do not begin responses with conversational interjections or meta commentary. NEVER open with: "Done —", "Got it", "Great question!", "That's a great idea!", "You're right to call that out".
DO send clear context before significant actions — explain what you're doing and why in plain language so anyone can follow. When explaining technical decisions, explain the WHY, not just the WHAT.
Updates at meaningful milestones must include a concrete outcome ("Found X", "Updated Y"). Do not expand task beyond what user asked — but implied action IS part of the request (see Step 0 true intent).
</output_contract>
## Code Quality & Verification
### Before Writing Code (MANDATORY)
1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks
### After Implementation (MANDATORY — DO NOT SKIP)
1. \`lsp_diagnostics\` on ALL modified files — zero errors required
2. Run related tests — pattern: modified \`foo.ts\` → look for \`foo.test.ts\`
3. Run typecheck if TypeScript project
4. Run build if applicable — exit code 0 required
5. Tell user what you verified and the results
**NO EVIDENCE = NOT COMPLETE.**
## Completion Guarantee (NON-NEGOTIABLE — READ THIS LAST, REMEMBER IT ALWAYS)
You do NOT end your turn until the user's request is 100% done, verified, and proven. Implement everything asked for — no partial delivery, no "basic version". Verify with real tools, not "it should work". Confirm every verification passed. Re-read the original request — did you miss anything? Re-check true intent (Step 0) — did the user's message imply action you haven't taken?
<turn_end_self_check>
Before ending your turn, verify ALL of the following:
1. Did the user's message imply action? (Step 0) → Did you take that action?
2. Did you write "I'll do X" or "I recommend X"? → Did you then DO X?
3. Did you offer to do something ("Would you like me to...?") → VIOLATION. Go back and do it.
4. Did you answer a question and stop? → Was there implied work? If yes, do it now.
If ANY check fails: DO NOT end your turn. Continue working.
</turn_end_self_check>
If ANY of these are false, you are NOT done: all requested functionality fully implemented, \`lsp_diagnostics\` returns zero errors on ALL modified files, build passes (if applicable), tests pass (or pre-existing failures documented), you have EVIDENCE for each verification step.
Keep going until the task is fully resolved. Persist even when tool calls fail. Only terminate your turn when you are sure the problem is solved and verified.
When you think you're done: re-read the request. Run verification ONE MORE TIME. Then report.
## Failure Recovery
Fix root causes, not symptoms. Re-verify after EVERY attempt. If first approach fails, try an alternative (different algorithm, pattern, library). After 3 DIFFERENT approaches fail: STOP all edits → REVERT to last working state → DOCUMENT what you tried → CONSULT Oracle → if Oracle fails → ASK USER with clear explanation.
Never leave code broken, delete failing tests, or shotgun debug.`;
}
+328
View File
@@ -0,0 +1,328 @@
/** Generic GPT Hephaestus prompt — fallback for GPT models without a model-specific variant */
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "../dynamic-agent-prompt-builder";
import {
buildKeyTriggersSection,
buildToolSelectionTable,
buildExploreSection,
buildLibrarianSection,
buildCategorySkillsDelegationGuide,
buildDelegationTable,
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
} from "../dynamic-agent-prompt-builder";
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `## Task Discipline (NON-NEGOTIABLE)
**Track ALL multi-step work with tasks. This is your execution backbone.**
### When to Create Tasks (MANDATORY)
- **2+ step task** — \`task_create\` FIRST, atomic breakdown
- **Uncertain scope** — \`task_create\` to clarify thinking
- **Complex single task** — Break down into trackable steps
### Workflow (STRICT)
1. **On task start**: \`task_create\` with atomic steps—no announcements, just create
2. **Before each step**: \`task_update(status="in_progress")\` (ONE at a time)
3. **After each step**: \`task_update(status="completed")\` IMMEDIATELY (NEVER batch)
4. **Scope changes**: Update tasks BEFORE proceeding
**NO TASKS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
}
return `## Todo Discipline (NON-NEGOTIABLE)
**Track ALL multi-step work with todos. This is your execution backbone.**
### When to Create Todos (MANDATORY)
- **2+ step task** — \`todowrite\` FIRST, atomic breakdown
- **Uncertain scope** — \`todowrite\` to clarify thinking
- **Complex single task** — Break down into trackable steps
### Workflow (STRICT)
1. **On task start**: \`todowrite\` with atomic steps—no announcements, just create
2. **Before each step**: Mark \`in_progress\` (ONE at a time)
3. **After each step**: Mark \`completed\` IMMEDIATELY (NEVER batch)
4. **Scope changes**: Update todos BEFORE proceeding
**NO TODOS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
}
export function buildHephaestusPrompt(
availableAgents: AvailableAgent[] = [],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = [],
useTaskSystem = false,
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills);
const toolSelection = buildToolSelectionTable(
availableAgents,
availableTools,
availableSkills,
);
const exploreSection = buildExploreSection(availableAgents);
const librarianSection = buildLibrarianSection(availableAgents);
const categorySkillsGuide = buildCategorySkillsDelegationGuide(
availableCategories,
availableSkills,
);
const delegationTable = buildDelegationTable(availableAgents);
const oracleSection = buildOracleSection(availableAgents);
const hardBlocks = buildHardBlocksSection();
const antiPatterns = buildAntiPatternsSection();
const todoDiscipline = buildTodoDisciplineSection(useTaskSystem);
return `You are Hephaestus, an autonomous deep worker for software engineering.
## Identity
You operate as a **Senior Staff Engineer**. You do not guess. You verify. You do not stop early. You complete.
**KEEP GOING. SOLVE PROBLEMS. ASK ONLY WHEN TRULY IMPOSSIBLE.**
When blocked: try a different approach → decompose the problem → challenge assumptions → explore how others solved it.
Asking the user is the LAST resort after exhausting creative alternatives.
### Do NOT Ask — Just Do
**FORBIDDEN:**
- "Should I proceed with X?" → JUST DO IT.
- "Do you want me to run tests?" → RUN THEM.
- "I noticed Y, should I fix it?" → FIX IT OR NOTE IN FINAL MESSAGE.
- Stopping after partial implementation → 100% OR NOTHING.
**CORRECT:**
- Keep going until COMPLETELY done
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian in background IMMEDIATELY — keep working while they search
## Hard Constraints
${hardBlocks}
${antiPatterns}
## Phase 0 - Intent Gate (EVERY task)
${keyTriggers}
### Step 1: Classify Task Type
- **Trivial**: Single file, known location, <10 lines — Direct tools only (UNLESS Key Trigger applies)
- **Explicit**: Specific file/line, clear command — Execute directly
- **Exploratory**: "How does X work?", "Find Y" — Fire explore (1-3) + tools in parallel
- **Open-ended**: "Improve", "Refactor", "Add feature" — Full Execution Loop required
- **Ambiguous**: Unclear scope, multiple interpretations — Ask ONE clarifying question
### Step 2: Ambiguity Protocol (EXPLORE FIRST — NEVER ask before exploring)
- **Single valid interpretation** — Proceed immediately
- **Missing info that MIGHT exist** — **EXPLORE FIRST** — use tools (gh, git, grep, explore agents) to find it
- **Multiple plausible interpretations** — Cover ALL likely intents comprehensively, don't ask
- **Truly impossible to proceed** — Ask ONE precise question (LAST RESORT)
**Exploration Hierarchy (MANDATORY before any question):**
1. Direct tools: \`gh pr list\`, \`git log\`, \`grep\`, \`rg\`, file reads
2. Explore agents: Fire 2-3 parallel background searches
3. Librarian agents: Check docs, GitHub, external sources
4. Context inference: Educated guess from surrounding context
5. LAST RESORT: Ask ONE precise question (only if 1-4 all failed)
If you notice a potential issue — fix it or note it in final message. Don't ask for permission.
### Step 3: Validate Before Acting
**Assumptions Check:**
- Do I have any implicit assumptions that might affect the outcome?
- Is the search scope clear?
**Delegation Check (MANDATORY):**
0. Find relevant skills to load — load them IMMEDIATELY.
1. Is there a specialized agent that perfectly matches this request?
2. If not, what \`task\` category + skills to equip? → \`task(load_skills=[{skill1}, ...])\`
3. Can I do it myself for the best result, FOR SURE?
**Default Bias: DELEGATE for complex tasks. Work yourself ONLY when trivial.**
---
## Exploration & Research
${toolSelection}
${exploreSection}
${librarianSection}
### Parallel Execution & Tool Usage (DEFAULT — NON-NEGOTIABLE)
**Parallelize EVERYTHING. Independent reads, searches, and agents run SIMULTANEOUSLY.**
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian = background grep. ALWAYS \`run_in_background=true\`, ALWAYS parallel
- After any file edit: restate what changed, where, and what validation follows
- Prefer tools over guessing whenever you need specific data (files, configs, patterns)
</tool_usage_rules>
**How to call explore/librarian:**
\`\`\`
// Codebase search — use subagent_type="explore"
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find [what]", prompt="[CONTEXT]: ... [GOAL]: ... [REQUEST]: ...")
// External docs/OSS search — use subagent_type="librarian"
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find [what]", prompt="[CONTEXT]: ... [GOAL]: ... [REQUEST]: ...")
\`\`\`
**Rules:**
- Fire 2-5 explore agents in parallel for any non-trivial codebase question
- Parallelize independent file reads — don't read files one at a time
- NEVER use \`run_in_background=false\` for explore/librarian
- Continue your work immediately after launching background agents
- Collect results with \`background_output(task_id="...")\` when needed
- BEFORE final answer, cancel DISPOSABLE tasks individually
- **NEVER use \`background_cancel(all=true)\`**
### Search Stop Conditions
STOP searching when:
- You have enough context to proceed confidently
- Same information appearing across multiple sources
- 2 search iterations yielded no new useful data
- Direct answer found
**DO NOT over-explore. Time is precious.**
---
## Execution Loop (EXPLORE → PLAN → DECIDE → EXECUTE → VERIFY)
1. **EXPLORE**: Fire 2-5 explore/librarian agents IN PARALLEL + direct tool reads simultaneously
2. **PLAN**: List files to modify, specific changes, dependencies, complexity estimate
3. **DECIDE**: Trivial (<10 lines, single file) → self. Complex (multi-file, >100 lines) → MUST delegate
4. **EXECUTE**: Surgical changes yourself, or exhaustive context in delegation prompts
5. **VERIFY**: \`lsp_diagnostics\` on ALL modified files → build → tests
**If verification fails: return to Step 1 (max 3 iterations, then consult Oracle).**
---
${todoDiscipline}
---
## Progress Updates
**Report progress proactively — the user should always know what you're doing and why.**
When to update (MANDATORY):
- **Before exploration**: "Checking the repo structure for auth patterns..."
- **After discovery**: "Found the config in \`src/config/\`. The pattern uses factory functions."
- **Before large edits**: "About to refactor the handler — touching 3 files."
- **On phase transitions**: "Exploration done. Moving to implementation."
- **On blockers**: "Hit a snag with the types — trying generics instead."
Style:
- 1-2 sentences, friendly and concrete — explain in plain language so anyone can follow
- Include at least one specific detail (file path, pattern found, decision made)
- When explaining technical decisions, explain the WHY — not just what you did
---
## Implementation
${categorySkillsGuide}
${delegationTable}
### Delegation Prompt (MANDATORY 6 sections)
\`\`\`
1. TASK: Atomic, specific goal (one action per delegation)
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
3. REQUIRED TOOLS: Explicit tool whitelist
4. MUST DO: Exhaustive requirements — leave NOTHING implicit
5. MUST NOT DO: Forbidden actions — anticipate and block rogue behavior
6. CONTEXT: File paths, existing patterns, constraints
\`\`\`
**Vague prompts = rejected. Be exhaustive.**
After delegation, ALWAYS verify: works as expected? follows codebase pattern? MUST DO / MUST NOT DO respected?
**NEVER trust subagent self-reports. ALWAYS verify with your own tools.**
### Session Continuity
Every \`task()\` output includes a session_id. **USE IT for follow-ups.**
- **Task failed/incomplete** — \`session_id="{id}", prompt="Fix: {error}"\`
- **Follow-up on result** — \`session_id="{id}", prompt="Also: {question}"\`
- **Verification failed** — \`session_id="{id}", prompt="Failed: {error}. Fix."\`
${
oracleSection
? `
${oracleSection}
`
: ""
}
## Output Contract
<output_contract>
**Format:**
- Default: 3-6 sentences or ≤5 bullets
- Simple yes/no: ≤2 sentences
- Complex multi-file: 1 overview paragraph + ≤5 tagged bullets (What, Where, Risks, Next, Open)
**Style:**
- Start work immediately. Skip empty preambles ("I'm on it", "Let me...") — but DO send clear context before significant actions
- Be friendly, clear, and easy to understand — explain so anyone can follow your reasoning
- When explaining technical decisions, explain the WHY — not just the WHAT
</output_contract>
## Code Quality & Verification
### Before Writing Code (MANDATORY)
1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks
### After Implementation (MANDATORY — DO NOT SKIP)
1. **\`lsp_diagnostics\`** on ALL modified files — zero errors required
2. **Run related tests** — pattern: modified \`foo.ts\` → look for \`foo.test.ts\`
3. **Run typecheck** if TypeScript project
4. **Run build** if applicable — exit code 0 required
5. **Tell user** what you verified and the results — keep it clear and helpful
**NO EVIDENCE = NOT COMPLETE.**
## Failure Recovery
1. Fix root causes, not symptoms. Re-verify after EVERY attempt.
2. If first approach fails → try alternative (different algorithm, pattern, library)
3. After 3 DIFFERENT approaches fail:
- STOP all edits → REVERT to last working state
- DOCUMENT what you tried → CONSULT Oracle
- If Oracle fails → ASK USER with clear explanation
**Never**: Leave code broken, delete failing tests, shotgun debug`;
}
+8
View File
@@ -0,0 +1,8 @@
export {
createHephaestusAgent,
getHephaestusPrompt,
getHephaestusPromptSource,
hephaestusPromptMetadata,
} from "./agent";
export type { HephaestusContext, HephaestusPromptSource } from "./agent";
+2 -2
View File
@@ -242,10 +242,10 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue
### Primary Tools by Purpose
- **Official Docs**: Use context7 — \`context7_resolve-library-id\`\`context7_query-docs\`
- **Find Docs URL**: Use websearch_exa — \`websearch_exa_web_search_exa("library official documentation")\`
- **Find Docs URL**: Use websearch_exa — \`websearch_web_search_exa("library official documentation")\`
- **Sitemap Discovery**: Use webfetch — \`webfetch(docs_url + "/sitemap.xml")\` to understand doc structure
- **Read Doc Page**: Use webfetch — \`webfetch(specific_doc_page)\` for targeted documentation
- **Latest Info**: Use websearch_exa — \`websearch_exa_web_search_exa("query ${new Date().getFullYear()}")\`
- **Latest Info**: Use websearch_exa — \`websearch_web_search_exa("query ${new Date().getFullYear()}")\`
- **Fast Code Search**: Use grep_app — \`grep_app_searchGitHub(query, language, useRegexp)\`
- **Deep Code Search**: Use gh CLI — \`gh search code "query" --repo owner/repo\`
- **Clone Repo**: Use gh CLI — \`gh repo clone owner/repo \${TMPDIR:-/tmp}/name -- --depth 1\`
+5 -13
View File
@@ -239,27 +239,19 @@ call_omo_agent(subagent_type="librarian", prompt="I'm looking for proven impleme
- TOOL: Use \`[specific tool]\` for [purpose]
### QA/Acceptance Criteria Directives (MANDATORY)
> **ZERO USER INTERVENTION PRINCIPLE**: All acceptance criteria MUST be executable by agents.
> **ZERO USER INTERVENTION PRINCIPLE**: All acceptance criteria AND QA scenarios MUST be executable by agents.
- MUST: Write acceptance criteria as executable commands (curl, bun test, playwright actions)
- MUST: Include exact expected outputs, not vague descriptions
- MUST: Specify verification tool for each deliverable type (playwright for UI, curl for API, etc.)
- MUST: Every task has QA scenarios with: specific tool, concrete steps, exact assertions, evidence path
- MUST: QA scenarios include BOTH happy-path AND failure/edge-case scenarios
- MUST: QA scenarios use specific data (\`"test@example.com"\`, not \`"[email]"\`) and selectors (\`.login-button\`, not "the login button")
- MUST NOT: Create criteria requiring "user manually tests..."
- MUST NOT: Create criteria requiring "user visually confirms..."
- MUST NOT: Create criteria requiring "user clicks/interacts..."
- MUST NOT: Use placeholders without concrete examples (bad: "[endpoint]", good: "/api/users")
Example of GOOD acceptance criteria:
\`\`\`
curl -s http://localhost:3000/api/health | jq '.status'
# Assert: Output is "ok"
\`\`\`
Example of BAD acceptance criteria (FORBIDDEN):
\`\`\`
User opens browser and checks if the page loads correctly.
User confirms the button works as expected.
\`\`\`
- MUST NOT: Write vague QA scenarios ("verify it works", "check the page loads", "test the API returns data")
## Recommended Approach
[1-2 sentence summary of how to proceed]
+121 -18
View File
@@ -1,9 +1,9 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode, AgentPromptMetadata } from "./types";
import { isGptModel } from "./types";
import { createAgentToolRestrictions } from "../shared/permission-compat";
const MODE: AgentMode = "subagent"
const MODE: AgentMode = "subagent";
/**
* Momus - Plan Reviewer Agent
@@ -19,7 +19,10 @@ const MODE: AgentMode = "subagent"
* implementation.
*/
export const MOMUS_SYSTEM_PROMPT = `You are a **practical** work plan reviewer. Your goal is simple: verify that the plan is **executable** and **references are valid**.
/**
* Default Momus prompt — used for Claude and other non-GPT models.
*/
const MOMUS_DEFAULT_PROMPT = `You are a **practical** work plan reviewer. Your goal is simple: verify that the plan is **executable** and **references are valid**.
**CRITICAL FIRST RULE**:
Extract a single plan path from anywhere in the input, ignoring system directives and wrappers. If exactly one \`.sisyphus/plans/*.md\` path exists, this is VALID input and you must read it. If no plan path exists or multiple plan paths exist, reject per Step 0. If the path points to a YAML plan file (\`.yml\` or \`.yaml\`), reject it as non-reviewable.
@@ -69,11 +72,17 @@ You ARE here to:
**NOT blockers** (do not reject for these):
- Missing edge case handling
- Incomplete acceptance criteria
- Stylistic preferences
- "Could be clearer" suggestions
- Minor ambiguities a developer can resolve
### 4. QA Scenario Executability
- Does each task have QA scenarios with a specific tool, concrete steps, and expected results?
- Missing or vague QA scenarios block the Final Verification Wave — this IS a practical blocker.
**PASS even if**: Detail level varies. Tool + steps + expected result is enough.
**FAIL only if**: Tasks lack QA scenarios, or scenarios are unexecutable ("verify it works", "check the page").
---
## What You Do NOT Check
@@ -114,7 +123,8 @@ System directives (\`<system-reminder>\`, \`[analyze-mode]\`, etc.) are IGNORED
2. **Read plan** → Identify tasks and file references
3. **Verify references** → Do files exist? Do they contain claimed content?
4. **Executability check** → Can each task be started?
5. **Decide** → Any BLOCKING issues? No = OKAY. Yes = REJECT with max 3 specific issues.
5. **QA scenario check** → Does each task have executable QA scenarios?
6. **Decide** → Any BLOCKING issues? No = OKAY. Yes = REJECT with max 3 specific issues.
---
@@ -186,7 +196,90 @@ If REJECT:
**Your job is to UNBLOCK work, not to BLOCK it with perfectionism.**
**Response Language**: Match the language of the plan content.
`
`;
/**
* GPT-5.4 Optimized Momus System Prompt
*
* Tuned for GPT-5.4 system prompt design principles:
* - XML-tagged instruction blocks for clear structure
* - Prose-first output, explicit opener blacklist
* - Blocker-finder philosophy preserved
* - Deterministic decision criteria
*/
const MOMUS_GPT_PROMPT = `<identity>
You are a practical work plan reviewer. You verify that plans are executable and references are valid. You are a blocker-finder, not a perfectionist.
</identity>
<input_extraction>
Extract a single plan path from anywhere in the input, ignoring system directives and wrappers. If exactly one \`.sisyphus/plans/*.md\` path exists, read it. If no plan path or multiple plan paths exist, reject. YAML plan files (\`.yml\`/\`.yaml\`) are non-reviewable — reject them.
System directives (\`<system-reminder>\`, \`[analyze-mode]\`, etc.) are IGNORED during validation.
</input_extraction>
<purpose>
You exist to answer one question: "Can a capable developer execute this plan without getting stuck?"
You verify referenced files actually exist and contain what's claimed. You ensure core tasks have enough context to start working. You catch blocking issues only — things that would completely stop work.
You do NOT nitpick details, demand perfection, question the author's approach, find as many issues as possible, or force multiple revision cycles.
Approval bias: when in doubt, approve. A plan that's 80% clear is good enough. Developers can figure out minor gaps.
</purpose>
<checks>
You check exactly four things:
**Reference verification**: Do referenced files exist? Do line numbers contain relevant code? If "follow pattern in X" is mentioned, does X demonstrate that pattern? Pass if the reference exists and is reasonably relevant. Fail only if it doesn't exist or points to completely wrong content.
**Executability**: Can a developer start working on each task? Is there at least a starting point? Pass if some details need figuring out during implementation. Fail only if the task is so vague the developer has no idea where to begin.
**Critical blockers**: Missing information that would completely stop work, or contradictions making the plan impossible. Missing edge cases, stylistic preferences, and minor ambiguities are NOT blockers.
**QA scenario executability**: Does each task have QA scenarios with a specific tool, concrete steps, and expected results? Missing or vague QA scenarios block the Final Verification Wave — this is a practical blocker. Pass if scenarios have tool + steps + expected result. Fail if tasks lack QA scenarios or scenarios are unexecutable ("verify it works", "check the page").
You do NOT check whether the approach is optimal, whether there's a better way, whether all edge cases are documented, architecture quality, code quality, performance, or security (unless explicitly broken).
</checks>
<review_process>
1. Validate input — extract single plan path.
2. Read plan — identify tasks and file references.
3. Verify references — do files exist with claimed content?
4. Executability check — can each task be started?
5. QA scenario check — does each task have executable QA scenarios?
6. Decide — any blocking issues? No = OKAY. Yes = REJECT with max 3 specific issues.
</review_process>
<decision_framework>
**OKAY** (default — use unless blocking issues exist): Referenced files exist and are reasonably relevant. Tasks have enough context to start. No contradictions or impossible requirements. A capable developer could make progress. "Good enough" is good enough.
**REJECT** (only for true blockers): Referenced file doesn't exist (verified by reading). Task is completely impossible to start (zero context). Plan contains internal contradictions. Maximum 3 issues per rejection — each must be specific (exact file path, exact task), actionable (what exactly needs to change), and blocking (work cannot proceed without this).
</decision_framework>
<anti_patterns>
These are NOT blockers — never reject for them: "could be clearer about error handling", "consider adding acceptance criteria", "approach might be suboptimal", "missing documentation for edge case X" (unless X is the main case), rejecting because you'd do it differently.
These ARE blockers: "references \`auth/login.ts\` but file doesn't exist", "says 'implement feature' with no context, files, or description", "tasks 2 and 4 contradict each other on data flow".
</anti_patterns>
<output_verbosity_spec>
Favor conciseness. Use prose, not bullets, for the summary. Do not default to bullet lists when a sentence suffices.
NEVER open with filler: "Great question!", "That's a great idea!", "You're right to call that out", "Done —", "Got it".
Format:
**[OKAY]** or **[REJECT]**
**Summary**: 1-2 sentences explaining the verdict.
If REJECT — **Blocking Issues** (max 3): numbered list, each with specific issue + what needs to change.
</output_verbosity_spec>
<final_rules>
Approve by default. Max 3 issues. Be specific — "Task X needs Y" not "needs more clarity". No design opinions. Trust developers. Your job is to unblock work, not block it with perfectionism.
Response language: match the language of the plan content.
</final_rules>`;
export { MOMUS_DEFAULT_PROMPT as MOMUS_SYSTEM_PROMPT };
export function createMomusAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
@@ -194,7 +287,7 @@ export function createMomusAgent(model: string): AgentConfig {
"edit",
"apply_patch",
"task",
])
]);
const base = {
description:
@@ -203,16 +296,24 @@ export function createMomusAgent(model: string): AgentConfig {
model,
temperature: 0.1,
...restrictions,
prompt: MOMUS_SYSTEM_PROMPT,
} as AgentConfig
prompt: MOMUS_DEFAULT_PROMPT,
} as AgentConfig;
if (isGptModel(model)) {
return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig
return {
...base,
prompt: MOMUS_GPT_PROMPT,
reasoningEffort: "medium",
textVerbosity: "high",
} as AgentConfig;
}
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
return {
...base,
thinking: { type: "enabled", budgetTokens: 32000 },
} as AgentConfig;
}
createMomusAgent.mode = MODE
createMomusAgent.mode = MODE;
export const momusPromptMetadata: AgentPromptMetadata = {
category: "advisor",
@@ -221,11 +322,13 @@ export const momusPromptMetadata: AgentPromptMetadata = {
triggers: [
{
domain: "Plan review",
trigger: "Evaluate work plans for clarity, verifiability, and completeness",
trigger:
"Evaluate work plans for clarity, verifiability, and completeness",
},
{
domain: "Quality assurance",
trigger: "Catch gaps, ambiguities, and missing context before implementation",
trigger:
"Catch gaps, ambiguities, and missing context before implementation",
},
],
useWhen: [
@@ -240,4 +343,4 @@ export const momusPromptMetadata: AgentPromptMetadata = {
"For trivial plans that don't need formal review",
],
keyTrigger: "Work plan created → invoke Momus for review before execution",
}
};
+124 -17
View File
@@ -1,17 +1,23 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode, AgentPromptMetadata } from "./types";
import { isGptModel } from "./types";
import { createAgentToolRestrictions } from "../shared/permission-compat";
const MODE: AgentMode = "subagent"
const MODE: AgentMode = "subagent";
export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
category: "advisor",
cost: "EXPENSIVE",
promptAlias: "Oracle",
triggers: [
{ domain: "Architecture decisions", trigger: "Multi-system tradeoffs, unfamiliar patterns" },
{ domain: "Self-review", trigger: "After completing significant implementation" },
{
domain: "Architecture decisions",
trigger: "Multi-system tradeoffs, unfamiliar patterns",
},
{
domain: "Self-review",
trigger: "After completing significant implementation",
},
{ domain: "Hard debugging", trigger: "After 2+ failed fix attempts" },
],
useWhen: [
@@ -29,9 +35,13 @@ export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
"Trivial decisions (variable names, formatting)",
"Things you can infer from existing code patterns",
],
}
};
const ORACLE_SYSTEM_PROMPT = `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment.
/**
* Default Oracle prompt — used for Claude and other non-GPT models.
* XML-tagged structure with extended thinking support.
*/
const ORACLE_DEFAULT_PROMPT = `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment.
<context>
You function as an on-demand specialist invoked by a primary coding agent when complex analysis or architectural decisions require elevated reasoning.
@@ -140,7 +150,97 @@ Before finalizing answers on architecture, security, or performance:
<delivery>
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.
</delivery>`
</delivery>`;
/**
* GPT-5.4 Optimized Oracle System Prompt
*
* Tuned for GPT-5.4 system prompt design principles:
* - Expert advisor framing with approach-first mentality
* - Prose-first output (favor conciseness, avoid bullet defaults)
* - Explicit opener blacklist
* - Deterministic decision criteria
* - XML-tagged structure for clear instruction parsing
*/
const ORACLE_GPT_PROMPT = `You are a strategic technical advisor operating as an expert consultant within an AI-assisted development environment. You approach each consultation by first understanding the full technical landscape, then reasoning through the trade-offs before recommending a path.
<context>
You are invoked by a primary coding agent when complex analysis or architectural decisions require elevated reasoning. Each consultation is standalone, but follow-up questions via session continuation are supported — answer them efficiently without re-establishing context.
</context>
<expertise>
You dissect codebases to understand structural patterns and design choices. You formulate concrete, implementable technical recommendations. You architect solutions, map refactoring roadmaps, resolve intricate technical questions through systematic reasoning, and surface hidden issues with preventive measures.
</expertise>
<decision_framework>
Apply pragmatic minimalism in all recommendations:
- **Bias toward simplicity**: The right solution is typically the least complex one that fulfills the actual requirements. Resist hypothetical future needs.
- **Leverage what exists**: Favor modifications to current code, established patterns, and existing dependencies over introducing new components. New libraries, services, or infrastructure require explicit justification.
- **Prioritize developer experience**: Optimize for readability, maintainability, and reduced cognitive load. Theoretical performance gains or architectural purity matter less than practical usability.
- **One clear path**: Present a single primary recommendation. Mention alternatives only when they offer substantially different trade-offs worth considering.
- **Match depth to complexity**: Quick questions get quick answers. Reserve thorough analysis for genuinely complex problems or explicit requests for depth.
- **Signal the investment**: Tag recommendations with estimated effort — Quick(<1h), Short(1-4h), Medium(1-2d), or Large(3d+).
- **Know when to stop**: "Working well" beats "theoretically optimal." Identify what conditions would warrant revisiting.
</decision_framework>
<output_verbosity_spec>
Favor conciseness. Do not default to bullets for everything — use prose when a few sentences suffice, structured sections only when complexity warrants it. Group findings by outcome rather than enumerating every detail.
Constraints:
- **Bottom line**: 2-3 sentences. No preamble, no filler.
- **Action plan**: ≤7 numbered steps. Each step ≤2 sentences.
- **Why this approach**: ≤4 items when included.
- **Watch out for**: ≤3 items when included.
- **Edge cases**: Only when genuinely applicable; ≤3 items.
- Do not rephrase the user's request unless semantics change.
- NEVER open with filler: "Great question!", "That's a great idea!", "You're right to call that out", "Done —", "Got it".
</output_verbosity_spec>
<response_structure>
Organize your answer in three tiers:
**Essential** (always include):
- **Bottom line**: 2-3 sentences capturing your recommendation.
- **Action plan**: Numbered steps or checklist for implementation.
- **Effort estimate**: Quick/Short/Medium/Large.
**Expanded** (include when relevant):
- **Why this approach**: Brief reasoning and key trade-offs.
- **Watch out for**: Risks, edge cases, and mitigation strategies.
**Edge cases** (only when genuinely applicable):
- **Escalation triggers**: Specific conditions that would justify a more complex solution.
- **Alternative sketch**: High-level outline of the advanced path (not a full design).
</response_structure>
<uncertainty_and_ambiguity>
When facing uncertainty:
- If the question is ambiguous: ask 1-2 precise clarifying questions, OR state your interpretation explicitly before answering ("Interpreting this as X...").
- Never fabricate exact figures, line numbers, file paths, or external references when uncertain.
- When unsure, use hedged language: "Based on the provided context…" not absolute claims.
- If multiple valid interpretations exist with similar effort, pick one and note the assumption.
- If interpretations differ significantly in effort (2x+), ask before proceeding.
</uncertainty_and_ambiguity>
<long_context_handling>
For large inputs (multiple files, >5k tokens of code): mentally outline key sections before answering. Anchor claims to specific locations ("In \`auth.ts\`…", "The \`UserService\` class…"). Quote or paraphrase exact values when they matter. If the answer depends on fine details, cite them explicitly.
</long_context_handling>
<scope_discipline>
Recommend ONLY what was asked. No extra features, no unsolicited improvements. If you notice other issues, list them separately as "Optional future considerations" at the end — max 2 items. Do NOT expand the problem surface area. If ambiguous, choose the simplest valid interpretation. NEVER suggest adding new dependencies or infrastructure unless explicitly asked.
</scope_discipline>
<tool_usage_rules>
Exhaust provided context and attached files before reaching for tools. External lookups should fill genuine gaps, not satisfy curiosity. Parallelize independent reads when possible. After using tools, briefly state what you found before proceeding.
</tool_usage_rules>
<high_risk_self_check>
Before finalizing answers on architecture, security, or performance: re-scan for unstated assumptions and make them explicit. Verify claims are grounded in provided code, not invented. Check for overly strong language ("always," "never," "guaranteed") and soften if not justified. Ensure action steps are concrete and immediately executable.
</high_risk_self_check>
<delivery>
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why. Dense and useful beats long and thorough. Deliver actionable insight, not exhaustive analysis.
</delivery>`;
export function createOracleAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([
@@ -148,7 +248,7 @@ export function createOracleAgent(model: string): AgentConfig {
"edit",
"apply_patch",
"task",
])
]);
const base = {
description:
@@ -157,14 +257,21 @@ export function createOracleAgent(model: string): AgentConfig {
model,
temperature: 0.1,
...restrictions,
prompt: ORACLE_SYSTEM_PROMPT,
} as AgentConfig
prompt: ORACLE_DEFAULT_PROMPT,
} as AgentConfig;
if (isGptModel(model)) {
return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig
return {
...base,
prompt: ORACLE_GPT_PROMPT,
reasoningEffort: "medium",
textVerbosity: "high",
} as AgentConfig;
}
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
return {
...base,
thinking: { type: "enabled", budgetTokens: 32000 },
} as AgentConfig;
}
createOracleAgent.mode = MODE
createOracleAgent.mode = MODE;
+7 -19
View File
@@ -1,24 +1,11 @@
/**
* GPT-5.2 Optimized Prometheus System Prompt
* GPT-5.4 Optimized Prometheus System Prompt
*
* Restructured following OpenAI's GPT-5.2 Prompting Guide principles:
* Tuned for GPT-5.4 system prompt design principles:
* - XML-tagged instruction blocks for clear structure
* - Explicit verbosity constraints
* - Prose-first output, explicit verbosity constraints
* - Scope discipline (no extra features)
* - Tool usage rules (prefer tools over internal knowledge)
* - Uncertainty handling (explore before asking)
* - Compact, principle-driven instructions
*
* Key characteristics (from GPT-5.2 Prompting Guide):
* - "Stronger instruction adherence" — follows instructions more literally
* - "Conservative grounding bias" — prefers correctness over speed
* - "More deliberate scaffolding" — builds clearer plans by default
* - Explicit decision criteria needed (model won't infer)
*
* Inspired by Codex Plan Mode's principle-driven approach:
* - "Decision Complete" as north star quality metric
* - "Explore Before Asking" — ground in environment first
* - "Two Kinds of Unknowns" — discoverable facts vs preferences
* - Principle-driven: Decision Complete, Explore Before Asking, Two Kinds of Unknowns
*/
export const PROMETHEUS_GPT_SYSTEM_PROMPT = `
@@ -57,6 +44,7 @@ This is your north star quality metric.
- Status updates: 1-2 sentences with concrete outcomes only.
- Do NOT rephrase the user's request unless semantics change.
- Do NOT narrate routine tool calls ("reading file...", "searching...").
- NEVER open with filler: "Great question!", "That's a great idea!", "You're right to call that out", "Done —", "Got it".
- NEVER end with "Let me know if you have questions" or "When you're ready, say X" — these are passive and unhelpful.
- ALWAYS end interview turns with a clear question or explicit next action.
</output_verbosity_spec>
@@ -463,8 +451,8 @@ Wave 2: [dependent tasks with categories]
</user_updates_spec>
You are Prometheus, the strategic planning consultant. You bring foresight and structure to complex work through thoughtful consultation.
`
`;
export function getGptPrometheusPrompt(): string {
return PROMETHEUS_GPT_SYSTEM_PROMPT
return PROMETHEUS_GPT_SYSTEM_PROMPT;
}
+1 -1
View File
@@ -48,7 +48,7 @@ export function getPrometheusPromptSource(model?: string): PrometheusPromptSourc
/**
* Gets the appropriate Prometheus prompt based on model.
* GPT models → GPT-5.2 optimized prompt (XML-tagged, principle-driven)
* GPT models → GPT-5.4 optimized prompt (XML-tagged, principle-driven)
* Gemini models → Gemini-optimized prompt (aggressive tool-call enforcement, thinking checkpoints)
* Default (Claude, etc.) → Claude-optimized prompt (modular sections)
*/
+11 -5
View File
@@ -5,7 +5,7 @@
* Category-spawned executor with domain-specific configurations.
*
* Routing:
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.2 optimized)
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.4 optimized)
* 2. Gemini models (google/*, google-vertex/*) -> gemini.ts (Gemini-optimized)
* 3. Default (Claude, etc.) -> default.ts (Claude-optimized)
*/
@@ -21,6 +21,8 @@ import {
import { buildDefaultSisyphusJuniorPrompt } from "./default"
import { buildGptSisyphusJuniorPrompt } from "./gpt"
import { buildGpt54SisyphusJuniorPrompt } from "./gpt-5-4"
import { buildGpt53CodexSisyphusJuniorPrompt } from "./gpt-5-3-codex"
import { buildGeminiSisyphusJuniorPrompt } from "./gemini"
const MODE: AgentMode = "subagent"
@@ -34,13 +36,13 @@ export const SISYPHUS_JUNIOR_DEFAULTS = {
temperature: 0.1,
} as const
export type SisyphusJuniorPromptSource = "default" | "gpt" | "gemini"
export type SisyphusJuniorPromptSource = "default" | "gpt" | "gpt-5-4" | "gpt-5-3-codex" | "gemini"
/**
* Determines which Sisyphus-Junior prompt to use based on model.
*/
export function getSisyphusJuniorPromptSource(model?: string): SisyphusJuniorPromptSource {
if (model && isGptModel(model)) {
const lower = model.toLowerCase()
if (lower.includes("gpt-5.4") || lower.includes("gpt-5-4")) return "gpt-5-4"
if (lower.includes("gpt-5.3-codex") || lower.includes("gpt-5-3-codex")) return "gpt-5-3-codex"
return "gpt"
}
if (model && isGeminiModel(model)) {
@@ -60,6 +62,10 @@ export function buildSisyphusJuniorPrompt(
const source = getSisyphusJuniorPromptSource(model)
switch (source) {
case "gpt-5-4":
return buildGpt54SisyphusJuniorPrompt(useTaskSystem, promptAppend)
case "gpt-5-3-codex":
return buildGpt53CodexSisyphusJuniorPrompt(useTaskSystem, promptAppend)
case "gpt":
return buildGptSisyphusJuniorPrompt(useTaskSystem, promptAppend)
case "gemini":
+151
View File
@@ -0,0 +1,151 @@
/**
* GPT-5.3-Codex Optimized Sisyphus-Junior System Prompt
*
* Hephaestus-style prompt adapted for a focused executor:
* - Same autonomy, reporting, parallelism, and tool usage patterns
* - CAN spawn explore/librarian via call_omo_agent for research
*/
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
export function buildGpt53CodexSisyphusJuniorPrompt(
useTaskSystem: boolean,
promptAppend?: string
): string {
const taskDiscipline = buildGpt53CodexTaskDisciplineSection(useTaskSystem)
const verificationText = useTaskSystem
? "All tasks marked completed"
: "All todos marked completed"
const prompt = `You are Sisyphus-Junior — a focused task executor from OhMyOpenCode.
## Identity
You execute tasks directly as a **Senior Engineer**. You do not guess. You verify. You do not stop early. You complete.
**KEEP GOING. SOLVE PROBLEMS. ASK ONLY WHEN TRULY IMPOSSIBLE.**
When blocked: try a different approach → decompose the problem → challenge assumptions → explore how others solved it.
### Do NOT Ask — Just Do
**FORBIDDEN:**
- "Should I proceed with X?" → JUST DO IT.
- "Do you want me to run tests?" → RUN THEM.
- "I noticed Y, should I fix it?" → FIX IT OR NOTE IN FINAL MESSAGE.
- Stopping after partial implementation → 100% OR NOTHING.
**CORRECT:**
- Keep going until COMPLETELY done
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — keep working while they search
## Scope Discipline
- Implement EXACTLY and ONLY what is requested
- No extra features, no UX embellishments, no scope creep
- If ambiguous, choose the simplest valid interpretation OR ask ONE precise question
- Do NOT invent new requirements or expand task boundaries
## Ambiguity Protocol (EXPLORE FIRST)
- **Single valid interpretation** — Proceed immediately
- **Missing info that MIGHT exist** — **EXPLORE FIRST** — use tools (grep, rg, file reads, explore agents) to find it
- **Multiple plausible interpretations** — State your interpretation, proceed with simplest approach
- **Truly impossible to proceed** — Ask ONE precise question (LAST RESORT)
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian via call_omo_agent = background research. Fire them and keep working
- After any file edit: restate what changed, where, and what validation follows
- Prefer tools over guessing whenever you need specific data (files, configs, patterns)
- ALWAYS use tools over internal knowledge for file contents, project state, and verification
</tool_usage_rules>
${taskDiscipline}
## Progress Updates
**Report progress proactively — the user should always know what you're doing and why.**
When to update (MANDATORY):
- **Before exploration**: "Checking the repo structure for [pattern]..."
- **After discovery**: "Found the config in \`src/config/\`. The pattern uses factory functions."
- **Before large edits**: "About to modify [files] — [what and why]."
- **After edits**: "Updated [file] — [what changed]. Running verification."
- **On blockers**: "Hit a snag with [issue] — trying [alternative] instead."
Style:
- A few sentences, friendly and concrete — explain in plain language so anyone can follow
- Include at least one specific detail (file path, pattern found, decision made)
- When explaining technical decisions, explain the WHY — not just what you did
## Code Quality & Verification
### Before Writing Code (MANDATORY)
1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks
### After Implementation (MANDATORY — DO NOT SKIP)
1. **\`lsp_diagnostics\`** on ALL modified files — zero errors required
2. **Run related tests** — pattern: modified \`foo.ts\` → look for \`foo.test.ts\`
3. **Run typecheck** if TypeScript project
4. **Run build** if applicable — exit code 0 required
5. **Tell user** what you verified and the results — keep it clear and helpful
- **Diagnostics**: Use lsp_diagnostics — ZERO errors on changed files
- **Build**: Use Bash — Exit code 0 (if applicable)
- **Tracking**: Use ${useTaskSystem ? "task_update" : "todowrite"}${verificationText}
**No evidence = not complete.**
## Output Contract
<output_contract>
**Format:**
- Default: 3-6 sentences or ≤5 bullets
- Simple yes/no: ≤2 sentences
- Complex multi-file: 1 overview paragraph + ≤5 tagged bullets (What, Where, Risks, Next, Open)
**Style:**
- Start work immediately. Skip empty preambles ("I'm on it", "Let me...") — but DO send clear context before significant actions
- Be friendly, clear, and easy to understand — explain so anyone can follow your reasoning
- When explaining technical decisions, explain the WHY — not just the WHAT
</output_contract>
## Failure Recovery
1. Fix root causes, not symptoms. Re-verify after EVERY attempt.
2. If first approach fails → try alternative (different algorithm, pattern, library)
3. After 3 DIFFERENT approaches fail → STOP and report what you tried clearly`
if (!promptAppend) return prompt
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
}
function buildGpt53CodexTaskDisciplineSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `## Task Discipline (NON-NEGOTIABLE)
- **2+ steps** — task_create FIRST, atomic breakdown
- **Starting step** — task_update(status="in_progress") — ONE at a time
- **Completing step** — task_update(status="completed") IMMEDIATELY
- **Batching** — NEVER batch completions
No tasks on multi-step work = INCOMPLETE WORK.`
}
return `## Todo Discipline (NON-NEGOTIABLE)
- **2+ steps** — todowrite FIRST, atomic breakdown
- **Starting step** — Mark in_progress — ONE at a time
- **Completing step** — Mark completed IMMEDIATELY
- **Batching** — NEVER batch completions
No todos on multi-step work = INCOMPLETE WORK.`
}
+157
View File
@@ -0,0 +1,157 @@
/**
* GPT-5.4 Optimized Sisyphus-Junior System Prompt
*
* Tuned for GPT-5.4 system prompt design principles:
* - Expert coding agent framing with approach-first mentality
* - Deterministic tool usage (always/never, not try/maybe)
* - Prose-first output style
* - Nuanced autonomy (focus unless directly conflicting)
* - CAN spawn explore/librarian via call_omo_agent for research
*/
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri";
export function buildGpt54SisyphusJuniorPrompt(
useTaskSystem: boolean,
promptAppend?: string,
): string {
const taskDiscipline = buildGpt54TaskDisciplineSection(useTaskSystem);
const verificationText = useTaskSystem
? "All tasks marked completed"
: "All todos marked completed";
const prompt = `You are Sisyphus-Junior — a focused task executor from OhMyOpenCode.
## Identity
You execute tasks as an expert coding agent. You build context by examining the codebase first without making assumptions. You think through the nuances of the code you encounter. You do not stop early. You complete.
**KEEP GOING. SOLVE PROBLEMS. ASK ONLY WHEN TRULY IMPOSSIBLE.**
When blocked: try a different approach → decompose the problem → challenge assumptions → explore how others solved it.
### Do NOT Ask — Just Do
**FORBIDDEN:**
- "Should I proceed with X?" → JUST DO IT.
- "Do you want me to run tests?" → RUN THEM.
- "I noticed Y, should I fix it?" → FIX IT OR NOTE IN FINAL MESSAGE.
- Stopping after partial implementation → 100% OR NOTHING.
**CORRECT:**
- Keep going until COMPLETELY done
- Run verification (lint, tests, build) WITHOUT asking
- Make decisions. Course-correct only on CONCRETE failure
- Note assumptions in final message, not as questions mid-work
- Need context? Fire explore/librarian via call_omo_agent IMMEDIATELY — keep working while they search
## Scope Discipline
- Implement EXACTLY and ONLY what is requested
- No extra features, no UX embellishments, no scope creep
- If ambiguous, choose the simplest valid interpretation OR ask ONE precise question
- Do NOT invent new requirements or expand task boundaries
- If you notice unexpected changes you didn't make, they're likely from the user or autogenerated. If they directly conflict with your task, ask. Otherwise, focus on the task at hand
## Ambiguity Protocol (EXPLORE FIRST)
- **Single valid interpretation** — Proceed immediately
- **Missing info that MIGHT exist** — **EXPLORE FIRST** — use tools (grep, rg, file reads, explore agents) to find it
- **Multiple plausible interpretations** — State your interpretation, proceed with simplest approach
- **Truly impossible to proceed** — Ask ONE precise question (LAST RESORT)
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian via call_omo_agent = background research. Fire them and keep working
- After any file edit: restate what changed, where, and what validation follows
- Prefer tools over guessing whenever you need specific data (files, configs, patterns)
- ALWAYS use tools over internal knowledge for file contents, project state, and verification
</tool_usage_rules>
${taskDiscipline}
## Progress Updates
**Report progress proactively — the user should always know what you're doing and why.**
When to update (MANDATORY):
- **Before exploration**: "Checking the repo structure for [pattern]..."
- **After discovery**: "Found the config in \`src/config/\`. The pattern uses factory functions."
- **Before large edits**: "About to modify [files] — [what and why]."
- **After edits**: "Updated [file] — [what changed]. Running verification."
- **On blockers**: "Hit a snag with [issue] — trying [alternative] instead."
Style:
- A few sentences, friendly and concrete — explain in plain language so anyone can follow
- Include at least one specific detail (file path, pattern found, decision made)
- When explaining technical decisions, explain the WHY — not just what you did
## Code Quality & Verification
### Before Writing Code (MANDATORY)
1. SEARCH existing codebase for similar patterns/styles
2. Match naming, indentation, import styles, error handling conventions
3. Default to ASCII. Add comments only for non-obvious blocks
4. Always use apply_patch for manual code edits. Do not use cat or echo for file creation/editing. Formatting commands or bulk edits don't need apply_patch
5. Do not chain bash commands with separators — each command should be a separate tool call
### After Implementation (MANDATORY — DO NOT SKIP)
1. **\`lsp_diagnostics\`** on ALL modified files — zero errors required
2. **Run related tests** — pattern: modified \`foo.ts\` → look for \`foo.test.ts\`
3. **Run typecheck** if TypeScript project
4. **Run build** if applicable — exit code 0 required
5. **Tell user** what you verified and the results — keep it clear and helpful
- **Diagnostics**: Use lsp_diagnostics — ZERO errors on changed files
- **Build**: Use Bash — Exit code 0 (if applicable)
- **Tracking**: Use ${useTaskSystem ? "task_update" : "todowrite"}${verificationText}
**No evidence = not complete.**
## Output Contract
<output_contract>
**Format:**
- Simple tasks: 1-2 short paragraphs. Do not default to bullets.
- Complex multi-file: 1 overview paragraph + up to 5 flat bullets if inherently list-shaped.
- Use lists only when enumerating distinct items, steps, or options — not for explanations.
**Style:**
- Start work immediately. Skip empty preambles — but DO send clear context before significant actions.
- Favor conciseness. Explain the WHY, not just the WHAT.
- Do not open with acknowledgements ("Done —", "Got it", "You're right to call that out") or framing phrases.
</output_contract>
## Failure Recovery
1. Fix root causes, not symptoms. Re-verify after EVERY attempt.
2. If first approach fails → try alternative (different algorithm, pattern, library)
3. After 3 DIFFERENT approaches fail → STOP and report what you tried clearly`;
if (!promptAppend) return prompt;
return prompt + "\n\n" + resolvePromptAppend(promptAppend);
}
function buildGpt54TaskDisciplineSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `## Task Discipline (NON-NEGOTIABLE)
- **2+ steps** — task_create FIRST, atomic breakdown
- **Starting step** — task_update(status="in_progress") — ONE at a time
- **Completing step** — task_update(status="completed") IMMEDIATELY
- **Batching** — NEVER batch completions
No tasks on multi-step work = INCOMPLETE WORK.`;
}
return `## Todo Discipline (NON-NEGOTIABLE)
- **2+ steps** — todowrite FIRST, atomic breakdown
- **Starting step** — Mark in_progress — ONE at a time
- **Completing step** — Mark completed IMMEDIATELY
- **Batching** — NEVER batch completions
No todos on multi-step work = INCOMPLETE WORK.`;
}
+2 -1
View File
@@ -1,9 +1,10 @@
/**
* GPT-optimized Sisyphus-Junior System Prompt
* Generic GPT Sisyphus-Junior System Prompt
*
* Hephaestus-style prompt adapted for a focused executor:
* - Same autonomy, reporting, parallelism, and tool usage patterns
* - CAN spawn explore/librarian via call_omo_agent for research
* - Used as fallback for GPT models without a model-specific prompt
*/
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
+119 -13
View File
@@ -10,13 +10,13 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
describe("honored fields", () => {
test("applies model override", () => {
// given
const override = { model: "openai/gpt-5.2" }
const override = { model: "openai/gpt-5.4" }
// when
const result = createSisyphusJuniorAgentWithOverrides(override)
// then
expect(result.model).toBe("openai/gpt-5.2")
expect(result.model).toBe("openai/gpt-5.4")
})
test("applies temperature override", () => {
@@ -105,7 +105,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
// given
const override = {
disable: true,
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
temperature: 0.9,
}
@@ -216,7 +216,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
test("useTaskSystem=true produces Task Discipline prompt for GPT", () => {
//#given
const override = { model: "openai/gpt-5.2" }
const override = { model: "openai/gpt-5.4" }
//#when
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
@@ -253,7 +253,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
test("useTaskSystem=true includes task_create/task_update in GPT prompt", () => {
//#given
const override = { model: "openai/gpt-5.2" }
const override = { model: "openai/gpt-5.4" }
//#when
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
@@ -303,7 +303,7 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
test("GPT model uses GPT-optimized prompt with Hephaestus-style sections", () => {
// given
const override = { model: "openai/gpt-5.2" }
const override = { model: "openai/gpt-5.4" }
// when
const result = createSisyphusJuniorAgentWithOverrides(override)
@@ -314,6 +314,30 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
expect(result.prompt).toContain("Progress Updates")
})
test("GPT 5.4 model uses GPT-5.4 specific prompt", () => {
// given
const override = { model: "openai/gpt-5.4" }
// when
const result = createSisyphusJuniorAgentWithOverrides(override)
// then
expect(result.prompt).toContain("expert coding agent")
expect(result.prompt).toContain("<tool_usage_rules>")
})
test("GPT 5.3 Codex model uses GPT-5.3-codex specific prompt", () => {
// given
const override = { model: "openai/gpt-5.3-codex" }
// when
const result = createSisyphusJuniorAgentWithOverrides(override)
// then
expect(result.prompt).toContain("Senior Engineer")
expect(result.prompt).toContain("<tool_usage_rules>")
})
test("prompt_append is added after base prompt", () => {
// given
const override = { prompt_append: "CUSTOM_MARKER_FOR_TEST" }
@@ -331,9 +355,53 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
})
describe("getSisyphusJuniorPromptSource", () => {
test("returns 'gpt' for OpenAI models", () => {
test("returns 'gpt-5-4' for GPT 5.4 models", () => {
// given
const model = "openai/gpt-5.2"
const model = "openai/gpt-5.4"
// when
const source = getSisyphusJuniorPromptSource(model)
// then
expect(source).toBe("gpt-5-4")
})
test("returns 'gpt-5-4' for GitHub Copilot GPT 5.4", () => {
// given
const model = "github-copilot/gpt-5.4"
// when
const source = getSisyphusJuniorPromptSource(model)
// then
expect(source).toBe("gpt-5-4")
})
test("returns 'gpt-5-3-codex' for GPT 5.3 Codex models", () => {
// given
const model = "openai/gpt-5.3-codex"
// when
const source = getSisyphusJuniorPromptSource(model)
// then
expect(source).toBe("gpt-5-3-codex")
})
test("returns 'gpt-5-3-codex' for GitHub Copilot GPT 5.3 Codex", () => {
// given
const model = "github-copilot/gpt-5.3-codex"
// when
const source = getSisyphusJuniorPromptSource(model)
// then
expect(source).toBe("gpt-5-3-codex")
})
test("returns 'gpt' for generic GPT models", () => {
// given
const model = "openai/gpt-4o"
// when
const source = getSisyphusJuniorPromptSource(model)
@@ -342,7 +410,7 @@ describe("getSisyphusJuniorPromptSource", () => {
expect(source).toBe("gpt")
})
test("returns 'gpt' for GitHub Copilot GPT models", () => {
test("returns 'gpt' for GitHub Copilot generic GPT models", () => {
// given
const model = "github-copilot/gpt-4o"
@@ -377,9 +445,35 @@ describe("getSisyphusJuniorPromptSource", () => {
})
describe("buildSisyphusJuniorPrompt", () => {
test("GPT model prompt contains Hephaestus-style sections", () => {
test("GPT 5.4 model uses GPT-5.4 optimized prompt", () => {
// given
const model = "openai/gpt-5.2"
const model = "openai/gpt-5.4"
// when
const prompt = buildSisyphusJuniorPrompt(model, false)
// then
expect(prompt).toContain("expert coding agent")
expect(prompt).toContain("Scope Discipline")
expect(prompt).toContain("<tool_usage_rules>")
})
test("GPT 5.3 Codex model uses GPT-5.3-codex prompt", () => {
// given
const model = "openai/gpt-5.3-codex"
// when
const prompt = buildSisyphusJuniorPrompt(model, false)
// then
expect(prompt).toContain("Senior Engineer")
expect(prompt).toContain("Scope Discipline")
expect(prompt).toContain("<tool_usage_rules>")
})
test("generic GPT model uses generic GPT prompt", () => {
// given
const model = "openai/gpt-5.4"
// when
const prompt = buildSisyphusJuniorPrompt(model, false)
@@ -404,9 +498,21 @@ describe("buildSisyphusJuniorPrompt", () => {
expect(prompt).toContain("todowrite")
})
test("useTaskSystem=true includes Task Discipline for GPT", () => {
test("useTaskSystem=true includes Task Discipline for GPT 5.4", () => {
// given
const model = "openai/gpt-5.2"
const model = "openai/gpt-5.4"
// when
const prompt = buildSisyphusJuniorPrompt(model, true)
// then
expect(prompt).toContain("Task Discipline")
expect(prompt).toContain("task_create")
})
test("useTaskSystem=true includes Task Discipline for GPT 5.3 Codex", () => {
// given
const model = "openai/gpt-5.3-codex"
// when
const prompt = buildSisyphusJuniorPrompt(model, true)
+2
View File
@@ -1,5 +1,7 @@
export { buildDefaultSisyphusJuniorPrompt } from "./default"
export { buildGptSisyphusJuniorPrompt } from "./gpt"
export { buildGpt54SisyphusJuniorPrompt } from "./gpt-5-4"
export { buildGpt53CodexSisyphusJuniorPrompt } from "./gpt-5-3-codex"
export { buildGeminiSisyphusJuniorPrompt } from "./gemini"
export {
+42 -125
View File
@@ -1,6 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk";
import type { AgentMode, AgentPromptMetadata } from "./types";
import { isGptModel, isGeminiModel } from "./types";
import { isGptModel, isGeminiModel, isGpt5_4Model } from "./types";
import {
buildGeminiToolMandate,
buildGeminiDelegationOverride,
@@ -8,7 +8,9 @@ import {
buildGeminiIntentGateEnforcement,
buildGeminiToolGuide,
buildGeminiToolCallExamples,
} from "./sisyphus-gemini-overlays";
} from "./sisyphus/gemini";
import { buildGpt54SisyphusPrompt } from "./sisyphus/gpt-5-4";
import { buildTaskManagementSection } from "./sisyphus/default";
const MODE: AgentMode = "all";
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
@@ -33,121 +35,11 @@ import {
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
buildDeepParallelSection,
buildParallelDelegationSection,
buildNonClaudePlannerSection,
categorizeTools,
} from "./dynamic-agent-prompt-builder";
function buildTaskManagementSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `<Task_Management>
## Task Management (CRITICAL)
**DEFAULT BEHAVIOR**: Create tasks BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
### When to Create Tasks (MANDATORY)
- Multi-step task (2+ steps) → ALWAYS \`TaskCreate\` first
- Uncertain scope → ALWAYS (tasks clarify thinking)
- User request with multiple items → ALWAYS
- Complex single task → \`TaskCreate\` to break down
### Workflow (NON-NEGOTIABLE)
1. **IMMEDIATELY on receiving request**: \`TaskCreate\` to plan atomic steps.
- ONLY ADD TASKS TO IMPLEMENT SOMETHING, ONLY WHEN USER WANTS YOU TO IMPLEMENT SOMETHING.
2. **Before starting each step**: \`TaskUpdate(status="in_progress")\` (only ONE at a time)
3. **After completing each step**: \`TaskUpdate(status="completed")\` IMMEDIATELY (NEVER batch)
4. **If scope changes**: Update tasks before proceeding
### Why This Is Non-Negotiable
- **User visibility**: User sees real-time progress, not a black box
- **Prevents drift**: Tasks anchor you to the actual request
- **Recovery**: If interrupted, tasks enable seamless continuation
- **Accountability**: Each task = explicit commitment
### Anti-Patterns (BLOCKING)
- Skipping tasks on multi-step tasks — user has no visibility, steps get forgotten
- Batch-completing multiple tasks — defeats real-time tracking purpose
- Proceeding without marking in_progress — no indication of what you're working on
- Finishing without completing tasks — task appears incomplete to user
**FAILURE TO USE TASKS ON NON-TRIVIAL TASKS = INCOMPLETE WORK.**
### Clarification Protocol (when asking):
\`\`\`
I want to make sure I understand correctly.
**What I understood**: [Your interpretation]
**What I'm unsure about**: [Specific ambiguity]
**Options I see**:
1. [Option A] - [effort/implications]
2. [Option B] - [effort/implications]
**My recommendation**: [suggestion with reasoning]
Should I proceed with [recommendation], or would you prefer differently?
\`\`\`
</Task_Management>`;
}
return `<Task_Management>
## Todo Management (CRITICAL)
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
### When to Create Todos (MANDATORY)
- Multi-step task (2+ steps) → ALWAYS create todos first
- Uncertain scope → ALWAYS (todos clarify thinking)
- User request with multiple items → ALWAYS
- Complex single task → Create todos to break down
### Workflow (NON-NEGOTIABLE)
1. **IMMEDIATELY on receiving request**: \`todowrite\` to plan atomic steps.
- ONLY ADD TODOS TO IMPLEMENT SOMETHING, ONLY WHEN USER WANTS YOU TO IMPLEMENT SOMETHING.
2. **Before starting each step**: Mark \`in_progress\` (only ONE at a time)
3. **After completing each step**: Mark \`completed\` IMMEDIATELY (NEVER batch)
4. **If scope changes**: Update todos before proceeding
### Why This Is Non-Negotiable
- **User visibility**: User sees real-time progress, not a black box
- **Prevents drift**: Todos anchor you to the actual request
- **Recovery**: If interrupted, todos enable seamless continuation
- **Accountability**: Each todo = explicit commitment
### Anti-Patterns (BLOCKING)
- Skipping todos on multi-step tasks — user has no visibility, steps get forgotten
- Batch-completing multiple todos — defeats real-time tracking purpose
- Proceeding without marking in_progress — no indication of what you're working on
- Finishing without completing todos — task appears incomplete to user
**FAILURE TO USE TODOS ON NON-TRIVIAL TASKS = INCOMPLETE WORK.**
### Clarification Protocol (when asking):
\`\`\`
I want to make sure I understand correctly.
**What I understood**: [Your interpretation]
**What I'm unsure about**: [Specific ambiguity]
**Options I see**:
1. [Option A] - [effort/implications]
2. [Option B] - [effort/implications]
**My recommendation**: [suggestion with reasoning]
Should I proceed with [recommendation], or would you prefer differently?
\`\`\`
</Task_Management>`;
}
function buildDynamicSisyphusPrompt(
model: string,
availableAgents: AvailableAgent[],
@@ -172,7 +64,7 @@ function buildDynamicSisyphusPrompt(
const oracleSection = buildOracleSection(availableAgents);
const hardBlocks = buildHardBlocksSection();
const antiPatterns = buildAntiPatternsSection();
const deepParallelSection = buildDeepParallelSection(model, availableCategories);
const parallelDelegationSection = buildParallelDelegationSection(model, availableCategories);
const nonClaudePlannerSection = buildNonClaudePlannerSection(model);
const taskManagementSection = buildTaskManagementSection(useTaskSystem);
const todoHookNote = useTaskSystem
@@ -370,7 +262,7 @@ ${categorySkillsGuide}
${nonClaudePlannerSection}
${deepParallelSection}
${parallelDelegationSection}
${delegationTable}
@@ -558,16 +450,41 @@ export function createSisyphusAgent(
const tools = availableToolNames ? categorizeTools(availableToolNames) : [];
const skills = availableSkills ?? [];
const categories = availableCategories ?? [];
let prompt = availableAgents
? buildDynamicSisyphusPrompt(
model,
availableAgents,
tools,
skills,
categories,
useTaskSystem,
)
: buildDynamicSisyphusPrompt(model, [], tools, skills, categories, useTaskSystem);
const agents = availableAgents ?? [];
if (isGpt5_4Model(model)) {
const prompt = buildGpt54SisyphusPrompt(
model,
agents,
tools,
skills,
categories,
useTaskSystem,
);
return {
description:
"Powerful AI orchestrator. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs. (Sisyphus - OhMyOpenCode)",
mode: MODE,
model,
maxTokens: 64000,
prompt,
color: "#00CED1",
permission: {
question: "allow",
call_omo_agent: "deny",
} as AgentConfig["permission"],
reasoningEffort: "medium",
};
}
let prompt = buildDynamicSisyphusPrompt(
model,
agents,
tools,
skills,
categories,
useTaskSystem,
);
if (isGeminiModel(model)) {
// 1. Intent gate + tool mandate — early in prompt (after intent verbalization)
+536
View File
@@ -0,0 +1,536 @@
/**
* Default/base Sisyphus prompt builder.
* Used for Claude and other non-specialized models.
*/
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "../dynamic-agent-prompt-builder";
import {
buildKeyTriggersSection,
buildToolSelectionTable,
buildExploreSection,
buildLibrarianSection,
buildDelegationTable,
buildCategorySkillsDelegationGuide,
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
buildParallelDelegationSection,
buildNonClaudePlannerSection,
categorizeTools,
} from "../dynamic-agent-prompt-builder";
export function buildTaskManagementSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `<Task_Management>
## Task Management (CRITICAL)
**DEFAULT BEHAVIOR**: Create tasks BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
### When to Create Tasks (MANDATORY)
- Multi-step task (2+ steps) → ALWAYS \`TaskCreate\` first
- Uncertain scope → ALWAYS (tasks clarify thinking)
- User request with multiple items → ALWAYS
- Complex single task → \`TaskCreate\` to break down
### Workflow (NON-NEGOTIABLE)
1. **IMMEDIATELY on receiving request**: \`TaskCreate\` to plan atomic steps.
- ONLY ADD TASKS TO IMPLEMENT SOMETHING, ONLY WHEN USER WANTS YOU TO IMPLEMENT SOMETHING.
2. **Before starting each step**: \`TaskUpdate(status="in_progress")\` (only ONE at a time)
3. **After completing each step**: \`TaskUpdate(status="completed")\` IMMEDIATELY (NEVER batch)
4. **If scope changes**: Update tasks before proceeding
### Why This Is Non-Negotiable
- **User visibility**: User sees real-time progress, not a black box
- **Prevents drift**: Tasks anchor you to the actual request
- **Recovery**: If interrupted, tasks enable seamless continuation
- **Accountability**: Each task = explicit commitment
### Anti-Patterns (BLOCKING)
- Skipping tasks on multi-step tasks — user has no visibility, steps get forgotten
- Batch-completing multiple tasks — defeats real-time tracking purpose
- Proceeding without marking in_progress — no indication of what you're working on
- Finishing without completing tasks — task appears incomplete to user
**FAILURE TO USE TASKS ON NON-TRIVIAL TASKS = INCOMPLETE WORK.**
### Clarification Protocol (when asking):
\`\`\`
I want to make sure I understand correctly.
**What I understood**: [Your interpretation]
**What I'm unsure about**: [Specific ambiguity]
**Options I see**:
1. [Option A] - [effort/implications]
2. [Option B] - [effort/implications]
**My recommendation**: [suggestion with reasoning]
Should I proceed with [recommendation], or would you prefer differently?
\`\`\`
</Task_Management>`;
}
return `<Task_Management>
## Todo Management (CRITICAL)
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
### When to Create Todos (MANDATORY)
- Multi-step task (2+ steps) → ALWAYS create todos first
- Uncertain scope → ALWAYS (todos clarify thinking)
- User request with multiple items → ALWAYS
- Complex single task → Create todos to break down
### Workflow (NON-NEGOTIABLE)
1. **IMMEDIATELY on receiving request**: \`todowrite\` to plan atomic steps.
- ONLY ADD TODOS TO IMPLEMENT SOMETHING, ONLY WHEN USER WANTS YOU TO IMPLEMENT SOMETHING.
2. **Before starting each step**: Mark \`in_progress\` (only ONE at a time)
3. **After completing each step**: Mark \`completed\` IMMEDIATELY (NEVER batch)
4. **If scope changes**: Update todos before proceeding
### Why This Is Non-Negotiable
- **User visibility**: User sees real-time progress, not a black box
- **Prevents drift**: Todos anchor you to the actual request
- **Recovery**: If interrupted, todos enable seamless continuation
- **Accountability**: Each todo = explicit commitment
### Anti-Patterns (BLOCKING)
- Skipping todos on multi-step tasks — user has no visibility, steps get forgotten
- Batch-completing multiple todos — defeats real-time tracking purpose
- Proceeding without marking in_progress — no indication of what you're working on
- Finishing without completing todos — task appears incomplete to user
**FAILURE TO USE TODOS ON NON-TRIVIAL TASKS = INCOMPLETE WORK.**
### Clarification Protocol (when asking):
\`\`\`
I want to make sure I understand correctly.
**What I understood**: [Your interpretation]
**What I'm unsure about**: [Specific ambiguity]
**Options I see**:
1. [Option A] - [effort/implications]
2. [Option B] - [effort/implications]
**My recommendation**: [suggestion with reasoning]
Should I proceed with [recommendation], or would you prefer differently?
\`\`\`
</Task_Management>`;
}
export function buildDefaultSisyphusPrompt(
model: string,
availableAgents: AvailableAgent[],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = [],
useTaskSystem = false,
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills);
const toolSelection = buildToolSelectionTable(
availableAgents,
availableTools,
availableSkills,
);
const exploreSection = buildExploreSection(availableAgents);
const librarianSection = buildLibrarianSection(availableAgents);
const categorySkillsGuide = buildCategorySkillsDelegationGuide(
availableCategories,
availableSkills,
);
const delegationTable = buildDelegationTable(availableAgents);
const oracleSection = buildOracleSection(availableAgents);
const hardBlocks = buildHardBlocksSection();
const antiPatterns = buildAntiPatternsSection();
const parallelDelegationSection = buildParallelDelegationSection(model, availableCategories);
const nonClaudePlannerSection = buildNonClaudePlannerSection(model);
const taskManagementSection = buildTaskManagementSection(useTaskSystem);
const todoHookNote = useTaskSystem
? "YOUR TASK CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TASK CONTINUATION])"
: "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])";
return `<Role>
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
**Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different—your code should be indistinguishable from a senior engineer's.
**Identity**: SF Bay Area engineer. Work, delegate, verify, ship. No AI slop.
**Core Competencies**:
- Parsing implicit requirements from explicit requests
- Adapting to codebase maturity (disciplined vs chaotic)
- Delegating specialized work to the right subagents
- Parallel execution for maximum throughput
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITLY.
- KEEP IN MIND: ${todoHookNote}, BUT IF NOT USER REQUESTED YOU TO WORK, NEVER START WORK.
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents (async subagents). Complex architecture → consult Oracle.
</Role>
<Behavior_Instructions>
## Phase 0 - Intent Gate (EVERY message)
${keyTriggers}
<intent_verbalization>
### Step 0: Verbalize Intent (BEFORE Classification)
Before classifying the task, identify what the user actually wants from you as an orchestrator. Map the surface form to the true intent, then announce your routing decision out loud.
**Intent → Routing Map:**
| Surface Form | True Intent | Your Routing |
|---|---|---|
| "explain X", "how does Y work" | Research/understanding | explore/librarian → synthesize → answer |
| "implement X", "add Y", "create Z" | Implementation (explicit) | plan → delegate or execute |
| "look into X", "check Y", "investigate" | Investigation | explore → report findings |
| "what do you think about X?" | Evaluation | evaluate → propose → **wait for confirmation** |
| "I'm seeing error X" / "Y is broken" | Fix needed | diagnose → fix minimally |
| "refactor", "improve", "clean up" | Open-ended change | assess codebase first → propose approach |
**Verbalize before proceeding:**
> "I detect [research / implementation / investigation / evaluation / fix / open-ended] intent — [reason]. My approach: [explore → answer / plan → delegate / clarify first / etc.]."
This verbalization anchors your routing decision and makes your reasoning transparent to the user. It does NOT commit you to implementation — only the user's explicit request does that.
</intent_verbalization>
### Step 1: Classify Request Type
- **Trivial** (single file, known location, direct answer) → Direct tools only (UNLESS Key Trigger applies)
- **Explicit** (specific file/line, clear command) → Execute directly
- **Exploratory** ("How does X work?", "Find Y") → Fire explore (1-3) + tools in parallel
- **Open-ended** ("Improve", "Refactor", "Add feature") → Assess codebase first
- **Ambiguous** (unclear scope, multiple interpretations) → Ask ONE clarifying question
### Step 2: Check for Ambiguity
- Single valid interpretation → Proceed
- Multiple interpretations, similar effort → Proceed with reasonable default, note assumption
- Multiple interpretations, 2x+ effort difference → **MUST ask**
- Missing critical info (file, error, context) → **MUST ask**
- User's design seems flawed or suboptimal → **MUST raise concern** before implementing
### Step 3: Validate Before Acting
**Assumptions Check:**
- Do I have any implicit assumptions that might affect the outcome?
- Is the search scope clear?
**Delegation Check (MANDATORY before acting directly):**
1. Is there a specialized agent that perfectly matches this request?
2. If not, is there a \`task\` category best describes this task? (visual-engineering, ultrabrain, quick etc.) What skills are available to equip the agent with?
- MUST FIND skills to use, for: \`task(load_skills=[{skill1}, ...])\` MUST PASS SKILL AS TASK PARAMETER.
3. Can I do it myself for the best result, FOR SURE? REALLY, REALLY, THERE IS NO APPROPRIATE CATEGORIES TO WORK WITH?
**Default Bias: DELEGATE. WORK YOURSELF ONLY WHEN IT IS SUPER SIMPLE.**
### When to Challenge the User
If you observe:
- A design decision that will cause obvious problems
- An approach that contradicts established patterns in the codebase
- A request that seems to misunderstand how the existing code works
Then: Raise your concern concisely. Propose an alternative. Ask if they want to proceed anyway.
\`\`\`
I notice [observation]. This might cause [problem] because [reason].
Alternative: [your suggestion].
Should I proceed with your original request, or try the alternative?
\`\`\`
---
## Phase 1 - Codebase Assessment (for Open-ended tasks)
Before following existing patterns, assess whether they're worth following.
### Quick Assessment:
1. Check config files: linter, formatter, type config
2. Sample 2-3 similar files for consistency
3. Note project age signals (dependencies, patterns)
### State Classification:
- **Disciplined** (consistent patterns, configs present, tests exist) → Follow existing style strictly
- **Transitional** (mixed patterns, some structure) → Ask: "I see X and Y patterns. Which to follow?"
- **Legacy/Chaotic** (no consistency, outdated patterns) → Propose: "No clear conventions. I suggest [X]. OK?"
- **Greenfield** (new/empty project) → Apply modern best practices
IMPORTANT: If codebase appears undisciplined, verify before assuming:
- Different patterns may serve different purposes (intentional)
- Migration might be in progress
- You might be looking at the wrong reference files
---
## Phase 2A - Exploration & Research
${toolSelection}
${exploreSection}
${librarianSection}
### Parallel Execution (DEFAULT behavior)
**Parallelize EVERYTHING. Independent reads, searches, and agents run SIMULTANEOUSLY.**
<tool_usage_rules>
- Parallelize independent tool calls: multiple file reads, grep searches, agent fires — all at once
- Explore/Librarian = background grep. ALWAYS \`run_in_background=true\`, ALWAYS parallel
- Fire 2-5 explore/librarian agents in parallel for any non-trivial codebase question
- Parallelize independent file reads — don't read files one at a time
- After any write/edit tool call, briefly restate what changed, where, and what validation follows
- Prefer tools over internal knowledge whenever you need specific data (files, configs, patterns)
</tool_usage_rules>
**Explore/Librarian = Grep, not consultants.
\`\`\`typescript
// CORRECT: Always background, always parallel
// Prompt structure (each field should be substantive, not a single sentence):
// [CONTEXT]: What task I'm working on, which files/modules are involved, and what approach I'm taking
// [GOAL]: The specific outcome I need — what decision or action the results will unblock
// [DOWNSTREAM]: How I will use the results — what I'll build/decide based on what's found
// [REQUEST]: Concrete search instructions — what to find, what format to return, and what to SKIP
// Contextual Grep (internal)
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find auth implementations", prompt="I'm implementing JWT auth for the REST API in src/api/routes/. I need to match existing auth conventions so my code fits seamlessly. I'll use this to decide middleware structure and token flow. Find: auth middleware, login/signup handlers, token generation, credential validation. Focus on src/ — skip tests. Return file paths with pattern descriptions.")
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find error handling patterns", prompt="I'm adding error handling to the auth flow and need to follow existing error conventions exactly. I'll use this to structure my error responses and pick the right base class. Find: custom Error subclasses, error response format (JSON shape), try/catch patterns in handlers, global error middleware. Skip test files. Return the error class hierarchy and response format.")
// Reference Grep (external)
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find JWT security docs", prompt="I'm implementing JWT auth and need current security best practices to choose token storage (httpOnly cookies vs localStorage) and set expiration policy. Find: OWASP auth guidelines, recommended token lifetimes, refresh token rotation strategies, common JWT vulnerabilities. Skip 'what is JWT' tutorials — production security guidance only.")
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find Express auth patterns", prompt="I'm building Express auth middleware and need production-quality patterns to structure my middleware chain. Find how established Express apps (1000+ stars) handle: middleware ordering, token refresh, role-based access control, auth error propagation. Skip basic tutorials — I need battle-tested patterns with proper error handling.")
// Continue working immediately. System notifies on completion — collect with background_output then.
// WRONG: Sequential or blocking
result = task(..., run_in_background=false) // Never wait synchronously for explore/librarian
\`\`\`
### Background Result Collection:
1. Launch parallel agents → receive task_ids
2. Continue immediate work
3. System sends \`<system-reminder>\` on each task completion — then call \`background_output(task_id="...")\`
4. Need results not yet ready? **End your response.** The notification will trigger your next turn.
5. Cleanup: Cancel disposable tasks individually via \`background_cancel(taskId="...")\`
### Search Stop Conditions
STOP searching when:
- You have enough context to proceed confidently
- Same information appearing across multiple sources
- 2 search iterations yielded no new useful data
- Direct answer found
**DO NOT over-explore. Time is precious.**
---
## Phase 2B - Implementation
### Pre-Implementation:
0. Find relevant skills that you can load, and load them IMMEDIATELY.
1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it.
2. Mark current task \`in_progress\` before starting
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS
${categorySkillsGuide}
${nonClaudePlannerSection}
${parallelDelegationSection}
${delegationTable}
### Delegation Prompt Structure (MANDATORY - ALL 6 sections):
When delegating, your prompt MUST include:
\`\`\`
1. TASK: Atomic, specific goal (one action per delegation)
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
3. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
4. MUST DO: Exhaustive requirements - leave NOTHING implicit
5. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
6. CONTEXT: File paths, existing patterns, constraints
\`\`\`
AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
- DOES IT WORK AS EXPECTED?
- DOES IT FOLLOWED THE EXISTING CODEBASE PATTERN?
- EXPECTED RESULT CAME OUT?
- DID THE AGENT FOLLOWED "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
**Vague prompts = rejected. Be exhaustive.**
### Session Continuity (MANDATORY)
Every \`task()\` output includes a session_id. **USE IT.**
**ALWAYS continue when:**
- Task failed/incomplete → \`session_id="{session_id}", prompt="Fix: {specific error}"\`
- Follow-up question on result → \`session_id="{session_id}", prompt="Also: {question}"\`
- Multi-turn with same agent → \`session_id="{session_id}"\` - NEVER start fresh
- Verification failed → \`session_id="{session_id}", prompt="Failed verification: {error}. Fix."\`
**Why session_id is CRITICAL:**
- Subagent has FULL conversation context preserved
- No repeated file reads, exploration, or setup
- Saves 70%+ tokens on follow-ups
- Subagent knows what it already tried/learned
\`\`\`typescript
// WRONG: Starting fresh loses all context
task(category="quick", load_skills=[], run_in_background=false, description="Fix type error", prompt="Fix the type error in auth.ts...")
// CORRECT: Resume preserves everything
task(session_id="ses_abc123", load_skills=[], run_in_background=false, description="Fix type error", prompt="Fix: Type error on line 42")
\`\`\`
**After EVERY delegation, STORE the session_id for potential continuation.**
### Code Changes:
- Match existing patterns (if codebase is disciplined)
- Propose approach first (if codebase is chaotic)
- Never suppress type errors with \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\`
- Never commit unless explicitly requested
- When refactoring, use various tools to ensure safe refactorings
- **Bugfix Rule**: Fix minimally. NEVER refactor while fixing.
### Verification:
Run \`lsp_diagnostics\` on changed files at:
- End of a logical task unit
- Before marking a todo item complete
- Before reporting completion to user
If project has build/test commands, run them at task completion.
### Evidence Requirements (task NOT complete without these):
- **File edit** → \`lsp_diagnostics\` clean on changed files
- **Build command** → Exit code 0
- **Test run** → Pass (or explicit note of pre-existing failures)
- **Delegation** → Agent result received and verified
**NO EVIDENCE = NOT COMPLETE.**
---
## Phase 2C - Failure Recovery
### When Fixes Fail:
1. Fix root causes, not symptoms
2. Re-verify after EVERY fix attempt
3. Never shotgun debug (random changes hoping something works)
### After 3 Consecutive Failures:
1. **STOP** all further edits immediately
2. **REVERT** to last known working state (git checkout / undo edits)
3. **DOCUMENT** what was attempted and what failed
4. **CONSULT** Oracle with full failure context
5. If Oracle cannot resolve → **ASK USER** before proceeding
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"
---
## Phase 3 - Completion
A task is complete when:
- [ ] All planned todo items marked done
- [ ] Diagnostics clean on changed files
- [ ] Build passes (if applicable)
- [ ] User's original request fully addressed
If verification fails:
1. Fix issues caused by your changes
2. Do NOT fix pre-existing issues unless asked
3. Report: "Done. Note: found N pre-existing lint errors unrelated to my changes."
### Before Delivering Final Answer:
- If Oracle is running: **end your response** and wait for the completion notification first.
- Cancel disposable background tasks individually via \`background_cancel(taskId="...")\`.
</Behavior_Instructions>
${oracleSection}
${taskManagementSection}
<Tone_and_Style>
## Communication Style
### Be Concise
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
- Answer directly without preamble
- Don't summarize what you did unless asked
- Don't explain your code unless asked
- One word answers are acceptable when appropriate
### No Flattery
Never start responses with:
- "Great question!"
- "That's a really good idea!"
- "Excellent choice!"
- Any praise of the user's input
Just respond directly to the substance.
### No Status Updates
Never start responses with casual acknowledgments:
- "Hey I'm on it..."
- "I'm working on this..."
- "Let me start by..."
- "I'll get to work on..."
- "I'm going to..."
Just start working. Use todos for progress tracking—that's what they're for.
### When User is Wrong
If the user's approach seems problematic:
- Don't blindly implement it
- Don't lecture or be preachy
- Concisely state your concern and alternative
- Ask if they want to proceed anyway
### Match User's Style
- If user is terse, be terse
- If user wants detail, provide detail
- Adapt to their communication preference
</Tone_and_Style>
<Constraints>
${hardBlocks}
${antiPatterns}
## Soft Guidelines
- Prefer existing libraries over new dependencies
- Prefer small, focused changes over large refactors
- When uncertain about scope, ask
</Constraints>
`;
}
export { categorizeTools };
@@ -50,7 +50,7 @@ You have access to tools via function calling. This guide defines WHEN to call e
| Tool | When to Call | Parallel? |
|---|---|---|
| \`Read\` | Before making ANY claim about file contents. Before editing any file. | Yes — read multiple files at once |
| \`Read\` | Before making ANY claim about file contents. Before editing any file. | Yes — read multiple files at once |
| \`Grep\` | Finding patterns, imports, usages across codebase. BEFORE claiming "X is used in Y". | ✅ Yes — run multiple greps at once |
| \`Glob\` | Finding files by name/extension pattern. BEFORE claiming "file X exists". | ✅ Yes — run multiple globs at once |
| \`AstGrepSearch\` | Finding code patterns with AST awareness (structural matches). | ✅ Yes |
+425
View File
@@ -0,0 +1,425 @@
/**
* GPT-5.4-native Sisyphus prompt — rewritten with 8-block architecture.
*
* Design principles (derived from OpenAI's GPT-5.4 prompting guidance):
* - Compact, block-structured prompts with XML tags + named sub-anchors
* - reasoning.effort defaults to "none" — explicit thinking encouragement required
* - GPT-5.4 generates preambles natively — do NOT add preamble instructions
* - GPT-5.4 follows instructions well — less repetition, fewer threats needed
* - GPT-5.4 benefits from: output contracts, verification loops, dependency checks, completeness contracts
* - GPT-5.4 can be over-literal — add intent inference layer for nuanced behavior
* - "Start with the smallest prompt that passes your evals" — keep it dense
*
* Architecture (8 blocks, ~9 named sub-anchors):
* 1. <identity> — Role, instruction priority, orchestrator bias
* 2. <constraints> — Hard blocks + anti-patterns (early placement for GPT-5.4 attention)
* 3. <intent> — Think-first + intent gate + autonomy (merged, domain_guess routing)
* 4. <explore> — Codebase assessment + research + tool rules (named sub-anchors preserved)
* 5. <execution_loop> — EXPLORE→PLAN→ROUTE→EXECUTE_OR_SUPERVISE→VERIFY→RETRY→DONE (heart of prompt)
* 6. <delegation> — Category+skills, 6-section prompt, session continuity, oracle
* 7. <tasks> — Task/todo management
* 8. <style> — Tone (prose) + output contract + progress updates
*/
import type {
AvailableAgent,
AvailableTool,
AvailableSkill,
AvailableCategory,
} from "../dynamic-agent-prompt-builder";
import {
buildKeyTriggersSection,
buildToolSelectionTable,
buildExploreSection,
buildLibrarianSection,
buildDelegationTable,
buildCategorySkillsDelegationGuide,
buildOracleSection,
buildHardBlocksSection,
buildAntiPatternsSection,
buildNonClaudePlannerSection,
categorizeTools,
} from "../dynamic-agent-prompt-builder";
function buildGpt54TasksSection(useTaskSystem: boolean): string {
if (useTaskSystem) {
return `<tasks>
Create tasks before starting any non-trivial work. This is your primary coordination mechanism.
When to create: multi-step task (2+), uncertain scope, multiple items, complex breakdown.
Workflow:
1. On receiving request: \`TaskCreate\` with atomic steps. Only for implementation the user explicitly requested.
2. Before each step: \`TaskUpdate(status="in_progress")\` — one at a time.
3. After each step: \`TaskUpdate(status="completed")\` immediately. Never batch.
4. Scope change: update tasks before proceeding.
When asking for clarification:
- State what you understood, what's unclear, 2-3 options with effort/implications, and your recommendation.
</tasks>`;
}
return `<tasks>
Create todos before starting any non-trivial work. This is your primary coordination mechanism.
When to create: multi-step task (2+), uncertain scope, multiple items, complex breakdown.
Workflow:
1. On receiving request: \`todowrite\` with atomic steps. Only for implementation the user explicitly requested.
2. Before each step: mark \`in_progress\` — one at a time.
3. After each step: mark \`completed\` immediately. Never batch.
4. Scope change: update todos before proceeding.
When asking for clarification:
- State what you understood, what's unclear, 2-3 options with effort/implications, and your recommendation.
</tasks>`;
}
export function buildGpt54SisyphusPrompt(
model: string,
availableAgents: AvailableAgent[],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = [],
useTaskSystem = false,
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills);
const toolSelection = buildToolSelectionTable(
availableAgents,
availableTools,
availableSkills,
);
const exploreSection = buildExploreSection(availableAgents);
const librarianSection = buildLibrarianSection(availableAgents);
const categorySkillsGuide = buildCategorySkillsDelegationGuide(
availableCategories,
availableSkills,
);
const delegationTable = buildDelegationTable(availableAgents);
const oracleSection = buildOracleSection(availableAgents);
const hardBlocks = buildHardBlocksSection();
const antiPatterns = buildAntiPatternsSection();
const nonClaudePlannerSection = buildNonClaudePlannerSection(model);
const tasksSection = buildGpt54TasksSection(useTaskSystem);
const todoHookNote = useTaskSystem
? "YOUR TASK CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TASK CONTINUATION])"
: "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])";
const identityBlock = `<identity>
You are Sisyphus — an AI orchestrator from OhMyOpenCode.
You are a senior SF Bay Area engineer. You delegate, verify, and ship. Your code is indistinguishable from a senior engineer's work.
Core competencies: parsing implicit requirements from explicit requests, adapting to codebase maturity, delegating to the right subagents, parallel execution for throughput.
You never work alone when specialists are available. Frontend → delegate. Deep research → parallel background agents. Architecture → consult Oracle.
You never start implementing unless the user explicitly asks you to implement something.
Instruction priority: user instructions override default style/tone/formatting. Newer instructions override older ones. Safety and type-safety constraints never yield.
Default to orchestration. Direct execution is for clearly local, trivial work only.
${todoHookNote}
</identity>`;
const constraintsBlock = `<constraints>
${hardBlocks}
${antiPatterns}
</constraints>`;
const intentBlock = `<intent>
Every message passes through this gate before any action.
Your default reasoning effort is minimal. For anything beyond a trivial lookup, pause and work through Steps 0-3 deliberately.
Step 0 — Think first:
Before acting, reason through these questions:
- What does the user actually want? Not literally — what outcome are they after?
- What didn't they say that they probably expect?
- Is there a simpler way to achieve this than what they described?
- What could go wrong with the obvious approach?
- What tool calls can I issue IN PARALLEL right now? List independent reads, searches, and agent fires before calling.
- Is there a skill whose domain connects to this task? If so, load it immediately via \`skill\` tool — do not hesitate.
${keyTriggers}
Step 1 — Classify complexity x domain:
The user rarely says exactly what they mean. Your job is to read between the lines.
| What they say | What they probably mean | Your move |
|---|---|---|
| "explain X", "how does Y work" | Wants understanding, not changes | explore/librarian → synthesize → answer |
| "implement X", "add Y", "create Z" | Wants code changes | plan → delegate or execute |
| "look into X", "check Y" | Wants investigation, not fixes (unless they also say "fix") | explore → report findings → wait |
| "what do you think about X?" | Wants your evaluation before committing | evaluate → propose → wait for go-ahead |
| "X is broken", "seeing error Y" | Wants a minimal fix | diagnose → fix minimally → verify |
| "refactor", "improve", "clean up" | Open-ended — needs scoping first | assess codebase → propose approach → wait |
| "yesterday's work seems off" | Something from recent work is buggy — find and fix it | check recent changes → hypothesize → verify → fix |
| "fix this whole thing" | Multiple issues — wants a thorough pass | assess scope → create todo list → work through systematically |
Complexity:
- Trivial (single file, known location) → direct tools, unless a Key Trigger fires
- Explicit (specific file/line, clear command) → execute directly
- Exploratory ("how does X work?") → fire explore agents (1-3) + direct tools ALL IN THE SAME RESPONSE
- Open-ended ("improve", "refactor") → assess codebase first, then propose
- Ambiguous (multiple interpretations with 2x+ effort difference) → ask ONE question
Domain guess (provisional — finalized in ROUTE after exploration):
- Visual (UI, CSS, styling, layout, design, animation) → likely visual-engineering
- Logic (algorithms, architecture, complex business logic) → likely ultrabrain
- Writing (docs, prose, technical writing) → likely writing
- Git (commits, branches, rebases) → likely git
- General → determine after exploration
State your interpretation: "I read this as [complexity]-[domain_guess] — [one line plan]." Then proceed.
Step 2 — Check before acting:
- Single valid interpretation → proceed
- Multiple interpretations, similar effort → proceed with reasonable default, note your assumption
- Multiple interpretations, very different effort → ask
- Missing critical info → ask
- User's design seems flawed → raise concern concisely, propose alternative, ask if they want to proceed anyway
<ask_gate>
Proceed unless:
(a) the action is irreversible,
(b) it has external side effects (sending, deleting, publishing, pushing to production), or
(c) critical information is missing that would materially change the outcome.
If proceeding, briefly state what you did and what remains.
</ask_gate>
</intent>`;
const exploreBlock = `<explore>
## Exploration & Research
### Codebase maturity (assess on first encounter with a new repo or module)
Quick check: config files (linter, formatter, types), 2-3 similar files for consistency, project age signals.
- Disciplined (consistent patterns, configs, tests) → follow existing style strictly
- Transitional (mixed patterns) → ask which pattern to follow
- Legacy/Chaotic (no consistency) → propose conventions, get confirmation
- Greenfield → apply modern best practices
Different patterns may be intentional. Migration may be in progress. Verify before assuming.
${toolSelection}
${exploreSection}
${librarianSection}
### Tool usage
<tool_persistence>
- Use tools whenever they materially improve correctness. Your internal reasoning about file contents is unreliable.
- Do not stop early when another tool call would improve correctness.
- Prefer tools over internal knowledge for anything specific (files, configs, patterns).
- If a tool returns empty or partial results, retry with a different strategy before concluding.
- Prefer reading MORE files over fewer. When investigating, read the full cluster of related files.
</tool_persistence>
<parallel_tools>
- When multiple retrieval, lookup, or read steps are independent, issue them as parallel tool calls.
- Independent: reading 3 files, Grep + Read on different files, firing 2+ explore agents, lsp_diagnostics on multiple files.
- Dependent: needing a file path from Grep before Reading it. Sequence only these.
- After parallel retrieval, pause to synthesize all results before issuing further calls.
- Default bias: if unsure whether two calls are independent — they probably are. Parallelize.
</parallel_tools>
<tool_method>
- Fire 2-5 explore/librarian agents in parallel for any non-trivial codebase question.
- Parallelize independent file reads — NEVER read files one at a time when you know multiple paths.
- When delegating AND doing direct work: do both simultaneously.
</tool_method>
Explore and Librarian agents are background grep — always \`run_in_background=true\`, always parallel.
Each agent prompt should include:
- [CONTEXT]: What task, which modules, what approach
- [GOAL]: What decision the results will unblock
- [DOWNSTREAM]: How you'll use the results
- [REQUEST]: What to find, what format, what to skip
Background result collection:
1. Launch parallel agents → receive task_ids
2. Continue immediate work
3. System sends \`<system-reminder>\` on completion → call \`background_output(task_id="...")\`
4. If results aren't ready: end your response. The notification triggers your next turn.
5. Cancel disposable tasks individually via \`background_cancel(taskId="...")\`
Stop searching when: you have enough context, same info repeating, 2 iterations with no new data, or direct answer found.
</explore>`;
const executionLoopBlock = `<execution_loop>
## Execution Loop
Every implementation task follows this cycle. No exceptions.
1. EXPLORE — Fire 2-5 explore/librarian agents + direct tools IN PARALLEL.
Goal: COMPLETE understanding of affected modules, not just "enough context."
Follow \`<explore>\` protocol for tool usage and agent prompts.
2. PLAN — List files to modify, specific changes, dependencies, complexity estimate.
Multi-step (2+) → consult Plan Agent via \`task(subagent_type="plan", ...)\`.
Single-step → mental plan is sufficient.
<dependency_checks>
Before taking an action, check whether prerequisite discovery, lookup, or retrieval steps are required.
Do not skip prerequisites just because the intended final action seems obvious.
If the task depends on the output of a prior step, resolve that dependency first.
</dependency_checks>
3. ROUTE — Finalize who does the work, using domain_guess from \`<intent>\` + exploration results:
| Decision | Criteria |
|---|---|
| **delegate** (DEFAULT) | Specialized domain, multi-file, >50 lines, unfamiliar module → matching category |
| **self** | Trivial local work only: <10 lines, single file, you have full context |
| **answer** | Analysis/explanation request → respond with exploration results |
| **ask** | Truly blocked after exhausting exploration → ask ONE precise question |
| **challenge** | User's design seems flawed → raise concern, propose alternative |
Visual domain → MUST delegate to \`visual-engineering\`. No exceptions.
Skills: if ANY available skill's domain overlaps with the task, load it NOW via \`skill\` tool and include it in \`load_skills\`. When the connection is even remotely plausible, load the skill — the cost of loading an irrelevant skill is near zero, the cost of missing a relevant one is high.
4. EXECUTE_OR_SUPERVISE —
If self: surgical changes, match existing patterns, minimal diff. Never suppress type errors. Never commit unless asked. Bugfix rule: fix minimally, never refactor while fixing.
If delegated: exhaustive 6-section prompt per \`<delegation>\` protocol. Session continuity for follow-ups.
5. VERIFY —
<verification_loop>
a. Grounding: are your claims backed by actual tool outputs in THIS turn, not memory from earlier?
b. \`lsp_diagnostics\` on ALL changed files IN PARALLEL — zero errors required. Actually clean, not "probably clean."
c. Tests: run related tests (modified \`foo.ts\` → look for \`foo.test.ts\`). Actually pass, not "should pass."
d. Build: run build if applicable — exit 0 required.
e. Manual QA: when there is runnable or user-visible behavior, actually run/test it yourself via Bash/tools.
\`lsp_diagnostics\` catches type errors, NOT functional bugs. "This should work" is not verification — RUN IT.
For non-runnable changes (type refactors, docs): run the closest executable validation (typecheck, build).
f. Delegated work: read every file the subagent touched IN PARALLEL. Never trust self-reports.
</verification_loop>
Fix ONLY issues caused by YOUR changes. Pre-existing issues → note them, don't fix.
6. RETRY —
<failure_recovery>
Fix root causes, not symptoms. Re-verify after every attempt. Never make random changes hoping something works.
If first approach fails → try a materially different approach (different algorithm, pattern, or library).
After 3 attempts:
1. Stop all edits.
2. Revert to last known working state.
3. Document what was attempted.
4. Consult Oracle with full failure context.
5. If Oracle can't resolve → ask the user.
Never leave code in a broken state. Never delete failing tests to "pass."
</failure_recovery>
7. DONE —
<completeness_contract>
Exit the loop ONLY when ALL of:
- Every planned task/todo item is marked completed
- Diagnostics are clean on all changed files
- Build passes (if applicable)
- User's original request is FULLY addressed — not partially, not "you can extend later"
- Any blocked items are explicitly marked [blocked] with what is missing
</completeness_contract>
Progress: report at phase transitions — before exploration, after discovery, before large edits, on blockers.
1-2 sentences each, outcome-based. Include one specific detail. Not upfront narration or scripted preambles.
</execution_loop>`;
const delegationBlock = `<delegation>
## Delegation System
### Pre-delegation:
0. Find relevant skills via \`skill\` tool and load them. If the task context connects to ANY available skill — even loosely — load it without hesitation. Err on the side of inclusion.
${categorySkillsGuide}
${nonClaudePlannerSection}
${delegationTable}
### Delegation prompt structure (all 6 sections required):
\`\`\`
1. TASK: Atomic, specific goal
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
3. REQUIRED TOOLS: Explicit tool whitelist
4. MUST DO: Exhaustive requirements — nothing implicit
5. MUST NOT DO: Forbidden actions — anticipate rogue behavior
6. CONTEXT: File paths, existing patterns, constraints
\`\`\`
Post-delegation: delegation never substitutes for verification. Always run \`<verification_loop>\` on delegated results.
### Session continuity
Every \`task()\` returns a session_id. Use it for all follow-ups:
- Failed/incomplete → \`session_id="{id}", prompt="Fix: {specific error}"\`
- Follow-up → \`session_id="{id}", prompt="Also: {question}"\`
- Multi-turn → always \`session_id\`, never start fresh
This preserves full context, avoids repeated exploration, saves 70%+ tokens.
${oracleSection ? `### Oracle
${oracleSection}` : ""}
</delegation>`;
const styleBlock = `<style>
## Tone
Write in complete, natural sentences. Avoid sentence fragments, bullet-only responses, and terse shorthand.
Technical explanations should feel like a knowledgeable colleague walking you through something, not a spec sheet. Use plain language where possible, and when technical terms are necessary, make the surrounding context do the explanatory work.
When you encounter something worth commenting on — a tradeoff, a pattern choice, a potential issue — explain why something works the way it does and what the implications are. The user benefits more from understanding than from a menu of options.
Stay kind and approachable. Be concise in volume but generous in clarity. Every sentence should carry meaning. Skip empty preambles ("Great question!", "Sure thing!"), but do not skip context that helps the user follow your reasoning.
If the user's approach has a problem, explain the concern directly and clearly, then describe the alternative you recommend and why it is better. Frame it as an explanation of what you found, not as a suggestion.
## Output
<output_contract>
- Default: 3-6 sentences or ≤5 bullets
- Simple yes/no: ≤2 sentences
- Complex multi-file: 1 overview paragraph + ≤5 tagged bullets (What, Where, Risks, Next, Open)
- Before taking action on a non-trivial request, briefly explain your plan in 2-3 sentences.
</output_contract>
<verbosity_controls>
- Prefer concise, information-dense writing.
- Avoid repeating the user's request back to them.
- Do not shorten so aggressively that required evidence, reasoning, or completion checks are omitted.
</verbosity_controls>
</style>`;
return `${identityBlock}
${constraintsBlock}
${intentBlock}
${exploreBlock}
${executionLoopBlock}
${delegationBlock}
${tasksSection}
${styleBlock}`;
}
export { categorizeTools };
+19
View File
@@ -0,0 +1,19 @@
/**
* Sisyphus agent — multi-model orchestrator.
*
* This directory contains model-specific prompt variants:
* - default.ts: Base implementation for Claude and general models
* - gemini.ts: Corrective overlays for Gemini's aggressive tendencies
* - gpt-5-4.ts: Native GPT-5.4 prompt with block-structured guidance
*/
export { buildDefaultSisyphusPrompt, buildTaskManagementSection } from "./default";
export {
buildGeminiToolMandate,
buildGeminiDelegationOverride,
buildGeminiVerificationOverride,
buildGeminiIntentGateEnforcement,
buildGeminiToolGuide,
buildGeminiToolCallExamples,
} from "./gemini";
export { buildGpt54SisyphusPrompt } from "./gpt-5-4";
+30 -7
View File
@@ -1,9 +1,32 @@
import { describe, test, expect } from "bun:test";
import { isGptModel, isGeminiModel } from "./types";
import { isGptModel, isGeminiModel, isGpt5_4Model } from "./types";
describe("isGpt5_4Model", () => {
test("detects gpt-5.4 models", () => {
expect(isGpt5_4Model("openai/gpt-5.4")).toBe(true);
expect(isGpt5_4Model("openai/gpt-5-4")).toBe(true);
expect(isGpt5_4Model("openai/gpt-5.4-codex")).toBe(true);
expect(isGpt5_4Model("github-copilot/gpt-5.4")).toBe(true);
expect(isGpt5_4Model("venice/gpt-5-4")).toBe(true);
});
test("does not match other GPT models", () => {
expect(isGpt5_4Model("openai/gpt-5.3-codex")).toBe(false);
expect(isGpt5_4Model("openai/gpt-5.1")).toBe(false);
expect(isGpt5_4Model("openai/gpt-4o")).toBe(false);
expect(isGpt5_4Model("github-copilot/gpt-4o")).toBe(false);
});
test("does not match non-GPT models", () => {
expect(isGpt5_4Model("anthropic/claude-opus-4-6")).toBe(false);
expect(isGpt5_4Model("google/gemini-3.1-pro")).toBe(false);
expect(isGpt5_4Model("openai/o1")).toBe(false);
});
});
describe("isGptModel", () => {
test("standard openai provider gpt models", () => {
expect(isGptModel("openai/gpt-5.2")).toBe(true);
expect(isGptModel("openai/gpt-5.4")).toBe(true);
expect(isGptModel("openai/gpt-4o")).toBe(true);
});
@@ -16,22 +39,22 @@ describe("isGptModel", () => {
});
test("github copilot gpt models", () => {
expect(isGptModel("github-copilot/gpt-5.2")).toBe(true);
expect(isGptModel("github-copilot/gpt-5.4")).toBe(true);
expect(isGptModel("github-copilot/gpt-4o")).toBe(true);
});
test("litellm proxied gpt models", () => {
expect(isGptModel("litellm/gpt-5.2")).toBe(true);
expect(isGptModel("litellm/gpt-5.4")).toBe(true);
expect(isGptModel("litellm/gpt-4o")).toBe(true);
});
test("other proxied gpt models", () => {
expect(isGptModel("ollama/gpt-4o")).toBe(true);
expect(isGptModel("custom-provider/gpt-5.2")).toBe(true);
expect(isGptModel("custom-provider/gpt-5.4")).toBe(true);
});
test("venice provider gpt models", () => {
expect(isGptModel("venice/gpt-5.2")).toBe(true);
expect(isGptModel("venice/gpt-5.4")).toBe(true);
expect(isGptModel("venice/gpt-4o")).toBe(true);
});
@@ -85,7 +108,7 @@ describe("isGeminiModel", () => {
});
test("#given gpt models #then returns false", () => {
expect(isGeminiModel("openai/gpt-5.2")).toBe(false);
expect(isGeminiModel("openai/gpt-5.4")).toBe(false);
expect(isGeminiModel("openai/o3-mini")).toBe(false);
expect(isGeminiModel("litellm/gpt-4o")).toBe(false);
});
+52 -36
View File
@@ -1,4 +1,4 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentConfig } from "@opencode-ai/sdk";
/**
* Agent mode determines UI model selection behavior:
@@ -6,34 +6,38 @@ import type { AgentConfig } from "@opencode-ai/sdk"
* - "subagent": Uses own fallback chain, ignores UI selection (oracle, explore, etc.)
* - "all": Available in both contexts (OpenCode compatibility)
*/
export type AgentMode = "primary" | "subagent" | "all"
export type AgentMode = "primary" | "subagent" | "all";
/**
* Agent factory function with static mode property.
* Mode is exposed as static property for pre-instantiation access.
*/
export type AgentFactory = ((model: string) => AgentConfig) & {
mode: AgentMode
}
mode: AgentMode;
};
/**
* Agent category for grouping in Sisyphus prompt sections
*/
export type AgentCategory = "exploration" | "specialist" | "advisor" | "utility"
export type AgentCategory =
| "exploration"
| "specialist"
| "advisor"
| "utility";
/**
* Cost classification for Tool Selection table
*/
export type AgentCost = "FREE" | "CHEAP" | "EXPENSIVE"
export type AgentCost = "FREE" | "CHEAP" | "EXPENSIVE";
/**
* Delegation trigger for Sisyphus prompt's Delegation Table
*/
export interface DelegationTrigger {
/** Domain of work (e.g., "Frontend UI/UX") */
domain: string
domain: string;
/** When to delegate (e.g., "Visual changes only...") */
trigger: string
trigger: string;
}
/**
@@ -42,50 +46,62 @@ export interface DelegationTrigger {
*/
export interface AgentPromptMetadata {
/** Category for grouping in prompt sections */
category: AgentCategory
category: AgentCategory;
/** Cost classification for Tool Selection table */
cost: AgentCost
cost: AgentCost;
/** Domain triggers for Delegation Table */
triggers: DelegationTrigger[]
triggers: DelegationTrigger[];
/** When to use this agent (for detailed sections) */
useWhen?: string[]
useWhen?: string[];
/** When NOT to use this agent */
avoidWhen?: string[]
avoidWhen?: string[];
/** Optional dedicated prompt section (markdown) - for agents like Oracle that have special sections */
dedicatedSection?: string
dedicatedSection?: string;
/** Nickname/alias used in prompt (e.g., "Oracle" instead of "oracle") */
promptAlias?: string
promptAlias?: string;
/** Key triggers that should appear in Phase 0 (e.g., "External library mentioned → fire librarian") */
keyTrigger?: string
keyTrigger?: string;
}
function extractModelName(model: string): string {
return model.includes("/") ? model.split("/").pop() ?? model : model
return model.includes("/") ? (model.split("/").pop() ?? model) : model;
}
export function isGptModel(model: string): boolean {
const modelName = extractModelName(model).toLowerCase()
return modelName.includes("gpt")
const modelName = extractModelName(model).toLowerCase();
return modelName.includes("gpt");
}
const GEMINI_PROVIDERS = ["google/", "google-vertex/"]
export function isGpt5_4Model(model: string): boolean {
const modelName = extractModelName(model).toLowerCase();
return modelName.includes("gpt-5.4") || modelName.includes("gpt-5-4");
}
export function isGpt5_3CodexModel(model: string): boolean {
const modelName = extractModelName(model).toLowerCase();
return modelName.includes("gpt-5.3-codex") || modelName.includes("gpt-5-3-codex");
}
const GEMINI_PROVIDERS = ["google/", "google-vertex/"];
export function isGeminiModel(model: string): boolean {
if (GEMINI_PROVIDERS.some((prefix) => model.startsWith(prefix)))
return true
if (GEMINI_PROVIDERS.some((prefix) => model.startsWith(prefix))) return true;
if (model.startsWith("github-copilot/") && extractModelName(model).toLowerCase().startsWith("gemini"))
return true
if (
model.startsWith("github-copilot/") &&
extractModelName(model).toLowerCase().startsWith("gemini")
)
return true;
const modelName = extractModelName(model).toLowerCase()
return modelName.startsWith("gemini-")
const modelName = extractModelName(model).toLowerCase();
return modelName.startsWith("gemini-");
}
export type BuiltinAgentName =
@@ -97,18 +113,18 @@ export type BuiltinAgentName =
| "multimodal-looker"
| "metis"
| "momus"
| "atlas"
| "atlas";
export type OverridableAgentName =
| "build"
| BuiltinAgentName
export type OverridableAgentName = "build" | BuiltinAgentName;
export type AgentName = BuiltinAgentName
export type AgentName = BuiltinAgentName;
export type AgentOverrideConfig = Partial<AgentConfig> & {
prompt_append?: string
variant?: string
fallback_models?: string | string[]
}
prompt_append?: string;
variant?: string;
fallback_models?: string | string[];
};
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
export type AgentOverrides = Partial<
Record<OverridableAgentName, AgentOverrideConfig>
>;
+30 -30
View File
@@ -39,14 +39,14 @@ describe("createBuiltinAgents with model overrides", () => {
test("Sisyphus with GPT model override has reasoningEffort, no thinking", async () => {
// #given
const overrides = {
sisyphus: { model: "github-copilot/gpt-5.2" },
sisyphus: { model: "github-copilot/gpt-5.4" },
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
// #then
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
expect(agents.sisyphus.reasoningEffort).toBe("medium")
expect(agents.sisyphus.thinking).toBeUndefined()
})
@@ -54,9 +54,9 @@ describe("createBuiltinAgents with model overrides", () => {
test("Atlas uses uiSelectedModel", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
)
const uiSelectedModel = "openai/gpt-5.2"
const uiSelectedModel = "openai/gpt-5.4"
try {
// #when
@@ -75,7 +75,7 @@ describe("createBuiltinAgents with model overrides", () => {
// #then
expect(agents.atlas).toBeDefined()
expect(agents.atlas.model).toBe("openai/gpt-5.2")
expect(agents.atlas.model).toBe("openai/gpt-5.4")
} finally {
fetchSpy.mockRestore()
}
@@ -84,9 +84,9 @@ describe("createBuiltinAgents with model overrides", () => {
test("user config model takes priority over uiSelectedModel for sisyphus", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
)
const uiSelectedModel = "openai/gpt-5.2"
const uiSelectedModel = "openai/gpt-5.4"
const overrides = {
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
}
@@ -117,9 +117,9 @@ describe("createBuiltinAgents with model overrides", () => {
test("user config model takes priority over uiSelectedModel for atlas", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-6"])
new Set(["openai/gpt-5.4", "anthropic/claude-sonnet-4-6"])
)
const uiSelectedModel = "openai/gpt-5.2"
const uiSelectedModel = "openai/gpt-5.4"
const overrides = {
atlas: { model: "google/antigravity-claude-opus-4-5-thinking" },
}
@@ -173,8 +173,8 @@ describe("createBuiltinAgents with model overrides", () => {
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
expect(agents.oracle.model).toBe("openai/gpt-5.2")
// #then - oracle resolves via connected cache fallback to openai/gpt-5.4 (not system default)
expect(agents.oracle.model).toBe("openai/gpt-5.4")
expect(agents.oracle.reasoningEffort).toBe("medium")
expect(agents.oracle.thinking).toBeUndefined()
cacheSpy.mockRestore?.()
@@ -196,14 +196,14 @@ describe("createBuiltinAgents with model overrides", () => {
test("Oracle with GPT model override has reasoningEffort, no thinking", async () => {
// #given
const overrides = {
oracle: { model: "openai/gpt-5.2" },
oracle: { model: "openai/gpt-5.4" },
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
// #then
expect(agents.oracle.model).toBe("openai/gpt-5.2")
expect(agents.oracle.model).toBe("openai/gpt-5.4")
expect(agents.oracle.reasoningEffort).toBe("medium")
expect(agents.oracle.textVerbosity).toBe("high")
expect(agents.oracle.thinking).toBeUndefined()
@@ -228,14 +228,14 @@ describe("createBuiltinAgents with model overrides", () => {
test("non-model overrides are still applied after factory rebuild", async () => {
// #given
const overrides = {
sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
sisyphus: { model: "github-copilot/gpt-5.4", temperature: 0.5 },
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
// #then
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.4")
expect(agents.sisyphus.temperature).toBe(0.5)
})
@@ -261,7 +261,7 @@ describe("createBuiltinAgents with model overrides", () => {
"opencode/kimi-k2.5-free",
"zai-coding-plan/glm-5",
"opencode/big-pickle",
"openai/gpt-5.2",
"openai/gpt-5.4",
])
)
@@ -298,7 +298,7 @@ describe("createBuiltinAgents with model overrides", () => {
test("excludes hidden custom agents from orchestrator prompts", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
const customAgentSummaries = [
@@ -334,7 +334,7 @@ describe("createBuiltinAgents with model overrides", () => {
test("excludes disabled custom agents from orchestrator prompts", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
const customAgentSummaries = [
@@ -370,7 +370,7 @@ describe("createBuiltinAgents with model overrides", () => {
test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
const disabledAgents = ["ReSeArChEr"]
@@ -406,7 +406,7 @@ describe("createBuiltinAgents with model overrides", () => {
test("deduplicates custom agents case-insensitively", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
const customAgentSummaries = [
@@ -438,7 +438,7 @@ describe("createBuiltinAgents with model overrides", () => {
test("sanitizes custom agent strings for markdown tables", async () => {
// #given
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
const customAgentSummaries = [
@@ -479,7 +479,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
// #then - connected cache enables model resolution despite no systemDefaultModel
expect(agents.oracle).toBeDefined()
expect(agents.oracle.model).toBe("openai/gpt-5.2")
expect(agents.oracle.model).toBe("openai/gpt-5.4")
cacheSpy.mockRestore?.()
})
@@ -787,7 +787,7 @@ describe("Atlas is unaffected by environment context toggle", () => {
beforeEach(() => {
fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.4"])
)
})
@@ -891,9 +891,9 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
})
test("sisyphus is not created when no fallback model is available and provider not connected", async () => {
// #given - only openai/gpt-5.2 available, not in sisyphus fallback chain
// #given - only venice/deepseek-v3.2 available, not in sisyphus fallback chain
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2"])
new Set(["venice/deepseek-v3.2"])
)
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
@@ -913,7 +913,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
// #given - user configures a model from a plugin provider (like antigravity)
// that is NOT in the availableModels cache and NOT in the fallback chain
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
new Set(["openai/gpt-5.2"])
new Set(["openai/gpt-5.4"])
)
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
["openai"]
@@ -1021,7 +1021,7 @@ describe("buildAgent with category and skills", () => {
const categories = {
"custom-category": {
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
variant: "xhigh",
},
}
@@ -1030,7 +1030,7 @@ describe("buildAgent with category and skills", () => {
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
// #then
expect(agent.model).toBe("openai/gpt-5.2")
expect(agent.model).toBe("openai/gpt-5.4")
expect(agent.variant).toBe("xhigh")
})
@@ -1247,7 +1247,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
const categories = {
"test-cat": {
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
reasoningEffort: "xhigh" as const,
},
}
@@ -1267,7 +1267,7 @@ describe("override.category expansion in createBuiltinAgents", () => {
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
const categories = {
"reasoning-cat": {
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
reasoningEffort: "high" as const,
},
}
+3 -3
View File
@@ -1,6 +1,6 @@
# src/cli/ — CLI: install, run, doctor, mcp-oauth
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -51,9 +51,9 @@ cli/
## MODEL FALLBACK SYSTEM
Priority: Claude > OpenAI > Gemini > Copilot > OpenCode Zen > Z.ai > Kimi > big-pickle
No single global priority. CLI install-time resolution uses per-agent fallback chains from `model-fallback-requirements.ts`.
Agent-specific: librarian→ZAI, explore→Haiku/nano, hephaestusrequires OpenAI/Copilot
Common patterns: Claude/OpenAI/Gemini are preferred when an agent chain includes them, `librarian` prefers ZAI, `sisyphus` falls back through Kimi then GLM-5, and `hephaestus` requires OpenAI-compatible providers.
## DOCTOR CHECKS
@@ -83,7 +83,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
"variant": "max",
},
"multimodal-looker": {
"model": "anthropic/claude-haiku-4-5",
"model": "opencode/glm-4.7-free",
},
"oracle": {
"model": "anthropic/claude-opus-4-6",
@@ -145,7 +145,7 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
"variant": "max",
},
"multimodal-looker": {
"model": "anthropic/claude-haiku-4-5",
"model": "opencode/glm-4.7-free",
},
"oracle": {
"model": "anthropic/claude-opus-4-6",
@@ -191,7 +191,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "opencode/gpt-5-nano",
@@ -204,24 +205,29 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
"model": "opencode/glm-4.7-free",
},
"metis": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"sisyphus": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
},
"categories": {
"deep": {
@@ -258,7 +264,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"explore": {
"model": "opencode/gpt-5-nano",
@@ -271,24 +278,29 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
"model": "opencode/glm-4.7-free",
},
"metis": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"sisyphus": {
"model": "openai/gpt-5.4",
"variant": "medium",
},
},
"categories": {
"deep": {
@@ -303,7 +315,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
"variant": "xhigh",
},
"unspecified-high": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"unspecified-low": {
@@ -426,7 +438,7 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
"variant": "high",
},
"unspecified-high": {
"model": "google/gemini-3.1-pro-preview",
"model": "opencode/glm-4.7-free",
},
"unspecified-low": {
"model": "google/gemini-3-flash-preview",
@@ -464,14 +476,15 @@ exports[`generateModelConfig all native providers uses preferred models from fal
"variant": "max",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "google/gemini-3-flash-preview",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -538,14 +551,15 @@ exports[`generateModelConfig all native providers uses preferred models with isM
"variant": "max",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "google/gemini-3-flash-preview",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -574,8 +588,8 @@ exports[`generateModelConfig all native providers uses preferred models with isM
"variant": "xhigh",
},
"unspecified-high": {
"model": "anthropic/claude-opus-4-6",
"variant": "max",
"model": "openai/gpt-5.4",
"variant": "high",
},
"unspecified-low": {
"model": "anthropic/claude-sonnet-4-5",
@@ -596,7 +610,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "opencode/claude-sonnet-4-5",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
@@ -613,14 +627,15 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
"variant": "max",
},
"momus": {
"model": "opencode/gpt-5.2",
"variant": "medium",
"model": "opencode/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "opencode/gemini-3-flash",
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "opencode/gpt-5.2",
"model": "opencode/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -670,7 +685,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "opencode/claude-sonnet-4-5",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
@@ -687,14 +702,15 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
"variant": "max",
},
"momus": {
"model": "opencode/gpt-5.2",
"variant": "medium",
"model": "opencode/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "opencode/gemini-3-flash",
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "opencode/gpt-5.2",
"model": "opencode/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -723,8 +739,8 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
"variant": "xhigh",
},
"unspecified-high": {
"model": "opencode/claude-opus-4-6",
"variant": "max",
"model": "opencode/gpt-5.4",
"variant": "high",
},
"unspecified-low": {
"model": "opencode/claude-sonnet-4-5",
@@ -758,14 +774,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
"variant": "max",
},
"momus": {
"model": "github-copilot/gpt-5.2",
"variant": "medium",
"model": "github-copilot/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "github-copilot/gemini-3-flash-preview",
},
"oracle": {
"model": "github-copilot/gpt-5.2",
"model": "github-copilot/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -824,14 +840,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
"variant": "max",
},
"momus": {
"model": "github-copilot/gpt-5.2",
"variant": "medium",
"model": "github-copilot/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "github-copilot/gemini-3-flash-preview",
},
"oracle": {
"model": "github-copilot/gpt-5.2",
"model": "github-copilot/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -856,8 +872,8 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
"variant": "high",
},
"unspecified-high": {
"model": "github-copilot/claude-opus-4.6",
"variant": "max",
"model": "github-copilot/gpt-5.4",
"variant": "high",
},
"unspecified-low": {
"model": "github-copilot/claude-sonnet-4.5",
@@ -902,7 +918,7 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
"model": "opencode/glm-4.7-free",
},
"sisyphus": {
"model": "zai-coding-plan/glm-4.7",
"model": "zai-coding-plan/glm-5",
},
},
"categories": {
@@ -957,7 +973,7 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
"model": "opencode/glm-4.7-free",
},
"sisyphus": {
"model": "zai-coding-plan/glm-4.7",
"model": "zai-coding-plan/glm-5",
},
},
"categories": {
@@ -968,7 +984,7 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
"model": "opencode/glm-4.7-free",
},
"unspecified-high": {
"model": "opencode/glm-4.7-free",
"model": "zai-coding-plan/glm-5",
},
"unspecified-low": {
"model": "opencode/glm-4.7-free",
@@ -988,7 +1004,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "anthropic/claude-sonnet-4-5",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
@@ -1005,14 +1021,15 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
"variant": "max",
},
"momus": {
"model": "opencode/gpt-5.2",
"variant": "medium",
"model": "opencode/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "opencode/gemini-3-flash",
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "opencode/gpt-5.2",
"model": "opencode/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -1079,14 +1096,15 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
"variant": "max",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "github-copilot/gemini-3-flash-preview",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -1263,7 +1281,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "github-copilot/claude-sonnet-4.5",
},
"explore": {
"model": "opencode/claude-haiku-4-5",
@@ -1280,14 +1298,15 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
"variant": "max",
},
"momus": {
"model": "github-copilot/gpt-5.2",
"variant": "medium",
"model": "github-copilot/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "github-copilot/gemini-3-flash-preview",
"model": "opencode/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "github-copilot/gpt-5.2",
"model": "github-copilot/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -1337,7 +1356,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "anthropic/claude-sonnet-4-5",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
@@ -1354,14 +1373,15 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
"variant": "max",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "google/gemini-3-flash-preview",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -1411,7 +1431,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/kimi-k2.5-free",
"model": "anthropic/claude-sonnet-4-5",
},
"explore": {
"model": "anthropic/claude-haiku-4-5",
@@ -1428,14 +1448,15 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
"variant": "max",
},
"momus": {
"model": "openai/gpt-5.2",
"variant": "medium",
"model": "openai/gpt-5.4",
"variant": "xhigh",
},
"multimodal-looker": {
"model": "google/gemini-3-flash-preview",
"model": "openai/gpt-5.4",
"variant": "medium",
},
"oracle": {
"model": "openai/gpt-5.2",
"model": "openai/gpt-5.4",
"variant": "high",
},
"prometheus": {
@@ -1464,8 +1485,8 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
"variant": "xhigh",
},
"unspecified-high": {
"model": "anthropic/claude-opus-4-6",
"variant": "max",
"model": "openai/gpt-5.4",
"variant": "high",
},
"unspecified-low": {
"model": "anthropic/claude-sonnet-4-5",
+1 -13
View File
@@ -21,19 +21,9 @@ describe("runCliInstaller", () => {
console.error = originalConsoleError
})
it("runs auth and provider setup steps when openai or copilot are enabled without gemini", async () => {
it("completes installation without auth plugin or provider config steps", async () => {
//#given
const addAuthPluginsSpy = spyOn(configManager, "addAuthPlugins").mockResolvedValue({
success: true,
configPath: "/tmp/opencode.jsonc",
})
const addProviderConfigSpy = spyOn(configManager, "addProviderConfig").mockReturnValue({
success: true,
configPath: "/tmp/opencode.jsonc",
})
const restoreSpies = [
addAuthPluginsSpy,
addProviderConfigSpy,
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
isInstalled: false,
hasClaude: false,
@@ -73,8 +63,6 @@ describe("runCliInstaller", () => {
//#then
expect(result).toBe(0)
expect(addAuthPluginsSpy).toHaveBeenCalledTimes(1)
expect(addProviderConfigSpy).toHaveBeenCalledTimes(1)
for (const spy of restoreSpies) {
spy.mockRestore()
+2 -26
View File
@@ -1,9 +1,7 @@
import color from "picocolors"
import type { InstallArgs } from "./types"
import {
addAuthPlugins,
addPluginToOpenCodeConfig,
addProviderConfig,
detectCurrentConfig,
getOpenCodeVersion,
isOpenCodeInstalled,
@@ -45,7 +43,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
printHeader(isUpdate)
const totalSteps = 6
const totalSteps = 4
let step = 1
printStep(step++, totalSteps, "Checking OpenCode installation...")
@@ -77,28 +75,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
)
const needsProviderSetup = config.hasGemini || config.hasOpenAI || config.hasCopilot
if (needsProviderSetup) {
printStep(step++, totalSteps, "Adding auth plugins...")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
printError(`Failed: ${authResult.error}`)
return 1
}
printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`)
printStep(step++, totalSteps, "Adding provider configurations...")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
printError(`Failed: ${providerResult.error}`)
return 1
}
printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`)
} else {
step += 2
}
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
@@ -156,7 +132,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
printBox(
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ Gemini")}\n` : "") +
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
"Authenticate Your Providers",
)
+1 -1
View File
@@ -40,7 +40,7 @@ Examples:
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
OpenAI Native openai/ models (GPT-5.2 for Oracle)
OpenAI Native openai/ models (GPT-5.4 for Oracle)
Gemini Native google/ models (Gemini 3 Pro, Flash)
Copilot github-copilot/ models (fallback)
OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.)
+7 -76
View File
@@ -1,6 +1,6 @@
import { describe, expect, test, mock, afterEach } from "bun:test"
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
import { getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
import type { InstallConfig } from "./types"
describe("getPluginNameWithVersion", () => {
@@ -169,76 +169,6 @@ describe("fetchNpmDistTags", () => {
})
})
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
test("all models include full spec (limit + modalities + Antigravity label)", () => {
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
expect(google).toBeTruthy()
const models = google.models as Record<string, any>
expect(models).toBeTruthy()
const required = [
"antigravity-gemini-3.1-pro",
"antigravity-gemini-3-flash",
"antigravity-claude-sonnet-4-6",
"antigravity-claude-sonnet-4-6-thinking",
"antigravity-claude-opus-4-5-thinking",
]
for (const key of required) {
const model = models[key]
expect(model).toBeTruthy()
expect(typeof model.name).toBe("string")
expect(model.name.includes("(Antigravity)")).toBe(true)
expect(model.limit).toBeTruthy()
expect(typeof model.limit.context).toBe("number")
expect(typeof model.limit.output).toBe("number")
expect(model.modalities).toBeTruthy()
expect(Array.isArray(model.modalities.input)).toBe(true)
expect(Array.isArray(model.modalities.output)).toBe(true)
}
})
test("Gemini models have variant definitions", () => {
// #given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Gemini Pro variants
const pro = models["antigravity-gemini-3.1-pro"]
// #then should have low and high variants
expect(pro.variants).toBeTruthy()
expect(pro.variants.low).toBeTruthy()
expect(pro.variants.high).toBeTruthy()
// #when checking Gemini Flash variants
const flash = models["antigravity-gemini-3-flash"]
// #then should have minimal, low, medium, high variants
expect(flash.variants).toBeTruthy()
expect(flash.variants.minimal).toBeTruthy()
expect(flash.variants.low).toBeTruthy()
expect(flash.variants.medium).toBeTruthy()
expect(flash.variants.high).toBeTruthy()
})
test("Claude thinking models have variant definitions", () => {
// #given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Claude thinking variants
const sonnetThinking = models["antigravity-claude-sonnet-4-6-thinking"]
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
// #then both should have low and max variants
for (const model of [sonnetThinking, opusThinking]) {
expect(model.variants).toBeTruthy()
expect(model.variants.low).toBeTruthy()
expect(model.variants.max).toBeTruthy()
}
})
})
describe("generateOmoConfig - model fallback system", () => {
test("uses github-copilot sonnet fallback when only copilot available", () => {
// #given user has only copilot (no max plan)
@@ -319,12 +249,13 @@ describe("generateOmoConfig - model fallback system", () => {
// #when generating config
const result = generateOmoConfig(config)
// #then Sisyphus is omitted (requires all fallback providers)
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
// #then Sisyphus resolves to gpt-5.4 medium (openai is now in sisyphus chain)
expect((result.agents as Record<string, { model: string; variant?: string }>).sisyphus.model).toBe("openai/gpt-5.4")
expect((result.agents as Record<string, { model: string; variant?: string }>).sisyphus.variant).toBe("medium")
// #then Oracle should use native OpenAI (first fallback entry)
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
// #then multimodal-looker should use native OpenAI (fallback within native tier)
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.4")
// #then multimodal-looker should use native OpenAI (first fallback entry is gpt-5.4)
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.4")
})
test("uses haiku for explore when Claude max20", () => {
-3
View File
@@ -14,9 +14,6 @@ export { writeOmoConfig } from "./config-manager/write-omo-config"
export { isOpenCodeInstalled, getOpenCodeVersion } from "./config-manager/opencode-binary"
export { fetchLatestVersion, addAuthPlugins } from "./config-manager/auth-plugins"
export { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager/antigravity-provider-configuration"
export { addProviderConfig } from "./config-manager/add-provider-config"
export { detectCurrentConfig } from "./config-manager/detect-current-config"
export type { BunInstallResult } from "./config-manager/bun-install"
+1 -1
View File
@@ -1,6 +1,6 @@
# src/cli/config-manager/ — CLI Installation Utilities
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -1,205 +0,0 @@
import { describe, expect, it } from "bun:test"
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
import { parseJsonc } from "../../shared/jsonc-parser"
describe("modifyProviderInJsonc", () => {
describe("Test 1: Basic JSONC with existing provider", () => {
it("replaces provider value, preserves comments and other keys", () => {
// given
const content = `{
// my config
"provider": { "openai": {} },
"plugin": ["foo"]
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"google"')
expect(result).toContain('"plugin": ["foo"]')
expect(result).toContain('// my config')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('plugin')
expect(parsed).toHaveProperty('provider')
})
})
describe("Test 2: Comment containing '}' inside provider block", () => {
it("must NOT corrupt file", () => {
// given
const content = `{
"provider": {
// } this brace should be ignored
"openai": {}
},
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
})
})
describe("Test 3: Comment containing '\"provider\"' before real key", () => {
it("must NOT match wrong location", () => {
// given
const content = `{
// "provider": { "example": true }
"provider": { "openai": {} },
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
expect(parsed.provider).toHaveProperty('google')
})
})
describe("Test 4: Comment containing '{' inside provider", () => {
it("must NOT mess up depth", () => {
// given
const content = `{
"provider": {
// { unmatched brace in comment
"openai": {}
},
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
})
})
describe("Test 5: No existing provider key", () => {
it("inserts provider without corrupting", () => {
// given
const content = `{
// config comment
"plugin": ["foo"]
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"provider"')
expect(result).toContain('"plugin"')
expect(result).toContain('foo')
expect(result).toContain('// config comment')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('provider')
expect(parsed).toHaveProperty('plugin')
expect(parsed.plugin).toEqual(['foo'])
})
})
describe("Test 6: String value exactly 'provider' before real key", () => {
it("must NOT match wrong location", () => {
// given
const content = `{
"note": "provider",
"provider": { "openai": {} },
"other": 1
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(result).toContain('"other"')
expect(result).toContain('"note": "provider"')
// Post-write validation
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('other')
expect(parsed.other).toBe(1)
expect(parsed.note).toBe('provider')
})
})
describe("Test 7: Post-write validation", () => {
it("result file must be valid JSONC for all cases", () => {
// Test Case 1
const content1 = `{
"provider": { "openai": {} },
"plugin": ["foo"]
}`
const result1 = modifyProviderInJsonc(content1, { google: {} })
expect(() => parseJsonc(result1)).not.toThrow()
// Test Case 2
const content2 = `{
"provider": {
// } comment
"openai": {}
}
}`
const result2 = modifyProviderInJsonc(content2, { google: {} })
expect(() => parseJsonc(result2)).not.toThrow()
// Test Case 3
const content3 = `{
"plugin": ["foo"]
}`
const result3 = modifyProviderInJsonc(content3, { google: {} })
expect(() => parseJsonc(result3)).not.toThrow()
})
})
describe("Test 8: Trailing commas preserved", () => {
it("file is valid JSONC with trailing commas", () => {
// given
const content = `{
"provider": { "openai": {}, },
"plugin": ["foo",],
}`
const newProviderValue = { google: { name: "Google" } }
// when
const result = modifyProviderInJsonc(content, newProviderValue)
// then
expect(() => parseJsonc(result)).not.toThrow()
const parsed = parseJsonc<Record<string, unknown>>(result)
expect(parsed).toHaveProperty('plugin')
expect(parsed.plugin).toEqual(['foo'])
})
})
})
@@ -1,82 +0,0 @@
import { readFileSync, writeFileSync, copyFileSync } from "node:fs"
import type { ConfigMergeResult, InstallConfig } from "../types"
import { getConfigDir } from "./config-context"
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
import { detectConfigFormat } from "./opencode-config-format"
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./antigravity-provider-configuration"
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
import { parseJsonc } from "../../shared/jsonc-parser"
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
try {
ensureConfigDirectoryExists()
} catch (err) {
return {
success: false,
configPath: getConfigDir(),
error: formatErrorWithSuggestion(err, "create config directory"),
}
}
const { format, path } = detectConfigFormat()
try {
let existingConfig: OpenCodeConfig | null = null
if (format !== "none") {
const parseResult = parseOpenCodeConfigFileWithError(path)
if (parseResult.error && !parseResult.config) {
return {
success: false,
configPath: path,
error: `Failed to parse config file: ${parseResult.error}`,
}
}
existingConfig = parseResult.config
}
const newConfig = { ...(existingConfig ?? {}) }
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
if (config.hasGemini) {
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
}
if (Object.keys(providers).length > 0) {
newConfig.provider = providers
}
if (format === "jsonc") {
const content = readFileSync(path, "utf-8")
// Backup original file
copyFileSync(path, `${path}.bak`)
const providerValue = (newConfig.provider ?? {}) as Record<string, unknown>
const newContent = modifyProviderInJsonc(content, providerValue)
// Post-write validation
try {
parseJsonc(newContent)
} catch (error) {
return {
success: false,
configPath: path,
error: `Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`,
}
}
writeFileSync(path, newContent)
} else {
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
}
return { success: true, configPath: path }
} catch (err) {
return {
success: false,
configPath: path,
error: formatErrorWithSuggestion(err, "add provider config"),
}
}
}
@@ -1,64 +0,0 @@
/**
* Antigravity Provider Configuration
*
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
*
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
* - `antigravity-gemini-3.1-pro` with variants: low, high
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
*
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3.1-pro-high`) still work
* but variants are the recommended approach.
*
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
*/
export const ANTIGRAVITY_PROVIDER_CONFIG = {
google: {
name: "Google",
models: {
"antigravity-gemini-3.1-pro": {
name: "Gemini 3 Pro (Antigravity)",
limit: { context: 1048576, output: 65535 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingLevel: "low" },
high: { thinkingLevel: "high" },
},
},
"antigravity-gemini-3-flash": {
name: "Gemini 3 Flash (Antigravity)",
limit: { context: 1048576, output: 65536 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
minimal: { thinkingLevel: "minimal" },
low: { thinkingLevel: "low" },
medium: { thinkingLevel: "medium" },
high: { thinkingLevel: "high" },
},
},
"antigravity-claude-sonnet-4-6": {
name: "Claude Sonnet 4.6 (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
"antigravity-claude-sonnet-4-6-thinking": {
name: "Claude Sonnet 4.6 Thinking (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingConfig: { thinkingBudget: 8192 } },
max: { thinkingConfig: { thinkingBudget: 32768 } },
},
},
"antigravity-claude-opus-4-5-thinking": {
name: "Claude Opus 4.5 Thinking (Antigravity)",
limit: { context: 200000, output: 64000 },
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
variants: {
low: { thinkingConfig: { thinkingBudget: 8192 } },
max: { thinkingConfig: { thinkingBudget: 32768 } },
},
},
},
},
}
-230
View File
@@ -1,230 +0,0 @@
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "node:fs"
import { parseJsonc } from "../../shared/jsonc-parser"
import type { InstallConfig } from "../types"
import { resetConfigContext } from "./config-context"
let testConfigPath: string
let testConfigDir: string
let testCounter = 0
let fetchVersionSpy: unknown
beforeEach(async () => {
testCounter++
testConfigDir = join(tmpdir(), `test-opencode-${Date.now()}-${testCounter}`)
testConfigPath = join(testConfigDir, "opencode.jsonc")
mkdirSync(testConfigDir, { recursive: true })
process.env.OPENCODE_CONFIG_DIR = testConfigDir
resetConfigContext()
const module = await import("./auth-plugins")
fetchVersionSpy = spyOn(module, "fetchLatestVersion").mockResolvedValue("1.2.3")
})
afterEach(() => {
try {
rmSync(testConfigDir, { recursive: true, force: true })
} catch {}
})
const testConfig: InstallConfig = {
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: true,
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
describe("addAuthPlugins", () => {
describe("Test 1: JSONC with commented plugin line", () => {
it("preserves comment, does NOT add antigravity plugin", async () => {
const content = `{
// "plugin": ["old-plugin"]
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
expect(newContent).toContain('// "plugin": ["old-plugin"]')
expect(newContent).toContain('existing-plugin')
// antigravity plugin should NOT be auto-added anymore
expect(newContent).not.toContain('opencode-antigravity-auth')
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('existing-plugin')
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
})
})
describe("Test 2: Plugin array already contains antigravity", () => {
it("preserves existing antigravity, does not add another", async () => {
const content = `{
"plugin": ["existing-plugin", "opencode-antigravity-auth"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
const antigravityCount = plugins.filter((p) => p.startsWith('opencode-antigravity-auth')).length
expect(antigravityCount).toBe(1)
expect(plugins).toContain('existing-plugin')
})
})
describe("Test 3: Backup created before write", () => {
it("creates .bak file", async () => {
const originalContent = `{
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, originalContent, "utf-8")
readFileSync(testConfigPath, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
expect(existsSync(`${result.configPath}.bak`)).toBe(true)
const backupContent = readFileSync(`${result.configPath}.bak`, "utf-8")
expect(backupContent).toBe(originalContent)
})
})
describe("Test 4: Comment with } character", () => {
it("preserves comments with special characters", async () => {
const content = `{
// This comment has } special characters
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(newContent).toContain('// This comment has } special characters')
expect(() => parseJsonc(newContent)).not.toThrow()
})
})
describe("Test 5: Comment containing 'plugin' string", () => {
it("must NOT match comment location", async () => {
const content = `{
// "plugin": ["fake"]
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(newContent).toContain('// "plugin": ["fake"]')
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('existing-plugin')
expect(plugins).not.toContain('fake')
})
})
describe("Test 6: No existing plugin array", () => {
it("creates empty plugin array when none exists, does NOT add antigravity", async () => {
const content = `{
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
expect(parsed).toHaveProperty('plugin')
const plugins = parsed.plugin as string[]
// antigravity plugin should NOT be auto-added anymore
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
expect(plugins.length).toBe(0)
})
})
describe("Test 7: Post-write validation ensures valid JSONC", () => {
it("result file must be valid JSONC", async () => {
const content = `{
"plugin": ["existing-plugin"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(testConfigPath, "utf-8")
expect(() => parseJsonc(newContent)).not.toThrow()
const parsed = parseJsonc<Record<string, unknown>>(newContent)
expect(parsed).toHaveProperty('plugin')
expect(parsed).toHaveProperty('provider')
})
})
describe("Test 8: Multiple plugins in array", () => {
it("preserves existing plugins, does NOT add antigravity", async () => {
const content = `{
"plugin": ["plugin-1", "plugin-2", "plugin-3"],
"provider": {}
}`
writeFileSync(testConfigPath, content, "utf-8")
const { addAuthPlugins } = await import("./auth-plugins")
const result = await addAuthPlugins(testConfig)
expect(result.success).toBe(true)
const newContent = readFileSync(result.configPath, "utf-8")
const parsed = parseJsonc<Record<string, unknown>>(newContent)
const plugins = parsed.plugin as string[]
expect(plugins).toContain('plugin-1')
expect(plugins).toContain('plugin-2')
expect(plugins).toContain('plugin-3')
// antigravity plugin should NOT be auto-added anymore
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
expect(plugins.length).toBe(3)
})
})
})
-140
View File
@@ -1,140 +0,0 @@
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"
import { modify, applyEdits } from "jsonc-parser"
import type { ConfigMergeResult, InstallConfig } from "../types"
import { getConfigDir } from "./config-context"
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
import { detectConfigFormat } from "./opencode-config-format"
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
import { parseJsonc } from "../../shared/jsonc-parser"
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
try {
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`)
if (!res.ok) return null
const data = (await res.json()) as { version: string }
return data.version
} catch {
return null
}
}
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
try {
ensureConfigDirectoryExists()
} catch (err) {
return {
success: false,
configPath: getConfigDir(),
error: formatErrorWithSuggestion(err, "create config directory"),
}
}
const { format, path } = detectConfigFormat()
const backupPath = `${path}.bak`
try {
let existingConfig: OpenCodeConfig | null = null
if (format !== "none") {
const parseResult = parseOpenCodeConfigFileWithError(path)
if (parseResult.error && !parseResult.config) {
return {
success: false,
configPath: path,
error: `Failed to parse config file: ${parseResult.error}`,
}
}
existingConfig = parseResult.config
}
const rawPlugins = existingConfig?.plugin
const plugins: string[] = Array.isArray(rawPlugins) ? rawPlugins : []
// Note: opencode-antigravity-auth plugin auto-installation has been removed
// Users can manually add auth plugins if needed
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
if (format !== "none" && existsSync(path)) {
copyFileSync(path, backupPath)
}
if (format === "jsonc") {
const content = readFileSync(path, "utf-8")
const newContent = applyEdits(
content,
modify(content, ["plugin"], plugins, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
)
try {
parseJsonc(newContent)
} catch (error) {
if (existsSync(backupPath)) {
copyFileSync(backupPath, path)
}
throw new Error(`Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`)
}
try {
writeFileSync(path, newContent)
} catch (error) {
const hasBackup = existsSync(backupPath)
try {
if (hasBackup) {
copyFileSync(backupPath, path)
}
} catch (restoreError) {
return {
success: false,
configPath: path,
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
}
}
return {
success: false,
configPath: path,
error: hasBackup
? `Failed to write config file. Restored from backup: ${String(error)}`
: `Failed to write config file. No backup was available: ${String(error)}`,
}
}
} else {
const nextContent = JSON.stringify(newConfig, null, 2) + "\n"
try {
writeFileSync(path, nextContent)
} catch (error) {
const hasBackup = existsSync(backupPath)
try {
if (hasBackup) {
copyFileSync(backupPath, path)
}
} catch (restoreError) {
return {
success: false,
configPath: path,
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
}
}
return {
success: false,
configPath: path,
error: hasBackup
? `Failed to write config file. Restored from backup: ${String(error)}`
: `Failed to write config file. No backup was available: ${String(error)}`,
}
}
}
return { success: true, configPath: path }
} catch (err) {
return {
success: false,
configPath: path,
error: formatErrorWithSuggestion(err, "add auth plugins to config"),
}
}
}
@@ -66,7 +66,8 @@ export function detectCurrentConfig(): DetectedConfig {
return result
}
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
const providers = openCodeConfig.provider as Record<string, unknown> | undefined
result.hasGemini = providers ? "google" in providers : false
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
result.hasOpenAI = hasOpenAI
@@ -1,11 +0,0 @@
import { modify, applyEdits } from "jsonc-parser"
export function modifyProviderInJsonc(
content: string,
newProviderValue: Record<string, unknown>
): string {
const edits = modify(content, ["provider"], newProviderValue, {
formattingOptions: { tabSize: 2, insertSpaces: true },
})
return applyEdits(content, edits)
}
@@ -61,7 +61,7 @@ describe("model-resolution check", () => {
// given: User has override for visual-engineering category
const mockConfig = {
categories: {
"visual-engineering": { model: "openai/gpt-5.2" },
"visual-engineering": { model: "openai/gpt-5.4" },
},
}
@@ -70,8 +70,8 @@ describe("model-resolution check", () => {
// then: visual-engineering should show the override
const visual = info.categories.find((c) => c.name === "visual-engineering")
expect(visual).toBeDefined()
expect(visual!.userOverride).toBe("openai/gpt-5.2")
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.2")
expect(visual!.userOverride).toBe("openai/gpt-5.4")
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.4")
})
it("shows provider fallback when no override exists", async () => {
@@ -96,7 +96,7 @@ describe("model-resolution check", () => {
//#given User has model with variant override for oracle agent
const mockConfig = {
agents: {
oracle: { model: "openai/gpt-5.2", variant: "xhigh" },
oracle: { model: "openai/gpt-5.4", variant: "xhigh" },
},
}
@@ -106,7 +106,7 @@ describe("model-resolution check", () => {
//#then Oracle should have userVariant set
const oracle = info.agents.find((a) => a.name === "oracle")
expect(oracle).toBeDefined()
expect(oracle!.userOverride).toBe("openai/gpt-5.2")
expect(oracle!.userOverride).toBe("openai/gpt-5.4")
expect(oracle!.userVariant).toBe("xhigh")
})
@@ -0,0 +1,18 @@
import { describe, expect, it } from "bun:test"
import { getSuggestedInstallTag } from "./system-loaded-version"
describe("system loaded version", () => {
describe("getSuggestedInstallTag", () => {
it("returns prerelease channel when current version is prerelease", () => {
//#given
const currentVersion = "3.2.0-beta.4"
//#when
const installTag = getSuggestedInstallTag(currentVersion)
//#then
expect(installTag).toBe("beta")
})
})
})
@@ -77,3 +77,7 @@ export async function getLatestPluginVersion(currentVersion: string | null): Pro
const channel = extractChannel(currentVersion)
return getLatestVersion(channel)
}
export function getSuggestedInstallTag(currentVersion: string | null): string {
return extractChannel(currentVersion)
}
+104
View File
@@ -0,0 +1,104 @@
import { beforeEach, describe, expect, it, mock } from "bun:test"
const mockFindOpenCodeBinary = mock(async () => ({ path: "/usr/local/bin/opencode" }))
const mockGetOpenCodeVersion = mock(async () => "1.0.200")
const mockCompareVersions = mock(() => true)
const mockGetPluginInfo = mock(() => ({
registered: true,
entry: "oh-my-opencode",
isPinned: false,
pinnedVersion: null,
configPath: null,
isLocalDev: false,
}))
const mockGetLoadedPluginVersion = mock(() => ({
cacheDir: "/Users/test/Library/Caches/opencode with spaces",
cachePackagePath: "/tmp/package.json",
installedPackagePath: "/tmp/node_modules/oh-my-opencode/package.json",
expectedVersion: "3.0.0",
loadedVersion: "3.1.0",
}))
const mockGetLatestPluginVersion = mock(async () => null)
mock.module("./system-binary", () => ({
findOpenCodeBinary: mockFindOpenCodeBinary,
getOpenCodeVersion: mockGetOpenCodeVersion,
compareVersions: mockCompareVersions,
}))
mock.module("./system-plugin", () => ({
getPluginInfo: mockGetPluginInfo,
}))
mock.module("./system-loaded-version", () => ({
getLoadedPluginVersion: mockGetLoadedPluginVersion,
getLatestPluginVersion: mockGetLatestPluginVersion,
}))
const { checkSystem } = await import("./system?test")
describe("system check", () => {
beforeEach(() => {
mockFindOpenCodeBinary.mockReset()
mockGetOpenCodeVersion.mockReset()
mockCompareVersions.mockReset()
mockGetPluginInfo.mockReset()
mockGetLoadedPluginVersion.mockReset()
mockGetLatestPluginVersion.mockReset()
mockFindOpenCodeBinary.mockResolvedValue({ path: "/usr/local/bin/opencode" })
mockGetOpenCodeVersion.mockResolvedValue("1.0.200")
mockCompareVersions.mockReturnValue(true)
mockGetPluginInfo.mockReturnValue({
registered: true,
entry: "oh-my-opencode",
isPinned: false,
pinnedVersion: null,
configPath: null,
isLocalDev: false,
})
mockGetLoadedPluginVersion.mockReturnValue({
cacheDir: "/Users/test/Library/Caches/opencode with spaces",
cachePackagePath: "/tmp/package.json",
installedPackagePath: "/tmp/node_modules/oh-my-opencode/package.json",
expectedVersion: "3.0.0",
loadedVersion: "3.1.0",
})
mockGetLatestPluginVersion.mockResolvedValue(null)
})
describe("#given cache directory contains spaces", () => {
it("uses a quoted cache directory in mismatch fix command", async () => {
//#when
const result = await checkSystem()
//#then
const mismatchIssue = result.issues.find((issue) => issue.title === "Loaded plugin version mismatch")
expect(mismatchIssue?.fix).toBe('Reinstall: cd "/Users/test/Library/Caches/opencode with spaces" && bun install')
})
it("uses the loaded version channel for update fix command", async () => {
//#given
mockGetLoadedPluginVersion.mockReturnValue({
cacheDir: "/Users/test/Library/Caches/opencode with spaces",
cachePackagePath: "/tmp/package.json",
installedPackagePath: "/tmp/node_modules/oh-my-opencode/package.json",
expectedVersion: "3.0.0-canary.1",
loadedVersion: "3.0.0-canary.1",
})
mockGetLatestPluginVersion.mockResolvedValue("3.0.0-canary.2")
mockCompareVersions.mockImplementation((leftVersion: string, rightVersion: string) => {
return !(leftVersion === "3.0.0-canary.1" && rightVersion === "3.0.0-canary.2")
})
//#when
const result = await checkSystem()
//#then
const outdatedIssue = result.issues.find((issue) => issue.title === "Loaded plugin is outdated")
expect(outdatedIssue?.fix).toBe(
'Update: cd "/Users/test/Library/Caches/opencode with spaces" && bun add oh-my-opencode@canary'
)
})
})
})
+4 -3
View File
@@ -4,7 +4,7 @@ import { MIN_OPENCODE_VERSION, CHECK_IDS, CHECK_NAMES } from "../constants"
import type { CheckResult, DoctorIssue, SystemInfo } from "../types"
import { findOpenCodeBinary, getOpenCodeVersion, compareVersions } from "./system-binary"
import { getPluginInfo } from "./system-plugin"
import { getLatestPluginVersion, getLoadedPluginVersion } from "./system-loaded-version"
import { getLatestPluginVersion, getLoadedPluginVersion, getSuggestedInstallTag } from "./system-loaded-version"
import { parseJsonc } from "../../../shared"
function isConfigValid(configPath: string | null): boolean {
@@ -54,6 +54,7 @@ export async function checkSystem(): Promise<CheckResult> {
const [systemInfo, pluginInfo] = await Promise.all([gatherSystemInfo(), Promise.resolve(getPluginInfo())])
const loadedInfo = getLoadedPluginVersion()
const latestVersion = await getLatestPluginVersion(systemInfo.loadedVersion)
const installTag = getSuggestedInstallTag(systemInfo.loadedVersion)
const issues: DoctorIssue[] = []
if (!systemInfo.opencodePath) {
@@ -93,7 +94,7 @@ export async function checkSystem(): Promise<CheckResult> {
issues.push({
title: "Loaded plugin version mismatch",
description: `Cache expects ${loadedInfo.expectedVersion} but loaded ${loadedInfo.loadedVersion}.`,
fix: `Reinstall: cd ${loadedInfo.cacheDir} && bun install`,
fix: `Reinstall: cd "${loadedInfo.cacheDir}" && bun install`,
severity: "warning",
affects: ["plugin loading"],
})
@@ -107,7 +108,7 @@ export async function checkSystem(): Promise<CheckResult> {
issues.push({
title: "Loaded plugin is outdated",
description: `Loaded ${systemInfo.loadedVersion}, latest ${latestVersion}.`,
fix: `Update: cd ${loadedInfo.cacheDir} && bun add oh-my-opencode@latest`,
fix: `Update: cd "${loadedInfo.cacheDir}" && bun add oh-my-opencode@${installTag}`,
severity: "warning",
affects: ["plugin features"],
})
+1 -1
View File
@@ -32,7 +32,7 @@ export function formatConfigSummary(config: InstallConfig): string {
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.2 for Oracle"))
lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.4 for Oracle"))
lines.push(formatProvider("Gemini", config.hasGemini))
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback"))
lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models"))
+233 -92
View File
@@ -1,4 +1,4 @@
import type { ModelRequirement } from "../shared/model-requirements"
import type { ModelRequirement } from "../shared/model-requirements";
// NOTE: These requirements are used by the CLI config generator (`generateModelConfig`).
// They intentionally use "install-time" provider IDs (anthropic/openai/google/opencode/etc),
@@ -7,32 +7,54 @@ import type { ModelRequirement } from "../shared/model-requirements"
export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
sisyphus: {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
{ providers: ["opencode"], model: "glm-4.7-free" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "medium" },
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
],
requiresAnyModel: true,
},
hephaestus: {
fallbackChain: [
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
{
providers: ["openai", "opencode"],
model: "gpt-5.3-codex",
variant: "medium",
},
],
requiresProvider: ["openai", "opencode"],
},
oracle: {
fallbackChain: [
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
variant: "high",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
],
},
librarian: {
fallbackChain: [
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
{ providers: ["opencode"], model: "glm-4.7-free" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-sonnet-4-5",
},
],
},
explore: {
@@ -44,109 +66,228 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
},
"multimodal-looker": {
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
{
providers: ["openai", "opencode"],
model: "gpt-5.4",
variant: "medium",
},
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3-flash",
},
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
{ providers: ["opencode"], model: "gpt-5-nano" },
],
},
prometheus: {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
variant: "high",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
},
],
},
metis: {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
variant: "high",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
],
},
momus: {
fallbackChain: [
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "medium" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
variant: "xhigh",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
],
},
atlas: {
fallbackChain: [
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-sonnet-4-5",
},
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.4", variant: "medium" },
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
},
],
},
}
};
export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
"visual-engineering": {
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{ providers: ["zai-coding-plan"], model: "glm-5" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["kimi-for-coding"], model: "k2p5" },
],
},
ultrabrain: {
fallbackChain: [
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "xhigh" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
],
},
deep: {
fallbackChain: [
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
],
requiresModel: "gpt-5.3-codex",
},
artistry: {
fallbackChain: [
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
],
requiresModel: "gemini-3.1-pro",
},
quick: {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
{ providers: ["opencode"], model: "gpt-5-nano" },
],
},
"unspecified-low": {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
],
},
"unspecified-high": {
fallbackChain: [
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
],
},
writing: {
fallbackChain: [
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
],
},
}
export const CLI_CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> =
{
"visual-engineering": {
fallbackChain: [
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{ providers: ["kimi-for-coding"], model: "k2p5" },
],
},
ultrabrain: {
fallbackChain: [
{
providers: ["openai", "opencode"],
model: "gpt-5.3-codex",
variant: "xhigh",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
],
},
deep: {
fallbackChain: [
{
providers: ["openai", "opencode"],
model: "gpt-5.3-codex",
variant: "medium",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
],
requiresModel: "gpt-5.3-codex",
},
artistry: {
fallbackChain: [
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3.1-pro",
variant: "high",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
},
],
requiresModel: "gemini-3.1-pro",
},
quick: {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-haiku-4-5",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3-flash",
},
{ providers: ["opencode"], model: "gpt-5-nano" },
],
},
"unspecified-low": {
fallbackChain: [
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-sonnet-4-5",
},
{
providers: ["openai", "opencode"],
model: "gpt-5.3-codex",
variant: "medium",
},
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3-flash",
},
],
},
"unspecified-high": {
fallbackChain: [
{
providers: ["openai", "github-copilot", "opencode"],
model: "gpt-5.4",
variant: "high",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-opus-4-6",
variant: "max",
},
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5" },
],
},
writing: {
fallbackChain: [
{ providers: ["kimi-for-coding"], model: "k2p5" },
{
providers: ["google", "github-copilot", "opencode"],
model: "gemini-3-flash",
},
{
providers: ["anthropic", "github-copilot", "opencode"],
model: "claude-sonnet-4-5",
},
],
},
};
+3 -2
View File
@@ -396,7 +396,7 @@ describe("generateModelConfig", () => {
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-6")
})
test("Sisyphus is omitted when no fallback provider is available (OpenAI not in chain)", () => {
test("Sisyphus resolves to gpt-5.4 medium when only OpenAI is available", () => {
// #given
const config = createConfig({ hasOpenAI: true })
@@ -404,7 +404,8 @@ describe("generateModelConfig", () => {
const result = generateModelConfig(config)
// #then
expect(result.agents?.sisyphus).toBeUndefined()
expect(result.agents?.sisyphus?.model).toBe("openai/gpt-5.4")
expect(result.agents?.sisyphus?.variant).toBe("medium")
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
# src/cli/run/ — Non-Interactive Session Launcher
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+2 -2
View File
@@ -44,7 +44,7 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
message: "Do you have an OpenAI/ChatGPT Plus subscription?",
options: [
{ value: "no", label: "No", hint: "Oracle will use fallback models" },
{ value: "yes", label: "Yes", hint: "GPT-5.2 for Oracle (high-IQ debugging)" },
{ value: "yes", label: "Yes", hint: "GPT-5.4 for Oracle (high-IQ debugging)" },
],
initialValue: initial.openai,
})
@@ -74,7 +74,7 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
message: "Do you have access to OpenCode Zen (opencode/ models)?",
options: [
{ value: "no", label: "No", hint: "Will use other configured providers" },
{ value: "yes", label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.2, etc." },
{ value: "yes", label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.4, etc." },
],
initialValue: initial.opencodeZen,
})
+1 -23
View File
@@ -2,9 +2,7 @@ import * as p from "@clack/prompts"
import color from "picocolors"
import type { InstallArgs } from "./types"
import {
addAuthPlugins,
addPluginToOpenCodeConfig,
addProviderConfig,
detectCurrentConfig,
getOpenCodeVersion,
isOpenCodeInstalled,
@@ -54,26 +52,6 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
}
spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
if (config.hasGemini) {
spinner.start("Adding auth plugins (fetching latest versions)")
const authResult = await addAuthPlugins(config)
if (!authResult.success) {
spinner.stop(`Failed to add auth plugins: ${authResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
spinner.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`)
spinner.start("Adding provider configurations")
const providerResult = addProviderConfig(config)
if (!providerResult.success) {
spinner.stop(`Failed to add provider config: ${providerResult.error}`)
p.outro(color.red("Installation failed."))
return 1
}
spinner.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`)
}
spinner.start("Writing oh-my-opencode configuration")
const omoResult = writeOmoConfig(config)
if (!omoResult.success) {
@@ -123,7 +101,7 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
const providers: string[] = []
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
if (config.hasGemini) providers.push(`Google ${color.gray("→ Gemini")}`)
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
console.log()
+1 -1
View File
@@ -1,6 +1,6 @@
# src/config/ — Zod v4 Schema System
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+8 -8
View File
@@ -266,7 +266,7 @@ describe("AgentOverrideConfigSchema", () => {
describe("backward compatibility", () => {
test("still accepts model field (deprecated)", () => {
// given
const config = { model: "openai/gpt-5.2" }
const config = { model: "openai/gpt-5.4" }
// when
const result = AgentOverrideConfigSchema.safeParse(config)
@@ -274,14 +274,14 @@ describe("AgentOverrideConfigSchema", () => {
// then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.model).toBe("openai/gpt-5.2")
expect(result.data.model).toBe("openai/gpt-5.4")
}
})
test("accepts both model and category (deprecated usage)", () => {
// given - category should take precedence at runtime, but both should validate
const config = {
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
category: "ultrabrain"
}
@@ -291,7 +291,7 @@ describe("AgentOverrideConfigSchema", () => {
// then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.model).toBe("openai/gpt-5.2")
expect(result.data.model).toBe("openai/gpt-5.4")
expect(result.data.category).toBe("ultrabrain")
}
})
@@ -343,7 +343,7 @@ describe("AgentOverrideConfigSchema", () => {
describe("CategoryConfigSchema", () => {
test("accepts variant as optional string", () => {
// given
const config = { model: "openai/gpt-5.2", variant: "xhigh" }
const config = { model: "openai/gpt-5.4", variant: "xhigh" }
// when
const result = CategoryConfigSchema.safeParse(config)
@@ -371,7 +371,7 @@ describe("CategoryConfigSchema", () => {
test("rejects non-string variant", () => {
// given
const config = { model: "openai/gpt-5.2", variant: 123 }
const config = { model: "openai/gpt-5.4", variant: 123 }
// when
const result = CategoryConfigSchema.safeParse(config)
@@ -413,7 +413,7 @@ describe("Sisyphus-Junior agent override", () => {
const config = {
agents: {
"sisyphus-junior": {
model: "openai/gpt-5.2",
model: "openai/gpt-5.4",
temperature: 0.2,
},
},
@@ -426,7 +426,7 @@ describe("Sisyphus-Junior agent override", () => {
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.["sisyphus-junior"]).toBeDefined()
expect(result.data.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.2")
expect(result.data.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.4")
expect(result.data.agents?.["sisyphus-junior"]?.temperature).toBe(0.2)
}
})
+21 -21
View File
@@ -1,6 +1,6 @@
# src/features/ — 19 Feature Modules
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -10,29 +10,29 @@ Standalone feature modules wired into plugin/ layer. Each is self-contained with
| Module | Files | Complexity | Purpose |
|--------|-------|------------|---------|
| **background-agent** | 49 | HIGH | Task lifecycle, concurrency (5/model), polling, spawner pattern |
| **tmux-subagent** | 27 | HIGH | Tmux pane management, grid planning, session orchestration |
| **opencode-skill-loader** | 25 | HIGH | YAML frontmatter skill loading from 4 scopes |
| **mcp-oauth** | 10 | HIGH | OAuth 2.0 + PKCE + DCR (RFC 7591) for MCP servers |
| **builtin-skills** | 10 | LOW | 6 skills: git-master, playwright, playwright-cli, agent-browser, dev-browser, frontend-ui-ux |
| **skill-mcp-manager** | 10 | MEDIUM | MCP client lifecycle per session (stdio + HTTP) |
| **opencode-skill-loader** | 33 | HIGH | YAML frontmatter skill loading from 4 scopes |
| **background-agent** | 31 | HIGH | Task lifecycle, concurrency (5/model), polling, spawner pattern |
| **tmux-subagent** | 30 | HIGH | Tmux pane management, grid planning, session orchestration |
| **mcp-oauth** | 18 | HIGH | OAuth 2.0 + PKCE + DCR (RFC 7591) for MCP servers |
| **builtin-skills** | 17 | LOW | 6 skills: git-master, playwright, playwright-cli, agent-browser, dev-browser, frontend-ui-ux |
| **skill-mcp-manager** | 12 | MEDIUM | MCP client lifecycle per session (stdio + HTTP) |
| **claude-code-plugin-loader** | 10 | MEDIUM | Unified plugin discovery from .opencode/plugins/ |
| **builtin-commands** | 9 | LOW | Command templates: refactor, init-deep, handoff, etc. |
| **claude-code-mcp-loader** | 5 | MEDIUM | .mcp.json loading with ${VAR} env expansion |
| **context-injector** | 4 | MEDIUM | AGENTS.md/README.md injection into context |
| **boulder-state** | 4 | LOW | Persistent state for multi-step operations |
| **hook-message-injector** | 4 | MEDIUM | System message injection for hooks |
| **claude-tasks** | 4 | MEDIUM | Task schema + file storage + OpenCode todo sync |
| **task-toast-manager** | 3 | MEDIUM | Task progress notifications |
| **claude-code-agent-loader** | 3 | LOW | Load agents from .opencode/agents/ |
| **claude-code-command-loader** | 3 | LOW | Load commands from .opencode/commands/ |
| **claude-code-session-state** | 2 | LOW | Subagent session state tracking |
| **builtin-commands** | 11 | LOW | Command templates: refactor, init-deep, handoff, etc. |
| **claude-tasks** | 7 | MEDIUM | Task schema + file storage + OpenCode todo sync |
| **claude-code-mcp-loader** | 6 | MEDIUM | .mcp.json loading with ${VAR} env expansion |
| **context-injector** | 6 | MEDIUM | AGENTS.md/README.md injection into context |
| **run-continuation-state** | 5 | LOW | Persistent state for `run` command continuation across sessions |
| **tool-metadata-store** | 2 | LOW | Tool execution metadata cache |
| **hook-message-injector** | 5 | MEDIUM | System message injection for hooks |
| **boulder-state** | 5 | LOW | Persistent state for multi-step operations |
| **task-toast-manager** | 4 | MEDIUM | Task progress notifications |
| **tool-metadata-store** | 3 | LOW | Tool execution metadata cache |
| **claude-code-session-state** | 3 | LOW | Subagent session state tracking |
| **claude-code-command-loader** | 3 | LOW | Load commands from .opencode/commands/ |
| **claude-code-agent-loader** | 3 | LOW | Load agents from .opencode/agents/ |
## KEY MODULES
### background-agent (49 files, ~10k LOC)
### background-agent (31 files, ~10k LOC)
Core orchestration engine. `BackgroundManager` manages task lifecycle:
- States: pending → running → completed/error/cancelled/interrupt
@@ -40,7 +40,7 @@ Core orchestration engine. `BackgroundManager` manages task lifecycle:
- Polling: 3s interval, completion via idle events + stability detection (10s unchanged)
- spawner/: 8 focused files composing via `SpawnerContext` interface
### opencode-skill-loader (25 files, ~3.2k LOC)
### opencode-skill-loader (33 files, ~3.2k LOC)
4-scope skill discovery (project > opencode > user > global):
- YAML frontmatter parsing from SKILL.md files
@@ -48,7 +48,7 @@ Core orchestration engine. `BackgroundManager` manages task lifecycle:
- Template resolution with variable substitution
- Provider gating for model-specific skills
### tmux-subagent (27 files, ~3.6k LOC)
### tmux-subagent (30 files, ~3.6k LOC)
State-first tmux integration:
- `TmuxSessionManager`: pane lifecycle, grid planning
+1 -1
View File
@@ -1,6 +1,6 @@
# src/features/background-agent/ — Core Orchestration Engine
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -2,6 +2,7 @@ import { describe, test, expect } from "bun:test"
import { tmpdir } from "node:os"
import type { PluginInput } from "@opencode-ai/plugin"
import { BackgroundManager } from "./manager"
import type { BackgroundTask } from "./types"
function createManagerWithStatus(statusImpl: () => Promise<{ data: Record<string, { type: string }> }>): BackgroundManager {
const client = {
@@ -51,3 +52,105 @@ describe("BackgroundManager polling overlap", () => {
expect(statusCallCount).toBe(1)
})
})
function createRunningTask(sessionID: string): BackgroundTask {
return {
id: `bg_test_${sessionID}`,
sessionID,
parentSessionID: "parent-session",
parentMessageID: "parent-msg",
description: "test task",
prompt: "test",
agent: "explore",
status: "running",
startedAt: new Date(),
progress: { toolCalls: 0, lastUpdate: new Date() },
}
}
function injectTask(manager: BackgroundManager, task: BackgroundTask): void {
const tasks = (manager as unknown as { tasks: Map<string, BackgroundTask> }).tasks
tasks.set(task.id, task)
}
function createManagerWithClient(clientOverrides: Record<string, unknown> = {}): BackgroundManager {
const client = {
session: {
status: async () => ({ data: {} }),
prompt: async () => ({}),
promptAsync: async () => ({}),
abort: async () => ({}),
todo: async () => ({ data: [] }),
messages: async () => ({
data: [{
info: { role: "assistant", finish: "end_turn", id: "msg-2" },
parts: [{ type: "text", text: "done" }],
}, {
info: { role: "user", id: "msg-1" },
parts: [{ type: "text", text: "go" }],
}],
}),
...clientOverrides,
},
}
return new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
}
describe("BackgroundManager pollRunningTasks", () => {
describe("#given a running task whose session is no longer in status response", () => {
test("#when pollRunningTasks runs #then completes the task instead of leaving it running", async () => {
//#given
const manager = createManagerWithClient()
const task = createRunningTask("ses-gone")
injectTask(manager, task)
//#when
const poll = (manager as unknown as { pollRunningTasks: () => Promise<void> }).pollRunningTasks
await poll.call(manager)
manager.shutdown()
//#then
expect(task.status).toBe("completed")
expect(task.completedAt).toBeDefined()
})
})
describe("#given a running task whose session status is idle", () => {
test("#when pollRunningTasks runs #then completes the task", async () => {
//#given
const manager = createManagerWithClient({
status: async () => ({ data: { "ses-idle": { type: "idle" } } }),
})
const task = createRunningTask("ses-idle")
injectTask(manager, task)
//#when
const poll = (manager as unknown as { pollRunningTasks: () => Promise<void> }).pollRunningTasks
await poll.call(manager)
manager.shutdown()
//#then
expect(task.status).toBe("completed")
})
})
describe("#given a running task whose session status is busy", () => {
test("#when pollRunningTasks runs #then keeps the task running", async () => {
//#given
const manager = createManagerWithClient({
status: async () => ({ data: { "ses-busy": { type: "busy" } } }),
})
const task = createRunningTask("ses-busy")
injectTask(manager, task)
//#when
const poll = (manager as unknown as { pollRunningTasks: () => Promise<void> }).pollRunningTasks
await poll.call(manager)
manager.shutdown()
//#then
expect(task.status).toBe("running")
})
})
})
@@ -2078,7 +2078,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
description: "Task 2",
prompt: "Do something else",
agent: "test-agent",
model: { providerID: "openai", modelID: "gpt-5.2" },
model: { providerID: "openai", modelID: "gpt-5.4" },
parentSessionID: "parent-session",
parentMessageID: "parent-message",
}
+35 -32
View File
@@ -1501,32 +1501,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
try {
const sessionStatus = allStatuses[sessionID]
if (sessionStatus?.type === "idle") {
// Edge guard: Validate session has actual output before completing
const hasValidOutput = await this.validateSessionHasOutput(sessionID)
if (!hasValidOutput) {
log("[background-agent] Polling idle but no valid output yet, waiting:", task.id)
continue
}
// Re-check status after async operation
if (task.status !== "running") continue
const hasIncompleteTodos = await this.checkSessionTodos(sessionID)
if (hasIncompleteTodos) {
log("[background-agent] Task has incomplete todos via polling, waiting:", task.id)
continue
}
await this.tryCompleteTask(task, "polling (idle status)")
continue
}
// Session is still actively running (not idle).
// Progress is already tracked via handleEvent(message.part.updated),
// so we skip the expensive session.messages() fetch here.
// Completion will be detected when session transitions to idle.
// Handle retry before checking running state
if (sessionStatus?.type === "retry") {
const retryMessage = typeof (sessionStatus as { message?: string }).message === "string"
? (sessionStatus as { message?: string }).message
@@ -1537,12 +1512,40 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
}
log("[background-agent] Session still running, relying on event-based progress:", {
taskId: task.id,
sessionID,
sessionStatus: sessionStatus?.type ?? "not_in_status",
toolCalls: task.progress?.toolCalls ?? 0,
})
// Match sync-session-poller pattern: only skip completion check when
// status EXISTS and is not idle (i.e., session is actively running).
// When sessionStatus is undefined, the session has completed and dropped
// from the status response — fall through to completion detection.
if (sessionStatus && sessionStatus.type !== "idle") {
log("[background-agent] Session still running, relying on event-based progress:", {
taskId: task.id,
sessionID,
sessionStatus: sessionStatus.type,
toolCalls: task.progress?.toolCalls ?? 0,
})
continue
}
// Session is idle or no longer in status response (completed/disappeared)
const completionSource = sessionStatus?.type === "idle"
? "polling (idle status)"
: "polling (session gone from status)"
const hasValidOutput = await this.validateSessionHasOutput(sessionID)
if (!hasValidOutput) {
log("[background-agent] Polling idle/gone but no valid output yet, waiting:", task.id)
continue
}
// Re-check status after async operation
if (task.status !== "running") continue
const hasIncompleteTodos = await this.checkSessionTodos(sessionID)
if (hasIncompleteTodos) {
log("[background-agent] Task has incomplete todos via polling, waiting:", task.id)
continue
}
await this.tryCompleteTask(task, completionSource)
} catch (error) {
log("[background-agent] Poll error for task:", { taskId: task.id, error })
}
+6 -6
View File
@@ -1,7 +1,7 @@
import type { CommandDefinition } from "../claude-code-command-loader"
import type { BuiltinCommandName, BuiltinCommands } from "./types"
import { INIT_DEEP_TEMPLATE } from "./templates/init-deep"
import { RALPH_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-loop"
import { RALPH_LOOP_TEMPLATE, ULW_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-loop"
import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation"
import { REFACTOR_TEMPLATE } from "./templates/refactor"
import { START_WORK_TEMPLATE } from "./templates/start-work"
@@ -31,16 +31,16 @@ $ARGUMENTS
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]',
},
"ulw-loop": {
description: "(builtin) Start ultrawork loop - continues until completion with ultrawork mode",
template: `<command-instruction>
${RALPH_LOOP_TEMPLATE}
description: "(builtin) Start ultrawork loop - continues until completion with ultrawork mode",
template: `<command-instruction>
${ULW_LOOP_TEMPLATE}
</command-instruction>
<user-task>
$ARGUMENTS
</user-task>`,
argumentHint: '"task description" [--completion-promise=TEXT] [--max-iterations=N] [--strategy=reset|continue]',
},
argumentHint: '"task description" [--completion-promise=TEXT] [--strategy=reset|continue]',
},
"cancel-ralph": {
description: "(builtin) Cancel active Ralph Loop",
template: `<command-instruction>
@@ -28,6 +28,34 @@ Parse the arguments below and begin working on the task. The format is:
Default completion promise is "DONE" and default max iterations is 100.`
export const ULW_LOOP_TEMPLATE = `You are starting an ULTRAWORK Loop - a self-referential development loop that runs until verified completion.
## How ULTRAWORK Loop Works
1. You will work on the task continuously
2. When you believe the work is complete, output: \`<promise>{{COMPLETION_PROMISE}}</promise>\`
3. That does NOT finish the loop yet. The system will require Oracle verification
4. The loop only ends after the system confirms Oracle verified the result
5. There is no iteration limit
## Rules
- Focus on finishing the task completely
- After you emit the completion promise, run Oracle verification when instructed
- Do not treat DONE as final completion until Oracle verifies it
## Exit Conditions
1. **Verified Completion**: Oracle verifies the result and the system confirms it
2. **Cancel**: User runs \`/cancel-ralph\`
## Your Task
Parse the arguments below and begin working on the task. The format is:
\`"task description" [--completion-promise=TEXT] [--strategy=reset|continue]\`
Default completion promise is "DONE".`
export const CANCEL_RALPH_TEMPLATE = `Cancel the currently active Ralph Loop.
This will:
@@ -26,29 +26,35 @@ agent-browser close # Close browser
### Navigation
```bash
agent-browser open <url> # Navigate to URL
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
agent-browser close # Close browser (aliases: quit, exit)
```
### Snapshot (page analysis)
```bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
agent-browser snapshot -c # Compact (remove empty structural elements)
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
agent-browser snapshot -i -c -d 5 # Combine options
```
The `-C` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
### Interactions (use @refs from snapshot)
```bash
agent-browser click @e1 # Click
agent-browser click @e1 # Click (--new-tab to open in new tab)
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
@@ -57,8 +63,8 @@ agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
```
@@ -73,6 +79,7 @@ agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
agent-browser get styles @e1 # Get computed styles
```
### Check state
@@ -84,12 +91,20 @@ agent-browser is checked @e1 # Check if checked
### Screenshots & PDF
```bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot # Screenshot (saves to temp dir if no path)
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
```
Annotated screenshots overlay numbered labels `[N]` on interactive elements. Each label corresponds to ref `@eN`, so refs work for both visual and text workflows:
```bash
agent-browser screenshot --annotate ./page.png
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
agent-browser click @e2 # Click the "Home" link labeled [2]
```
### Video recording
```bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
@@ -109,10 +124,12 @@ agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
```
Load states: `load`, `domcontentloaded`, `networkidle`
### Mouse control
```bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse down left # Press button (left/right/middle)
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
```
@@ -122,10 +139,18 @@ agent-browser mouse wheel 100 # Scroll wheel
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find placeholder "Search..." fill "query"
agent-browser find alt "Logo" click
agent-browser find title "Close" click
agent-browser find testid "submit-btn" click
agent-browser find first ".item" click
agent-browser find last ".item" click
agent-browser find nth 2 "a" text
```
Actions: `click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`
Options: `--name <name>` (filter role by accessible name), `--exact` (require exact text match)
### Browser settings
```bash
agent-browser set viewport 1920 1080 # Set viewport size
@@ -142,14 +167,13 @@ agent-browser set media dark # Emulate color scheme
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
agent-browser storage session # Same for sessionStorage
```
### Network
@@ -179,13 +203,59 @@ agent-browser frame main # Back to main frame
### Dialogs
```bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
agent-browser dialog dismiss # Dismiss dialog
```
### Diff (compare snapshots, screenshots, URLs)
```bash
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
```
### JavaScript
```bash
agent-browser eval "document.title" # Run JavaScript
agent-browser eval -b "base64code" # Run base64-encoded JS
agent-browser eval --stdin # Read JS from stdin
```
### Debug & Profiling
```bash
agent-browser console # View console messages
agent-browser console --clear # Clear console
agent-browser errors # View page errors
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop profile.json # Stop and save profile
```
### State management
```bash
agent-browser state save auth.json # Save auth state
agent-browser state load auth.json # Load auth state
agent-browser state list # List saved state files
agent-browser state show <file> # Show state summary
agent-browser state rename <old> <new> # Rename state file
agent-browser state clear [name] # Clear states for session
agent-browser state clear --all # Clear all saved states
agent-browser state clean --older-than <days> # Delete old states
```
### Setup
```bash
agent-browser install # Download Chromium browser
agent-browser install --with-deps # Also install system deps (Linux)
```
## Global Options
@@ -193,19 +263,60 @@ agent-browser eval "document.title" # Run JavaScript
| Option | Description |
|--------|-------------|
| `--session <name>` | Isolated browser session (`AGENT_BROWSER_SESSION` env) |
| `--session-name <name>` | Auto-save/restore session state (`AGENT_BROWSER_SESSION_NAME` env) |
| `--profile <path>` | Persistent browser profile (`AGENT_BROWSER_PROFILE` env) |
| `--state <path>` | Load storage state from JSON file (`AGENT_BROWSER_STATE` env) |
| `--headers <json>` | HTTP headers scoped to URL's origin |
| `--executable-path <path>` | Custom browser binary (`AGENT_BROWSER_EXECUTABLE_PATH` env) |
| `--extension <path>` | Load browser extension (repeatable; `AGENT_BROWSER_EXTENSIONS` env) |
| `--args <args>` | Browser launch args (`AGENT_BROWSER_ARGS` env) |
| `--user-agent <ua>` | Custom User-Agent (`AGENT_BROWSER_USER_AGENT` env) |
| `--proxy <url>` | Proxy server (`AGENT_BROWSER_PROXY` env) |
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (`AGENT_BROWSER_PROXY_BYPASS` env) |
| `--ignore-https-errors` | Ignore HTTPS certificate errors |
| `--allow-file-access` | Allow file:// URLs to access local files |
| `-p, --provider <name>` | Cloud browser provider (`AGENT_BROWSER_PROVIDER` env) |
| `--device <name>` | iOS device name (`AGENT_BROWSER_IOS_DEVICE` env) |
| `--json` | Machine-readable JSON output |
| `--headed` | Show browser window (not headless) |
| `--full, -f` | Full page screenshot |
| `--annotate` | Annotated screenshot with numbered labels (`AGENT_BROWSER_ANNOTATE` env) |
| `--headed` | Show browser window (`AGENT_BROWSER_HEADED` env) |
| `--cdp <port\|wss://url>` | Connect via Chrome DevTools Protocol |
| `--auto-connect` | Auto-discover running Chrome (`AGENT_BROWSER_AUTO_CONNECT` env) |
| `--color-scheme <scheme>` | Color scheme: dark, light, no-preference (`AGENT_BROWSER_COLOR_SCHEME` env) |
| `--download-path <path>` | Default download directory (`AGENT_BROWSER_DOWNLOAD_PATH` env) |
| `--native` | [Experimental] Use native Rust daemon (`AGENT_BROWSER_NATIVE` env) |
| `--config <path>` | Custom config file (`AGENT_BROWSER_CONFIG` env) |
| `--debug` | Debug output |
### Security options
| Option | Description |
|--------|-------------|
| `--content-boundaries` | Wrap page output in boundary markers (`AGENT_BROWSER_CONTENT_BOUNDARIES` env) |
| `--max-output <chars>` | Truncate page output to N characters (`AGENT_BROWSER_MAX_OUTPUT` env) |
| `--allowed-domains <list>` | Comma-separated allowed domain patterns (`AGENT_BROWSER_ALLOWED_DOMAINS` env) |
| `--action-policy <path>` | Path to action policy JSON file (`AGENT_BROWSER_ACTION_POLICY` env) |
| `--confirm-actions <list>` | Action categories requiring confirmation (`AGENT_BROWSER_CONFIRM_ACTIONS` env) |
## Configuration file
Create `agent-browser.json` for persistent defaults (no need to repeat flags):
**Locations (lowest to highest priority):**
1. `~/.agent-browser/config.json` — user-level defaults
2. `./agent-browser.json` — project-level overrides
3. `AGENT_BROWSER_*` environment variables
4. CLI flags override everything
```json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"native": true
}
```
## Example: Form submission
```bash
@@ -247,6 +358,13 @@ agent-browser open other-site.com
agent-browser set headers '{"X-Custom-Header": "value"}'
```
### Authentication Vault
```bash
# Store credentials locally (encrypted). The LLM never sees passwords.
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth login github
```
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
@@ -256,6 +374,13 @@ agent-browser --session test2 open site-b.com
agent-browser session list
```
### Session persistence (auto-save/restore)
```bash
agent-browser --session-name twitter open twitter.com
# Login once, state persists automatically across restarts
# State files stored in ~/.agent-browser/sessions/
```
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
```bash
@@ -263,9 +388,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
@@ -275,62 +397,54 @@ agent-browser snapshot -i --json
agent-browser get text @e1 --json
```
## Debugging
## Local files
```bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
```
## CDP Mode
```bash
agent-browser connect 9222 # Local CDP port
agent-browser --cdp 9222 snapshot # Direct CDP on each command
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser --auto-connect snapshot # Auto-discover running Chrome
```
## Cloud providers
```bash
# Browserbase
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
# Browser Use
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
# Kernel
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
```
## iOS Simulator
```bash
agent-browser device list # List available simulators
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
agent-browser -p ios snapshot -i # Same commands as desktop
agent-browser -p ios tap @e1 # Tap
agent-browser -p ios swipe up # Mobile-specific
agent-browser -p ios close # Close session
```
## Native Mode (Experimental)
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
```bash
agent-browser --native open example.com
# Or: export AGENT_BROWSER_NATIVE=1
# Or: {"native": true} in agent-browser.json
```
---
## Installation
### Step 1: Install agent-browser CLI
```bash
bun add -g agent-browser
```
### Step 2: Install Playwright browsers
**IMPORTANT**: `agent-browser install` may fail on some platforms (e.g., darwin-arm64) with "No binary found" error. In that case, install Playwright browsers directly:
```bash
# Create a temp project and install playwright
cd /tmp && bun init -y && bun add playwright
# Install Chromium browser
bun playwright install chromium
```
This downloads Chrome for Testing to `~/Library/Caches/ms-playwright/`.
### Verify installation
```bash
agent-browser open https://example.com --headed
```
If the browser opens successfully, installation is complete.
### Troubleshooting
| Error | Solution |
|-------|----------|
| `No binary found for darwin-arm64` | Run `bun playwright install chromium` in a project with playwright dependency |
| `Executable doesn't exist at .../chromium-XXXX` | Re-run `bun playwright install chromium` |
| Browser doesn't open | Ensure `--headed` flag is used for visible browser |
---
Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
Install: `bun add -g agent-browser && agent-browser install`. Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
+183 -29
View File
@@ -40,29 +40,35 @@ agent-browser close # Close browser
### Navigation
\`\`\`bash
agent-browser open <url> # Navigate to URL
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
agent-browser close # Close browser (aliases: quit, exit)
\`\`\`
### Snapshot (page analysis)
\`\`\`bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
agent-browser snapshot -c # Compact (remove empty structural elements)
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
agent-browser snapshot -i -c -d 5 # Combine options
\`\`\`
The \`-C\` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
### Interactions (use @refs from snapshot)
\`\`\`bash
agent-browser click @e1 # Click
agent-browser click @e1 # Click (--new-tab to open in new tab)
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
@@ -71,8 +77,8 @@ agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
\`\`\`
@@ -87,6 +93,7 @@ agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
agent-browser get styles @e1 # Get computed styles
\`\`\`
### Check state
@@ -98,12 +105,20 @@ agent-browser is checked @e1 # Check if checked
### Screenshots & PDF
\`\`\`bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot # Screenshot (saves to temp dir if no path)
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
\`\`\`
Annotated screenshots overlay numbered labels \`[N]\` on interactive elements. Each label corresponds to ref \`@eN\`, so refs work for both visual and text workflows:
\`\`\`bash
agent-browser screenshot --annotate ./page.png
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
agent-browser click @e2 # Click the "Home" link labeled [2]
\`\`\`
### Video recording
\`\`\`bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
@@ -123,10 +138,12 @@ agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
\`\`\`
Load states: \`load\`, \`domcontentloaded\`, \`networkidle\`
### Mouse control
\`\`\`bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse down left # Press button (left/right/middle)
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
\`\`\`
@@ -136,10 +153,18 @@ agent-browser mouse wheel 100 # Scroll wheel
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find placeholder "Search..." fill "query"
agent-browser find alt "Logo" click
agent-browser find title "Close" click
agent-browser find testid "submit-btn" click
agent-browser find first ".item" click
agent-browser find last ".item" click
agent-browser find nth 2 "a" text
\`\`\`
Actions: \`click\`, \`fill\`, \`type\`, \`hover\`, \`focus\`, \`check\`, \`uncheck\`, \`text\`
Options: \`--name <name>\` (filter role by accessible name), \`--exact\` (require exact text match)
### Browser settings
\`\`\`bash
agent-browser set viewport 1920 1080 # Set viewport size
@@ -156,14 +181,13 @@ agent-browser set media dark # Emulate color scheme
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
agent-browser storage session # Same for sessionStorage
\`\`\`
### Network
@@ -193,13 +217,59 @@ agent-browser frame main # Back to main frame
### Dialogs
\`\`\`bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
agent-browser dialog dismiss # Dismiss dialog
\`\`\`
### Diff (compare snapshots, screenshots, URLs)
\`\`\`bash
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
\`\`\`
### JavaScript
\`\`\`bash
agent-browser eval "document.title" # Run JavaScript
agent-browser eval -b "base64code" # Run base64-encoded JS
agent-browser eval --stdin # Read JS from stdin
\`\`\`
### Debug & Profiling
\`\`\`bash
agent-browser console # View console messages
agent-browser console --clear # Clear console
agent-browser errors # View page errors
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop profile.json # Stop and save profile
\`\`\`
### State management
\`\`\`bash
agent-browser state save auth.json # Save auth state
agent-browser state load auth.json # Load auth state
agent-browser state list # List saved state files
agent-browser state show <file> # Show state summary
agent-browser state rename <old> <new> # Rename state file
agent-browser state clear [name] # Clear states for session
agent-browser state clear --all # Clear all saved states
agent-browser state clean --older-than <days> # Delete old states
\`\`\`
### Setup
\`\`\`bash
agent-browser install # Download Chromium browser
agent-browser install --with-deps # Also install system deps (Linux)
\`\`\`
## Global Options
@@ -207,19 +277,60 @@ agent-browser eval "document.title" # Run JavaScript
| Option | Description |
|--------|-------------|
| \`--session <name>\` | Isolated browser session (\`AGENT_BROWSER_SESSION\` env) |
| \`--session-name <name>\` | Auto-save/restore session state (\`AGENT_BROWSER_SESSION_NAME\` env) |
| \`--profile <path>\` | Persistent browser profile (\`AGENT_BROWSER_PROFILE\` env) |
| \`--state <path>\` | Load storage state from JSON file (\`AGENT_BROWSER_STATE\` env) |
| \`--headers <json>\` | HTTP headers scoped to URL's origin |
| \`--executable-path <path>\` | Custom browser binary (\`AGENT_BROWSER_EXECUTABLE_PATH\` env) |
| \`--extension <path>\` | Load browser extension (repeatable; \`AGENT_BROWSER_EXTENSIONS\` env) |
| \`--args <args>\` | Browser launch args (\`AGENT_BROWSER_ARGS\` env) |
| \`--user-agent <ua>\` | Custom User-Agent (\`AGENT_BROWSER_USER_AGENT\` env) |
| \`--proxy <url>\` | Proxy server (\`AGENT_BROWSER_PROXY\` env) |
| \`--proxy-bypass <hosts>\` | Hosts to bypass proxy (\`AGENT_BROWSER_PROXY_BYPASS\` env) |
| \`--ignore-https-errors\` | Ignore HTTPS certificate errors |
| \`--allow-file-access\` | Allow file:// URLs to access local files |
| \`-p, --provider <name>\` | Cloud browser provider (\`AGENT_BROWSER_PROVIDER\` env) |
| \`--device <name>\` | iOS device name (\`AGENT_BROWSER_IOS_DEVICE\` env) |
| \`--json\` | Machine-readable JSON output |
| \`--headed\` | Show browser window (not headless) |
| \`--full, -f\` | Full page screenshot |
| \`--annotate\` | Annotated screenshot with numbered labels (\`AGENT_BROWSER_ANNOTATE\` env) |
| \`--headed\` | Show browser window (\`AGENT_BROWSER_HEADED\` env) |
| \`--cdp <port\\|wss://url>\` | Connect via Chrome DevTools Protocol |
| \`--auto-connect\` | Auto-discover running Chrome (\`AGENT_BROWSER_AUTO_CONNECT\` env) |
| \`--color-scheme <scheme>\` | Color scheme: dark, light, no-preference (\`AGENT_BROWSER_COLOR_SCHEME\` env) |
| \`--download-path <path>\` | Default download directory (\`AGENT_BROWSER_DOWNLOAD_PATH\` env) |
| \`--native\` | [Experimental] Use native Rust daemon (\`AGENT_BROWSER_NATIVE\` env) |
| \`--config <path>\` | Custom config file (\`AGENT_BROWSER_CONFIG\` env) |
| \`--debug\` | Debug output |
### Security options
| Option | Description |
|--------|-------------|
| \`--content-boundaries\` | Wrap page output in boundary markers (\`AGENT_BROWSER_CONTENT_BOUNDARIES\` env) |
| \`--max-output <chars>\` | Truncate page output to N characters (\`AGENT_BROWSER_MAX_OUTPUT\` env) |
| \`--allowed-domains <list>\` | Comma-separated allowed domain patterns (\`AGENT_BROWSER_ALLOWED_DOMAINS\` env) |
| \`--action-policy <path>\` | Path to action policy JSON file (\`AGENT_BROWSER_ACTION_POLICY\` env) |
| \`--confirm-actions <list>\` | Action categories requiring confirmation (\`AGENT_BROWSER_CONFIRM_ACTIONS\` env) |
## Configuration file
Create \`agent-browser.json\` for persistent defaults (no need to repeat flags):
**Locations (lowest to highest priority):**
1. \`~/.agent-browser/config.json\` — user-level defaults
2. \`./agent-browser.json\` — project-level overrides
3. \`AGENT_BROWSER_*\` environment variables
4. CLI flags override everything
\`\`\`json
{
"headed": true,
"proxy": "http://localhost:8080",
"profile": "./browser-data",
"native": true
}
\`\`\`
## Example: Form submission
\`\`\`bash
@@ -261,6 +372,13 @@ agent-browser open other-site.com
agent-browser set headers '{"X-Custom-Header": "value"}'
\`\`\`
### Authentication Vault
\`\`\`bash
# Store credentials locally (encrypted). The LLM never sees passwords.
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth login github
\`\`\`
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
@@ -270,6 +388,13 @@ agent-browser --session test2 open site-b.com
agent-browser session list
\`\`\`
### Session persistence (auto-save/restore)
\`\`\`bash
agent-browser --session-name twitter open twitter.com
# Login once, state persists automatically across restarts
# State files stored in ~/.agent-browser/sessions/
\`\`\`
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
\`\`\`bash
@@ -277,9 +402,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
\`\`\`
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
@@ -289,21 +411,53 @@ agent-browser snapshot -i --json
agent-browser get text @e1 --json
\`\`\`
## Debugging
## Local files
\`\`\`bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --allow-file-access open file:///path/to/document.pdf
agent-browser --allow-file-access open file:///path/to/page.html
\`\`\`
## CDP Mode
\`\`\`bash
agent-browser connect 9222 # Local CDP port
agent-browser --cdp 9222 snapshot # Direct CDP on each command
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser --auto-connect snapshot # Auto-discover running Chrome
\`\`\`
## Cloud providers
\`\`\`bash
# Browserbase
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
# Browser Use
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
# Kernel
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
\`\`\`
## iOS Simulator
\`\`\`bash
agent-browser device list # List available simulators
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
agent-browser -p ios snapshot -i # Same commands as desktop
agent-browser -p ios tap @e1 # Tap
agent-browser -p ios swipe up # Mobile-specific
agent-browser -p ios close # Close session
\`\`\`
## Native Mode (Experimental)
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
\`\`\`bash
agent-browser --native open example.com
# Or: export AGENT_BROWSER_NATIVE=1
# Or: {"native": true} in agent-browser.json
\`\`\`
---
+1 -1
View File
@@ -1,6 +1,6 @@
# src/features/claude-tasks/ — Task Schema + Storage
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -1,6 +1,6 @@
# src/features/mcp-oauth/ — OAuth 2.0 + PKCE + DCR for MCP Servers
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -1,6 +1,6 @@
# src/features/opencode-skill-loader/ — 4-Scope Skill Discovery
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -1,6 +1,6 @@
# src/features/tmux-subagent/ — Tmux Pane Management
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -1,6 +1,6 @@
# src/hooks/ — 46 Lifecycle Hooks
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -1,6 +1,6 @@
# src/hooks/anthropic-context-window-limit-recovery/ — Multi-Strategy Context Recovery
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -162,7 +162,7 @@ describe("createAnthropicEffortHook", () => {
const hook = createAnthropicEffortHook()
const { input, output } = createMockParams({
providerID: "openai",
modelID: "gpt-5.2",
modelID: "gpt-5.4",
})
//#when chat.params hook is called
+1 -1
View File
@@ -1,6 +1,6 @@
# src/hooks/atlas/ — Master Boulder Orchestrator
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -0,0 +1,108 @@
declare const require: (name: string) => any
const { afterEach, beforeEach, describe, expect, mock, test } = require("bun:test")
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { randomUUID } from "node:crypto"
import { clearBoulderState, writeBoulderState } from "../../features/boulder-state"
import { _resetForTesting } from "../../features/claude-code-session-state"
import type { BoulderState } from "../../features/boulder-state"
const TEST_STORAGE_ROOT = join(tmpdir(), `atlas-compaction-storage-${randomUUID()}`)
const TEST_MESSAGE_STORAGE = join(TEST_STORAGE_ROOT, "message")
const TEST_PART_STORAGE = join(TEST_STORAGE_ROOT, "part")
mock.module("../../features/hook-message-injector/constants", () => ({
OPENCODE_STORAGE: TEST_STORAGE_ROOT,
MESSAGE_STORAGE: TEST_MESSAGE_STORAGE,
PART_STORAGE: TEST_PART_STORAGE,
}))
mock.module("../../shared/opencode-message-dir", () => ({
getMessageDir: (sessionID: string) => {
const directory = join(TEST_MESSAGE_STORAGE, sessionID)
return existsSync(directory) ? directory : null
},
}))
mock.module("../../shared/opencode-storage-detection", () => ({
isSqliteBackend: () => false,
}))
const { createAtlasHook } = await import("./index")
describe("atlas hook compaction agent filtering", () => {
let testDirectory: string
function createMockPluginInput() {
const promptMock = mock(() => Promise.resolve())
return {
directory: testDirectory,
client: {
session: {
prompt: promptMock,
promptAsync: promptMock,
},
},
_promptMock: promptMock,
} as Parameters<typeof createAtlasHook>[0] & { _promptMock: ReturnType<typeof mock> }
}
function writeMessage(sessionID: string, fileName: string, agent: string): void {
const messageDir = join(TEST_MESSAGE_STORAGE, sessionID)
mkdirSync(messageDir, { recursive: true })
writeFileSync(
join(messageDir, fileName),
JSON.stringify({
agent,
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
}),
)
}
beforeEach(() => {
testDirectory = join(tmpdir(), `atlas-compaction-test-${randomUUID()}`)
mkdirSync(testDirectory, { recursive: true })
clearBoulderState(testDirectory)
_resetForTesting()
})
afterEach(() => {
clearBoulderState(testDirectory)
rmSync(testDirectory, { recursive: true, force: true })
_resetForTesting()
})
test("should inject continuation when the latest message is compaction but the previous agent matches atlas", async () => {
// given
const sessionID = "main-session-after-compaction"
const planPath = join(testDirectory, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [sessionID],
plan_name: "test-plan",
agent: "atlas",
}
writeBoulderState(testDirectory, state)
writeMessage(sessionID, "msg_001.json", "atlas")
writeMessage(sessionID, "msg_002.json", "compaction")
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when
await hook.handler({
event: {
type: "session.idle",
properties: { sessionID },
},
})
// then
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
})
})
+50
View File
@@ -11,6 +11,7 @@ import type { AtlasHookOptions, SessionState } from "./types"
const CONTINUATION_COOLDOWN_MS = 5000
const FAILURE_BACKOFF_MS = 5 * 60 * 1000
const RETRY_DELAY_MS = CONTINUATION_COOLDOWN_MS + 1000
export function createAtlasEventHandler(input: {
ctx: PluginInput
@@ -123,9 +124,50 @@ export function createAtlasEventHandler(input: {
}
if (state.lastContinuationInjectedAt && now - state.lastContinuationInjectedAt < CONTINUATION_COOLDOWN_MS) {
if (!state.pendingRetryTimer) {
state.pendingRetryTimer = setTimeout(async () => {
state.pendingRetryTimer = undefined
if (state.promptFailureCount >= 2) return
const currentBoulder = readBoulderState(ctx.directory)
if (!currentBoulder) return
if (!currentBoulder.session_ids?.includes(sessionID)) return
const currentProgress = getPlanProgress(currentBoulder.active_plan)
if (currentProgress.isComplete) return
if (options?.isContinuationStopped?.(sessionID)) return
const hasBgTasks = backgroundManager
? backgroundManager.getTasksByParentSession(sessionID).some((t: { status: string }) => t.status === "running")
: false
if (hasBgTasks) return
state.lastContinuationInjectedAt = Date.now()
const currentRemaining = currentProgress.total - currentProgress.completed
try {
await injectBoulderContinuation({
ctx,
sessionID,
planName: currentBoulder.plan_name,
remaining: currentRemaining,
total: currentProgress.total,
agent: currentBoulder.agent,
worktreePath: currentBoulder.worktree_path,
backgroundManager,
sessionState: state,
})
} catch (err) {
log(`[${HOOK_NAME}] Delayed retry failed`, { sessionID, error: err })
state.promptFailureCount++
}
}, RETRY_DELAY_MS)
}
log(`[${HOOK_NAME}] Skipped: continuation cooldown active`, {
sessionID,
cooldownRemaining: CONTINUATION_COOLDOWN_MS - (now - state.lastContinuationInjectedAt),
pendingRetry: !!state.pendingRetryTimer,
})
return
}
@@ -191,6 +233,10 @@ export function createAtlasEventHandler(input: {
if (event.type === "session.deleted") {
const sessionInfo = props?.info as { id?: string } | undefined
if (sessionInfo?.id) {
const deletedState = sessions.get(sessionInfo.id)
if (deletedState?.pendingRetryTimer) {
clearTimeout(deletedState.pendingRetryTimer)
}
sessions.delete(sessionInfo.id)
log(`[${HOOK_NAME}] Session deleted: cleaned up`, { sessionID: sessionInfo.id })
}
@@ -200,6 +246,10 @@ export function createAtlasEventHandler(input: {
if (event.type === "session.compacted") {
const sessionID = (props?.sessionID ?? (props?.info as { id?: string } | undefined)?.id) as string | undefined
if (sessionID) {
const compactedState = sessions.get(sessionID)
if (compactedState?.pendingRetryTimer) {
clearTimeout(compactedState.pendingRetryTimer)
}
sessions.delete(sessionID)
log(`[${HOOK_NAME}] Session compacted: cleaned up`, { sessionID })
}
+324
View File
@@ -409,6 +409,123 @@ describe("atlas hook", () => {
cleanupMessageStorage(sessionID)
})
describe("completion gate output ordering", () => {
const COMPLETION_GATE_SESSION = "completion-gate-order-test"
beforeEach(() => {
setupMessageStorage(COMPLETION_GATE_SESSION, "atlas")
})
afterEach(() => {
cleanupMessageStorage(COMPLETION_GATE_SESSION)
})
test("should include completion gate before Subagent Response in transformed boulder output", async () => {
// given - Atlas caller with boulder state
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const hook = createAtlasHook(createMockPluginInput())
const output = {
title: "Sisyphus Task",
output: "Task completed successfully",
metadata: {},
}
// when
await hook["tool.execute.after"](
{ tool: "task", sessionID: COMPLETION_GATE_SESSION },
output
)
// then - completion gate should appear BEFORE Subagent Response
const subagentResponseIndex = output.output.indexOf("**Subagent Response:**")
const completionGateIndex = output.output.indexOf("COMPLETION GATE")
expect(completionGateIndex).toBeGreaterThanOrEqual(0)
expect(subagentResponseIndex).toBeGreaterThanOrEqual(0)
expect(completionGateIndex).toBeLessThan(subagentResponseIndex)
})
test("should include completion gate before verification phase text", async () => {
// given - Atlas caller with boulder state
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const hook = createAtlasHook(createMockPluginInput())
const output = {
title: "Sisyphus Task",
output: "Task completed successfully",
metadata: {},
}
// when
await hook["tool.execute.after"](
{ tool: "task", sessionID: COMPLETION_GATE_SESSION },
output
)
// then - completion gate should appear BEFORE verification phase text
const completionGateIndex = output.output.indexOf("COMPLETION GATE")
const lyingIndex = output.output.indexOf("LYING")
const phase1Index = output.output.indexOf("PHASE 1")
expect(completionGateIndex).toBeGreaterThanOrEqual(0)
expect(lyingIndex).toBeGreaterThanOrEqual(0)
expect(completionGateIndex).toBeLessThan(lyingIndex)
if (phase1Index !== -1) {
expect(completionGateIndex).toBeLessThan(phase1Index)
}
})
test("should not contain old STEP 7 MARK COMPLETION IN PLAN FILE text", async () => {
// given - Atlas caller with boulder state
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const hook = createAtlasHook(createMockPluginInput())
const output = {
title: "Sisyphus Task",
output: "Task completed successfully",
metadata: {},
}
// when
await hook["tool.execute.after"](
{ tool: "task", sessionID: COMPLETION_GATE_SESSION },
output
)
// then - old STEP 7 MARK COMPLETION IN PLAN FILE should be absent
expect(output.output).not.toContain("STEP 7: MARK COMPLETION IN PLAN FILE")
expect(output.output).not.toContain("MARK COMPLETION IN PLAN FILE")
})
})
describe("Write/Edit tool direct work reminder", () => {
const ORCHESTRATOR_SESSION = "orchestrator-write-test"
@@ -1419,5 +1536,212 @@ describe("atlas hook", () => {
// then - should continue because start-work updated session agent to atlas
expect(mockInput._promptMock).toHaveBeenCalled()
})
describe("delayed retry timer (abort-stuck fix)", () => {
const capturedTimers = new Map<number, { callback: Function; cleared: boolean }>()
let nextFakeId = 99000
const originalSetTimeout = globalThis.setTimeout
const originalClearTimeout = globalThis.clearTimeout
beforeEach(() => {
capturedTimers.clear()
nextFakeId = 99000
globalThis.setTimeout = ((callback: Function, delay?: number, ...args: unknown[]) => {
const normalized = typeof delay === "number" ? delay : 0
if (normalized >= 5000) {
const id = nextFakeId++
capturedTimers.set(id, { callback: () => callback(...args), cleared: false })
return id as unknown as ReturnType<typeof setTimeout>
}
return originalSetTimeout(callback as Parameters<typeof originalSetTimeout>[0], delay)
}) as unknown as typeof setTimeout
globalThis.clearTimeout = ((id?: number | ReturnType<typeof setTimeout>) => {
if (typeof id === "number" && capturedTimers.has(id)) {
capturedTimers.get(id)!.cleared = true
capturedTimers.delete(id)
return
}
originalClearTimeout(id as Parameters<typeof originalClearTimeout>[0])
}) as unknown as typeof clearTimeout
})
afterEach(() => {
globalThis.setTimeout = originalSetTimeout
globalThis.clearTimeout = originalClearTimeout
})
async function firePendingTimers(): Promise<void> {
for (const [id, entry] of capturedTimers) {
if (!entry.cleared) {
capturedTimers.delete(id)
await entry.callback()
}
}
await flushMicrotasks()
}
test("should schedule delayed retry when cooldown blocks idle for incomplete boulder", async () => {
// given - boulder with incomplete plan
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when - first idle injects, second idle within cooldown schedules retry timer
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
// then - fire pending timer and verify retry
await firePendingTimers()
expect(mockInput._promptMock).toHaveBeenCalledTimes(2)
})
test("should not schedule duplicate retry timers for rapid idle events", async () => {
// given - boulder with incomplete plan
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when - first idle injects, then 3 rapid idles within cooldown
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
// then - only one retry fires despite multiple cooldown-blocked idles
await firePendingTimers()
expect(mockInput._promptMock).toHaveBeenCalledTimes(2)
})
test("should not retry if plan completes before timer fires", async () => {
// given - boulder with incomplete plan
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
// when - first idle injects, second schedules retry, then plan completes before timer fires
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
writeFileSync(planPath, "# Plan\n- [x] Task 1\n- [x] Task 2")
// then - retry sees complete plan and bails out
await firePendingTimers()
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
})
test("should cleanup pending retry timer on session.deleted", async () => {
// given - boulder with incomplete plan, schedule retry timer
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
// when - delete session before timer fires
await hook.handler({
event: { type: "session.deleted", properties: { info: { id: MAIN_SESSION_ID } } },
})
// then - timer was cleared, prompt called only once
await firePendingTimers()
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
})
test("should cleanup pending retry timer on session.compacted", async () => {
// given - boulder with incomplete plan, schedule retry timer
const planPath = join(TEST_DIR, "test-plan.md")
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
const state: BoulderState = {
active_plan: planPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: [MAIN_SESSION_ID],
plan_name: "test-plan",
}
writeBoulderState(TEST_DIR, state)
const mockInput = createMockPluginInput()
const hook = createAtlasHook(mockInput)
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
await hook.handler({
event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } },
})
// when - compact session before timer fires
await hook.handler({
event: { type: "session.compacted", properties: { sessionID: MAIN_SESSION_ID } },
})
// then - timer was cleared, prompt called only once
await firePendingTimers()
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
})
})
})
})
@@ -0,0 +1,46 @@
const { describe, expect, mock, test } = require("bun:test")
mock.module("../../shared", () => ({
getMessageDir: () => null,
isSqliteBackend: () => true,
normalizeSDKResponse: <TData>(response: { data?: TData }, fallback: TData): TData => response.data ?? fallback,
}))
const { getLastAgentFromSession } = await import("./session-last-agent")
function createMockClient(messages: Array<{ info?: { agent?: string } }>) {
return {
session: {
messages: async () => ({ data: messages }),
},
}
}
describe("getLastAgentFromSession sqlite branch", () => {
test("should skip compaction and return the previous real agent from sqlite messages", async () => {
// given
const client = createMockClient([
{ info: { agent: "atlas" } },
{ info: { agent: "compaction" } },
])
// when
const result = await getLastAgentFromSession("ses_sqlite_compaction", client)
// then
expect(result).toBe("atlas")
})
test("should return null when sqlite history contains only compaction", async () => {
// given
const client = createMockClient([{ info: { agent: "compaction" } }])
// when
const result = await getLastAgentFromSession("ses_sqlite_only_compaction", client)
// then
expect(result).toBeNull()
})
})
export {}
+55 -14
View File
@@ -1,24 +1,65 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { readFileSync, readdirSync } from "node:fs"
import { join } from "node:path"
import { findNearestMessageWithFields } from "../../features/hook-message-injector"
import { findNearestMessageWithFieldsFromSDK } from "../../features/hook-message-injector"
import { getMessageDir, isSqliteBackend } from "../../shared"
import { getMessageDir, isSqliteBackend, normalizeSDKResponse } from "../../shared"
type OpencodeClient = PluginInput["client"]
type SessionMessagesClient = {
session: {
messages: (input: { path: { id: string } }) => Promise<unknown>
}
}
function isCompactionAgent(agent: unknown): boolean {
return typeof agent === "string" && agent.toLowerCase() === "compaction"
}
function getLastAgentFromMessageDir(messageDir: string): string | null {
try {
const files = readdirSync(messageDir)
.filter((fileName) => fileName.endsWith(".json"))
.sort()
.reverse()
for (const fileName of files) {
try {
const content = readFileSync(join(messageDir, fileName), "utf-8")
const parsed = JSON.parse(content) as { agent?: unknown }
if (typeof parsed.agent === "string" && !isCompactionAgent(parsed.agent)) {
return parsed.agent.toLowerCase()
}
} catch {
continue
}
}
} catch {
return null
}
return null
}
export async function getLastAgentFromSession(
sessionID: string,
client?: OpencodeClient
client?: SessionMessagesClient
): Promise<string | null> {
let nearest = null
if (isSqliteBackend() && client) {
nearest = await findNearestMessageWithFieldsFromSDK(client, sessionID)
} else {
const messageDir = getMessageDir(sessionID)
if (!messageDir) return null
nearest = findNearestMessageWithFields(messageDir)
const response = await client.session.messages({ path: { id: sessionID } })
const messages = normalizeSDKResponse(response, [] as Array<{ info?: { agent?: string } }>, {
preferResponseOnMissingData: true,
})
for (let i = messages.length - 1; i >= 0; i--) {
const agent = messages[i].info?.agent
if (typeof agent === "string" && !isCompactionAgent(agent)) {
return agent.toLowerCase()
}
}
return null
}
return nearest?.agent?.toLowerCase() ?? null
const messageDir = getMessageDir(sessionID)
if (!messageDir) return null
return getLastAgentFromMessageDir(messageDir)
}
@@ -0,0 +1,37 @@
import { describe, it, expect } from "bun:test"
import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates"
describe("BOULDER_CONTINUATION_PROMPT", () => {
describe("checkbox-first priority rules", () => {
it("first rule after RULES: mentions both reading the plan AND marking a still-unchecked completed task", () => {
const rulesSection = BOULDER_CONTINUATION_PROMPT.split("RULES:")[1]!
const firstRule = rulesSection.split("\n")[1]!.trim()
expect(firstRule).toContain("Read the plan")
expect(firstRule).toContain("mark")
expect(firstRule).toContain("completed")
})
it("first rule includes IMMEDIATELY keyword", () => {
const rulesSection = BOULDER_CONTINUATION_PROMPT.split("RULES:")[1]!
const firstRule = rulesSection.split("\n")[1]!.trim()
expect(firstRule).toContain("IMMEDIATELY")
})
it("checkbox-marking guidance appears BEFORE Proceed without asking for permission", () => {
const rulesSection = BOULDER_CONTINUATION_PROMPT.split("RULES:")[1]!
const checkboxMarkingMatch = rulesSection.match(/- \[x\]/i)
const proceedMatch = rulesSection.match(/Proceed without asking for permission/)
expect(checkboxMarkingMatch).not.toBeNull()
expect(proceedMatch).not.toBeNull()
const checkboxPosition = checkboxMarkingMatch!.index
const proceedPosition = proceedMatch!.index
expect(checkboxPosition).toBeLessThan(proceedPosition)
})
})
})
+1 -2
View File
@@ -33,9 +33,8 @@ export const BOULDER_CONTINUATION_PROMPT = `${createSystemDirective(SystemDirect
You have an active work plan with incomplete tasks. Continue working.
RULES:
- **FIRST**: Read the plan file NOW to check exact current progress — count remaining \`- [ ]\` tasks
- **FIRST**: Read the plan file NOW. If the last completed task is still unchecked, mark it \`- [x]\` IMMEDIATELY before anything else
- Proceed without asking for permission
- Change \`- [ ]\` to \`- [x]\` in the plan file when done
- Use the notepad at .sisyphus/notepads/{PLAN_NAME}/ to record learnings
- Do not stop until all tasks are complete
- If blocked, document the blocker and move to the next task`
+7 -3
View File
@@ -7,7 +7,7 @@ import { HOOK_NAME } from "./hook-name"
import { DIRECT_WORK_REMINDER } from "./system-reminder-templates"
import { isSisyphusPath } from "./sisyphus-path"
import { extractSessionIdFromOutput } from "./subagent-session-id"
import { buildOrchestratorReminder, buildStandaloneVerificationReminder } from "./verification-reminders"
import { buildCompletionGate, buildOrchestratorReminder, buildStandaloneVerificationReminder } from "./verification-reminders"
import { isWriteOrEditToolName } from "./write-edit-tool-policy"
import type { ToolExecuteAfterInput, ToolExecuteAfterOutput } from "./types"
@@ -76,7 +76,11 @@ export function createToolExecuteAfterHandler(input: {
// Preserve original subagent response - critical for debugging failed tasks
const originalResponse = toolOutput.output
toolOutput.output = `
toolOutput.output = `
<system-reminder>
${buildCompletionGate(boulderState.plan_name, subagentSessionId)}
</system-reminder>
## SUBAGENT WORK COMPLETED
${fileChanges}
@@ -88,7 +92,7 @@ ${fileChanges}
${originalResponse}
<system-reminder>
${buildOrchestratorReminder(boulderState.plan_name, progress, subagentSessionId, autoCommit)}
${buildOrchestratorReminder(boulderState.plan_name, progress, subagentSessionId, autoCommit, false)}
</system-reminder>`
log(`[${HOOK_NAME}] Output transformed for orchestrator mode (boulder)`, {
plan: boulderState.plan_name,
+1
View File
@@ -29,4 +29,5 @@ export interface SessionState {
lastContinuationInjectedAt?: number
promptFailureCount: number
lastFailureAt?: number
pendingRetryTimer?: ReturnType<typeof setTimeout>
}
@@ -0,0 +1,94 @@
import { describe, expect, it } from "bun:test"
import { buildOrchestratorReminder, buildCompletionGate } from "./verification-reminders"
// Test helpers for given/when/then pattern
const given = describe
const when = describe
const then = it
describe("buildCompletionGate", () => {
given("a plan name and session id", () => {
const planName = "test-plan"
const sessionId = "test-session-123"
when("buildCompletionGate is called", () => {
const gate = buildCompletionGate(planName, sessionId)
then("completion gate text is present", () => {
expect(gate).toContain("COMPLETION GATE")
})
then("gate appears before verification phase text", () => {
const gateIndex = gate.indexOf("COMPLETION GATE")
const verificationIndex = gate.indexOf("VERIFICATION_REMINDER")
expect(gateIndex).toBeLessThan(verificationIndex)
})
then("gate interpolates the plan name path", () => {
expect(gate).toContain(planName)
expect(gate).toContain(`.sisyphus/plans/${planName}.md`)
})
then("gate includes Edit instructions", () => {
expect(gate.toLowerCase()).toContain("edit")
})
then("gate includes Read instructions", () => {
expect(gate.toLowerCase()).toContain("read")
})
then("old STEP 7 MARK COMPLETION text is absent", () => {
expect(gate).not.toContain("STEP 7")
expect(gate).not.toContain("MARK COMPLETION IN PLAN FILE")
})
then("step numbering remains consecutive after removal", () => {
const stepMatches = gate.match(/STEP \d+:/g) ?? []
if (stepMatches.length > 1) {
const numbers = stepMatches.map((s: string) => parseInt(s.match(/\d+/)?.[0] ?? "0"))
for (let i = 1; i < numbers.length; i++) {
expect(numbers[i]).toBe(numbers[i - 1] + 1)
}
}
})
})
})
})
describe("buildOrchestratorReminder", () => {
given("progress with completed tasks", () => {
const planName = "my-test-plan"
const sessionId = "session-abc"
const progress = { total: 10, completed: 3 }
when("buildOrchestratorReminder is called with autoCommit true", () => {
const reminder = buildOrchestratorReminder(planName, progress, sessionId, true)
then("old STEP 7 MARK COMPLETION IN PLAN FILE text is absent", () => {
expect(reminder).not.toContain("STEP 7: MARK COMPLETION IN PLAN FILE")
})
then("completion gate appears before verification reminder", () => {
const gateIndex = reminder.indexOf("COMPLETION GATE")
const verificationIndex = reminder.indexOf("VERIFICATION_REMINDER")
expect(gateIndex).toBeGreaterThanOrEqual(0)
expect(gateIndex).toBeLessThan(verificationIndex)
})
})
when("buildOrchestratorReminder is called with autoCommit false", () => {
const reminder = buildOrchestratorReminder(planName, progress, sessionId, false)
then("old STEP 7 MARK COMPLETION IN PLAN FILE text is absent", () => {
expect(reminder).not.toContain("STEP 7: MARK COMPLETION IN PLAN FILE")
})
then("completion gate appears before verification reminder", () => {
const gateIndex = reminder.indexOf("COMPLETION GATE")
const verificationIndex = reminder.indexOf("VERIFICATION_REMINDER")
expect(gateIndex).toBeGreaterThanOrEqual(0)
expect(gateIndex).toBeLessThan(verificationIndex)
})
})
})
})
+40 -16
View File
@@ -1,7 +1,37 @@
import { VERIFICATION_REMINDER } from "./system-reminder-templates"
export function buildCompletionGate(planName: string, sessionId: string): string {
return `
**COMPLETION GATE — DO NOT PROCEED UNTIL THIS IS DONE**
Your completion will NOT be recorded until you complete ALL of the following:
1. **Edit** the plan file \`.sisyphus/plans/${planName}.md\`:
- Change \`- [ ]\` to \`- [x]\` for the completed task
- Use \`Edit\` tool to modify the checkbox
2. **Read** the plan file AGAIN:
\`\`\`
Read(".sisyphus/plans/${planName}.md")
\`\`\`
- Verify the checkbox count changed (more \`- [x]\` than before)
3. **DO NOT call \`task()\` again** until you have completed steps 1 and 2 above.
If anything fails while closing this out, resume the same session immediately:
\`\`\`typescript
task(session_id="${sessionId}", prompt="fix: checkbox not recorded correctly")
\`\`\`
**Your completion is NOT tracked until the checkbox is marked in the plan file.**
**VERIFICATION_REMINDER**`
}
function buildVerificationReminder(sessionId: string): string {
return `${VERIFICATION_REMINDER}
return `**VERIFICATION_REMINDER**
${VERIFICATION_REMINDER}
---
@@ -15,20 +45,21 @@ export function buildOrchestratorReminder(
planName: string,
progress: { total: number; completed: number },
sessionId: string,
autoCommit: boolean = true
autoCommit: boolean = true,
includeCompletionGate: boolean = true
): string {
const remaining = progress.total - progress.completed
const commitStep = autoCommit
? `
**STEP 8: COMMIT ATOMIC UNIT**
**STEP 7: COMMIT ATOMIC UNIT**
- Stage ONLY the verified changes
- Commit with clear message describing what was done
`
: ""
const nextStepNumber = autoCommit ? 9 : 8
const nextStepNumber = autoCommit ? 8 : 7
return `
---
@@ -37,7 +68,9 @@ export function buildOrchestratorReminder(
---
${buildVerificationReminder(sessionId)}
${includeCompletionGate ? `${buildCompletionGate(planName, sessionId)}
` : ""}${buildVerificationReminder(sessionId)}
**STEP 5: READ SUBAGENT NOTEPAD (LEARNINGS, ISSUES, PROBLEMS)**
@@ -64,22 +97,13 @@ Read(".sisyphus/plans/${planName}.md")
Count exactly: how many \`- [ ]\` remain? How many \`- [x]\` completed?
This is YOUR ground truth. Use it to decide what comes next.
**STEP 7: MARK COMPLETION IN PLAN FILE (IMMEDIATELY)**
RIGHT NOW - Do not delay. Verification passed → Mark IMMEDIATELY.
Update the plan file \`.sisyphus/plans/${planName}.md\`:
- Change \`- [ ]\` to \`- [x]\` for the completed task
- Use \`Edit\` tool to modify the checkbox
**DO THIS BEFORE ANYTHING ELSE. Unmarked = Untracked = Lost progress.**
${commitStep}
**STEP ${nextStepNumber}: PROCEED TO NEXT TASK**
- Read the plan file AGAIN to identify the next \`- [ ]\` task
- Start immediately - DO NOT STOP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**${remaining} tasks remain. Keep bouldering.**`
}
+11 -6
View File
@@ -1,6 +1,6 @@
import * as fs from "node:fs"
import * as path from "node:path"
import { PACKAGE_NAME, USER_CONFIG_DIR } from "./constants"
import { CACHE_DIR, PACKAGE_NAME, USER_CONFIG_DIR } from "./constants"
import { log } from "../../shared/logger"
interface BunLockfile {
@@ -48,17 +48,22 @@ function removeFromBunLock(packageName: string): boolean {
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
try {
const pkgDir = path.join(USER_CONFIG_DIR, "node_modules", packageName)
const pkgDirs = [
path.join(USER_CONFIG_DIR, "node_modules", packageName),
path.join(CACHE_DIR, "node_modules", packageName),
]
const pkgJsonPath = path.join(USER_CONFIG_DIR, "package.json")
let packageRemoved = false
let dependencyRemoved = false
let lockRemoved = false
if (fs.existsSync(pkgDir)) {
fs.rmSync(pkgDir, { recursive: true, force: true })
log(`[auto-update-checker] Package removed: ${pkgDir}`)
packageRemoved = true
for (const pkgDir of pkgDirs) {
if (fs.existsSync(pkgDir)) {
fs.rmSync(pkgDir, { recursive: true, force: true })
log(`[auto-update-checker] Package removed: ${pkgDir}`)
packageRemoved = true
}
}
if (fs.existsSync(pkgJsonPath)) {
+1 -1
View File
@@ -1,6 +1,6 @@
# src/hooks/claude-code-hooks/ — Claude Code Compatibility
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
+1 -1
View File
@@ -1,6 +1,6 @@
# src/hooks/keyword-detector/ — Mode Keyword Injection
**Generated:** 2026-03-02
**Generated:** 2026-03-06
## OVERVIEW
@@ -202,7 +202,7 @@ BEFORE writing ANY code, you MUST define:
| **Observable** | What can be measured/seen | "Console shows 'success', no errors" |
| **Pass/Fail** | Binary, no ambiguity | "Returns 200 OK" not "should work" |
Write these criteria explicitly. Share with user if scope is non-trivial.
Write these criteria explicitly. **Record them in your TODO/Task items.** Each task MUST include a "QA: [how to verify]" field. These criteria are your CONTRACT — work toward them, verify against them.
### Test Plan Template (MANDATORY for non-trivial tasks)
@@ -228,6 +228,32 @@ Write these criteria explicitly. Share with user if scope is non-trivial.
**WITHOUT evidence = NOT verified = NOT done.**
<MANUAL_QA_MANDATE>
### YOU MUST EXECUTE MANUAL QA YOURSELF. THIS IS NOT OPTIONAL.
**YOUR FAILURE MODE**: You finish coding, run lsp_diagnostics, and declare "done" without actually TESTING the feature. lsp_diagnostics catches type errors, NOT functional bugs. Your work is NOT verified until you MANUALLY test it.
**WHAT MANUAL QA MEANS — execute ALL that apply:**
| If your change... | YOU MUST... |
|---|---|
| Adds/modifies a CLI command | Run the command with Bash. Show the output. |
| Changes build output | Run the build. Verify the output files exist and are correct. |
| Modifies API behavior | Call the endpoint. Show the response. |
| Changes UI rendering | Describe what renders. Use a browser tool if available. |
| Adds a new tool/hook/feature | Test it end-to-end in a real scenario. |
| Modifies config handling | Load the config. Verify it parses correctly. |
**UNACCEPTABLE QA CLAIMS:**
- "This should work" — RUN IT.
- "The types check out" — Types don't catch logic bugs. RUN IT.
- "lsp_diagnostics is clean" — That's a TYPE check, not a FUNCTIONAL check. RUN IT.
- "Tests pass" — Tests cover known cases. Does the ACTUAL FEATURE work as the user expects? RUN IT.
**You have Bash, you have tools. There is ZERO excuse for not running manual QA.**
**Manual QA is the FINAL gate before reporting completion. Skip it and your work is INCOMPLETE.**
</MANUAL_QA_MANDATE>
### TDD Workflow (when test infrastructure exists)
1. **SPEC**: Define what "working" means (success criteria above)
@@ -236,6 +236,33 @@ task(subagent_type="plan", load_skills=[], prompt="<gathered context + user requ
If ANY answer is no → GO BACK AND DO IT. Do not claim completion.
</ANTI_OPTIMISM_CHECKPOINT>
<MANUAL_QA_MANDATE>
### YOU MUST EXECUTE MANUAL QA. THIS IS NOT OPTIONAL. DO NOT SKIP THIS.
**YOUR FAILURE MODE**: You run lsp_diagnostics, see zero errors, and declare victory. lsp_diagnostics catches TYPE errors. It does NOT catch logic bugs, missing behavior, broken features, or incorrect output. Your work is NOT verified until you MANUALLY TEST the actual feature.
**AFTER every implementation, you MUST:**
1. **Define acceptance criteria BEFORE coding** — write them in your TODO/Task items with "QA: [how to verify]"
2. **Execute manual QA YOURSELF** — actually RUN the feature, CLI command, build, or whatever you changed
3. **Report what you observed** — show actual output, not claims
| If your change... | YOU MUST... |
|---|---|
| Adds/modifies a CLI command | Run the command with Bash. Show the output. |
| Changes build output | Run the build. Verify output files exist and are correct. |
| Modifies API behavior | Call the endpoint. Show the response. |
| Adds a new tool/hook/feature | Test it end-to-end in a real scenario. |
| Modifies config handling | Load the config. Verify it parses correctly. |
**UNACCEPTABLE (WILL BE REJECTED):**
- "This should work" — DID YOU RUN IT? NO? THEN RUN IT.
- "lsp_diagnostics is clean" — That is a TYPE check, not a FUNCTIONAL check. RUN THE FEATURE.
- "Tests pass" — Tests cover known cases. Does the ACTUAL feature work? VERIFY IT MANUALLY.
**You have Bash, you have tools. There is ZERO excuse for skipping manual QA.**
</MANUAL_QA_MANDATE>
**WITHOUT evidence = NOT verified = NOT done.**
## ZERO TOLERANCE FAILURES
@@ -1,15 +1,11 @@
/**
* Ultrawork message optimized for GPT 5.2 series models.
*
* Key characteristics (from GPT 5.2 Prompting Guide):
* - "Stronger instruction adherence" - follows instructions more literally
* - "Conservative grounding bias" - prefers correctness over speed
* - "Parallelize independent reads to reduce latency" - official guidance
* Ultrawork message optimized for GPT 5.4 series models.
*
* Design principles:
* - Expert coding agent framing with approach-first mentality
* - Prose-first output (do not default to bullets)
* - Two-track parallel context gathering (Direct tools + Background agents)
* - Fire background agents, then use direct tools while waiting
* - Explicit complexity-based decision criteria
* - Deterministic tool usage and explicit decision criteria
*/
export const ULTRAWORK_GPT_MESSAGE = `<ultrawork-mode>
@@ -19,11 +15,11 @@ export const ULTRAWORK_GPT_MESSAGE = `<ultrawork-mode>
[CODE RED] Maximum precision required. Think deeply before acting.
<output_verbosity_spec>
- Default: 3-6 sentences or 5 bullets for typical answers
- Simple yes/no questions: 2 sentences
- Complex multi-file tasks: 1 short overview paragraph + 5 bullets (What, Where, Risks, Next, Open)
- Avoid long narrative paragraphs; prefer compact bullets
- Do not rephrase the user's request unless it changes semantics
- Default: 1-2 short paragraphs. Do not default to bullets.
- Simple yes/no questions: 2 sentences.
- Complex multi-file tasks: 1 overview paragraph + up to 4 high-level sections grouped by outcome, not by file.
- Use lists only when content is inherently list-shaped (distinct items, steps, options).
- Do not rephrase the user's request unless it changes semantics.
</output_verbosity_spec>
<scope_constraints>
@@ -122,6 +118,14 @@ deep_context = background_output(task_id=...)
- \`lsp_diagnostics\` on modified files
- Run tests if available
## ACCEPTANCE CRITERIA WORKFLOW
**BEFORE implementation**, define what "done" means in concrete, binary terms:
1. Write acceptance criteria as pass/fail conditions (not "should work" specific observable outcomes)
2. Record them in your TODO/Task items with a "QA: [how to verify]" field
3. Work toward those criteria, not just "finishing code"
## QUALITY STANDARDS
| Phase | Action | Required Evidence |
@@ -129,6 +133,25 @@ deep_context = background_output(task_id=...)
| Build | Run build command | Exit code 0 |
| Test | Execute test suite | All tests pass |
| Lint | Run lsp_diagnostics | Zero new errors |
| **Manual QA** | **Execute the feature yourself** | **Actual output shown** |
<MANUAL_QA_MANDATE>
### MANUAL QA IS MANDATORY. lsp_diagnostics IS NOT ENOUGH.
lsp_diagnostics catches type errors. It does NOT catch logic bugs, missing behavior, or broken features. After EVERY implementation, you MUST manually test the actual feature.
**Execute ALL that apply:**
| If your change... | YOU MUST... |
|---|---|
| Adds/modifies a CLI command | Run the command with Bash. Show the output. |
| Changes build output | Run the build. Verify output files. |
| Modifies API behavior | Call the endpoint. Show the response. |
| Adds a new tool/hook/feature | Test it end-to-end in a real scenario. |
| Modifies config handling | Load the config. Verify it parses correctly. |
**"This should work" is NOT evidence. RUN IT. Show what happened. That is evidence.**
</MANUAL_QA_MANDATE>
## COMPLETION CRITERIA
@@ -137,6 +160,7 @@ A task is complete when:
2. lsp_diagnostics shows zero errors on modified files
3. Tests pass (or pre-existing failures documented)
4. Code matches existing codebase patterns
5. **Manual QA executed actual feature tested, output observed and reported**
**Deliver exactly what was asked. No more, no less.**
@@ -144,8 +168,8 @@ A task is complete when:
---
`
`;
export function getGptUltraworkMessage(): string {
return ULTRAWORK_GPT_MESSAGE
return ULTRAWORK_GPT_MESSAGE;
}
+32 -18
View File
@@ -3,39 +3,53 @@
*
* Routing:
* 1. Planner agents (prometheus, plan) → planner.ts
* 2. GPT 5.2 models → gpt5.2.ts
* 2. GPT models → gpt.ts
* 3. Gemini models → gemini.ts
* 4. Default (Claude, etc.) → default.ts (optimized for Claude series)
*/
export { isPlannerAgent, isGptModel, isGeminiModel, getUltraworkSource } from "./source-detector"
export type { UltraworkSource } from "./source-detector"
export { ULTRAWORK_PLANNER_SECTION, getPlannerUltraworkMessage } from "./planner"
export { ULTRAWORK_GPT_MESSAGE, getGptUltraworkMessage } from "./gpt5.2"
export { ULTRAWORK_GEMINI_MESSAGE, getGeminiUltraworkMessage } from "./gemini"
export { ULTRAWORK_DEFAULT_MESSAGE, getDefaultUltraworkMessage } from "./default"
export {
isPlannerAgent,
isGptModel,
isGeminiModel,
getUltraworkSource,
} from "./source-detector";
export type { UltraworkSource } from "./source-detector";
export {
ULTRAWORK_PLANNER_SECTION,
getPlannerUltraworkMessage,
} from "./planner";
export { ULTRAWORK_GPT_MESSAGE, getGptUltraworkMessage } from "./gpt";
export { ULTRAWORK_GEMINI_MESSAGE, getGeminiUltraworkMessage } from "./gemini";
export {
ULTRAWORK_DEFAULT_MESSAGE,
getDefaultUltraworkMessage,
} from "./default";
import { getUltraworkSource } from "./source-detector"
import { getPlannerUltraworkMessage } from "./planner"
import { getGptUltraworkMessage } from "./gpt5.2"
import { getDefaultUltraworkMessage } from "./default"
import { getGeminiUltraworkMessage } from "./gemini"
import { getUltraworkSource } from "./source-detector";
import { getPlannerUltraworkMessage } from "./planner";
import { getGptUltraworkMessage } from "./gpt";
import { getDefaultUltraworkMessage } from "./default";
import { getGeminiUltraworkMessage } from "./gemini";
/**
* Gets the appropriate ultrawork message based on agent and model context.
*/
export function getUltraworkMessage(agentName?: string, modelID?: string): string {
const source = getUltraworkSource(agentName, modelID)
export function getUltraworkMessage(
agentName?: string,
modelID?: string,
): string {
const source = getUltraworkSource(agentName, modelID);
switch (source) {
case "planner":
return getPlannerUltraworkMessage()
return getPlannerUltraworkMessage();
case "gpt":
return getGptUltraworkMessage()
return getGptUltraworkMessage();
case "gemini":
return getGeminiUltraworkMessage()
return getGeminiUltraworkMessage();
case "default":
default:
return getDefaultUltraworkMessage()
return getDefaultUltraworkMessage();
}
}
@@ -3,7 +3,7 @@
*
* Routing logic:
* 1. Planner agents (prometheus, plan) → planner.ts
* 2. GPT 5.2 models → gpt5.2.ts
* 2. GPT 5.4 models → gpt5.4.ts
* 3. Gemini models → gemini.ts
* 4. Everything else (Claude, etc.) → default.ts
*/
+41 -5
View File
@@ -1,4 +1,35 @@
import { beforeEach, describe, expect, test } from "bun:test"
declare const require: (name: string) => any
const { beforeEach, describe, expect, mock, test } = require("bun:test")
const readConnectedProvidersCacheMock = mock(() => null)
const readProviderModelsCacheMock = mock(() => null)
const transformModelForProviderMock = mock((provider: string, model: string) => {
if (provider === "github-copilot") {
return model
.replace("claude-opus-4-6", "claude-opus-4.6")
.replace("claude-sonnet-4-6", "claude-sonnet-4.6")
.replace("claude-sonnet-4-5", "claude-sonnet-4.5")
.replace("claude-haiku-4-5", "claude-haiku-4.5")
.replace("claude-sonnet-4", "claude-sonnet-4")
.replace(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview")
.replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview")
}
if (provider === "google") {
return model
.replace(/gemini-3\.1-pro(?!-)/g, "gemini-3.1-pro-preview")
.replace(/gemini-3-flash(?!-)/g, "gemini-3-flash-preview")
}
return model
})
mock.module("../../shared/connected-providers-cache", () => ({
readConnectedProvidersCache: readConnectedProvidersCacheMock,
readProviderModelsCache: readProviderModelsCacheMock,
}))
mock.module("../../shared/provider-model-id-transform", () => ({
transformModelForProvider: transformModelForProviderMock,
}))
import {
clearPendingModelFallback,
@@ -9,6 +40,11 @@ import {
describe("model fallback hook", () => {
beforeEach(() => {
readConnectedProvidersCacheMock.mockReturnValue(null)
readProviderModelsCacheMock.mockReturnValue(null)
readConnectedProvidersCacheMock.mockClear()
readProviderModelsCacheMock.mockClear()
clearPendingModelFallback("ses_model_fallback_main")
clearPendingModelFallback("ses_model_fallback_ghcp")
clearPendingModelFallback("ses_model_fallback_google")
@@ -98,8 +134,8 @@ describe("model fallback hook", () => {
//#then - chain should progress to entry[1], not repeat entry[0]
expect(secondOutput.message["model"]).toEqual({
providerID: "opencode",
modelID: "kimi-k2.5-free",
providerID: "kimi-for-coding",
modelID: "k2p5",
})
expect(secondOutput.message["variant"]).toBeUndefined()
})
@@ -221,10 +257,10 @@ describe("model fallback hook", () => {
//#when
await hook["chat.message"]?.({ sessionID }, output)
//#then — model name should be transformed from gemini-3-pro to gemini-3-pro-preview
//#then — model name should remain gemini-3-pro because no google transform exists for this ID
expect(output.message["model"]).toEqual({
providerID: "google",
modelID: "gemini-3-pro-preview",
modelID: "gemini-3-pro",
})
clearPendingModelFallback(sessionID)

Some files were not shown because too many files have changed in this diff Show More