Skip to content
Closed
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: 10 additions & 2 deletions .archon/workflows/maintainer/marketplace-pr-review-and-merge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -286,20 +286,28 @@ nodes:
case "$DECISION" in
auto_merge)
echo "Decision: AUTO_MERGE — approving and merging PR #$PR_NUM"
gh pr review "$PR_NUM" --approve --body "**Marketplace Auto-Review: Approved & Merging**
APPROVE_BODY="**Marketplace Auto-Review: Approved & Merging**

$REASON

*Reviewed and auto-merged by Archon marketplace-pr-review-and-merge workflow.*"
# Best-effort approval — GITHUB_TOKEN can't approve unless the repo
# has "Allow GitHub Actions to create and approve pull requests"
# enabled in Settings → Actions → General. The comment below always
# lands so the PR author still gets notified.
gh pr review "$PR_NUM" --approve --body "$APPROVE_BODY" \
|| gh pr comment "$PR_NUM" --body "$APPROVE_BODY"
gh pr merge "$PR_NUM" --squash --delete-branch --subject "feat(marketplace): add ${SLUG:-marketplace-entry}"
;;
auto_approve)
echo "Decision: AUTO_APPROVE — approving PR #$PR_NUM (manual merge required)"
gh pr review "$PR_NUM" --approve --body "**Marketplace Auto-Review: Approved**
APPROVE_BODY="**Marketplace Auto-Review: Approved**

$REASON

*Reviewed by Archon marketplace-pr-review-and-merge workflow. A maintainer will merge.*"
gh pr review "$PR_NUM" --approve --body "$APPROVE_BODY" \
|| gh pr comment "$PR_NUM" --body "$APPROVE_BODY"
;;
request_changes)
echo "Decision: REQUEST_CHANGES — requesting changes on PR #$PR_NUM"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/marketplace-auto-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
pull_request_target:
paths:
- "packages/docs-web/src/data/marketplace.ts"
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, ready_for_review]

jobs:
auto-review:
Expand Down
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.12] - 2026-05-14

Orchestrator prompt-cache fix, SDK termination edge cases, marketplace expansion, and broad workflow fixes.

### Added

- **New marketplace workflow `archon-idea-to-wo`**: interactive 8-node workflow that turns a raw idea into BKM-format Work Orders through four AI phases with approval gates between each. Authored by @lamachine, published via the community marketplace registry (closes #1647).

### Changed

- `maintainer-review-pr`: dropped the direction/scope gate node. The gate used Pi/Minimax to return a structured JSON verdict that the DAG branched on, but Pi intermittently wrapped the JSON in markdown fences or preamble prose, silently skipping every downstream review. In practice the gate returned "review" on every hand-picked PR (13/13 runs over two days), adding no signal. Workflow now reviews all PRs directly (#1675).

### Fixed

- **Claude `stop_sequence` terminations no longer fail as "SDK returned success"**: the Claude Agent SDK's `SDKResultSuccess` declares `is_error: boolean` (not literal `false`), and stop-sequence terminations carry `is_error: true` alongside `subtype: 'success'` — its encoding of "non-default termination, not a failure". The Claude provider now normalises this pair to a clean success at the provider boundary, with defense-in-depth guards in `dag-executor` (main + loop branches) and `orchestrator-agent` (direct chat) so a third-party `IAgentProvider` forwarding the raw SDK pair can't reintroduce the bug. Workflows using `output_format` (which implies a stop sequence) — including the `archon-fix-github-issue` `classify` → `synthesize` pipeline — now complete cleanly instead of throwing `Node 'X' failed: SDK returned success`. Closes #1425.
- **Claude `stop_sequence` terminations no longer fail as "SDK returned success"**: the Claude Agent SDK's `SDKResultSuccess` declares `is_error: boolean` (not literal `false`), and stop-sequence terminations carry `is_error: true` alongside `subtype: 'success'` — its encoding of "non-default termination, not a failure". The Claude provider now normalises this pair to a clean success at the provider boundary, with defense-in-depth guards in `dag-executor` (main + loop branches) and `orchestrator-agent` (direct chat) so a third-party `IAgentProvider` forwarding the raw SDK pair can't reintroduce the bug. Workflows using `output_format` (which implies a stop sequence) — including the `archon-fix-github-issue` `classify` → `synthesize` pipeline — now complete cleanly instead of throwing `Node 'X' failed: SDK returned success`. Closes #1425 (#1662).
- **Orchestrator prompt caching restored**: static system context (projects, workflows, routing rules) was embedded in the per-turn `prompt`, forcing the Anthropic API to rebuild the cache prefix on every request (high `cache_creation_input_tokens`, zero `cache_read_input_tokens`). Moved into `systemPrompt.append`, which extends the Claude Code preset and is part of the cacheable system prefix. Fixes #1591 (#1634).
- **Native Claude tools no longer stripped when `skills:` is set without `allowed_tools:`**: the AgentDefinition wrapper previously defaulted `tools` to `['Skill']` only, removing Read/Bash/Write/etc. Now omits `tools` when not explicitly set, letting the SDK provide its full default tool set; `Skill` is still appended when `allowed_tools` is explicit (#1605, #1661).
- **SSH repo URLs from non-GitHub hosts**: the SSH-to-HTTPS converter only matched `git@github.com:` literally, so custom SSH host aliases, GitHub Enterprise, Gitea, GitLab, and Bitbucket SSH URLs produced workspace paths containing literal `git@<host>:` segments — `ENOTDIR` on Windows, malformed owner extraction on Unix. Now uses a generic `git@([^:]+):(.+)` regex at both call sites. Closes #1614 (#1656).
- **`$node.output.field` for Pi/Minimax structured output**: provider-parsed fence-wrapped or preamble-prefixed JSON was captured locally but never persisted onto `NodeOutput`. Downstream `substituteNodeOutputRefs` and `condition-evaluator` consumers then `JSON.parse`d the original prose-prefixed text, threw, and resolved `$node.output.field` to empty. `structuredOutput` is now persisted on `NodeOutput` (single-shot + loop-terminal-iteration success paths) and both consumers prefer the parsed object. Closes #1571 (#1654).
- **Marketplace auto-review CI**: workflow now triggers on `ready_for_review` (was missed by default `pull_request_target` event list); `gh pr review --approve` falls back to `gh pr comment` when GitHub Actions lacks approve permission, so PR authors still receive the review even without "Allow GitHub Actions to create and approve pull requests" enabled.

## [0.3.11] - 2026-05-12

Expand Down
10 changes: 5 additions & 5 deletions homebrew/archon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,28 @@
class Archon < Formula
desc "Remote agentic coding platform - control AI assistants from anywhere"
homepage "https://github.com/coleam00/Archon"
version "0.3.11"
version "0.3.12"
license "MIT"

on_macos do
on_arm do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-darwin-arm64"
sha256 "8f533a879a68edd2b67ca6a9d1d558af853bf3a72cd184e299d8daaac5c26120"
sha256 "c7d53db2672db5126c83f7cf71f5f604de2c4fbeb01996d5cc49741c69c7c421"
end
on_intel do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-darwin-x64"
sha256 "af01596ad112c24c7691cc9a8198bbc31dc205012bf465c692f5f288170f0404"
sha256 "315bf0b27a5748da0ac87dcdeb66ea31b4aa20428ef8183b83161f10f456237a"
end
end

on_linux do
on_arm do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-linux-arm64"
sha256 "ad8913efa0027bda86bf809f6873bde24dd777e076bafc3d5cd40db351dcdcf9"
sha256 "f1897c6cf53a3390b3ab340f79017034efe58b3b6d3d3dded0ce8d263d3782cb"
end
on_intel do
url "https://github.com/coleam00/Archon/releases/download/v#{version}/archon-linux-x64"
sha256 "a5a287e3347cb2c6764889bb86cfe261636ae37db12d3145042cb2c69d81fad5"
sha256 "87c36e9f6368c0658028ab6c867b35138bed5ae3f9d3781d699d876f42bcc0e8"
end
end

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "archon",
"version": "0.3.11",
"version": "0.3.12",
"private": true,
"workspaces": [
"packages/*"
Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/adapters",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/cli",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/cli.ts",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/core",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/docs-web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/docs-web",
"version": "0.3.11",
"version": "0.3.12",
"private": true,
"scripts": {
"dev": "astro dev",
Expand Down
11 changes: 11 additions & 0 deletions packages/docs-web/src/content/docs/guides/authoring-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ tags: [GitLab, Review] # Optional: explicit Web UI filter tags. Overri
# keyword-based tag inference. An empty list (`tags: []`)
# suppresses inference and shows no tags. Omit to fall
# back to inferred tags (the default).
mutates_checkout: false # Optional: when false, the executor allows CONCURRENT runs
# on the same checkout — the path-lock guard is disabled and
# `getActiveWorkflowRunByPath` is not consulted. Safe only when
# every node's writes are per-run-scoped (e.g. artifacts/reports
# inside `$artifactsDir`, never `git commit` or other writes to
# the working tree). Default (omitted or true) keeps the lock
# active so two concurrent runs against the same checkout fail
# fast on the second. DISTINCT from `worktree.enabled: false`:
# that flag controls whether a fresh worktree is created at all;
# `mutates_checkout` controls whether the lock-guard runs ONCE
# you have a checkout (worktree or otherwise).

# Required for DAG-based
nodes:
Expand Down
24 changes: 24 additions & 0 deletions packages/docs-web/src/data/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,28 @@ export const marketplaceEntries: MarketplaceEntry[] = [
tags: ['automation'],
archonVersionCompat: '>=0.3.0',
},
{
slug: 'archon-idea-to-wo',
name: 'Idea to Work Orders',
author: 'lamachine',
description:
'Interactive 8-node workflow that turns a raw idea into BKM-format Work Orders through four AI phases with approval gates between each: understand the idea, scope and approach, risk and decomposition, generate WOs. Output is a directory of self-contained WO files ready to hand to archon-piv-loop.',
sourceUrl:
'https://github.com/coleam00/archon-idea-to-wo/tree/3b0d5d828a4cb375d50bb1252f5e016c44242d01/.archon',
sha: '3b0d5d828a4cb375d50bb1252f5e016c44242d01',
tags: ['planning', 'development'],
archonVersionCompat: '>=0.3.0',
},
{
slug: 'archon-smart-mr-review',
name: 'Smart GitLab MR Review',
author: 'lraphael',
description:
'GitLab counterpart to archon-smart-pr-review. Adaptive code review of a GitLab MR — Haiku classifies which review agents are relevant, runs them in parallel, posts resolvable Discussion threads, and auto-approves on 0 critical findings.',
sourceUrl:
'https://github.com/lraphael/archon-gitlab-workflows/tree/55ca73498f0ead87d86c22ef0efa67482b311700/archon-smart-mr-review',
sha: '55ca73498f0ead87d86c22ef0efa67482b311700',
tags: ['review', 'automation'],
archonVersionCompat: '>=0.3.0',
},
];
2 changes: 1 addition & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/git",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
41 changes: 41 additions & 0 deletions packages/git/src/exec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
import { execFile } from 'child_process';
import { existsSync } from 'fs';
import { mkdir as fsMkdir } from 'fs/promises';
import { promisify } from 'util';

const promisifiedExecFile = promisify(execFile);

/**
* Resolve the bash binary path in a platform-aware way.
*
* On Windows, CreateProcess searches the System32 directory BEFORE the PATH
* env var. Bare `spawn('bash', ...)` therefore resolves to
* `C:\Windows\System32\bash.exe` (the WSL launcher), whose bash has broken
* `${VAR}` expansion when invoked in `-c` mode and uses `/mnt/c/` path
* convention instead of `/c/`. Both break workflow bash nodes.
*
* Fix: on Windows, default to the Git Bash absolute path. Overridable via
* ARCHON_BASH_PATH for non-standard Git installs (e.g. user-scope installer
* at %LOCALAPPDATA%\Programs\Git\bin\bash.exe).
*
* When ARCHON_BASH_PATH is set we eagerly validate the path exists so a typo
* surfaces a clear error here instead of as an opaque ENOENT inside the first
* bash-node fire. The check is best-effort: a path that exists at validate
* time can still race-disappear before exec, but the common case (typo in
* the env var) is what we want to catch immediately.
*
* See: coleam00/Archon#1326
*/
export function resolveBashPath(): string {
const override = process.env.ARCHON_BASH_PATH;
if (override) {
if (!existsSync(override)) {
throw new Error(
`ARCHON_BASH_PATH points to a path that does not exist: '${override}'. ` +
'Either unset the env var to fall back to the platform default, or correct the path ' +
'(on Windows, a common user-scope Git install path is ' +
"'%LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe')."
);
}
return override;
}
if (process.platform === 'win32') {
return 'C:\\Program Files\\Git\\bin\\bash.exe';
}
return 'bash';
}

/** Wrapper around child_process.execFile for test mockability */
export async function execFileAsync(
cmd: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/git/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type {
export { toRepoPath, toBranchName, toWorktreePath } from './types';

// Process and filesystem wrappers
export { execFileAsync, mkdirAsync } from './exec';
export { execFileAsync, mkdirAsync, resolveBashPath } from './exec';

// Worktree operations
export {
Expand Down
2 changes: 1 addition & 1 deletion packages/isolation/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/isolation",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/paths/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/paths",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/providers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/providers",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/server",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"main": "./src/index.ts",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/web",
"version": "0.3.11",
"version": "0.3.12",
"private": true,
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/workflows/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@archon/workflows",
"version": "0.3.11",
"version": "0.3.12",
"type": "module",
"exports": {
"./schemas/*": "./src/schemas/*.ts",
Expand Down
23 changes: 15 additions & 8 deletions packages/workflows/src/dag-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1437,14 +1437,21 @@ describe('executeDagWorkflow -- bash nodes', () => {
{ ...minimalConfig, envVars: { MY_SECRET: 'abc123' } }
);

expect(execSpy).toHaveBeenCalledWith(
'bash',
['-c', 'echo ok'],
expect.objectContaining({
env: expect.objectContaining({ MY_SECRET: 'abc123' }),
})
);
execSpy.mockRestore();
// Expected bash command is platform-aware: `bash` on Linux/macOS, absolute
// Git Bash path on Windows (per resolveBashPath() — coleam00/Archon#1326).
// Wrap the assertion + mockRestore in try/finally so the spy doesn't leak
// into subsequent tests if the assertion fails.
try {
expect(execSpy).toHaveBeenCalledWith(
git.resolveBashPath(),
['-c', 'echo ok'],
expect.objectContaining({
env: expect.objectContaining({ MY_SECRET: 'abc123' }),
})
);
} finally {
execSpy.mockRestore();
}
});

it('bash node output with shell metacharacters does not inject into downstream bash script', async () => {
Expand Down
26 changes: 16 additions & 10 deletions packages/workflows/src/dag-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { readFile } from 'fs/promises';
import { isAbsolute, resolve as resolvePath } from 'path';
import { execFileAsync } from '@archon/git';
import { execFileAsync, resolveBashPath } from '@archon/git';
import { discoverScriptsForCwd } from './script-discovery';
import type {
IWorkflowPlatform,
Expand Down Expand Up @@ -1351,8 +1351,9 @@ async function executeBashNode(
...(envVars ?? {}),
};

const bashPath = resolveBashPath();
try {
const { stdout, stderr } = await execFileAsync('bash', ['-c', finalScript], {
const { stdout, stderr } = await execFileAsync(bashPath, ['-c', finalScript], {
cwd,
timeout,
env: subprocessEnv,
Expand Down Expand Up @@ -1410,7 +1411,7 @@ async function executeBashNode(
if (isTimeout) {
errorMsg = `${label} timed out after ${String(timeout)}ms`;
} else if (err.message?.includes('ENOENT')) {
errorMsg = `${label} failed: bash executable not found in PATH`;
errorMsg = `Bash node '${node.id}' failed: bash executable not found at '${bashPath}'. Set ARCHON_BASH_PATH if Git Bash is installed elsewhere (e.g. user-scope installer at %LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe).`;
} else if (err.message?.includes('EACCES')) {
Comment on lines 1413 to 1415

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

Use platform-aware ENOENT hints and reuse the already-resolved bash path.

Current ENOENT text is Windows-specific on all platforms, and Line 2127 recomputes the path instead of using loopBashPath (can desync diagnostics if env changes).

🔧 Suggested patch
+function getBashResolutionHint(): string {
+  return process.platform === 'win32'
+    ? 'Set ARCHON_BASH_PATH if Git Bash is installed elsewhere (e.g. user-scope installer at %LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe).'
+    : "Ensure 'bash' is installed or set ARCHON_BASH_PATH to a valid bash executable.";
+}
+
 ...
-    } else if (err.message?.includes('ENOENT')) {
-      errorMsg = `Bash node '${node.id}' failed: bash executable not found at '${bashPath}'. Set ARCHON_BASH_PATH if Git Bash is installed elsewhere (e.g. user-scope installer at %LOCALAPPDATA%\\Programs\\Git\\bin\\bash.exe).`;
+    } else if (err.message?.includes('ENOENT')) {
+      errorMsg = `Bash node '${node.id}' failed: bash executable not found at '${bashPath}'. ${getBashResolutionHint()}`;
 ...
-          throw new Error(
-            `Loop node '${node.id}' until_bash failed: cannot execute bash at '${resolveBashPath()}' (${bashErr.code}). Set ARCHON_BASH_PATH if Git Bash is installed elsewhere.`
-          );
+          throw new Error(
+            `Loop node '${node.id}' until_bash failed: cannot execute bash at '${loopBashPath}' (${bashErr.code}). ${getBashResolutionHint()}`
+          );

Also applies to: 2126-2128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workflows/src/dag-executor.ts` around lines 1376 - 1378, The ENOENT
branch currently emits a Windows-specific hint and recomputes the bash path;
update it to reuse the already-resolved loopBashPath variable (instead of
recomputing bashPath) and emit a platform-aware message: on Windows mention
setting ARCHON_BASH_PATH and an example Git Bash path, on POSIX mention
installing bash or ensuring it’s on PATH; include the node id (node.id) and the
loopBashPath value in the errorMsg for clear diagnostics.

errorMsg = `${label} failed: permission denied (check cwd permissions)`;
} else {
Expand Down Expand Up @@ -2169,18 +2170,23 @@ async function executeLoopNode(
nodeOutputs,
true // escapedForBash
);
await execFileAsync('bash', ['-c', substitutedBash], { cwd });
const loopBashPath = resolveBashPath();
await execFileAsync(loopBashPath, ['-c', substitutedBash], { cwd });
bashComplete = true; // exit 0 = complete
} catch (e) {
const bashErr = e as NodeJS.ErrnoException;
// ENOENT or other system errors are unexpected — log them
if (bashErr.code === 'ENOENT') {
getLog().warn(
{ err: bashErr, nodeId: node.id, iteration: i },
'loop_node.until_bash_exec_error'
// System-level errors (ENOENT/EACCES) mean the bash binary itself is
// unreachable or unexecutable — that's environment breakage, not a
// condition-not-met outcome. Surface immediately so the loop fails
// fast instead of burning iterations against a broken binary.
if (bashErr.code === 'ENOENT' || bashErr.code === 'EACCES') {
getLog().error({ err: bashErr, nodeId: node.id, iteration: i }, 'loop.until_bash_failed');
throw new Error(
`Loop node '${node.id}' until_bash failed: cannot execute bash at '${resolveBashPath()}' (${bashErr.code}). Set ARCHON_BASH_PATH if Git Bash is installed elsewhere.`
);
}
bashComplete = false; // non-zero exit = not complete
// Non-zero exit from the bash script = condition not met yet, keep looping.
bashComplete = false;
}
}

Expand Down
Loading
Loading