Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- **Configurable setup script timeout** — Setup scripts used to be killed after a hard-coded 5 minutes, which was too short for steps like full database restores. You can now raise the limit per repository with `setupScriptTimeoutMs` on a repository entry, or for the global setup script with `global_setup_script_timeout_ms`, both in `~/.cyrus/config.json`. Values are in milliseconds; the default is still 300000 (5 minutes). ([CYPACK-1080](https://linear.app/ceedar/issue/CYPACK-1080))

### Fixed
- **Working directory context now shows actual path** — The `<working_directory>` in agent session prompts previously showed "Will be created based on issue" instead of the actual worktree path. It now correctly displays the real workspace directory. ([CYPACK-1088](https://linear.app/ceedar/issue/CYPACK-1088), [#1110](https://github.com/ceedaragents/cyrus/pull/1110))

Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/services/WorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ export class WorkerService {
): Promise<Workspace> => {
return this.gitService.createGitWorktree(issue, repositories, {
globalSetupScript: edgeConfig.global_setup_script,
globalSetupScriptTimeoutMs:
edgeConfig.global_setup_script_timeout_ms,
baseBranchOverrides: options?.baseBranchOverrides,
});
},
Expand Down
14 changes: 14 additions & 0 deletions docs/CONFIG_FILE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ Routes Linear issues with specific labels to this repository. This is useful whe

Example: `["backend", "api"]` - Only process issues that have the "backend" or "api" label

### `setupScriptTimeoutMs` (number)

Timeout in milliseconds for the repository setup script (`cyrus-setup.sh` et al.). Defaults to `300000` (5 minutes). Raise this when the setup script performs long-running work such as restoring a database dump.

Example: `"setupScriptTimeoutMs": 1800000` - allow the setup script up to 30 minutes before it is killed

See [Setup Scripts](./SETUP_SCRIPTS.md) for details on how setup scripts work.

---

## Routing Priority Order
Expand Down Expand Up @@ -382,6 +390,12 @@ Sets default allowed tools for each prompt type across all repositories. Reposit

Path to a script that runs for all repositories when creating new worktrees. See the main README for details on setup scripts.

### `global_setup_script_timeout_ms` (number)

Timeout in milliseconds for the global setup script. Defaults to `300000` (5 minutes). Raise this when the global setup script performs long-running work (for example restoring a shared database).

Example: `"global_setup_script_timeout_ms": 1800000` - allow the global setup script up to 30 minutes before it is killed.

---

## Tool Configuration Priority
Expand Down
42 changes: 41 additions & 1 deletion docs/SETUP_SCRIPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ echo "Repository setup complete for issue: $LINEAR_ISSUE_IDENTIFIER"

Make sure the script is executable: `chmod +x cyrus-setup.sh`

### Increasing the timeout

By default the repository setup script is killed after 5 minutes. If your
setup does something longer-running (for example restoring a database dump),
add `setupScriptTimeoutMs` to the repository entry in `~/.cyrus/config.json`:

```json
{
"repositories": [
{
"id": "workspace-123456",
"name": "my-app",
"repositoryPath": "/path/to/repo",
"setupScriptTimeoutMs": 1800000
}
]
}
```

The value is in milliseconds (`1800000` = 30 minutes).

---

## Global Setup Script
Expand Down Expand Up @@ -69,8 +90,27 @@ Both scripts receive the same environment variables and run in the worktree dire

Make sure the script is executable: `chmod +x /opt/cyrus/bin/global-setup.sh`

### Increasing the timeout

By default the global setup script is killed after 5 minutes. To raise the
limit (for example when the script restores a shared database), add
`global_setup_script_timeout_ms` to `~/.cyrus/config.json`:

```json
{
"repositories": [...],
"global_setup_script": "/opt/cyrus/bin/global-setup.sh",
"global_setup_script_timeout_ms": 1800000
}
```

The value is in milliseconds (`1800000` = 30 minutes). Per-repository setup
scripts use `setupScriptTimeoutMs` on the repository entry instead.

### Error Handling

- If the global script fails, Cyrus logs the error but continues with repository script execution
- Both scripts have a 5-minute timeout to prevent hanging
- Both scripts default to a 5-minute timeout; raise it with
`global_setup_script_timeout_ms` (global) or `setupScriptTimeoutMs`
(per-repository) when longer setup steps are needed
- Script failures don't prevent worktree creation
10 changes: 10 additions & 0 deletions packages/core/schemas/EdgeConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@
}
},
"additionalProperties": false
},
"setupScriptTimeoutMs": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": [
Expand Down Expand Up @@ -546,6 +551,11 @@
"global_setup_script": {
"type": "string"
},
"global_setup_script_timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"defaultAllowedTools": {
"type": "array",
"items": {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/schemas/EdgeConfigPayload.json
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,11 @@
}
},
"additionalProperties": false
},
"setupScriptTimeoutMs": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["id", "name", "repositoryPath", "baseBranch"],
Expand Down Expand Up @@ -540,6 +545,11 @@
"global_setup_script": {
"type": "string"
},
"global_setup_script_timeout_ms": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"defaultAllowedTools": {
"type": "array",
"items": {
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schemas/RepositoryConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@
}
},
"additionalProperties": false
},
"setupScriptTimeoutMs": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": [
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schemas/RepositoryConfigPayload.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@
}
},
"additionalProperties": false
},
"setupScriptTimeoutMs": {
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": ["id", "name", "repositoryPath", "baseBranch"],
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/config-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ export const RepositoryConfigSchema = z.object({

// Repository-specific user access control
userAccessControl: UserAccessControlConfigSchema.optional(),

/**
* Timeout for the repository setup script (`cyrus-setup.sh` et al.) in
* milliseconds. Overrides the default 5-minute timeout, useful for
* long-running setup steps like database restores. Defaults to 300000.
*/
setupScriptTimeoutMs: z.number().int().positive().optional(),
});

/**
Expand Down Expand Up @@ -375,6 +382,13 @@ export const EdgeConfigSchema = z.object({
/** Optional path to global setup script that runs for all repositories */
global_setup_script: z.string().optional(),

/**
* Timeout for the global setup script in milliseconds. Overrides the
* default 5-minute timeout. Useful for long-running setup steps like
* database restores. Defaults to 300000.
*/
global_setup_script_timeout_ms: z.number().int().positive().optional(),

/** Default tools to allow across all repositories */
defaultAllowedTools: z.array(z.string()).optional(),

Expand Down
2 changes: 2 additions & 0 deletions packages/core/test/json-schema-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe("JSON Schema export", () => {
"defaultModel",
"defaultFallbackModel",
"global_setup_script",
"global_setup_script_timeout_ms",
"defaultAllowedTools",
"defaultDisallowedTools",
"issueUpdateTrigger",
Expand Down Expand Up @@ -118,6 +119,7 @@ describe("JSON Schema export", () => {
"promptTemplatePath",
"labelPrompts",
"userAccessControl",
"setupScriptTimeoutMs",
];
for (const field of fields) {
expect(schema.properties).toHaveProperty(field);
Expand Down
37 changes: 33 additions & 4 deletions packages/edge-worker/src/GitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ import type {
import { createLogger, getDefaultWorktreesDir, type ILogger } from "cyrus-core";
import { WorktreeIncludeService } from "./WorktreeIncludeService.js";

/** Default setup script timeout: 5 minutes. */
export const DEFAULT_SETUP_SCRIPT_TIMEOUT_MS = 5 * 60 * 1000;

export interface CreateGitWorktreeOptions {
globalSetupScript?: string;
/**
* Timeout for the global setup script in milliseconds.
* Defaults to {@link DEFAULT_SETUP_SCRIPT_TIMEOUT_MS} (5 minutes).
*/
globalSetupScriptTimeoutMs?: number;
/**
* Override workspace base directory. Required for 0-repo workspaces.
* For 1+ repos, defaults to the first repository's workspaceBaseDir.
Expand Down Expand Up @@ -141,6 +149,7 @@ export class GitService {
scriptType: "global" | "repository",
workspacePath: string,
issue: Issue,
timeoutMs: number = DEFAULT_SETUP_SCRIPT_TIMEOUT_MS,
): Promise<void> {
// Expand ~ to home directory
const expandedPath = scriptPath.replace(/^~/, homedir());
Expand Down Expand Up @@ -202,16 +211,17 @@ export class GitService {
LINEAR_ISSUE_IDENTIFIER: issue.identifier,
LINEAR_ISSUE_TITLE: issue.title || "",
},
timeout: 5 * 60 * 1000, // 5 minute timeout
timeout: timeoutMs,
});

this.logger.info(
`✅ ${scriptType === "global" ? "Global" : "Repository"} setup script completed successfully`,
);
} catch (error) {
const timeoutMinutes = Math.round((timeoutMs / 60_000) * 10) / 10;
const errorMessage =
(error as any).signal === "SIGTERM"
? "Script execution timed out (exceeded 5 minutes)"
? `Script execution timed out (exceeded ${timeoutMinutes} minutes)`
: (error as Error).message;

this.logger.error(
Expand Down Expand Up @@ -469,6 +479,7 @@ export class GitService {
): Promise<Workspace> {
const {
globalSetupScript,
globalSetupScriptTimeoutMs,
workspaceBaseDir: overrideBaseDir,
baseBranchOverrides,
} = options ?? {};
Expand All @@ -494,6 +505,7 @@ export class GitService {
"global",
workspacePath,
issue,
globalSetupScriptTimeoutMs,
);
}

Expand All @@ -516,6 +528,7 @@ export class GitService {
globalSetupScript,
undefined,
overrideValue,
globalSetupScriptTimeoutMs,
);
}

Expand All @@ -529,7 +542,13 @@ export class GitService {

// Run global setup script once in the parent directory
if (globalSetupScript) {
await this.runSetupScript(globalSetupScript, "global", parentPath, issue);
await this.runSetupScript(
globalSetupScript,
"global",
parentPath,
issue,
globalSetupScriptTimeoutMs,
);
}

const repoPaths: Record<string, string> = {};
Expand Down Expand Up @@ -587,6 +606,7 @@ export class GitService {
globalSetupScript?: string,
workspacePathOverride?: string,
baseBranchOverride?: string,
globalSetupScriptTimeoutMs?: number,
): Promise<Workspace> {
this.logger.info(
`createSingleRepoWorktree for ${repository.name} (id=${repository.id}): baseBranchOverride=${baseBranchOverride ?? "undefined"}`,
Expand Down Expand Up @@ -824,6 +844,7 @@ export class GitService {
"global",
workspacePath,
issue,
globalSetupScriptTimeoutMs,
);
}

Expand All @@ -832,6 +853,7 @@ export class GitService {
repository.repositoryPath,
workspacePath,
issue,
repository.setupScriptTimeoutMs,
);

return {
Expand Down Expand Up @@ -1036,6 +1058,7 @@ export class GitService {
repositoryPath: string,
workspacePath: string,
issue: Issue,
timeoutMs?: number,
): Promise<void> {
const isWindows = process.platform === "win32";
const setupScripts = [
Expand Down Expand Up @@ -1079,7 +1102,13 @@ export class GitService {

if (scriptToRun) {
const scriptPath = join(repositoryPath, scriptToRun.file);
await this.runSetupScript(scriptPath, "repository", workspacePath, issue);
await this.runSetupScript(
scriptPath,
"repository",
workspacePath,
issue,
timeoutMs,
);
}
}
}
Loading
Loading