Skip to content
Merged
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
12 changes: 12 additions & 0 deletions packages/core/src/config/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ export interface RepoConfig {
* @example [".env", ".archon", "data/fixtures/"]
*/
copyFiles?: string[];

/**
* Initialize git submodules in new worktrees.
* Runs `git submodule update --init --recursive` after worktree creation
* when the repo contains a `.gitmodules` file. Repos without submodules
* pay zero cost (the check short-circuits).
*
* Set to `false` to skip submodule init (e.g., when submodules are not
* needed by any workflow or when fetch cost is prohibitive).
* @default true
*/
initSubmodules?: boolean;
};

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/docs-web/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ worktree:
copyFiles: # Optional: Additional files to copy to worktrees
- .env.example -> .env # Rename during copy
- .vscode # Copy entire directory
initSubmodules: true # Optional: default true — auto-detects .gitmodules and runs
# `git submodule update --init --recursive`. Set false to opt out.

# Documentation directory
docs:
Expand Down Expand Up @@ -164,6 +166,8 @@ This is useful when you maintain coding style or identity preferences in `~/.cla

**Defaults behavior:** The app's bundled default commands and workflows are loaded at runtime and merged with repo-specific ones. Repo commands/workflows override app defaults by name. Set `defaults.loadDefaultCommands: false` or `defaults.loadDefaultWorkflows: false` to disable runtime loading.

**Submodule behavior:** When a repo contains `.gitmodules`, submodules are initialized in new worktrees by default (git's `worktree add` does not do this). The check is a cheap filesystem probe — repos without submodules pay zero cost. Submodule init failure throws a classified error (credentials, network, timeout) rather than silently producing a worktree with empty submodule directories. Set `worktree.initSubmodules: false` to opt out.

**Base branch behavior:** Before creating a worktree, the canonical workspace is synced to the latest code. Resolution order:
1. If `worktree.baseBranch` is set: Uses the configured branch. **Fails with an error** if the branch doesn't exist on remote (no silent fallback).
2. If omitted: Auto-detects the default branch via `git remote show origin`. Works without any config for standard repos.
Expand Down
14 changes: 14 additions & 0 deletions packages/isolation/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ describe('classifyIsolationError', () => {
const result = classifyIsolationError(new Error('unknown error'));
expect(result).toContain('Could not create isolated workspace');
});

test('matches "submodule initialization failed" with opt-out guidance', () => {
const result = classifyIsolationError(
new Error('Submodule initialization failed: fatal: could not read from remote repository')
);
expect(result).toContain('Submodule initialization failed');
expect(result).toContain('initSubmodules: false');
});
});

describe('isKnownIsolationError', () => {
Expand Down Expand Up @@ -87,6 +95,12 @@ describe('isKnownIsolationError', () => {
expect(isKnownIsolationError(new Error('branch not found'))).toBe(true);
});

test('identifies submodule initialization failure as known', () => {
expect(
isKnownIsolationError(new Error('Submodule initialization failed: network unreachable'))
).toBe(true);
});

test('returns false for unknown errors', () => {
expect(isKnownIsolationError(new TypeError('cannot read property of null'))).toBe(false);
});
Expand Down
177 changes: 96 additions & 81 deletions packages/isolation/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,79 +16,108 @@ export class IsolationBlockedError extends Error {
}
}

/**
* Single source of truth for isolation error classification.
*
* `known: true` means the error is a recognized infrastructure/config failure
* that should produce a user-facing "blocked" message. `known: false` means
* it's classifiable (we have a helpful message) but still a programming /
* user-input bug that should crash rather than be absorbed as blocked state.
*/
const ERROR_PATTERNS: { pattern: string; message: string; known: boolean }[] = [
{
pattern: 'permission denied',
message:
'**Error:** Permission denied while creating workspace. Check file system permissions.',
known: true,
},
{
pattern: 'eacces',
message:
'**Error:** Permission denied while creating workspace. Check file system permissions.',
known: true,
},
{
pattern: 'timeout',
message: '**Error:** Timed out creating workspace. Git repository may be slow or unavailable.',
known: true,
},
{
pattern: 'no space left',
message: '**Error:** No disk space available for new workspace.',
known: true,
},
{
pattern: 'enospc',
message: '**Error:** No disk space available for new workspace.',
known: true,
},
{
pattern: 'not a git repository',
message: '**Error:** Target path is not a valid git repository.',
known: true,
},
{
// Deliberately not `known` — this is a user-input / registration bug,
// not an infrastructure failure. Surface classification, but crash.
pattern: 'cannot extract owner/repo',
message:
'**Error:** Repository path is too short to extract owner and repo name. ' +
'Re-register the codebase with a full path (e.g. `/home/user/owner/repo`).',
known: false,
},
{
pattern: 'branch not found',
message:
'**Error:** Branch not found. The requested branch may have been deleted or not yet pushed.',
known: true,
},
{
pattern: 'no base branch configured',
message:
'**Error:** No base branch configured. Set `worktree.baseBranch` in `.archon/config.yaml` ' +
'or use the `--from` flag to select a branch (e.g., `--from dev`).',
known: true,
},
{
pattern: 'belongs to a different clone',
message:
'**Error:** A worktree at the target path was created by a different local clone. ' +
'Remove it from that clone, or register this codebase from the same local path.',
known: true,
},
{
pattern: 'cannot verify worktree ownership',
message:
'**Error:** Cannot verify ownership of an existing worktree at the target path. ' +
'Check file system permissions and remove any unrelated git directories at that path.',
known: true,
},
{
pattern: 'cannot adopt',
message:
'**Error:** Refused to adopt an existing directory at the worktree path. ' +
'Remove it or choose a different branch/codebase registration.',
known: true,
},
{
pattern: 'submodule initialization failed',
message:
'**Error:** Submodule initialization failed. Check credentials and network access to ' +
'submodule remotes, or set `worktree.initSubmodules: false` in `.archon/config.yaml` ' +
'to opt out if submodules are not needed for your workflows.',
known: true,
},
];

/**
* Classify isolation creation errors into user-friendly messages.
*/
export function classifyIsolationError(err: Error): string {
const stderr = (err as Error & { stderr?: string }).stderr ?? '';
const errorLower = `${err.message} ${stderr}`.toLowerCase();

const errorPatterns: { pattern: string; message: string }[] = [
{
pattern: 'permission denied',
message:
'**Error:** Permission denied while creating workspace. Check file system permissions.',
},
{
pattern: 'eacces',
message:
'**Error:** Permission denied while creating workspace. Check file system permissions.',
},
{
pattern: 'timeout',
message:
'**Error:** Timed out creating workspace. Git repository may be slow or unavailable.',
},
{
pattern: 'no space left',
message: '**Error:** No disk space available for new workspace.',
},
{
pattern: 'enospc',
message: '**Error:** No disk space available for new workspace.',
},
{
pattern: 'not a git repository',
message: '**Error:** Target path is not a valid git repository.',
},
{
pattern: 'cannot extract owner/repo',
message:
'**Error:** Repository path is too short to extract owner and repo name. ' +
'Re-register the codebase with a full path (e.g. `/home/user/owner/repo`).',
},
{
pattern: 'branch not found',
message:
'**Error:** Branch not found. The requested branch may have been deleted or not yet pushed.',
},
{
pattern: 'no base branch configured',
message:
'**Error:** No base branch configured. Set `worktree.baseBranch` in `.archon/config.yaml` ' +
'or use the `--from` flag to select a branch (e.g., `--from dev`).',
},
{
pattern: 'belongs to a different clone',
message:
'**Error:** A worktree at the target path was created by a different local clone. ' +
'Remove it from that clone, or register this codebase from the same local path.',
},
{
pattern: 'cannot verify worktree ownership',
message:
'**Error:** Cannot verify ownership of an existing worktree at the target path. ' +
'Check file system permissions and remove any unrelated git directories at that path.',
},
{
pattern: 'cannot adopt',
message:
'**Error:** Refused to adopt an existing directory at the worktree path. ' +
'Remove it or choose a different branch/codebase registration.',
},
];

for (const { pattern, message } of errorPatterns) {
for (const { pattern, message } of ERROR_PATTERNS) {
if (errorLower.includes(pattern)) {
return message;
}
Expand All @@ -108,19 +137,5 @@ export function isKnownIsolationError(err: Error): boolean {
const stderr = (err as Error & { stderr?: string }).stderr ?? '';
const errorLower = `${err.message} ${stderr}`.toLowerCase();

const knownPatterns = [
'permission denied',
'eacces',
'timeout',
'no space left',
'enospc',
'not a git repository',
'branch not found',
'no base branch configured',
'belongs to a different clone',
'cannot verify worktree ownership',
'cannot adopt',
];

return knownPatterns.some(pattern => errorLower.includes(pattern));
return ERROR_PATTERNS.some(({ pattern, known }) => known && errorLower.includes(pattern));
}
Loading
Loading