mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
feat: inject effort as soft default via CLI flags instead of env var (#104)
CLAUDE_CODE_EFFORT_LEVEL hard-locks effort for the whole session and makes
Claude reject in-session /effort switching (incl. ultracode). Carry effort
as a dedicated payload field instead, injected at spawn as a soft default:
- regular levels (incl. max) -> claude --effort <level>
(the settings effortLevel key is enum([low,medium,high,xhigh]) with
.catch(undefined), so max would be silently dropped there)
- ultracode -> claude --settings '{"ultracode":true}'
(dedicated boolean settings key, rejected by the --effort flag)
Changes:
- add effort enum field to create/quick-start/ralph-loop schemas and thread
it through Session -> CreateSessionOptions/RespawnPaneOptions -> spawn
- buildEffortCliArgs() in session-cli-builder, shared by tmux spawn command
and direct-PTY fallback args
- frontend: buildEnvOverrides() no longer emits CLAUDE_CODE_EFFORT_LEVEL;
validated effort goes into payloads via getEffortSetting()
- settings UI: add Ultracode option to the Thinking Effort dropdown
- legacy migration: Session constructor extracts CLAUDE_CODE_EFFORT_LEVEL
from persisted envOverrides; applyEnvOverrides() unsets the stale tmux
session var so respawned panes are no longer locked
- tests: test/effort-injection.test.ts (13 cases)
Co-authored-by: Teigen <teigenzhang@gmail.com>
This commit is contained in:
@@ -1104,8 +1104,9 @@
|
||||
<option value="high">High</option>
|
||||
<option value="xhigh">XHigh</option>
|
||||
<option value="max">Max</option>
|
||||
<option value="ultracode">Ultracode (multi-agent workflows)</option>
|
||||
</select>
|
||||
<span class="form-hint">Set CLAUDE_CODE_EFFORT_LEVEL for all new sessions (default = no override)</span>
|
||||
<span class="form-hint">Default effort for new Claude sessions — soft default, switchable anytime in-session via /effort (e.g. /effort ultracode)</span>
|
||||
</div>
|
||||
<!-- Nice Priority Section -->
|
||||
<div class="form-section-header">Nice Priority</div>
|
||||
|
||||
@@ -1032,10 +1032,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
const enabledItems = config.generatedPlan?.filter(i => i.enabled);
|
||||
|
||||
try {
|
||||
const envOverrides = this.buildEnvOverrides(
|
||||
this.getCaseSettings(config.caseName),
|
||||
this.loadAppSettingsFromStorage()
|
||||
);
|
||||
const ralphGlobalSettings = this.loadAppSettingsFromStorage();
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(config.caseName), ralphGlobalSettings);
|
||||
const effort = this.getEffortSetting(ralphGlobalSettings);
|
||||
const res = await fetch('/api/ralph-loop/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -1047,6 +1046,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
enableRespawn: config.enableRespawn,
|
||||
planItems: enabledItems?.length ? enabledItems : undefined,
|
||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
@@ -22,12 +22,24 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (caseSettings?.agentTeams || globalSettings?.agentTeamsEnabled) {
|
||||
env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
|
||||
}
|
||||
if (globalSettings?.thinkingEffort) {
|
||||
env.CLAUDE_CODE_EFFORT_LEVEL = globalSettings.thinkingEffort;
|
||||
}
|
||||
// NOTE: thinkingEffort is intentionally NOT emitted as CLAUDE_CODE_EFFORT_LEVEL —
|
||||
// the env var hard-locks effort and blocks in-session /effort switching (e.g.,
|
||||
// ultracode). It flows as the dedicated `effort` payload field instead, which the
|
||||
// backend injects as a `--settings` soft default. See getEffortSetting().
|
||||
return env;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve the effort level for new sessions from global settings.
|
||||
* Returns a valid effort string or undefined (= no override, CLI default).
|
||||
* Sent as the `effort` payload field — backend turns it into `claude --settings ...`.
|
||||
*/
|
||||
getEffortSetting(globalSettings) {
|
||||
const effort = globalSettings?.thinkingEffort;
|
||||
const valid = ['low', 'medium', 'high', 'xhigh', 'max', 'ultracode'];
|
||||
return valid.includes(effort) ? effort : undefined;
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Quick Start
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@@ -337,6 +349,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
const globalSettings = this.loadAppSettingsFromStorage();
|
||||
const envOverrides = this.buildEnvOverrides(caseSettings, globalSettings);
|
||||
const hasEnvOverrides = Object.keys(envOverrides).length > 0;
|
||||
const effort = this.getEffortSetting(globalSettings);
|
||||
const useOpus1m = caseSettings.opusContext1m || globalSettings.opusContext1mEnabled;
|
||||
const modelOverride = useOpus1m ? 'opus[1m]' : '';
|
||||
|
||||
@@ -349,6 +362,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
body: JSON.stringify({
|
||||
workingDir, name,
|
||||
...(hasEnvOverrides ? { envOverrides } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
...(modelOverride !== undefined ? { modelOverride } : {}),
|
||||
})
|
||||
}).then(r => r.json())
|
||||
@@ -538,7 +552,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick-start with opencode mode (auto-allow tools by default)
|
||||
// Quick-start with opencode mode (auto-allow tools by default).
|
||||
// No `effort` field — it's Claude-specific (OpenCode has no /effort).
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
|
||||
const res = await fetch('/api/quick-start', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1244,7 +1244,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Match by path (not basename) so linked/renamed cases still resolve correctly.
|
||||
const matchingCase = (this.cases || []).find((c) => c.path === workingDir);
|
||||
const caseName = matchingCase?.name || workingDir.split('/').pop() || '';
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), this.loadAppSettingsFromStorage());
|
||||
const globalSettings = this.loadAppSettingsFromStorage();
|
||||
const envOverrides = this.buildEnvOverrides(this.getCaseSettings(caseName), globalSettings);
|
||||
const effort = this.getEffortSetting(globalSettings);
|
||||
const createRes = await fetch('/api/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -1253,6 +1255,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
name,
|
||||
resumeSessionId: sessionId,
|
||||
...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
|
||||
...(effort ? { effort } : {}),
|
||||
}),
|
||||
});
|
||||
const createData = await createRes.json();
|
||||
|
||||
@@ -268,8 +268,16 @@ export function registerRalphRoutes(
|
||||
);
|
||||
}
|
||||
|
||||
const { caseName, taskDescription, completionPhrase, maxIterations, enableRespawn, planItems, envOverrides } =
|
||||
parseBody(RalphLoopStartSchema, req.body);
|
||||
const {
|
||||
caseName,
|
||||
taskDescription,
|
||||
completionPhrase,
|
||||
maxIterations,
|
||||
enableRespawn,
|
||||
planItems,
|
||||
envOverrides,
|
||||
effort,
|
||||
} = parseBody(RalphLoopStartSchema, req.body);
|
||||
|
||||
const casePath = join(CASES_DIR, caseName);
|
||||
|
||||
@@ -315,6 +323,7 @@ export function registerRalphRoutes(
|
||||
claudeMode: rlClaudeModeConfig.claudeMode,
|
||||
allowedTools: rlClaudeModeConfig.allowedTools,
|
||||
envOverrides,
|
||||
effort,
|
||||
});
|
||||
|
||||
// Configure Ralph tracker
|
||||
|
||||
@@ -335,6 +335,7 @@ export function registerSessionRoutes(
|
||||
openCodeConfig: mode === 'opencode' ? body.openCodeConfig : undefined,
|
||||
resumeSessionId: validatedResumeId,
|
||||
envOverrides: body.envOverrides,
|
||||
effort: body.effort,
|
||||
});
|
||||
|
||||
ctx.addSession(session);
|
||||
@@ -1106,6 +1107,7 @@ export function registerSessionRoutes(
|
||||
mode = 'claude',
|
||||
openCodeConfig,
|
||||
envOverrides,
|
||||
effort,
|
||||
} = parseBody(QuickStartSchema, req.body);
|
||||
|
||||
// Check OpenCode availability if requested
|
||||
@@ -1186,6 +1188,7 @@ export function registerSessionRoutes(
|
||||
allowedTools: qsClaudeModeConfig.allowedTools,
|
||||
openCodeConfig: mode === 'opencode' ? openCodeConfig : undefined,
|
||||
envOverrides,
|
||||
effort,
|
||||
});
|
||||
|
||||
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
|
||||
|
||||
@@ -80,6 +80,15 @@ const safeEnvOverridesSchema = z
|
||||
}
|
||||
);
|
||||
|
||||
// ========== Effort Level ==========
|
||||
|
||||
/**
|
||||
* Claude CLI effort level for new sessions. Injected as a `--settings` soft default
|
||||
* (NOT the CLAUDE_CODE_EFFORT_LEVEL env var, which would hard-lock the session and
|
||||
* block in-session `/effort` switching). `ultracode` enables dynamic workflow orchestration.
|
||||
*/
|
||||
const effortLevelSchema = z.enum(['low', 'medium', 'high', 'xhigh', 'max', 'ultracode']).optional();
|
||||
|
||||
// ========== Session Routes ==========
|
||||
|
||||
/**
|
||||
@@ -124,6 +133,8 @@ export const CreateSessionSchema = z.object({
|
||||
mode: z.enum(['claude', 'shell', 'opencode']).optional(),
|
||||
name: z.string().max(100).optional(),
|
||||
envOverrides: safeEnvOverridesSchema,
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
effort: effortLevelSchema,
|
||||
/** Model override to write to .claude/settings.local.json (e.g., "opus[1m]"). Empty string clears. */
|
||||
modelOverride: z.string().max(50).optional(),
|
||||
openCodeConfig: OpenCodeConfigSchema,
|
||||
@@ -179,6 +190,8 @@ export const QuickStartSchema = z.object({
|
||||
mode: z.enum(['claude', 'shell', 'opencode']).optional(),
|
||||
openCodeConfig: OpenCodeConfigSchema,
|
||||
envOverrides: safeEnvOverridesSchema,
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
effort: effortLevelSchema,
|
||||
});
|
||||
|
||||
// ========== Hook Events ==========
|
||||
@@ -543,6 +556,8 @@ export const RalphLoopStartSchema = z.object({
|
||||
maxIterations: z.number().int().min(0).max(1000).nullable().default(10),
|
||||
enableRespawn: z.boolean().default(false),
|
||||
envOverrides: safeEnvOverridesSchema,
|
||||
/** Claude CLI effort level (soft default via --settings, switchable in-session via /effort) */
|
||||
effort: effortLevelSchema,
|
||||
planItems: z
|
||||
.array(
|
||||
z.object({
|
||||
|
||||
@@ -1716,6 +1716,8 @@ export class WebServer extends EventEmitter {
|
||||
const recoveryClaudeMode = await this.getClaudeModeConfig();
|
||||
// Recover envOverrides from the internal __envOverrides field written by
|
||||
// session-manager (see updateSessionState). Cast to read the non-public field.
|
||||
// Note: a legacy CLAUDE_CODE_EFFORT_LEVEL entry is auto-migrated to `effort`
|
||||
// by the Session constructor (env var would hard-lock /effort switching).
|
||||
const savedEnvOverrides = (savedState as { __envOverrides?: Record<string, string> })?.__envOverrides;
|
||||
const session = new Session({
|
||||
id: muxSession.sessionId, // Preserve the original session ID
|
||||
@@ -1728,6 +1730,7 @@ export class WebServer extends EventEmitter {
|
||||
claudeMode: recoveryClaudeMode.claudeMode,
|
||||
allowedTools: recoveryClaudeMode.allowedTools,
|
||||
envOverrides: savedEnvOverrides,
|
||||
effort: savedState?.effort,
|
||||
});
|
||||
|
||||
// Update session name if it was a "Restored:" placeholder or doesn't match saved name
|
||||
|
||||
Reference in New Issue
Block a user