Skip to content

fix(server): wire ~/.archon/workflows/ into single-workflow REST endpoints#1525

Open
blankse wants to merge 1 commit intocoleam00:devfrom
blankse:fix/1524-home-scoped-workflow-endpoints
Open

fix(server): wire ~/.archon/workflows/ into single-workflow REST endpoints#1525
blankse wants to merge 1 commit intocoleam00:devfrom
blankse:fix/1524-home-scoped-workflow-endpoints

Conversation

@blankse
Copy link
Copy Markdown

@blankse blankse commented May 1, 2026

Summary

  • Problem: GET, PUT, and DELETE /api/workflows/{name} all skip the home-scoped layer (~/.archon/workflows/), even though the list endpoint advertises those workflows in the dashboard. End-result: a home-scoped workflow shows up in the list, can be run from the CLI, but the run-detail page is stuck on "Loading graph…" forever because the GET that fetches the DAG topology returns 404. Saving from the dashboard with no project selected lands the YAML in ~/.archon/.archon/workflows/ (the legacy pre-0.x path the migration warning explicitly tells users to abandon).
  • Why it matters: Reproduced locally with a team-shared workflow set symlinked into ~/.archon/workflows/. Discovery sees them, the dashboard lists them, runs work — only the detail page hangs. CLAUDE.md documents the priority bundled < global < project; the single-workflow endpoints just walk project → bundled, missing the middle layer entirely.
  • What changed: GET now reads getHomeWorkflowsPath()/<name>.yaml between the project and bundled tiers, returning source: 'global'. PUT/DELETE now operate on ~/.archon/workflows/ (not ~/.archon/.archon/workflows/) when no project is registered. The WorkflowSource type already had 'global'; the discovery layer already used it.
  • What did NOT change (scope boundary): WorkflowExecution.tsx's "Loading graph…" state with no error path. Even after the API fix, an actual 404 / network error still hangs the UI forever — flagged in bug(server): GET/PUT/DELETE /api/workflows/{name} miss home-scoped (~/.archon/workflows/) — workflow detail page hangs #1524 as a separate Web UI follow-up.

UX Journey

Before

User                   Web UI                   API server                       Filesystem
────                   ──────                   ──────────                       ──────────
clicks a run row ────▶                                                              
in dashboard           /workflows/runs/<id>                                         
                       loads run via                                                
                       GET /workflows/runs/<id> ─▶ 200 OK                          
                                                                                    
                       fetches DAG via                                              
                       GET /workflows/<name>  ──▶ readFile(/proj/.archon/wf...)──▶ ENOENT
                                                  Object.hasOwn(BUNDLED, name)─?─▶ false
                                                  return 404                       
                                                                                    
                       (Workflow exists at ~/.archon/workflows/<name>.yaml,        
                        but the GET handler never looks there.)                    
                                                                                    
                       useQuery returns error;                                      
                       component renders "Loading graph…" forever                  
                       (no error UI, no fallback)                                  
                                                                                    
sees infinite spinner; no obvious recovery path

After

User                   Web UI                   API server                       Filesystem
────                   ──────                   ──────────                       ──────────
clicks a run row ────▶                                                              
in dashboard                                                                        
                       fetches DAG via                                              
                       GET /workflows/<name>  ──▶ readFile(/proj/.archon/wf...)──▶ ENOENT
                                                  *readFile(~/.archon/workflows/*  
                                                   *<name>.yaml)              ──▶ HIT*
                                                  return 200, source: 'global'    
                                                                                    
                       useQuery hydrates;                                          
                       graph renders                                               
                                                                                    
sees DAG immediately

Architecture Diagram

Before

                              ┌─────────────────────┐
                              │  Web UI             │
                              │  Run-detail page    │
                              └──────────┬──────────┘
                                         │ GET /api/workflows/{name}
                                         ▼
                              ┌─────────────────────┐
                              │  getWorkflowRoute   │
                              └──────────┬──────────┘
                                         │
                       ┌─────────────────┼─────────────────┐
                       ▼                                   ▼
              ┌────────────────┐                  ┌────────────────┐
              │ <cwd>/.archon/ │                  │ BUNDLED_       │
              │   workflows/   │                  │  WORKFLOWS map │
              │  (project)     │                  │  (or default-  │
              │                │                  │   path in dev) │
              └────────────────┘                  └────────────────┘

                              ┌────────────────────┐
                              │  ~/.archon/        │  ← exists, populated,
                              │   workflows/       │    visible to LIST endpoint,
                              │  (home-scoped)     │    NEVER CONSULTED here
                              └────────────────────┘

After

                              ┌─────────────────────┐
                              │  Web UI             │
                              │  Run-detail page    │
                              └──────────┬──────────┘
                                         │ GET /api/workflows/{name}
                                         ▼
                              ┌─────────────────────┐
                              │  getWorkflowRoute   │
                              └──────────┬──────────┘
                                         │
                  ┌──────────────────────┼──────────────────────┐
                  ▼                      ▼                      ▼
         ┌────────────────┐   ┌─────────────────────┐   ┌────────────────┐
         │ <cwd>/.archon/ │   │ [+] ~/.archon/      │   │ BUNDLED_       │
         │   workflows/   │ ▶ │     workflows/      │ ▶ │  WORKFLOWS map │
         │  (project)     │   │ [+] (home-scoped)   │   │  (or default-  │
         │                │   │     source:'global' │   │   path in dev) │
         └────────────────┘   └─────────────────────┘   └────────────────┘

                              ┌─────────────────────┐
                              │  PUT/DELETE         │
                              │   no cwd ?          │
                              │   no codebase ?     │
                              └──────────┬──────────┘
                                         │
                  Before                 │                After
        ────────────────────────         │     ─────────────────────────
        join(getArchonHome(),            │     getHomeWorkflowsPath()
              workflowFolder)            │     = ~/.archon/workflows/
        = ~/.archon/.archon/             │     (current home-scoped path,
                workflows/               │      source:'global')
        (legacy migration target)        │

Connection inventory:

From To Status Notes
getWorkflowRoute<cwd>/.archon/workflows/ unchanged tier 1
getWorkflowRoute~/.archon/workflows/ new tier 2 (was missing)
getWorkflowRouteBUNDLED_WORKFLOWS unchanged tier 3
saveWorkflowRoute<cwd>/.archon/workflows/ unchanged when project resolved
saveWorkflowRoute~/.archon/workflows/ modified target was ~/.archon/.archon/workflows/ (legacy)
deleteWorkflowRoute~/.archon/workflows/ modified target same correction as PUT
Discovery (discoverWorkflowsWithConfig) → ~/.archon/workflows/ unchanged already correct, served as the reference behavior

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: server
  • Module: server:api-routes

Change Metadata

  • Change type: bug
  • Primary scope: server

Linked Issue

Validation Evidence

$ bun run check:bundled
bundled-defaults.generated.ts is up to date (36 commands, 20 workflows).

$ bun run type-check
all 10 packages: Exited with code 0

$ NODE_OPTIONS='--max-old-space-size=8192' bun run lint --max-warnings 0
EXIT=0   # 0 errors, 0 warnings

$ bun run format:check
All matched files use Prettier code style!

$ bun --filter @archon/server test
api.workflows.test.ts: 30 pass / 0 fail (3 new tests + 1 updated test)
all other server suites green
  • Evidence provided: api.workflows.test.ts adds three regression tests covering the GET home-scoped hit, the project-shadows-home priority, and DELETE on a real home-scoped file. The existing PUT "no cwd / no codebases" test was updated to verify the YAML lands in ~/.archon/workflows/ (not the legacy path) and is reported with source: 'global'. Pre-existing loader.test.ts / dag-executor.test.ts failures on dev reproduce on a clean checkout in this WSL2 environment without my changes — unrelated.
  • Skipped commands: bun run lint without raised heap OOMs in WSL2 on dev (pre-existing). Ran with NODE_OPTIONS='--max-old-space-size=8192' and got clean.
  • Manual: ran curl http://localhost:3000/api/workflows/df-implement-with-preview-fast against a fresh server before the fix → 404 (workflow at ~/.archon/workflows/...); after the fix returns 200 with source: 'global' and the run-detail page renders the DAG.

Security Impact

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? Slightly tightened: ~/.archon/workflows/ was already read by the discovery code path; this PR adds read access for the same path from the GET handler and redirects PUT/DELETE writes from the legacy ~/.archon/.archon/workflows/ to ~/.archon/workflows/. No new directory the server didn't already touch via discovery.

Compatibility / Migration

  • Backward compatible? Yes for read paths. PUT/DELETE change where no-project saves land: previously ~/.archon/.archon/workflows/, now ~/.archon/workflows/. Anyone with workflows already saved at the legacy path continues to be told by workflow-discovery.ts to migrate via the documented mv command (warning is unchanged). New saves go to the correct location automatically.
  • Config/env changes? No
  • Database migration needed? No

Human Verification

  • Verified scenarios:
    • GET against a home-scoped workflow returns 200 with source: 'global' (real fixture under tmpdir + process.env.ARCHON_HOME).
    • GET respects priority: a project-scoped same-named workflow shadows the home-scoped one.
    • GET returns 404 when nothing matches (tier-3 bundled fallback intact).
    • PUT with no codebase + no cwd writes to ~/.archon/workflows/<name>.yaml and returns source: 'global'. Verified the file is on disk at the expected path.
    • DELETE removes the home-scoped file under ~/.archon/workflows/ and returns deleted: true. Verified the file is actually gone.
    • End-to-end: rebuilt with the fix, opened a stuck run-detail page in the dashboard — graph renders.
  • Edge cases checked:
    • Project-scoped GET still wins when both cwd and home have the same name.
    • Bundled defaults still resolve when neither project nor home has the file.
    • Existing PUT/DELETE with cwd set unchanged — project paths still go through getWorkflowFolderSearchPaths().
    • DELETE of a bundled name still 400s (refused).
  • What was not verified: the Web UI "Loading graph…" silent-spinner state itself — this PR is API-only by design, see bug(server): GET/PUT/DELETE /api/workflows/{name} miss home-scoped (~/.archon/workflows/) — workflow detail page hangs #1524 follow-up.

Side Effects / Blast Radius

  • Affected subsystems: only packages/server/src/routes/api.ts (3 endpoints).
  • Potential unintended effects: a user who has been creating workflows via the dashboard with no project selected and expecting them at the legacy path would see new saves go to the new path. The migration warning has been telling those users to move their files for the same reason, so this is alignment, not breakage.
  • Guardrails/monitoring: log events workflow.fetch_failed, workflow.fetch_home_failed, workflow.save_failed, workflow.delete_failed keep the existing shape; a regression in the home tier surfaces as workflow.fetch_home_failed.

Rollback Plan

  • Fast rollback: revert the single commit on dev. No state migration. Existing files at ~/.archon/workflows/ written by the new PUT path will be re-discovered via the LIST endpoint regardless (that part already worked).
  • Feature flags: none — change is unconditional.
  • Observable failure symptoms: GET returns 404 again for any home-scoped workflow → run-detail page hangs again. Caught by the new api.workflows.test.ts tests.

Risks and Mitigations

  • Risk: the home-scoped readFile adds one extra filesystem stat on the GET path for every project-scoped miss. Negligible; it's a single ENOENT-or-hit on a fixed path. Mitigation: none required.
  • Risk: a future schema change to WorkflowSource could re-surface the literal string in the JSON response. Mitigation: tests assert source === 'global' directly so any drift fails CI.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved workflow discovery with proper precedence layering: project-scoped workflows now correctly shadow home-scoped versions
    • Enhanced workflow save and delete operations to properly resolve target directories
    • Better fallback handling for home-directory workflow management
  • Tests

    • Added regression coverage for workflow path resolution and precedence validation across all workflow endpoints

…ingle-workflow REST endpoints

Background

`GET /api/workflows` (list) reads home-scoped workflows correctly via
`discoverWorkflowsWithConfig` — files placed under `~/.archon/workflows/`
appear in the dashboard, run successfully, and persist across projects
exactly as documented in CLAUDE.md (`bundled < global < project`).

The single-workflow REST endpoints (`GET`, `PUT`, `DELETE
/api/workflows/{name}`) never got the memo. Their lookup ladder skips
straight from project-scoped to bundled, so any home-scoped workflow
returns 404 from `GET`. The Web UI's run-detail page calls `getWorkflow`
to render the DAG graph; the 404 leaves it stuck on "Loading graph…"
forever.

`PUT` and `DELETE` had a related but distinct bug: when no codebase is
registered they fell back to `workingDir = getArchonHome()` and joined
the workflow folder as `<archonHome>/.archon/workflows/` — which is the
legacy pre-0.x location whose only remaining purpose is to emit the
migration warning. New workflows created from the dashboard with no
project landed there silently and stayed invisible to discovery.

Closes coleam00#1524.

Change

1. `GET /api/workflows/{name}` gains a step 2 between project-scoped and
   bundled: read `getHomeWorkflowsPath()/<name>.yaml` and return
   `source: 'global'` on hit. Mirrors the discovery layer's documented
   priority. ENOENT falls through; any other error surfaces as 500.

2. `PUT /api/workflows/{name}`: when no `cwd` and no codebase exists,
   write to `getHomeWorkflowsPath()` directly (`source: 'global'`)
   instead of joining `getArchonHome()` with the project workflow
   folder. Project-scoped saves are unchanged.

3. `DELETE /api/workflows/{name}`: same correction as PUT — operate on
   `getHomeWorkflowsPath()` for the no-project case.

The `WorkflowSource` type already includes `'global'`; the discovery
code already labels home-scoped workflows that way. The endpoints now
agree with the rest of the system.

Tests

- New: GET returns `source: 'global'` for a home-scoped file.
- New: GET project-shadows-home (regression cover for the documented
  priority).
- New: DELETE removes a real home-scoped file when no project is
  registered, and verifies the file actually disappears from
  `~/.archon/workflows/`.
- Updated: the existing "fall back to getArchonHome() when no cwd"
  PUT test now asserts `source: 'global'` and verifies the file lands
  at `~/.archon/workflows/<name>.yaml`, not at the legacy
  `<archonHome>/.archon/workflows/...` path.
- Tests pull `ARCHON_HOME` via `process.env`, matching the existing
  pattern in this file.

Out of scope

`WorkflowExecution.tsx` shows "Loading graph…" without a timeout or
error path when the GET returns anything other than 200 — flagged in
coleam00#1524 as a separate Web UI follow-up. Not touched here.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

The workflow API endpoints now implement tiered workflow discovery: GET reads from project-scoped, then home-scoped (~/.archon/workflows/), returning source: 'global' for home-scoped results; PUT and DELETE resolve targets using validated cwd or first registered codebase default_cwd, otherwise defaulting to home-scoped paths with updated logging.

Changes

Cohort / File(s) Summary
Workflow Route Implementation
packages/server/src/routes/api.ts
Added fallback discovery layer for GET endpoint to read home-scoped workflows with source: 'global' after project-scoped lookup fails; refactored PUT/DELETE path resolution to prioritize projectDir from validated cwd or default_cwd, falling back to home-scoped directory; improved error logging for save operations.
Workflow Route Tests
packages/server/src/routes/api.workflows.test.ts
Added regression tests verifying GET returns home-scoped workflows with source: "global" and that project-scoped workflows shadow home-scoped ones; added PUT tests confirming fallback saves to home-scoped directory; added DELETE test verifying deletion from home-scoped directory when no cwd/codebases registered.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Through nested paths the workflows quest,
Home-scoped fallbacks do their best,
Project shadows global light,
Discovery layered, precedence right! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main change: wiring the home-scoped workflows directory into the single-workflow REST endpoints (GET, PUT, DELETE).
Description check ✅ Passed The description is comprehensive and follows the template structure with all required sections: Summary, UX Journey (Before/After), Architecture Diagram with connection inventory, Labels, Change Metadata, Linked Issues, Validation Evidence, Security Impact, Compatibility, Human Verification, Side Effects, Rollback Plan, and Risks/Mitigations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/server/src/routes/api.workflows.test.ts`:
- Around line 256-293: The test mutates process.env.ARCHON_HOME and the finally
block unconditionally deletes it; capture the original value before setting
ARCHON_HOME (e.g., const prevArchonHome = process.env.ARCHON_HOME), set
process.env.ARCHON_HOME = testArchonHome for the test, and in the finally block
restore it (process.env.ARCHON_HOME = prevArchonHome or delete if prevArchonHome
is undefined) instead of always deleting; apply the same pattern to the other
affected tests (the blocks around lines 295-334, 397-437, 537-569) to avoid
leaking environment state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d109f173-1b47-4e93-b7d3-8e8bbd72cb68

📥 Commits

Reviewing files that changed from the base of the PR and between 69b2c89 and 5be4214.

📒 Files selected for processing (2)
  • packages/server/src/routes/api.ts
  • packages/server/src/routes/api.workflows.test.ts

Comment on lines +256 to +293
test('returns home-scoped workflow with source:global when only ~/.archon/workflows/ has it', async () => {
// Regression for #1524: home-scoped workflows are listed by the
// dashboard but were unfetchable via GET, breaking the run detail page.
const testArchonHome = join(tmpdir(), `archon-home-get-${Date.now()}`);
const homeWorkflowDir = join(testArchonHome, 'workflows');
await mkdir(homeWorkflowDir, { recursive: true });
await writeFile(
join(homeWorkflowDir, 'team-shared.yaml'),
'name: team-shared\ndescription: shared\nnodes:\n - id: n\n command: c\n'
);
process.env.ARCHON_HOME = testArchonHome;

try {
const app = createTestApp();
registerApiRoutes(app, {} as WebAdapter, {} as ConversationLockManager);

// No project-scoped match; falls through to home-scoped.
mockListCodebases.mockImplementationOnce(async () => []);
mockParseWorkflow.mockReturnValueOnce({
workflow: makeTestWorkflow({ name: 'team-shared', description: 'shared' }),
error: null,
});

const response = await app.request('/api/workflows/team-shared');
expect(response.status).toBe(200);
const body = (await response.json()) as {
source: string;
filename: string;
workflow: { name: string };
};
expect(body.source).toBe('global');
expect(body.filename).toBe('team-shared.yaml');
expect(body.workflow.name).toBe('team-shared');
} finally {
delete process.env.ARCHON_HOME;
await rm(testArchonHome, { recursive: true, force: true });
}
});
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 | ⚡ Quick win

Restore any pre-existing ARCHON_HOME instead of always deleting it.

These tests mutate a process-global env var, but finally always deletes it. If the runner already had ARCHON_HOME set, later tests inherit the wrong environment and the suite becomes order-dependent.

Suggested fix
-    process.env.ARCHON_HOME = testArchonHome;
+    const previousArchonHome = process.env.ARCHON_HOME;
+    process.env.ARCHON_HOME = testArchonHome;

     try {
       // ...
     } finally {
-      delete process.env.ARCHON_HOME;
+      if (previousArchonHome === undefined) {
+        delete process.env.ARCHON_HOME;
+      } else {
+        process.env.ARCHON_HOME = previousArchonHome;
+      }
       await rm(testArchonHome, { recursive: true, force: true });
     }

As per coding guidelines, **/*.test.ts: "keep tests deterministic — no flaky timing or network dependence without guardrails".

Also applies to: 295-334, 397-437, 537-569

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

In `@packages/server/src/routes/api.workflows.test.ts` around lines 256 - 293, The
test mutates process.env.ARCHON_HOME and the finally block unconditionally
deletes it; capture the original value before setting ARCHON_HOME (e.g., const
prevArchonHome = process.env.ARCHON_HOME), set process.env.ARCHON_HOME =
testArchonHome for the test, and in the finally block restore it
(process.env.ARCHON_HOME = prevArchonHome or delete if prevArchonHome is
undefined) instead of always deleting; apply the same pattern to the other
affected tests (the blocks around lines 295-334, 397-437, 537-569) to avoid
leaking environment state.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented May 4, 2026

@blankse related to #1524 — home-scoped workflow REST endpoints.

@leex279
Copy link
Copy Markdown
Collaborator

leex279 commented May 4, 2026

@blankse this PR looks similar to #1560, which was closed on 2026-05-04 (closed without merging). You may want to read the discussion there before pushing further.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(server): GET/PUT/DELETE /api/workflows/{name} miss home-scoped (~/.archon/workflows/) — workflow detail page hangs

3 participants