diff --git a/.gitignore b/.gitignore
index 2f4f5ebf..212fbbe0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Dependencies
-.sisyphus/
+.sisyphus/*
+!.sisyphus/rules/
node_modules/
# Build output
diff --git a/.sisyphus/rules/modular-code-enforcement.md b/.sisyphus/rules/modular-code-enforcement.md
new file mode 100644
index 00000000..dea6062b
--- /dev/null
+++ b/.sisyphus/rules/modular-code-enforcement.md
@@ -0,0 +1,117 @@
+---
+globs: ["**/*.ts", "**/*.tsx"]
+alwaysApply: false
+description: "Enforces strict modular code architecture: SRP, no monolithic index.ts, 200 LOC hard limit"
+---
+
+
+
+# Modular Code Architecture — Zero Tolerance Policy
+
+This rule is NON-NEGOTIABLE. Violations BLOCK all further work until resolved.
+
+## Rule 1: index.ts is an ENTRY POINT, NOT a dumping ground
+
+`index.ts` files MUST ONLY contain:
+- Re-exports (`export { ... } from "./module"`)
+- Factory function calls that compose modules
+- Top-level wiring/registration (hook registration, plugin setup)
+
+`index.ts` MUST NEVER contain:
+- Business logic implementation
+- Helper/utility functions
+- Type definitions beyond simple re-exports
+- Multiple unrelated responsibilities mixed together
+
+**If you find mixed logic in index.ts**: Extract each responsibility into its own dedicated file BEFORE making any other changes. This is not optional.
+
+## Rule 2: No Catch-All Files — utils.ts / service.ts are CODE SMELLS
+
+A single `utils.ts`, `helpers.ts`, `service.ts`, or `common.ts` is a **gravity well** — every unrelated function gets tossed in, and it grows into an untestable, unreviewable blob.
+
+**These file names are BANNED as top-level catch-alls.** Instead:
+
+| Anti-Pattern | Refactor To |
+|--------------|-------------|
+| `utils.ts` with `formatDate()`, `slugify()`, `retry()` | `date-formatter.ts`, `slugify.ts`, `retry.ts` |
+| `service.ts` handling auth + billing + notifications | `auth-service.ts`, `billing-service.ts`, `notification-service.ts` |
+| `helpers.ts` with 15 unrelated exports | One file per logical domain |
+
+**Design for reusability from the start.** Each module should be:
+- **Independently importable** — no consumer should need to pull in unrelated code
+- **Self-contained** — its dependencies are explicit, not buried in a shared grab-bag
+- **Nameable by purpose** — the filename alone tells you what it does
+
+If you catch yourself typing `utils.ts` or `service.ts`, STOP and name the file after what it actually does.
+
+## Rule 3: Single Responsibility Principle — ABSOLUTE
+
+Every `.ts` file MUST have exactly ONE clear, nameable responsibility.
+
+**Self-test**: If you cannot describe the file's purpose in ONE short phrase (e.g., "parses YAML frontmatter", "matches rules against file paths"), the file does too much. Split it.
+
+| Signal | Action |
+|--------|--------|
+| File has 2+ unrelated exported functions | **SPLIT NOW** — each into its own module |
+| File mixes I/O with pure logic | **SPLIT NOW** — separate side effects from computation |
+| File has both types and implementation | **SPLIT NOW** — types.ts + implementation.ts |
+| You need to scroll to understand the file | **SPLIT NOW** — it's too large |
+
+## Rule 4: 200 LOC Hard Limit — CODE SMELL DETECTOR
+
+Any `.ts`/`.tsx` file exceeding **200 lines of code** (excluding prompt strings, template literals containing prompts, and `.md` content) is an **immediate code smell**.
+
+**When you detect a file > 200 LOC**:
+1. **STOP** current work
+2. **Identify** the multiple responsibilities hiding in the file
+3. **Extract** each responsibility into a focused module
+4. **Verify** each resulting file is < 200 LOC and has a single purpose
+5. **Resume** original work
+
+Prompt-heavy files (agent definitions, skill definitions) where the bulk of content is template literal prompt text are EXEMPT from the LOC count — but their non-prompt logic must still be < 200 LOC.
+
+### How to Count LOC
+
+**Count these** (= actual logic):
+- Import statements
+- Variable/constant declarations
+- Function/class/interface/type definitions
+- Control flow (`if`, `for`, `while`, `switch`, `try/catch`)
+- Expressions, assignments, return statements
+- Closing braces `}` that belong to logic blocks
+
+**Exclude these** (= not logic):
+- Blank lines
+- Comment-only lines (`//`, `/* */`, `/** */`)
+- Lines inside template literals that are prompt/instruction text (e.g., the string body of `` const prompt = `...` ``)
+- Lines inside multi-line strings used as documentation/prompt content
+
+**Quick method**: Read the file → subtract blank lines, comment-only lines, and prompt string content → remaining count = LOC.
+
+**Example**:
+```typescript
+// 1 import { foo } from "./foo"; ← COUNT
+// 2 ← SKIP (blank)
+// 3 // Helper for bar ← SKIP (comment)
+// 4 export function bar(x: number) { ← COUNT
+// 5 const prompt = ` ← COUNT (declaration)
+// 6 You are an assistant. ← SKIP (prompt text)
+// 7 Follow these rules: ← SKIP (prompt text)
+// 8 `; ← COUNT (closing)
+// 9 return process(prompt, x); ← COUNT
+// 10 } ← COUNT
+```
+→ LOC = **5** (lines 1, 4, 5, 9, 10). Not 10.
+
+When in doubt, **round up** — err on the side of splitting.
+
+## How to Apply
+
+When reading, writing, or editing ANY `.ts`/`.tsx` file:
+
+1. **Check the file you're touching** — does it violate any rule above?
+2. **If YES** — refactor FIRST, then proceed with your task
+3. **If creating a new file** — ensure it has exactly one responsibility and stays under 200 LOC
+4. **If adding code to an existing file** — verify the addition doesn't push the file past 200 LOC or add a second responsibility. If it does, extract into a new module.
+
+
diff --git a/src/config/schema.ts b/src/config/schema.ts
index 936c96a8..3b70a31f 100644
--- a/src/config/schema.ts
+++ b/src/config/schema.ts
@@ -101,6 +101,7 @@ export const HookNameSchema = z.enum([
"stop-continuation-guard",
"tasks-todowrite-disabler",
"write-existing-file-guard",
+ "anthropic-effort",
])
export const BuiltinCommandNameSchema = z.enum([
diff --git a/src/hooks/anthropic-effort/index.test.ts b/src/hooks/anthropic-effort/index.test.ts
new file mode 100644
index 00000000..be965c0a
--- /dev/null
+++ b/src/hooks/anthropic-effort/index.test.ts
@@ -0,0 +1,215 @@
+import { describe, expect, it } from "bun:test"
+import { createAnthropicEffortHook } from "./index"
+
+interface ChatParamsInput {
+ sessionID: string
+ agent: { name?: string }
+ model: { providerID: string; modelID: string; id?: string; api?: { npm?: string } }
+ provider: { id: string }
+ message: { variant?: string }
+}
+
+interface ChatParamsOutput {
+ temperature?: number
+ topP?: number
+ topK?: number
+ options: Record
+}
+
+function createMockParams(overrides: {
+ providerID?: string
+ modelID?: string
+ variant?: string
+ agentName?: string
+ existingOptions?: Record
+}): { input: ChatParamsInput; output: ChatParamsOutput } {
+ const providerID = overrides.providerID ?? "anthropic"
+ const modelID = overrides.modelID ?? "claude-opus-4-6"
+ const variant = "variant" in overrides ? overrides.variant : "max"
+ const agentName = overrides.agentName ?? "sisyphus"
+ const existingOptions = overrides.existingOptions ?? {}
+
+ return {
+ input: {
+ sessionID: "test-session",
+ agent: { name: agentName },
+ model: { providerID, modelID },
+ provider: { id: providerID },
+ message: { variant },
+ },
+ output: {
+ temperature: 0.1,
+ options: { ...existingOptions },
+ },
+ }
+}
+
+describe("createAnthropicEffortHook", () => {
+ describe("opus 4-6 with variant max", () => {
+ it("should inject effort max for anthropic opus-4-6 with variant max", async () => {
+ //#given anthropic opus-4-6 model with variant max
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({})
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should be injected into options
+ expect(output.options.effort).toBe("max")
+ })
+
+ it("should inject effort max for github-copilot claude-opus-4-6", async () => {
+ //#given github-copilot provider with claude-opus-4-6
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ providerID: "github-copilot",
+ modelID: "claude-opus-4-6",
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should be injected (github-copilot resolves to anthropic)
+ expect(output.options.effort).toBe("max")
+ })
+
+ it("should inject effort max for opencode provider with claude-opus-4-6", async () => {
+ //#given opencode provider with claude-opus-4-6
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ providerID: "opencode",
+ modelID: "claude-opus-4-6",
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should be injected
+ expect(output.options.effort).toBe("max")
+ })
+
+ it("should handle normalized model ID with dots (opus-4.6)", async () => {
+ //#given model ID with dots instead of hyphens
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ modelID: "claude-opus-4.6",
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then should normalize and inject effort
+ expect(output.options.effort).toBe("max")
+ })
+ })
+
+ describe("conditions NOT met - should skip", () => {
+ it("should NOT inject effort when variant is not max", async () => {
+ //#given opus-4-6 with variant high (not max)
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({ variant: "high" })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should NOT be injected
+ expect(output.options.effort).toBeUndefined()
+ })
+
+ it("should NOT inject effort when variant is undefined", async () => {
+ //#given opus-4-6 with no variant
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({ variant: undefined })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should NOT be injected
+ expect(output.options.effort).toBeUndefined()
+ })
+
+ it("should NOT inject effort for non-opus model", async () => {
+ //#given claude-sonnet-4-5 (not opus)
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ modelID: "claude-sonnet-4-5",
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should NOT be injected
+ expect(output.options.effort).toBeUndefined()
+ })
+
+ it("should NOT inject effort for non-anthropic provider with non-claude model", async () => {
+ //#given openai provider with gpt model
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ providerID: "openai",
+ modelID: "gpt-5.2",
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should NOT be injected
+ expect(output.options.effort).toBeUndefined()
+ })
+
+ it("should NOT throw when model.modelID is undefined", async () => {
+ //#given model with undefined modelID (runtime edge case)
+ const hook = createAnthropicEffortHook()
+ const input = {
+ sessionID: "test-session",
+ agent: { name: "sisyphus" },
+ model: { providerID: "anthropic", modelID: undefined as unknown as string },
+ provider: { id: "anthropic" },
+ message: { variant: "max" as const },
+ }
+ const output = { temperature: 0.1, options: {} }
+
+ //#when chat.params hook is called with undefined modelID
+ await hook["chat.params"](input, output)
+
+ //#then should gracefully skip without throwing
+ expect(output.options.effort).toBeUndefined()
+ })
+ })
+
+ describe("preserves existing options", () => {
+ it("should NOT overwrite existing effort if already set", async () => {
+ //#given options already have effort set
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ existingOptions: { effort: "high" },
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then existing effort should be preserved
+ expect(output.options.effort).toBe("high")
+ })
+
+ it("should preserve other existing options when injecting effort", async () => {
+ //#given options with existing thinking config
+ const hook = createAnthropicEffortHook()
+ const { input, output } = createMockParams({
+ existingOptions: {
+ thinking: { type: "enabled", budgetTokens: 31999 },
+ },
+ })
+
+ //#when chat.params hook is called
+ await hook["chat.params"](input, output)
+
+ //#then effort should be added without affecting thinking
+ expect(output.options.effort).toBe("max")
+ expect(output.options.thinking).toEqual({
+ type: "enabled",
+ budgetTokens: 31999,
+ })
+ })
+ })
+})
diff --git a/src/hooks/anthropic-effort/index.ts b/src/hooks/anthropic-effort/index.ts
new file mode 100644
index 00000000..141933cb
--- /dev/null
+++ b/src/hooks/anthropic-effort/index.ts
@@ -0,0 +1,56 @@
+import { log } from "../../shared"
+
+const OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i
+
+function normalizeModelID(modelID: string): string {
+ return modelID.replace(/\.(\d+)/g, "-$1")
+}
+
+function isClaudeProvider(providerID: string, modelID: string): boolean {
+ if (["anthropic", "opencode"].includes(providerID)) return true
+ if (providerID === "github-copilot" && modelID.toLowerCase().includes("claude")) return true
+ return false
+}
+
+function isOpus46(modelID: string): boolean {
+ const normalized = normalizeModelID(modelID)
+ return OPUS_4_6_PATTERN.test(normalized)
+}
+
+interface ChatParamsInput {
+ sessionID: string
+ agent: { name?: string }
+ model: { providerID: string; modelID: string }
+ provider: { id: string }
+ message: { variant?: string }
+}
+
+interface ChatParamsOutput {
+ temperature?: number
+ topP?: number
+ topK?: number
+ options: Record
+}
+
+export function createAnthropicEffortHook() {
+ return {
+ "chat.params": async (
+ input: ChatParamsInput,
+ output: ChatParamsOutput
+ ): Promise => {
+ const { model, message } = input
+ if (!model?.modelID || !model?.providerID) return
+ if (message.variant !== "max") return
+ if (!isClaudeProvider(model.providerID, model.modelID)) return
+ if (!isOpus46(model.modelID)) return
+ if (output.options.effort !== undefined) return
+
+ output.options.effort = "max"
+ log("anthropic-effort: injected effort=max", {
+ sessionID: input.sessionID,
+ provider: model.providerID,
+ model: model.modelID,
+ })
+ },
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index f1750aee..ee210e66 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -40,6 +40,7 @@ import {
createTasksTodowriteDisablerHook,
createWriteExistingFileGuardHook,
} from "./hooks";
+import { createAnthropicEffortHook } from "./hooks/anthropic-effort";
import {
contextCollector,
createContextInjectorMessagesTransformHook,
@@ -294,6 +295,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const taskResumeInfo = createTaskResumeInfoHook();
+ const anthropicEffort = isHookEnabled("anthropic-effort")
+ ? createAnthropicEffortHook()
+ : null;
+
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
const backgroundManager = new BackgroundManager(
@@ -550,6 +555,29 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
return {
tool: filteredTools,
+ "chat.params": async (
+ input: {
+ sessionID: string
+ agent: string
+ model: Record
+ provider: Record
+ message: Record
+ },
+ output: {
+ temperature: number
+ topP: number
+ topK: number
+ options: Record
+ },
+ ) => {
+ const model = input.model as { providerID: string; modelID: string }
+ const message = input.message as { variant?: string }
+ await anthropicEffort?.["chat.params"]?.(
+ { ...input, agent: { name: input.agent }, model, provider: input.provider as { id: string }, message },
+ output,
+ );
+ },
+
"chat.message": async (input, output) => {
if (input.agent) {
setSessionAgent(input.sessionID, input.agent);