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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`archon setup` no longer writes to `<repo>/.env`.** Prior versions unconditionally wrote the generated config to both `~/.archon/.env` and `<repo>/.env`, destroying user-added secrets and silently downgrading PostgreSQL configs to SQLite when re-run in "Add" mode. The write side now targets exactly one archon-owned file (home or project scope via `--scope`), merges into existing content by default, and writes a timestamped backup. `<repo>/.env` is never touched — it belongs to the user's target project. (#1303)
- **CLI and server no longer silently lose repo-local env vars.** Previously, env vars in `<repo>/.env` were parsed, deleted from `process.env` by `stripCwdEnv()`, and the only output operators saw was `[dotenv@17.3.1] injecting env (0) from .env` — which read as "file was empty." Workflows that needed `SLACK_WEBHOOK` or similar had no way to recover without knowing to use `~/.archon/.env`. The new `<cwd>/.archon/.env` path + archon-owned log lines make the load state observable and recoverable. (#1302)
- **Bumped transitive `axios` to `^1.15.0` via root `overrides` to clear CVE-2025-62718** (NO_PROXY bypass via hostname normalization → potential SSRF). Archon pulls `axios` transitively through `@slack/bolt` and `@slack/web-api`; both semver ranges (`^1.12.0` and `^1.13.5`) accept the override cleanly, so no API surface changes. Credits @stefans71 for identifying and reporting the vulnerability in #1153. Closes #1053.
- **Stale workspace symlink no longer reported as "not in a git repository" by the CLI.** When `archon workflow run` (or `--resume`) is invoked from a valid git repo whose `~/.archon/workspaces/<owner>/<repo>/source` symlink points somewhere else (common after moving/renaming the checkout), auto-registration fails but the repo is fine. Previously both the worktree-creation and resume paths fell through to the generic `Cannot create worktree: not in a git repository` / `Cannot resume: Not in a git repository` errors — a lie that sent users down the wrong diagnostic path. Both sites now preserve the registration error and throw `Cannot {create worktree,resume}: repository registration failed.` with the original cause and a concrete cleanup hint (`Remove the stale workspace entry at <path> and retry`) when the failure matches the `createProjectSourceSymlink()` shape. Credits @Bortlesboat for identifying the root cause and the parser approach in #1157. Closes #1146.

- **Server startup no longer marks actively-running workflows as failed.** The `failOrphanedRuns()` call has been removed from `packages/server/src/index.ts` to match the CLI precedent (`packages/cli/src/cli.ts:256-258`). Per the new CLAUDE.md principle "No Autonomous Lifecycle Mutation Across Process Boundaries", a stuck `running` row is now transitioned explicitly by the user: via the per-row Cancel/Abandon buttons on the dashboard workflow card, or `archon workflow abandon <run-id>` from the CLI. (`archon workflow cleanup` is a separate command that deletes OLD terminal runs for disk hygiene — it does not handle stuck `running` rows.) Closes #1216.
- **`MCP server connection failed: <plugin>` noise no longer surfaces in workflow runs.** The dag-executor now loads the workflow node's `mcp:` config file once and filters the SDK's failure message to only the servers the workflow actually configured. User-level Claude plugin MCPs (e.g. `telegram` inherited from `~/.claude/`) that fail to connect in the headless subprocess are debug-logged as `dag.mcp_plugin_connection_suppressed` instead of being forwarded to the conversation. Other provider warnings (⚠️) surface unchanged. Credits @MrFadiAi for reporting the issue in #1134 (that PR was 9 days stale and conflicting; this is a fresh re-do on current `dev`).
Expand Down
156 changes: 156 additions & 0 deletions packages/cli/src/commands/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,114 @@ describe('workflowRunCommand', () => {
expect(createCallsAfter).toBe(createCallsBefore);
});

// -------------------------------------------------------------------------
// Stale workspace source-symlink → truthful CLI error
// -------------------------------------------------------------------------

it('surfaces auto-registration failures instead of claiming the repo is invalid', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { registerRepository } = await import('@archon/core');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');
const gitModule = await import('@archon/git');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [makeTestWorkflowWithSource({ name: 'assist', description: 'Help' })],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce(null);
(gitModule.findRepoRoot as ReturnType<typeof mock>).mockResolvedValueOnce('/test/path');
(registerRepository as ReturnType<typeof mock>).mockRejectedValueOnce(
new Error(
'Source symlink at /home/test/.archon/workspaces/acme/widget/source already points to ' +
'/home/test/.archon/workspaces/widget, expected /test/path'
)
);

const error = await workflowRunCommand('/test/path', 'assist', 'hello', {}).catch(
err => err as Error
);

expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('Cannot create worktree: repository registration failed.');
expect(error.message).toContain(
'Remove the stale workspace entry at /home/test/.archon/workspaces/acme/widget and retry'
);
expect(error.message).not.toContain('not in a git repository');
});

it('surfaces auto-registration failures on --resume instead of claiming the repo is invalid', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { registerRepository } = await import('@archon/core');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');
const gitModule = await import('@archon/git');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [makeTestWorkflowWithSource({ name: 'assist', description: 'Help' })],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce(null);
(gitModule.findRepoRoot as ReturnType<typeof mock>).mockResolvedValueOnce('/test/path');
(registerRepository as ReturnType<typeof mock>).mockRejectedValueOnce(
new Error(
'Source symlink at /home/test/.archon/workspaces/acme/widget/source already points to ' +
'/home/test/.archon/workspaces/widget, expected /test/path'
)
);

const error = await workflowRunCommand('/test/path', 'assist', 'hello', {
resume: true,
}).catch(err => err as Error);

expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('Cannot resume: repository registration failed.');
expect(error.message).toContain(
'Remove the stale workspace entry at /home/test/.archon/workspaces/acme/widget and retry'
);
expect(error.message).not.toContain('Not in a git repository');
});

it('falls back to generic workspace hint when registration error has an unrecognized shape', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { registerRepository } = await import('@archon/core');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');
const gitModule = await import('@archon/git');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [makeTestWorkflowWithSource({ name: 'assist', description: 'Help' })],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce(null);
(gitModule.findRepoRoot as ReturnType<typeof mock>).mockResolvedValueOnce('/test/path');
(registerRepository as ReturnType<typeof mock>).mockRejectedValueOnce(
new Error("EACCES: permission denied, mkdir '/home/test/.archon/workspaces/acme'")
);

const error = await workflowRunCommand('/test/path', 'assist', 'hello', {}).catch(
err => err as Error
);

expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('Cannot create worktree: repository registration failed.');
expect(error.message).toContain('EACCES: permission denied');
// Path-separator-agnostic check: on Windows path.join normalizes to `\`,
// on POSIX to `/`. Assert the hint prefix + the final segment separately.
expect(error.message).toContain('Check your Archon workspace registration under');
expect(error.message).toMatch(/workspaces\b/);
expect(error.message).not.toContain('Remove the stale workspace entry');
});

// -------------------------------------------------------------------------
// Workflow-level `worktree.enabled` policy
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -2410,3 +2518,51 @@ describe('workflowRunCommand — progress rendering', () => {
expect(stderrSpy).toHaveBeenCalledWith('[slow] Completed (1m30s)\n');
});
});

// ---------------------------------------------------------------------------
// extractStaleWorkspaceEntry — parser edge cases
// ---------------------------------------------------------------------------

describe('extractStaleWorkspaceEntry', () => {
it('extracts the workspace dir from a POSIX source-symlink error', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(
extractStaleWorkspaceEntry(
'Source symlink at /home/user/.archon/workspaces/acme/widget/source already points to /other, expected /here'
)
).toBe('/home/user/.archon/workspaces/acme/widget');
});

it('extracts the workspace dir from a Windows source-symlink error (backslash sep)', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(
extractStaleWorkspaceEntry(
'Source symlink at C:\\Users\\me\\.archon\\workspaces\\acme\\widget\\source already points to D:\\x, expected D:\\y'
)
).toBe('C:\\Users\\me\\.archon\\workspaces\\acme\\widget');
});

it('returns null when the prefix does not match (unrelated error)', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(extractStaleWorkspaceEntry('ENOENT: no such file or directory')).toBeNull();
});

it('returns null when the prefix matches but the delimiter is missing', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(
extractStaleWorkspaceEntry('Source symlink at /some/path (truncated message)')
).toBeNull();
});

it('returns null when the source path has no path separator at all', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(
extractStaleWorkspaceEntry('Source symlink at bareword already points to /x, expected /y')
).toBeNull();
});

it('returns null on an empty input', async () => {
const { extractStaleWorkspaceEntry } = await import('./workflow');
expect(extractStaleWorkspaceEntry('')).toBeNull();
});
});
62 changes: 61 additions & 1 deletion packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from '@archon/core';
import { WORKFLOW_EVENT_TYPES, type WorkflowEventType } from '@archon/workflows/store';
import { configureIsolation, getIsolationProvider } from '@archon/isolation';
import { createLogger } from '@archon/paths';
import { createLogger, getArchonHome } from '@archon/paths';
import { join } from 'node:path';
import { createWorkflowDeps } from '@archon/core/workflows/store-adapter';
import { discoverWorkflowsWithConfig } from '@archon/workflows/workflow-discovery';
import { resolveWorkflowName } from '@archon/workflows/router';
Expand Down Expand Up @@ -77,6 +78,57 @@ function generateConversationId(): string {
return `cli-${String(timestamp)}-${random}`;
}

/**
* Parses the "Source symlink at X already points to Y, expected Z" error
* thrown by `createProjectSourceSymlink` in @archon/paths. Cross-package
* string contract — if that throw site changes wording, this parser silently
* stops matching. Returns the workspace dir (parent of the `source` link) so
* the caller can emit an exact cleanup path, or null if unrecognized.
*/
export function extractStaleWorkspaceEntry(message: string): string | null {
const prefix = 'Source symlink at ';
const delimiter = ' already points to ';
if (!message.startsWith(prefix)) return null;

const remainder = message.slice(prefix.length);
const delimiterIndex = remainder.indexOf(delimiter);
if (delimiterIndex === -1) return null;

const sourcePath = remainder.slice(0, delimiterIndex).trim();
const lastSeparator = Math.max(sourcePath.lastIndexOf('/'), sourcePath.lastIndexOf('\\'));
return lastSeparator === -1 ? null : sourcePath.slice(0, lastSeparator);
}

/**
* Wraps a codebase auto-registration failure for either the worktree-create or
* resume path. Preserves the original error message and delegates hint detail
* to `extractStaleWorkspaceEntry`; falls back to a workspace-root pointer when
* the error shape is unrecognized.
*/
function buildRegistrationFailureError(action: string, error: Error): Error {
const staleWorkspaceEntry = extractStaleWorkspaceEntry(error.message);
let hint: string;
if (staleWorkspaceEntry) {
hint = `Hint: Remove the stale workspace entry at ${staleWorkspaceEntry} and retry, or use --no-worktree to skip isolation.`;
} else {
// Guard against a throwing getArchonHome() (misconfigured env vars, etc.):
// the registration error we're wrapping is the load-bearing one — we'd
// rather lose the exact path in the hint than replace it with a secondary
// home-resolution error that masks the root cause.
try {
const workspacesPath = join(getArchonHome(), 'workspaces');
hint = `Hint: Check your Archon workspace registration under ${workspacesPath} and retry, or use --no-worktree to skip isolation.`;
} catch {
hint =
'Hint: Check your Archon workspace registration and retry, or use --no-worktree to skip isolation.';
}
}

return new Error(
`Cannot ${action}: repository registration failed.\nError: ${error.message}\n${hint}`
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/** Render a workflow event to stderr as a progress line. Called only when --quiet is not set. */
function renderWorkflowEvent(event: WorkflowEmitterEvent, verbose: boolean): void {
switch (event.type) {
Expand Down Expand Up @@ -316,6 +368,7 @@ export async function workflowRunCommand(
// Try to find a codebase for this directory
let codebase = null;
let codebaseLookupError: Error | null = null;
let codebaseRegistrationError: Error | null = null;
try {
codebase = await codebaseDb.findCodebaseByDefaultCwd(cwd);
} catch (error) {
Expand Down Expand Up @@ -361,6 +414,7 @@ export async function workflowRunCommand(
}
} catch (error) {
const err = error as Error;
codebaseRegistrationError = err;
getLog().warn(
{ err, errorType: err.constructor.name, repoRoot },
'cli.codebase_auto_registration_failed'
Expand All @@ -385,6 +439,9 @@ export async function workflowRunCommand(
'Hint: Check your database connection before using --resume.'
);
}
if (codebaseRegistrationError) {
throw buildRegistrationFailureError('resume', codebaseRegistrationError);
}
throw new Error(
'Cannot resume: Not in a git repository.\n' +
'Either run from a git repo or use /clone first.'
Expand Down Expand Up @@ -544,6 +601,9 @@ export async function workflowRunCommand(
'Hint: Check your database connection, or use --no-worktree to skip isolation.'
);
}
if (codebaseRegistrationError) {
throw buildRegistrationFailureError('create worktree', codebaseRegistrationError);
}
throw new Error(
'Cannot create worktree: not in a git repository.\n' +
'Run from within a git repo, or use --no-worktree to skip isolation.'
Expand Down
12 changes: 7 additions & 5 deletions packages/docs-web/src/content/docs/getting-started/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,17 +482,19 @@ The CLI is standalone, but if you also want to interact via Telegram, Slack, Dis

## Troubleshooting

### "Cannot create worktree: not in a git repository" (but the repo exists)
### "Cannot create worktree: repository registration failed" (stale workspace symlink)

The real cause is usually a stale symlink from a previous Archon run with a different path. Look for this in the error output:
This happens when `~/.archon/workspaces/<owner>/<repo>/source` is a symlink pointing at a previous checkout (common after moving or renaming the repo). The error message includes the exact cleanup path to follow:

```
Source symlink at ~/.archon/workspaces/.../source already points to <old-path>, expected <new-path>
Cannot create worktree: repository registration failed.
Error: Source symlink at ~/.archon/workspaces/<owner>/<repo>/source already points to <old-path>, expected <new-path>
Hint: Remove the stale workspace entry at ~/.archon/workspaces/<owner>/<repo> and retry, or use --no-worktree to skip isolation.
```
Comment on lines 489 to 493
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language to the fenced code block.

Static analysis (markdownlint-cli2 MD040) flags the example block as missing a language identifier. Use text (or similar) so the CI lint stays clean and the code fence gets consistent rendering with the rest of the doc.

📝 Proposed fix
-```
+```text
 Cannot create worktree: repository registration failed.
 Error: Source symlink at ~/.archon/workspaces/<owner>/<repo>/source already points to <old-path>, expected <new-path>
 Hint: Remove the stale workspace entry at ~/.archon/workspaces/<owner>/<repo> and retry, or use --no-worktree to skip isolation.
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.0)</summary>

[warning] 489-489: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @packages/docs-web/src/content/docs/getting-started/overview.md around lines
489 - 493, Add a language identifier to the fenced code block containing the
"Cannot create worktree: repository registration failed." example by changing
the opening fence from totext so the block is fenced as ```text; locate
the block that begins with the exact text "Cannot create worktree: repository
registration failed." in overview.md and update only the opening fence to
include "text" (no other content changes).


</details>

<!-- fingerprinting:phantom:medusa:nectarine:40625e77-8497-433d-a84f-91a7b1f27590 -->

<!-- This is an auto-generated comment by CodeRabbit -->


Fix it by manually deleting the stale workspace folder at `~/.archon/workspaces/<github-user>/<repo-name>` and retrying the command.
Follow the hint — delete the stale workspace folder and re-run, or pass `--no-worktree` to skip isolation for one run.

> In the future, `archon isolation cleanup` will handle this automatically.
> On Archon versions before this fix, the same root cause surfaced as the misleading "Cannot create worktree: not in a git repository" (even though the repo was valid). If you see that string, upgrade and you'll get the actionable message above.

---

Expand Down
Loading