import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
-export const AGENT_IDLE_TIMEOUT_S = 300
+// ── Agent ────────────────────────────────────────────────────────────────────
export const AGENT_ACTOR_MODEL = 'github-copilot/claude-sonnet-4.6'
export const AGENT_CRITIC_MODEL = 'github-copilot/gpt-5.4'
-export const BRANCH_PREFIX = 'agent/issue'
+export const AGENT_IDLE_TIMEOUT_S = 300
-export const COMPLETION_SIGNAL = '<promise>COMPLETE</promise>'
+export const AGENT_ITERATION_BUDGET = 50
-export const CONTEXT_HASH_RADIUS = 3
+export const AGENT_MAX_CRITIC_ROUNDS = 10
+
+export const AGENT_PLANNER_MODEL = 'github-copilot/claude-opus-4.6'
+
+export const AGENT_TASK_TIMEOUT_MS = 6_000_000
+
+// ── Git ──────────────────────────────────────────────────────────────────────
+
+export const GIT_BRANCH_PREFIX = 'agent/issue'
+
+export const GIT_PUSH_TIMEOUT_MS = 60_000
+
+export const GIT_TIMEOUT_MS = 30_000
+
+// ── Docker ───────────────────────────────────────────────────────────────────
export const DOCKER_IMAGE = 'sandcastle-sandbox'
}
}
-export const GIT_TIMEOUT_MS = 30_000
-
-export const GRACE_TIMEOUT_MS = 30_000
+// ── GitHub ───────────────────────────────────────────────────────────────────
-export const HASH_PREFIX_LENGTH = 16
+export const GITHUB_ISSUE_LABEL = 'sandcastle'
-export const ITERATION_BUDGET_PER_ROUND = 50
+export const GITHUB_MAX_ISSUES_FETCH = 50
-export const ISSUE_LABEL = 'sandcastle'
+export const GITHUB_MAX_PRS_FETCH = 200
-export const MAX_ISSUES_FETCH = 50
+// ── Validation ───────────────────────────────────────────────────────────────
-export const MAX_PRS_FETCH = 200
+export const VALIDATION_COMMAND =
+ 'pnpm format && pnpm typecheck && pnpm lint && pnpm build && pnpm test'
-export const MAX_PARALLEL = 5
+export const VALIDATION_TIMEOUT_MS = 300_000
-export const MAX_STDERR_CHARS = 500
+// ── Limits & Protocol ────────────────────────────────────────────────────────
-export const MAX_CRITIC_ROUNDS = 10
+export const COMPLETION_SIGNAL = '<promise>COMPLETE</promise>'
-export const MAX_TITLE_LENGTH = 200
+export const CONTEXT_HASH_RADIUS = 3
-export const AGENT_PLANNER_MODEL = 'github-copilot/claude-opus-4.6'
+export const GRACE_TIMEOUT_MS = 30_000
-export const PUSH_TIMEOUT_MS = 60_000
+export const HASH_PREFIX_LENGTH = 16
-export const TASK_TIMEOUT_MS = 100 * 60 * 1000
+export const MAX_PARALLEL = 5
-export const VALIDATION_COMMAND =
- 'pnpm format && pnpm typecheck && pnpm lint && pnpm build && pnpm test'
+export const MAX_STDERR_CHARS = 500
-export const VALIDATION_TIMEOUT_MS = 300_000
+export const MAX_TITLE_CHARS = 200
import type { LoopResult, TaskSpec } from './types.js'
import {
+ GIT_PUSH_TIMEOUT_MS,
GIT_TIMEOUT_MS,
MAX_STDERR_CHARS,
- PUSH_TIMEOUT_MS,
VALIDATION_COMMAND,
VALIDATION_TIMEOUT_MS,
} from './constants.js'
try {
await execFileAsync('git', ['push', '--force-with-lease', 'origin', 'HEAD'], {
cwd,
- timeout: PUSH_TIMEOUT_MS,
+ timeout: GIT_PUSH_TIMEOUT_MS,
})
return true
} catch (pushErr: unknown) {
['push', 'origin', `HEAD:refs/heads/rescue/${spec.branch}-${suffix}`],
{
cwd,
- timeout: PUSH_TIMEOUT_MS,
+ timeout: GIT_PUSH_TIMEOUT_MS,
}
)
console.warn(
try {
await execFileAsync('git', ['push', '-u', 'origin', 'HEAD'], {
cwd,
- timeout: PUSH_TIMEOUT_MS,
+ timeout: GIT_PUSH_TIMEOUT_MS,
})
return true
} catch (pushErr: unknown) {
import { ConcurrencyPool } from './concurrency-pool.js'
import {
- BRANCH_PREFIX,
+ AGENT_ITERATION_BUDGET,
+ AGENT_MAX_CRITIC_ROUNDS,
+ AGENT_TASK_TIMEOUT_MS,
DOCKER_IMAGE,
DOCKER_MOUNTS,
- ISSUE_LABEL,
- ITERATION_BUDGET_PER_ROUND,
- MAX_CRITIC_ROUNDS,
+ GIT_BRANCH_PREFIX,
+ GITHUB_ISSUE_LABEL,
MAX_PARALLEL,
- TASK_TIMEOUT_MS,
} from './constants.js'
import { runRefinementLoop } from './refinement-loop.js'
import { implementStrategy } from './strategies/implement/strategy.js'
import { GithubIssueSource } from './task-source.js'
const source = new GithubIssueSource({
- branchPrefix: BRANCH_PREFIX,
+ branchPrefix: GIT_BRANCH_PREFIX,
dockerImage: DOCKER_IMAGE,
- label: ISSUE_LABEL,
+ label: GITHUB_ISSUE_LABEL,
})
let tasks: TaskSpec[]
pool.run(async () => {
const ac = new AbortController()
const timer = setTimeout(() => {
- ac.abort(new Error(`Task #${spec.id} timed out after ${String(TASK_TIMEOUT_MS)}ms`))
- }, TASK_TIMEOUT_MS)
+ ac.abort(new Error(`Task #${spec.id} timed out after ${String(AGENT_TASK_TIMEOUT_MS)}ms`))
+ }, AGENT_TASK_TIMEOUT_MS)
timer.unref()
try {
})
const loopResult = await runRefinementLoop(spec, sandbox, implementStrategy, {
- iterationBudget: ITERATION_BUDGET_PER_ROUND,
- maxRounds: MAX_CRITIC_ROUNDS,
+ iterationBudget: AGENT_ITERATION_BUDGET,
+ maxRounds: AGENT_MAX_CRITIC_ROUNDS,
postLoopValidationRetry: true,
signal: ac.signal,
})
AGENT_ACTOR_MODEL,
AGENT_CRITIC_MODEL,
AGENT_IDLE_TIMEOUT_S,
+ AGENT_ITERATION_BUDGET,
+ AGENT_MAX_CRITIC_ROUNDS,
COMPLETION_SIGNAL,
CONTEXT_HASH_RADIUS,
HASH_PREFIX_LENGTH,
- ITERATION_BUDGET_PER_ROUND,
- MAX_CRITIC_ROUNDS,
} from './constants.js'
import { runValidation } from './finalizer.js'
import { parseFindingsSafe } from './types.js'
*/
function resolveLoopOptions (opts: RefinementLoopOptions | undefined): ResolvedLoopOptions {
return {
- budget: opts?.iterationBudget ?? ITERATION_BUDGET_PER_ROUND,
- maxRounds: opts?.maxRounds ?? MAX_CRITIC_ROUNDS,
+ budget: opts?.iterationBudget ?? AGENT_ITERATION_BUDGET,
+ maxRounds: opts?.maxRounds ?? AGENT_MAX_CRITIC_ROUNDS,
onRoundComplete: opts?.onRoundComplete ?? (() => undefined),
}
}
COMPLETION_SIGNAL,
DOCKER_MOUNTS,
GIT_TIMEOUT_MS,
- MAX_ISSUES_FETCH,
- MAX_PRS_FETCH,
- MAX_TITLE_LENGTH,
- TASK_TIMEOUT_MS,
+ GITHUB_MAX_ISSUES_FETCH,
+ GITHUB_MAX_PRS_FETCH,
+ AGENT_TASK_TIMEOUT_MS,
+ MAX_TITLE_CHARS,
} from './constants.js'
import { execFileAsync, toErrorMessage } from './utils.js'
},
promptFile: './.sandcastle/plan-prompt.md',
sandbox: docker({ imageName: this.dockerImage, mounts: [...DOCKER_MOUNTS] }),
- signal: AbortSignal.timeout(TASK_TIMEOUT_MS),
+ signal: AbortSignal.timeout(AGENT_TASK_TIMEOUT_MS),
})
} catch (err: unknown) {
console.error(`Planner timed out or failed: ${toErrorMessage(err)}`)
'--json',
'number,title,labels,body',
'--limit',
- String(MAX_ISSUES_FETCH),
+ String(GITHUB_MAX_ISSUES_FETCH),
'--label',
this.label,
],
'--json',
'headRefName',
'--limit',
- String(MAX_PRS_FETCH),
+ String(GITHUB_MAX_PRS_FETCH),
],
{ encoding: 'utf-8', maxBuffer: 8 * 1024 * 1024, timeout: GIT_TIMEOUT_MS }
)
if (typeof item.id !== 'string' || !/^\d+$/.test(item.id)) return false
if (typeof item.branch !== 'string' || !this.branchPattern.test(item.branch)) return false
if (typeof item.title !== 'string') return false
- if (item.title.length > MAX_TITLE_LENGTH) return false
+ if (item.title.length > MAX_TITLE_CHARS) return false
// eslint-disable-next-line no-control-regex
if (/[\x00-\x1f]/.test(item.title)) return false
return true