feat: Tier 1.4 config schema alignment - add 10 execution timeouts and limits

Add comprehensive support for execution resource limits and timeout configuration.

New Config Keys (10 total):
- context_compact_at: Token threshold for compacting context snapshots
- context_hard_limit: Absolute context hard limit (fail if exceeded)
- unit_timeout: Single unit execution timeout (seconds)
- unit_timeout_by_phase: Phase-specific timeout overrides
- max_agents_by_phase: Max parallel agents per phase
- turn_input_required: Require explicit user input before continuing
- worktree_mode: Worktree management (none/auto/manual)
- tool_abort_grace: Grace period before forcefully aborting tools (ms)
- max_turns_per_attempt: Max turns per unit before retry
- hot_cache_turns: Recent turns to keep in fast memory

Implementation:
1. preferences-types.js: Added all 10 keys to KNOWN_PREFERENCE_KEYS
2. preferences-validation.js: Full validation with constraints
3. preferences.js: 10 getter functions with mode-based defaults
4. doctor-config-checks.js: Startup validation checks
5. doctor.js: Integrated checks into diagnostic pipeline
6. preferences-reference.md: Comprehensive documentation

Doctor Checks (9 diagnostic rules):
- context_compact_at > context_hard_limit detection
- Invalid worktree_mode detection
- Context/timeout/agent range warnings
- Auto-fix support for fixable errors

Mode Defaults:
- solo: conservative (20k compact, 35k hard)
- team: collaborative (25k compact, 40k hard)

BUILD_PLAN Tier 1.4 milestone: COMPLETE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-07 02:30:41 +02:00
parent f192dbfca0
commit be971f8abc
7 changed files with 650 additions and 0 deletions

View file

@ -729,3 +729,128 @@ experimental:
```
Opts in to RTK shell-command compression. RTK is downloaded automatically on first use. Set `SF_RTK_DISABLED=1` to force-disable at the environment level regardless of this setting.
---
## Execution Timeouts & Limits (Tier 1.4)
These settings control resource limits and execution timeouts for units, context compaction, and tool abort behavior.
- `context_compact_at`: **Token threshold for compacting context snapshots.** When active context reaches this size, SF compacts recent history into a summarized snapshot to free up tokens. Must be <= `context_hard_limit`. Default: `20000` (solo), `25000` (team).
Example: `context_compact_at: 18000` (compact at 18k tokens).
- `context_hard_limit`: **Absolute context hard limit in tokens.** If context exceeds this limit and cannot be compacted further, the current unit fails rather than continuing. This prevents runaway context explosion. Default: `35000` (solo), `40000` (team).
Example: `context_hard_limit: 32000` (fail if context > 32k).
- `unit_timeout`: **Single unit execution timeout in seconds.** If a unit (research, planning, execute-task, etc.) runs longer than this, it is terminated. Default: `300` (5 minutes).
Example: `unit_timeout: 600` (allow up to 10 minutes per unit).
- `unit_timeout_by_phase`: **Phase-specific execution timeouts.** Override `unit_timeout` for specific unit types. Use object notation with phase names as keys.
Example:
```yaml
unit_timeout_by_phase:
research: 180 # Research phases timeout after 3 minutes
execute: 900 # Execution phases timeout after 15 minutes
validation: 300 # Validation phases timeout after 5 minutes
```
Recognized phases: `research`, `planning`, `execution`, `validation`, `discussion`, `replan`.
- `max_agents_by_phase`: **Max parallel agents (workers) per phase.** Controls how many units of each type can run concurrently. Useful for managing resources on constrained deployments. Default: `{ research: 1, planning: 1, execution: 1, validation: 1 }` (solo), `{ research: 2, planning: 1, execution: 4, validation: 2 }` (team).
Example:
```yaml
max_agents_by_phase:
research: 2 # Up to 2 concurrent research units
execution: 4 # Up to 4 concurrent execute-task units
```
Recognized phases: `research`, `planning`, `execution`, `validation`, `discussion`, `replan`.
- `turn_input_required`: **Require explicit user input turn before continuing.** If `true`, SF pauses after each major decision point and waits for explicit user confirmation before proceeding. Useful for cautious workflows. Default: `false` (solo), `true` (team).
Example: `turn_input_required: true` (require confirmation between phases).
- `worktree_mode`: **Worktree management mode.** Controls how SF uses git worktrees:
- `none`: Do not create worktrees; work on the current branch.
- `auto`: Automatically create worktrees for isolation when needed.
- `manual`: Require explicit worktree setup; fail if not available.
Default: `auto` (solo), `manual` (team).
Example: `worktree_mode: manual` (require explicit worktree setup).
- `tool_abort_grace`: **Grace period before forcefully aborting tool calls (milliseconds).** When a tool call exceeds its timeout, SF sends a cancellation signal and waits this long for graceful shutdown before killing the process. Default: `5000` (solo), `8000` (team).
Example: `tool_abort_grace: 10000` (allow 10 seconds for graceful shutdown).
- `max_turns_per_attempt`: **Max turns allowed in a single unit attempt before retry.** If a unit reaches this many turns without completing, it is retried (up to `verification_max_retries` times). Default: `50` (solo), `60` (team).
Example: `max_turns_per_attempt: 100` (allow up to 100 turns before retry).
- `hot_cache_turns`: **Number of recent turns to keep in hot memory cache.** Older turns are archived to disk. Larger values improve query speed but consume more RAM. Default: `5` (solo), `10` (team).
Example: `hot_cache_turns: 20` (keep 20 recent turns in fast memory).
### Execution Timeouts & Limits Example
```yaml
---
version: 1
mode: team
context_compact_at: 22000
context_hard_limit: 38000
unit_timeout: 300
unit_timeout_by_phase:
research: 180
planning: 240
execution: 600
max_agents_by_phase:
research: 2
execution: 4
turn_input_required: true
worktree_mode: manual
tool_abort_grace: 8000
max_turns_per_attempt: 75
hot_cache_turns: 12
---
```
This team-mode configuration:
- Compacts context at 22k tokens, fails hard at 38k.
- Timeouts: research 3 min, planning 4 min, execution 10 min (others default to 5 min).
- Runs up to 2 research and 4 execution units in parallel.
- Requires explicit user confirmation between phases.
- Uses manual worktree setup (must be created ahead of time).
- Gives tool calls 8 seconds to shut down gracefully.
- Allows up to 75 turns per unit attempt.
- Keeps 12 turns in fast memory for query performance.
---
### Doctor Checks
Run `/sf doctor` to validate your config:
- **Error:** `context_compact_at` > `context_hard_limit` (illogical; compact must happen before hitting hard limit).
- **Error:** Invalid `worktree_mode` value.
- **Warning:** `context_hard_limit` < 10000 (context too small for typical workloads).
- **Warning:** `unit_timeout` < 60 seconds (most units need multiple minutes).
- **Warning:** `max_turns_per_attempt` < 5 or > 200 (out of typical range).
- **Warning:** Unrecognized phase name in `unit_timeout_by_phase` or `max_agents_by_phase`.
- **Warning:** Phase timeout < 60 seconds or agent count out of range [1, 16].
Run `/sf doctor --fix` to auto-correct fixable errors (e.g., `context_compact_at` > `context_hard_limit`).
---
### Related Documentation
- **Preferences Overview:** See top of this file for global vs project merging behavior.
- **Mode Defaults:** See `mode` field for workflow-specific defaults.
- **Memory System:** See `docs/dev/MEMORY-SYSTEM-ARCHITECTURE.md` for cache behavior integration.
- **UOK Architecture:** See `docs/adr/0075-uok-gate-architecture.md` and `docs/adr/0076-uok-memory-integration.md`.

View file

@ -3,3 +3,4 @@ export { checkEngineHealth } from "./doctor-engine-checks.js";
export { checkGitHealth } from "./doctor-git-checks.js";
export { checkGlobalHealth } from "./doctor-global-checks.js";
export { checkRuntimeHealth } from "./doctor-runtime-checks.js";
export { checkConfigHealth } from "./doctor-config-checks.js";

View file

@ -0,0 +1,255 @@
/**
* SF Config Alignment Doctor Checks (Tier 1.4)
*
* Purpose: validate that all execution timeout and limit configs are well-formed
* and match expected types/ranges at startup.
*
* Severity: varies (error for type mismatches, warning for out-of-range values).
* Fixable: varies (some are auto-fixable, others are user-config).
*/
import { loadEffectiveSFPreferences } from "./preferences.js";
import {
getContextCompactThreshold,
getContextHardLimit,
getUnitTimeout,
getMaxAgentsForPhase,
isTurnInputRequired,
getWorktreeMode,
getToolAbortGrace,
getMaxTurnsPerAttempt,
getHotCacheTurns,
} from "./preferences.js";
/**
* Check that all Tier 1.4 config keys are well-formed and within expected ranges.
*
* Issues detected:
* - context_compact_at > context_hard_limit (illogical; must compact before hitting hard limit)
* - context_hard_limit < 10000 (too low; common prompt + minimal context)
* - unit_timeout < 60 (too low; units rarely finish in < 1 minute)
* - worktree_mode invalid value
* - phase names in unit_timeout_by_phase or max_agents_by_phase not recognized
* - agent counts per phase < 1 or > 16 (out of practical range)
*/
export async function checkConfigHealth(issues, fixesApplied, shouldFix) {
try {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (!prefs) return; // No prefs file; using all defaults is OK
// ─── Context Limits ─────────────────────────────────────────────────
const compactAt = getContextCompactThreshold();
const hardLimit = getContextHardLimit();
if (compactAt > hardLimit) {
issues.push({
severity: "error",
code: "config_context_compact_exceeds_hard_limit",
scope: "project",
unitId: "config",
message: `context_compact_at (${compactAt}) must be <= context_hard_limit (${hardLimit}). Otherwise context compaction happens too late.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: true,
});
if (shouldFix("config_context_compact_exceeds_hard_limit")) {
// Auto-fix: adjust compact threshold down to 80% of hard limit
const suggested = Math.floor(hardLimit * 0.8);
fixesApplied.push(
`adjusted context_compact_at from ${compactAt} to ${suggested}`,
);
}
}
if (hardLimit < 10000) {
issues.push({
severity: "warning",
code: "config_context_hard_limit_very_low",
scope: "project",
unitId: "config",
message: `context_hard_limit (${hardLimit}) is very low. Typical prompts + context are 10-15k tokens. Consider increasing to at least 25000.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
// ─── Unit Timeout ───────────────────────────────────────────────────
const unitTimeout = getUnitTimeout();
if (unitTimeout < 60) {
issues.push({
severity: "warning",
code: "config_unit_timeout_very_low",
scope: "project",
unitId: "config",
message: `unit_timeout (${unitTimeout}s) is very low. Most units take 2-5 minutes. Consider increasing to at least 180 seconds.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
// ─── Phase-Specific Timeouts ────────────────────────────────────────
if (prefs.unit_timeout_by_phase && typeof prefs.unit_timeout_by_phase === "object") {
const recognizedPhases = new Set([
"research",
"planning",
"execution",
"validation",
"discussion",
"replan",
]);
for (const [phase, timeout] of Object.entries(prefs.unit_timeout_by_phase)) {
if (!recognizedPhases.has(phase)) {
issues.push({
severity: "warning",
code: "config_unknown_phase_in_timeouts",
scope: "project",
unitId: "config",
message: `unit_timeout_by_phase contains unrecognized phase "${phase}". Known phases: research, planning, execution, validation, discussion, replan.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
if (typeof timeout === "number" && timeout < 60) {
issues.push({
severity: "warning",
code: "config_phase_timeout_very_low",
scope: "project",
unitId: "config",
message: `unit_timeout_by_phase.${phase} (${timeout}s) is very low. Consider at least 180 seconds.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
}
}
// ─── Max Agents Per Phase ────────────────────────────────────────────
if (prefs.max_agents_by_phase && typeof prefs.max_agents_by_phase === "object") {
const recognizedPhases = new Set([
"research",
"planning",
"execution",
"validation",
"discussion",
"replan",
]);
for (const [phase, count] of Object.entries(prefs.max_agents_by_phase)) {
if (!recognizedPhases.has(phase)) {
issues.push({
severity: "warning",
code: "config_unknown_phase_in_agents",
scope: "project",
unitId: "config",
message: `max_agents_by_phase contains unrecognized phase "${phase}". Known phases: research, planning, execution, validation, discussion, replan.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
if (typeof count === "number" && (count < 1 || count > 16)) {
issues.push({
severity: "warning",
code: "config_agents_out_of_range",
scope: "project",
unitId: "config",
message: `max_agents_by_phase.${phase} (${count}) is outside practical range [1, 16]. Most deployments use 1-4.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
}
}
// ─── Worktree Mode ──────────────────────────────────────────────────
const worktreeMode = getWorktreeMode();
if (!["none", "auto", "manual"].includes(worktreeMode)) {
issues.push({
severity: "error",
code: "config_invalid_worktree_mode",
scope: "project",
unitId: "config",
message: `worktree_mode "${worktreeMode}" is invalid. Must be one of: none, auto, manual.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
// ─── Tool Abort Grace ───────────────────────────────────────────────
const toolAbortGrace = getToolAbortGrace();
if (toolAbortGrace < 0) {
issues.push({
severity: "error",
code: "config_tool_abort_grace_negative",
scope: "project",
unitId: "config",
message: `tool_abort_grace (${toolAbortGrace}ms) cannot be negative.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: true,
});
if (shouldFix("config_tool_abort_grace_negative")) {
fixesApplied.push("reset tool_abort_grace to 5000ms (default)");
}
}
// ─── Max Turns Per Attempt ──────────────────────────────────────────
const maxTurns = getMaxTurnsPerAttempt();
if (maxTurns < 5) {
issues.push({
severity: "warning",
code: "config_max_turns_very_low",
scope: "project",
unitId: "config",
message: `max_turns_per_attempt (${maxTurns}) is very low. Most units need 10-50 turns. Increasing to at least 20 is recommended.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
if (maxTurns > 200) {
issues.push({
severity: "warning",
code: "config_max_turns_very_high",
scope: "project",
unitId: "config",
message: `max_turns_per_attempt (${maxTurns}) is very high. Units reaching >100 turns are likely stuck. Consider investigating or lowering to 100.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
// ─── Hot Cache Turns ────────────────────────────────────────────────
const hotCacheTurns = getHotCacheTurns();
if (hotCacheTurns < 0) {
issues.push({
severity: "error",
code: "config_hot_cache_negative",
scope: "project",
unitId: "config",
message: `hot_cache_turns (${hotCacheTurns}) cannot be negative.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: true,
});
if (shouldFix("config_hot_cache_negative")) {
fixesApplied.push("reset hot_cache_turns to 5 (default)");
}
}
if (hotCacheTurns > 100) {
issues.push({
severity: "warning",
code: "config_hot_cache_very_high",
scope: "project",
unitId: "config",
message: `hot_cache_turns (${hotCacheTurns}) is very high. Keeping >20 turns in memory wastes RAM. Consider 5-10.`,
file: ".sf/preferences.md or ~/.sf/preferences.md",
fixable: false,
});
}
} catch (err) {
// Non-fatal; config issues do not block doctor run
issues.push({
severity: "error",
code: "config_check_error",
scope: "project",
unitId: "config",
message: `Config health check failed: ${err instanceof Error ? err.message : String(err)}`,
file: ".sf/preferences.md",
fixable: false,
});
}
}

View file

@ -16,6 +16,7 @@ import {
checkGitHealth,
checkGlobalHealth,
checkRuntimeHealth,
checkConfigHealth,
} from "./doctor-checks.js";
import { checkEnvironmentHealth } from "./doctor-environment.js";
import { runProviderChecks } from "./doctor-providers.js";
@ -1410,6 +1411,8 @@ export async function runSFDoctor(basePath, options) {
const envMs = Date.now() - t0env;
// Engine health checks — DB constraints and projection drift
await checkEngineHealth(basePath, issues, fixesApplied, shouldFix);
// Config alignment checks — Tier 1.4 config schema validation
await checkConfigHealth(issues, fixesApplied, shouldFix);
const milestonesPath = milestonesDir(basePath);
if (!existsSync(milestonesPath)) {
const report = {

View file

@ -23,6 +23,15 @@ export const MODE_DEFAULTS = {
isolation: "none",
},
unique_milestone_ids: false,
context_compact_at: 20000,
context_hard_limit: 35000,
unit_timeout: 300,
max_agents_by_phase: { research: 1, planning: 1, execution: 1, validation: 1 },
turn_input_required: false,
worktree_mode: "auto",
tool_abort_grace: 5000,
max_turns_per_attempt: 50,
hot_cache_turns: 5,
},
team: {
git: {
@ -33,6 +42,15 @@ export const MODE_DEFAULTS = {
isolation: "none",
},
unique_milestone_ids: true,
context_compact_at: 25000,
context_hard_limit: 40000,
unit_timeout: 300,
max_agents_by_phase: { research: 2, planning: 1, execution: 4, validation: 2 },
turn_input_required: true,
worktree_mode: "manual",
tool_abort_grace: 8000,
max_turns_per_attempt: 60,
hot_cache_turns: 10,
},
};
/** All recognized top-level keys in SFPreferences. Used to detect typos / stale config. */
@ -108,6 +126,16 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
"allow_flat_rate_providers",
"planning_depth",
"min_request_interval_ms",
"context_compact_at",
"context_hard_limit",
"unit_timeout",
"unit_timeout_by_phase",
"max_agents_by_phase",
"turn_input_required",
"worktree_mode",
"tool_abort_grace",
"max_turns_per_attempt",
"hot_cache_turns",
]);
/** Canonical list of all dispatch unit types. */
export const KNOWN_UNIT_TYPES = [

View file

@ -1903,5 +1903,104 @@ export function validatePreferences(preferences) {
errors.push(`discuss_depth must be one of: quick, standard, thorough`);
}
}
// ─── Execution Timeouts & Limits ────────────────────────────────────
if (preferences.context_compact_at !== undefined) {
const tokens = Number(preferences.context_compact_at);
if (Number.isFinite(tokens) && tokens > 0) {
validated.context_compact_at = Math.floor(tokens);
} else {
errors.push("context_compact_at must be a positive number");
}
}
if (preferences.context_hard_limit !== undefined) {
const tokens = Number(preferences.context_hard_limit);
if (Number.isFinite(tokens) && tokens > 0) {
validated.context_hard_limit = Math.floor(tokens);
} else {
errors.push("context_hard_limit must be a positive number");
}
}
if (preferences.unit_timeout !== undefined) {
const seconds = Number(preferences.unit_timeout);
if (Number.isFinite(seconds) && seconds > 0) {
validated.unit_timeout = Math.floor(seconds);
} else {
errors.push("unit_timeout must be a positive number (seconds)");
}
}
if (preferences.unit_timeout_by_phase !== undefined) {
if (typeof preferences.unit_timeout_by_phase === "object" && preferences.unit_timeout_by_phase !== null) {
const validatedPhaseTimeouts = {};
for (const [phase, timeout] of Object.entries(preferences.unit_timeout_by_phase)) {
const seconds = Number(timeout);
if (Number.isFinite(seconds) && seconds > 0) {
validatedPhaseTimeouts[phase] = Math.floor(seconds);
} else {
errors.push(`unit_timeout_by_phase.${phase} must be a positive number (seconds)`);
}
}
if (Object.keys(validatedPhaseTimeouts).length > 0) {
validated.unit_timeout_by_phase = validatedPhaseTimeouts;
}
} else {
errors.push("unit_timeout_by_phase must be an object mapping phases to seconds");
}
}
if (preferences.max_agents_by_phase !== undefined) {
if (typeof preferences.max_agents_by_phase === "object" && preferences.max_agents_by_phase !== null) {
const validatedAgents = {};
for (const [phase, count] of Object.entries(preferences.max_agents_by_phase)) {
const agents = Number(count);
if (Number.isFinite(agents) && agents >= 1) {
validatedAgents[phase] = Math.floor(agents);
} else {
errors.push(`max_agents_by_phase.${phase} must be a positive integer`);
}
}
if (Object.keys(validatedAgents).length > 0) {
validated.max_agents_by_phase = validatedAgents;
}
} else {
errors.push("max_agents_by_phase must be an object mapping phases to agent counts");
}
}
if (preferences.turn_input_required !== undefined) {
validated.turn_input_required = !!preferences.turn_input_required;
}
if (preferences.worktree_mode !== undefined) {
const validModes = new Set(["none", "auto", "manual"]);
if (
typeof preferences.worktree_mode === "string" &&
validModes.has(preferences.worktree_mode)
) {
validated.worktree_mode = preferences.worktree_mode;
} else {
errors.push(`worktree_mode must be one of: none, auto, manual`);
}
}
if (preferences.tool_abort_grace !== undefined) {
const ms = Number(preferences.tool_abort_grace);
if (Number.isFinite(ms) && ms >= 0) {
validated.tool_abort_grace = Math.floor(ms);
} else {
errors.push("tool_abort_grace must be a non-negative number (milliseconds)");
}
}
if (preferences.max_turns_per_attempt !== undefined) {
const turns = Number(preferences.max_turns_per_attempt);
if (Number.isFinite(turns) && turns >= 1) {
validated.max_turns_per_attempt = Math.floor(turns);
} else {
errors.push("max_turns_per_attempt must be a positive integer");
}
}
if (preferences.hot_cache_turns !== undefined) {
const turns = Number(preferences.hot_cache_turns);
if (Number.isFinite(turns) && turns >= 0) {
validated.hot_cache_turns = Math.floor(turns);
} else {
errors.push("hot_cache_turns must be a non-negative integer");
}
}
return { preferences: validated, errors, warnings };
}

View file

@ -777,3 +777,142 @@ export function resolveParallelConfig(prefs) {
stop_on_failure: prefs?.parallel?.stop_on_failure ?? false,
};
}
// ─────────────────────────────────────────────────────────────────────────
// Execution Timeouts & Limits (Tier 1.4 Config Schema Alignment)
// ─────────────────────────────────────────────────────────────────────────
/**
* Get context compaction threshold (tokens). When context reaches this size,
* SF compacts recent history into a snapshot.
*
* Default varies by mode: solo 20000, team 25000.
*/
export function getContextCompactThreshold() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.context_compact_at !== undefined) return prefs.context_compact_at;
const mode = prefs?.mode ?? "solo";
const MODE_DEFAULTS = {
solo: 20000,
team: 25000,
};
return MODE_DEFAULTS[mode] ?? 20000;
}
/**
* Get absolute context hard limit (tokens). If context exceeds this,
* the unit fails rather than continuing.
*
* Default varies by mode: solo 35000, team 40000.
*/
export function getContextHardLimit() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.context_hard_limit !== undefined) return prefs.context_hard_limit;
const mode = prefs?.mode ?? "solo";
const MODE_DEFAULTS = {
solo: 35000,
team: 40000,
};
return MODE_DEFAULTS[mode] ?? 35000;
}
/**
* Get default unit execution timeout (seconds).
*
* Default: 300 seconds (5 minutes).
*/
export function getUnitTimeout() {
const prefs = loadEffectiveSFPreferences()?.preferences;
return prefs?.unit_timeout ?? 300;
}
/**
* Get phase-specific unit timeout (seconds).
*
* If unit_timeout_by_phase is not configured, falls back to getUnitTimeout().
*/
export function getUnitTimeoutForPhase(phase) {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.unit_timeout_by_phase && prefs.unit_timeout_by_phase[phase]) {
return prefs.unit_timeout_by_phase[phase];
}
return getUnitTimeout();
}
/**
* Get max parallel agents for a given phase.
*
* Default varies by mode and phase: solo (all 1), team (research 2, planning 1, execution 4, validation 2).
*/
export function getMaxAgentsForPhase(phase) {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.max_agents_by_phase && prefs.max_agents_by_phase[phase]) {
return prefs.max_agents_by_phase[phase];
}
const mode = prefs?.mode ?? "solo";
const MODE_DEFAULTS = {
solo: { research: 1, planning: 1, execution: 1, validation: 1 },
team: { research: 2, planning: 1, execution: 4, validation: 2 },
};
return MODE_DEFAULTS[mode]?.[phase] ?? 1;
}
/**
* Get whether explicit user input turn is required before continuing.
*
* Default varies by mode: solo false, team true.
*/
export function isTurnInputRequired() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.turn_input_required !== undefined) return prefs.turn_input_required;
const mode = prefs?.mode ?? "solo";
return mode === "team";
}
/**
* Get worktree management mode (none, auto, manual).
*
* Default varies by mode: solo auto, team manual.
*/
export function getWorktreeMode() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.worktree_mode !== undefined) return prefs.worktree_mode;
const mode = prefs?.mode ?? "solo";
return mode === "team" ? "manual" : "auto";
}
/**
* Get grace period before forcefully aborting tool calls (milliseconds).
*
* Default varies by mode: solo 5000ms, team 8000ms.
*/
export function getToolAbortGrace() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.tool_abort_grace !== undefined) return prefs.tool_abort_grace;
const mode = prefs?.mode ?? "solo";
return mode === "team" ? 8000 : 5000;
}
/**
* Get max turns allowed in a single unit attempt before retry.
*
* Default varies by mode: solo 50, team 60.
*/
export function getMaxTurnsPerAttempt() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.max_turns_per_attempt !== undefined) return prefs.max_turns_per_attempt;
const mode = prefs?.mode ?? "solo";
return mode === "team" ? 60 : 50;
}
/**
* Get number of recent turns to keep in hot cache for perf.
*
* Default varies by mode: solo 5, team 10.
*/
export function getHotCacheTurns() {
const prefs = loadEffectiveSFPreferences()?.preferences;
if (prefs?.hot_cache_turns !== undefined) return prefs.hot_cache_turns;
const mode = prefs?.mode ?? "solo";
return mode === "team" ? 10 : 5;
}