Skip to content

Fix #1516: non-destructive sync default + post-message reminder#1536

Open
ztech-gthb wants to merge 12 commits intocoleam00:devfrom
ztech-gthb:fix/issue-1516-soft-sync-workspace
Open

Fix #1516: non-destructive sync default + post-message reminder#1536
ztech-gthb wants to merge 12 commits intocoleam00:devfrom
ztech-gthb:fix/issue-1516-soft-sync-workspace

Conversation

@ztech-gthb
Copy link
Copy Markdown

@ztech-gthb ztech-gthb commented May 2, 2026

Summary

UX Journey

Before

Turn N:
  user                  archon                                  source/
  ────                  ──────                                  ───────
  "fix the typo" ─────▶ discoverAllWorkflows
                        └─▶ syncWorkspace (reset --hard) ─────▶  HEAD = origin/main  [silent reset]
                        └─▶ aiClient.sendQuery
  "edited foo.ts"  ◀──  agent edits + commits ──────────────▶   HEAD = X (new commit)

Turn N+1:
  "now also rename" ──▶ discoverAllWorkflows
                        └─▶ syncWorkspace (reset --hard) ─────▶  HEAD = origin/main  [DESTROYED commit X]
                        └─▶ aiClient.sendQuery
  "renamed bar"   ◀──  agent reads source/, sees                 (foo.ts edit gone, agent re-fixes
                        "the typo isn't fixed", redoes,           the typo and re-commits, then
                        commits ──────────────────────────────▶   the cycle repeats next turn)

After

Turn N:
  user                  archon                                  source/
  ────                  ──────                                  ───────
  "fix the typo" ─────▶ discoverAllWorkflows
                        └─▶ syncWorkspace (mode: fast-forward) ▶ [HEAD unchanged unless behind]
                        └─▶ aiClient.sendQuery
  "edited foo.ts"  ◀──  agent edits + commits ──────────────▶   HEAD = X
                        └─▶ post-message reminder
  "⚠ source/ has 1 unpushed commit on <branch>" ◀──── (reminder)

Turn N+1:
  "now also rename" ──▶ discoverAllWorkflows
                        └─▶ syncWorkspace (mode: fast-forward) ▶ state='ahead' → noop, [X PRESERVED]
                        └─▶ aiClient.sendQuery
  "renamed bar"   ◀──  agent reads source/, sees X is there,
                        builds on top, commits Y ─────────────▶   HEAD = Y (parent: X)
                        └─▶ "⚠ 2 unpushed commits"   ◀────────── (reminder)

Architecture Diagram

Before

orchestrator-agent.ts:handleMessage
        │
        └─▶ discoverAllWorkflows
              └─▶ syncWorkspace(default_cwd, undefined, { resetAfterFetch: isManagedClone })
                       │
                       └─▶ git/repo.ts:syncWorkspace
                             ├─ fetch
                             └─ if isManagedClone: reset --hard origin/<auto-detected-branch>

isolation/worktree.ts:syncWorkspaceBeforeCreate
        │
        └─▶ syncWorkspace(repoPath, configBranch, { resetAfterFetch: isManagedClone })
                       └─▶ same as above

After

orchestrator-agent.ts:handleMessage
        │
        ├─▶ discoverAllWorkflows
        │     └─▶ syncWorkspace(default_cwd, default_branch, /* default mode: 'fast-forward' */)   [~]
        │              │
        │              └─▶ git/repo.ts:syncWorkspace                                                [~]
        │                    ├─ fetch (unchanged)
        │                    ├─ inspect HEAD vs origin (state: in_sync|behind|ahead|diverged|dirty) [+]
        │                    ├─ mode='reset': reset --hard origin/<branch> (legitimate callers)
        │                    └─ mode='fast-forward': merge --ff-only iff state='behind' AND
        │                                            current branch == target  [+ branch check]
        │
        └─▶ post-message-reminder.ts:reportUnpushedWorkInSource                                     [+]
              └─▶ countCommitsAhead + hasUncommittedChanges → system_status SSE

isolation/worktree.ts:syncWorkspaceBeforeCreate
        │
        └─▶ syncWorkspace(repoPath, configBranch, { mode: isManagedClone ? 'reset' : 'fast-forward' })
                                                                                                    [~]

git/branch.ts
  ├─ getCurrentBranch(repoPath)                                                                     [+]
  └─ countCommitsAhead(repoPath, branch)                                                            [+]

Connection inventory:

From To Status Notes
orchestrator-agent.ts syncWorkspace modified now passes default_branch from DB and uses default mode: 'fast-forward'
worktree.ts syncWorkspace modified passes explicit mode: 'reset' for managed clones
orchestrator-agent.ts post-message-reminder.ts new end of handleMessage calls reminder if codebase attached
post-message-reminder.ts git/branch.ts:countCommitsAhead new unpushed-count helper
post-message-reminder.ts git/branch.ts:hasUncommittedChanges unchanged consumer existing helper, new caller
clone.ts:cloneRepository getCurrentBranch new detects post-clone branch, persists into default_branch
handleRegisterProject getCurrentBranch new detects branch on /register-project
db/codebases.ts:createCodebase default_branch column modified new optional field, NULL means "auto-detect"

Label Snapshot

  • Risk: risk: medium — touches the canonical clone of every chat session, but the destructive path is preserved for legitimate callers and the new path is strictly less destructive than the old default.
  • Size: size: M
  • Scope: core, git, isolation, db, server
  • Module: core:orchestrator, git:repo, isolation:worktree, core:db.codebases

Change Metadata

  • Change type: bug
  • Primary scope: multi

Linked Issue

Validation Evidence (required)

bun run type-check      # clean across all 10 packages
bun run test            # ~1200 expects, 0 fails (after CodeRabbit-driven test fixture updates)
# Per-commit: lint-staged (eslint --max-warnings 0 + prettier --write) clean on every staged file

Specific test runs exercised by the touched code:

bun --filter @archon/git test              # 143 pass / 0 fail
bun --filter @archon/isolation test        # 251 pass / 0 fail (across 4 sub-suites)
bun --filter @archon/core test             # 815 pass / 0 fail (across 14 sub-suites)

CodeRabbit review round 1 — all five actionable comments addressed (commits 1db8f711, 4c5fcd7a, 7c617811, 70a39b10, ef76cd67) plus one self-spotted test fixture gap (0031e945).

End-to-end manual verification in production-equivalent setup (Docker Compose, real DB, real codebase): see Human Verification section below.

Security Impact (required)

  • New permissions/capabilities? No.
  • New external network calls? Nogit fetch is the same network operation as before; this PR does fewer destructive ops, not more.
  • Secrets/tokens handling changed? No.
  • File system access scope changed? No — same paths, less destructive ops on them.
  • The branch check before fast-forward (70a39b10) explicitly prevents an unintended HEAD movement when the local checkout is on a topic branch — fixes a subtle correctness gap rather than introducing one.

Compatibility / Migration

  • Backward compatible? Mostly yes — chat-tick behaviour changes from "destructive on every message" to "preserve local state" (a strict UX improvement). One subtle behavioural change in worktree.ts: locally-registered repos under non-managed paths previously got resetAfterFetch: false (fetch only); now they get mode: 'fast-forward' (fetch + ff-merge if behind). Discussed inline at the call site; happy to revert to fetch-only for that branch if reviewers prefer the strict pre-PR semantics there.
  • Config/env changes? No.
  • Database migration needed? Yes — adds nullable default_branch column to remote_agent_codebases:
    • PostgreSQL: migration 022_add_default_branch_to_codebases.sql (idempotent, no DEFAULT — preserves NULL on upgrade).
    • SQLite: migrateColumns() adds the column on adapter init (also no DEFAULT).
    • Baseline 000_combined.sql updated.
    • Upgrade path: existing rows stay NULL (which is interpreted as "not yet detected" → falls back to getDefaultBranch auto-detect). No manual data fix required.

Human Verification (required)

End-to-end test in production-equivalent setup (macOS Docker Compose, real SQLite DB, real managed clone of FortiGapMinder repo):

  • Pre-test: HEAD ca7c0ed (= origin/main), branch fix/marimo-dag-mo-import, working tree clean.

  • Setup: git pull --ff-only to advance HEAD to fb3ec5c, then committed .gitignore + .agents/skills/marimo-notebook/ locally on the feature branch (without pushing). HEAD now 2 commits ahead of origin/fix/marimo-dag-mo-import.

  • Trigger: free-text chat message "auf welchem Branch stehen wir?" (a read-only-from-the-user-side question that forces the agent into source/).

  • Observed:

    • Agent's bash call hit /.archon/workspaces/.../FortiGapMinder/source (= the canonical clone, exactly the path that used to be reset every tick).
    • Agent answered "Wir stehen auf Branch fix/marimo-dag-mo-import" — correctly read git state.
    • System SSE event posted: "source/ has 2 unpushed commits on fix/marimo-dag-mo-import. Push or commit + push to preserve — local-only state may be lost on the next worktree creation, manual checkout, or re-clone." (= the reminder)
    • Reflog: no new reset: moving to origin/... entry. HEAD still at ddfae43 (the latest local commit). Both commits intact.
    • Pre-PR equivalent would have produced: HEAD reset to ca7c0ed (or fb3ec5c), both local commits orphaned, .gitignore and the marimo skill files gone from the working tree.
  • Edge cases checked: state='dirty' from untracked files (current isDirty counts untracked → blocks ff-merge — defensive but somewhat over-eager; happy to add a follow-up to use --untracked-files=no if reviewers prefer ff-merging through untracked).

  • What was not verified: the explicit mode: 'reset' path on a worktree-creation has been exercised by automated unit tests and by an archon-assist workflow run in the same setup (which logged two reset: moving to origin/main events as expected). Direct manual verification of preservation across a parallel-push race was not run.

Side Effects / Blast Radius (required)

  • Affected subsystems: every chat message that has a codebase attached touches the modified path. Worktree creation also routes through syncWorkspace and is exercised by the same change.
  • Potential unintended effects:
    • state='dirty' is dominant over ancestor checks (untracked files block ff-merge). Not destructive, but defensive — a follow-up to refine with --untracked-files=no is in mind.
    • The new default_branch column is consumed in two places (orchestrator-agent.ts:434 and worktree.ts:syncWorkspaceBeforeCreate). Existing rows with NULL fall back to auto-detect (= pre-PR behaviour for branch resolution).
  • Guardrails: SSE system_status event surfaces Fast-forwarded ... and diverged states to the UI. Existing workspace.sync_completed debug log carries the new state field for grepability.

Rollback Plan (required)

  • Fast rollback: git revert of the merge commit, redeploy. Any DB rows with non-NULL default_branch keep that column populated; no data loss. The migration is forward-only but harmless if rolled back to old code (column simply unused).
  • Feature flags: none — could be added if review prefers a rollout flag for the chat-tick mode change. (Not done initially because the new mode is strictly less destructive than the old one and trivially observable via reflog.)
  • Observable failure symptoms:

Risks and Mitigations


🤖 Investigation, write-up, and implementation produced with extensive back-and-forth using Claude — final code, naming choices, and architectural decisions reviewed and accepted by ztech-gthb.

Summary by CodeRabbit

  • New Features
    • Default repository branch is now detected on clone/registration and stored for future syncs.
    • Workspace sync defaults to a safer, non‑destructive "fast‑forward" mode that preserves local work.
    • Sync outcomes now report clearer states (in_sync/behind/ahead/diverged/dirty) and emit fewer, more actionable messages.
    • Repositories are checked for unpushed commits/uncommitted changes and will generate a single structured reminder when needed.

Zolto added 3 commits May 2, 2026 12:31
…1273)

Backports the not-yet-merged PR coleam00#1273 fix as a prerequisite for coleam00#1516. syncWorkspace was
   always called with baseBranch=undefined, causing auto-detection to origin/HEAD and git reset --hard on every message for managed clones.

- Codebase.default_branch
  (nullable) + createCodebase persist

- cloneRepository detects post-clone branch and stores it

- syncWorkspace receives toBranchName(default_branch) with null→undefined
   fallback

- getCurrentBranch() helper in @archon/git

- handleRegisterProject detects current branch on register

- Codebase OpenAPI schema (nullable)

-
  Migration 022 (no DEFAULT — preserve NULL on upgrade)

- SQLite migrateColumns entry

- 000_combined.sql baseline updated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d68bcc24-7a8d-477f-a18f-cce841dcaebc

📥 Commits

Reviewing files that changed from the base of the PR and between 518cefa and be9dcb7.

📒 Files selected for processing (1)
  • packages/core/src/orchestrator/orchestrator-agent.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/orchestrator/orchestrator-agent.ts

📝 Walkthrough

Walkthrough

Adds a nullable default_branch to codebases, persists detected branch at clone/registration, refactors git sync to a mode-driven API with pre-sync state reporting, adds branch helpers and unpushed-work reporting, and updates call sites and tests to the new sync contract.

Changes

Default Branch Tracking & Sync Mode Refactor

Layer / File(s) Summary
Database Schema
migrations/000_combined.sql, migrations/022_add_default_branch_to_codebases.sql
Adds nullable default_branch TEXT to remote_agent_codebases; migration uses ALTER TABLE ... ADD COLUMN IF NOT EXISTS and avoids a SQL DEFAULT.
Schema Migration Runner
packages/core/src/db/adapters/sqlite.ts
migrateColumns() checks for and conditionally adds default_branch; createSchema() no longer sets DEFAULT 'main' for default_branch.
Type & API Shapes
packages/core/src/types/index.ts, packages/server/src/routes/schemas/codebase.schemas.ts
Codebase type and server schema gain `default_branch: string
Persistence
packages/core/src/db/codebases.ts, packages/core/src/db/codebases.test.ts
createCodebase accepts and persists `default_branch?: string
Clone/Register Flow
packages/core/src/handlers/clone.ts
After clone, detect branch via git rev-parse --abbrev-ref HEAD (non-fatal) and pass detected branch into registerRepoAtPath, which persists default_branch.
Orchestrator Integration
packages/core/src/orchestrator/orchestrator-agent.ts, packages/core/src/orchestrator/orchestrator-agent.test.ts
Discovery syncs canonical clone using codebase.default_branch; registration detects current branch via getCurrentBranch; structured sync events narrowed to specific states; post-response calls reportUnpushedWorkInSource.
Unpushed Work Reporting
packages/core/src/orchestrator/post-message-reminder.ts
Adds reportUnpushedWorkInSource(platform, conversationId, codebase) that checks hasUncommittedChanges and countCommitsAhead and emits a single structured system event when local unpushed/uncommitted work exists; errors are logged and swallowed.
Git Utilities
packages/git/src/branch.ts, packages/git/src/index.ts
Adds getCurrentBranch(repoPath) and countCommitsAhead(repoPath, branch) (returns -1 when origin/<branch> is missing); re-exports updated.
Sync Types & Behavior
packages/git/src/types.ts, packages/git/src/repo.ts, packages/git/src/git.test.ts
Adds `SyncMode = 'fast-forward'
Provider & Tests
packages/isolation/src/providers/worktree.ts, packages/isolation/src/providers/worktree.test.ts
Worktree provider selects { mode } based on canonical path and calls syncWorkspace(..., { mode }); tests updated for new call shape and return shape including state.

Sequence Diagram

sequenceDiagram
    participant Client
    participant CloneHandler as Clone Handler
    participant DB as Database
    participant Orchestrator
    participant Git as Git Utils
    participant SyncManager as Sync Manager

    Client->>CloneHandler: request clone
    CloneHandler->>Git: git clone <repo>
    Git-->>CloneHandler: cloned
    CloneHandler->>Git: getCurrentBranch(repoPath)
    Git-->>CloneHandler: branch name
    CloneHandler->>DB: createCodebase(..., default_branch)
    DB-->>CloneHandler: created

    Orchestrator->>DB: load codebase
    DB-->>Orchestrator: codebase (with default_branch)
    Orchestrator->>SyncManager: syncWorkspace(path, default_branch, {mode: 'fast-forward'})
    SyncManager->>Git: git fetch origin/<branch>
    Git-->>SyncManager: fetch ok
    SyncManager->>SyncManager: inspect HEAD vs origin (dirty/ancestor)
    alt state == 'behind' and current branch == branchToSync
        SyncManager->>Git: git merge --ff-only origin/<branch>
    else if mode == 'reset'
        SyncManager->>Git: git reset --hard origin/<branch>
    else
        SyncManager-->>Orchestrator: no-op (preserve local state)
    end
    SyncManager-->>Orchestrator: WorkspaceSyncResult {state, synced, updated}
    Orchestrator->>Git: countCommitsAhead(path, branch)
    Git-->>Orchestrator: aheadCount / -1
    Orchestrator->>Orchestrator: reportUnpushedWorkInSource if needed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I sniffed the branch the clone forgot,
I saved its name in a cozy spot.
Fast‑forwards tiptoe, resets stay near,
I count the hops that haven't left here.
🌱 A rabbit's nod: your branches are not lost.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly references issues #1516 and #1273, clearly communicating the two main problems being addressed: non-destructive sync default and post-message reminder functionality.
Description check ✅ Passed The PR description comprehensively covers all required template sections: summary with problem/impact/changes, detailed UX journeys before/after, architecture diagrams with connection inventory, validation evidence with test results, security/compatibility/migration analysis, human verification details, and rollback plan.
Linked Issues check ✅ Passed The PR fully addresses objectives from #1273 (pass codebase.default_branch to syncWorkspace, persist detected branch at clone/register time, add branch-awareness to fast-forward logic) and #1516 (eliminate destructive resets on every message, preserve local commits, implement non-destructive default while keeping explicit reset for worktree creation).
Out of Scope Changes check ✅ Passed All code changes are directly scoped to addressing #1516 and #1273: syncWorkspace mode refactoring, default_branch persistence, post-message reminder, branch detection, and test updates. CLI behavior, worktree-mode workflows, and forge-adapter unchanged as documented. UI truncation (#1531) explicitly deferred to separate PR.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

✏️ 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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/git/src/repo.ts (1)

121-128: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Narrow the “branch not found” detection.

errorMessage.includes('not found') also matches repo-level failures like repository not found, so callers with a configured base branch can get the wrong remediation path. Match only remote-ref-specific messages here and let the rest fall through to the generic fetch error.

Possible fix
     if (
       baseBranch &&
-      (errorMessage.includes("couldn't find remote ref") || errorMessage.includes('not found'))
+      (
+        errorMessage.includes("couldn't find remote ref") ||
+        /remote ref .* not found/.test(errorMessage)
+      )
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/git/src/repo.ts` around lines 121 - 128, The branch-not-found
detection is too broad because errorMessage.includes('not found') also matches
repo-level errors; update the conditional that checks baseBranch and
errorMessage (the block using baseBranch and errorMessage in repo.ts) to only
detect remote-ref-specific messages—e.g. keep the existing "couldn't find remote
ref" check and replace the generic 'not found' check with a more specific test
such as matching "remote ref .*not found" or another remote-ref-specific
substring/regex—so repository-level "not found" errors fall through to the
generic fetch error handling.
packages/core/src/orchestrator/orchestrator-agent.test.ts (1)

1054-1065: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

makeDispatchCodebase and makeApprovalCodebase are missing default_branch: null.

makeCodebase (line 217) and makeCodebaseForSync (line 841) were both updated to include default_branch: null, but makeDispatchCodebase (lines 1054-1065) and makeApprovalCodebase (lines 1190-1200, same structure) were not. Their return values are missing a field required by the Codebase interface. Tests pass only because the mock is typed as Promise<unknown>, so TypeScript doesn't enforce the shape — at runtime, codebase.default_branch resolves to undefined, which coincidentally behaves identically to null under ?? undefined. If the mock typing is ever tightened to Promise<Codebase>, both helpers will produce type errors.

🛠️ Proposed fix
 function makeDispatchCodebase() {
   return {
     id: 'codebase-1',
     name: 'test-repo',
     repository_url: null,
     default_cwd: '/repos/test-repo',
+    default_branch: null,
     ai_assistant_type: 'claude' as const,
     commands: {},
     created_at: new Date(),
     updated_at: new Date(),
   };
 }

Apply the same change to makeApprovalCodebase at lines 1190-1200.

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

In `@packages/core/src/orchestrator/orchestrator-agent.test.ts` around lines 1054
- 1065, The helpers makeDispatchCodebase and makeApprovalCodebase are missing
the required Codebase field default_branch; update both functions to include
default_branch: null in their returned object shapes (matching the change
already applied to makeCodebase and makeCodebaseForSync) so the mocked objects
conform to the Codebase interface and will type-check if the mock promise is
tightened to Promise<Codebase>.
migrations/000_combined.sql (1)

313-316: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing idempotent ALTER TABLE for migration 022 and stale version comment.

The upgrade section (lines 274–315) includes an idempotent ADD COLUMN IF NOT EXISTS for every prior migration (e.g., migration 021 at lines 313–315), but there is no corresponding entry for migration 022. Existing databases that are brought up to date by replaying 000_combined.sql (the intended idempotent upgrade path) will never receive default_branch because the column only appears in the CREATE TABLE IF NOT EXISTS block (which is a no-op for pre-existing tables). Additionally, the version comment on line 2 still reads 001-020 even though migration 021's ALTER is already present here.

🛠️ Proposed additions
 -- From migration 021: allow_env_keys on codebases
 ALTER TABLE remote_agent_codebases
   ADD COLUMN IF NOT EXISTS allow_env_keys BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- From migration 022: default_branch on codebases
+ALTER TABLE remote_agent_codebases
+  ADD COLUMN IF NOT EXISTS default_branch TEXT;
--- Version: Combined (final state after migrations 001-020)
+-- Version: Combined (final state after migrations 001-022)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@migrations/000_combined.sql` around lines 313 - 316, The upgrade path is
missing an idempotent ALTER for migration 022 and the file header version
comment is stale; add an idempotent ALTER TABLE for remote_agent_codebases to
ADD COLUMN IF NOT EXISTS default_branch (nullable text) so replaying
000_combined.sql will create that column for existing tables, and update the
top-of-file version comment (currently "001-020") to include migration 021/022
(e.g. "001-022") so the combined migration header reflects the included changes.
🧹 Nitpick comments (2)
packages/isolation/src/providers/worktree.test.ts (1)

2268-2273: ⚡ Quick win

Add the managed-clone counterpart to this mode assertion.

These cases only exercise the local-repo fast-forward path. Please add a repo-under-getArchonWorkspacesPath() case that expects { mode: 'reset' }, since that branch is the one preserving the old known-clean-base behavior for managed clones.

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

In `@packages/isolation/src/providers/worktree.test.ts` around lines 2268 - 2273,
Add a managed-clone test that mirrors the existing assertion but uses a repo
path under getArchonWorkspacesPath() and expects mode 'reset'; specifically,
invoke whatever triggers syncWorkspaceSpy with a path built from
getArchonWorkspacesPath() (e.g., `${getArchonWorkspacesPath()}/owner/repo` or
the test helper that produces a managed workspace) and assert
syncWorkspaceSpy.toHaveBeenCalledWith('/workspace/owner/repo', undefined, {
mode: 'reset' }) so the managed-clone branch is covered alongside the existing
fast-forward assertion.
packages/core/src/handlers/clone.ts (1)

284-306: ⚡ Quick win

Consider delegating to getCurrentBranch from @archon/git rather than inlining the git command.

The block duplicates the logic already in packages/git/src/branch.ts:getCurrentBranch. Per the coding guidelines, @archon/git functions should be preferred for git operations. The !== 'HEAD' guard on line 294 is necessary because git rev-parse --abbrev-ref HEAD exits with success in detached HEAD state and outputs the literal string HEAD, which would corrupt default_branch if stored as-is. However, that guard can still be applied after calling getCurrentBranch, keeping the duplication out of clone.ts:

♻️ Proposed refactor

Add getCurrentBranch to the import from @archon/git:

-import { execFileAsync } from '@archon/git';
+import { execFileAsync, getCurrentBranch, toRepoPath as _toRepoPath } from '@archon/git';

Replace the inline block (lines 284-299):

-  let detectedBranch: string | undefined;
-  try {
-    const { stdout } = await execFileAsync(
-      'git',
-      ['-C', targetPath, 'rev-parse', '--abbrev-ref', 'HEAD'],
-      { timeout: 5000 }
-    );
-    const branch = stdout.trim();
-    if (branch && branch !== 'HEAD') {
-      detectedBranch = branch;
-    }
-  } catch {
-    // Non-fatal — leave undefined so DB default applies
-  }
+  let detectedBranch: string | undefined;
+  try {
+    const branch = await getCurrentBranch(targetPath);
+    // Discard 'HEAD' (detached HEAD state) — not a valid default branch
+    if (branch !== 'HEAD') {
+      detectedBranch = branch;
+    }
+  } catch {
+    // Non-fatal — leave undefined so DB default applies
+  }

Note: if detached-HEAD handling should be a general concern in @archon/git, consider having getCurrentBranch itself return null in that case instead.

As per coding guidelines: "Use @archon/git functions for git operations; use execFileAsync (not exec) when calling git directly."

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

In `@packages/core/src/handlers/clone.ts` around lines 284 - 306, Replace the
inline git call with the shared helper: import getCurrentBranch from `@archon/git`
and use it to determine the branch for targetPath instead of calling
execFileAsync directly; assign its result to detectedBranch, then apply the
existing guard to ignore the literal 'HEAD' (i.e., if branch && branch !==
'HEAD' set detectedBranch), keep the try/catch semantics so failures remain
non-fatal, remove the execFileAsync block and any direct git invocation, and
then call registerRepoAtPath(targetPath, `${ownerName}/${repoName}`, workingUrl,
detectedBranch) as before.
🤖 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/core/src/db/adapters/sqlite.ts`:
- Around line 219-230: The schema mismatch comes from declaring
remote_agent_codebases.default_branch with a DEFAULT 'main' in createSchema()
while the migration adds the column without a default; update createSchema() so
the remote_agent_codebases table defines default_branch as TEXT (nullable) with
no DEFAULT clause, and ensure any ALTER TABLE in this file (the existing
migration block that runs "ALTER TABLE remote_agent_codebases ADD COLUMN
default_branch TEXT") remains adding a plain nullable TEXT column (no DEFAULT);
target symbols: createSchema(), remote_agent_codebases, and the migration ALTER
TABLE/PRAGMA check around default_branch.

In `@packages/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 920-925: The code checks conversation.codebase_id from the stale
in-memory `conversation` object and can miss newly persisted attachments;
refresh the conversation state before deciding to call
reportUnpushedWorkInSource. After dispatchOrchestratorWorkflow() (or wherever
the DB write happens), re-fetch the conversation (or use the updated
conversation returned by that call) and then perform the check: if
updatedConversation.codebase_id find the attached codebase in `codebases` and
call reportUnpushedWorkInSource(platform, conversationId, attachedCodebase).
Ensure you reference the existing symbols `dispatchOrchestratorWorkflow`,
`conversation`/`updatedConversation`, `conversationId`, `codebases`, and
`reportUnpushedWorkInSource` when locating where to add the refresh.
- Around line 436-439: Before calling syncWorkspace, guard so the merge only
runs when HEAD matches the target branch: obtain the current branch via
getCurrentBranch() and compare it to the branch derived for sync
(toBranchName(codebase.default_branch)); if they do not match, no-op and return
a successful/neutral syncResult. Alternatively, implement this same guard inside
the syncWorkspace implementation in `@archon/git` so syncWorkspace itself checks
HEAD==branchToSync before performing git merge --ff-only origin/<branchToSync>.
Ensure references to syncWorkspace, getCurrentBranch, toBranchName, and
toRepoPath are used to locate and implement the check.

In `@packages/core/src/orchestrator/post-message-reminder.ts`:
- Around line 43-48: The call to toRepoPath(codebase.default_cwd) is executed
outside the try/catch so its thrown error can escape and violate the "Non-fatal"
guarantee; move the toRepoPath(...) (and the branchName derivation that depends
on codebase.default_branch) inside the existing try block (or wrap them in a
small try/catch) so any exception from toRepoPath or toBranchName is caught and
logged at debug and swallowed, preserving the non-fatal behavior of the
post-message-reminder logic where platform.sendStructuredEvent is used.

In `@packages/isolation/src/providers/worktree.ts`:
- Around line 783-786: The current isManagedClone detection uses startsWith on a
stringified repoPath which can false-match siblings; update the logic that sets
mode (variable mode of type SyncMode) so it only treats a repo as managed when
repoPath is actually inside getArchonWorkspacesPath(), e.g. normalize both paths
and either (a) compare path.relative(getArchonWorkspacesPath(), repoPath) and
check it does not start with '..' and is not equal to ''/'. or (b) append a path
separator to the normalized workspaces root and use startsWith(rootWithSep) to
guarantee a boundary — change the isManagedClone calculation to use one of these
approaches before selecting 'reset' vs 'fast-forward'.

---

Outside diff comments:
In `@migrations/000_combined.sql`:
- Around line 313-316: The upgrade path is missing an idempotent ALTER for
migration 022 and the file header version comment is stale; add an idempotent
ALTER TABLE for remote_agent_codebases to ADD COLUMN IF NOT EXISTS
default_branch (nullable text) so replaying 000_combined.sql will create that
column for existing tables, and update the top-of-file version comment
(currently "001-020") to include migration 021/022 (e.g. "001-022") so the
combined migration header reflects the included changes.

In `@packages/core/src/orchestrator/orchestrator-agent.test.ts`:
- Around line 1054-1065: The helpers makeDispatchCodebase and
makeApprovalCodebase are missing the required Codebase field default_branch;
update both functions to include default_branch: null in their returned object
shapes (matching the change already applied to makeCodebase and
makeCodebaseForSync) so the mocked objects conform to the Codebase interface and
will type-check if the mock promise is tightened to Promise<Codebase>.

In `@packages/git/src/repo.ts`:
- Around line 121-128: The branch-not-found detection is too broad because
errorMessage.includes('not found') also matches repo-level errors; update the
conditional that checks baseBranch and errorMessage (the block using baseBranch
and errorMessage in repo.ts) to only detect remote-ref-specific messages—e.g.
keep the existing "couldn't find remote ref" check and replace the generic 'not
found' check with a more specific test such as matching "remote ref .*not found"
or another remote-ref-specific substring/regex—so repository-level "not found"
errors fall through to the generic fetch error handling.

---

Nitpick comments:
In `@packages/core/src/handlers/clone.ts`:
- Around line 284-306: Replace the inline git call with the shared helper:
import getCurrentBranch from `@archon/git` and use it to determine the branch for
targetPath instead of calling execFileAsync directly; assign its result to
detectedBranch, then apply the existing guard to ignore the literal 'HEAD'
(i.e., if branch && branch !== 'HEAD' set detectedBranch), keep the try/catch
semantics so failures remain non-fatal, remove the execFileAsync block and any
direct git invocation, and then call registerRepoAtPath(targetPath,
`${ownerName}/${repoName}`, workingUrl, detectedBranch) as before.

In `@packages/isolation/src/providers/worktree.test.ts`:
- Around line 2268-2273: Add a managed-clone test that mirrors the existing
assertion but uses a repo path under getArchonWorkspacesPath() and expects mode
'reset'; specifically, invoke whatever triggers syncWorkspaceSpy with a path
built from getArchonWorkspacesPath() (e.g.,
`${getArchonWorkspacesPath()}/owner/repo` or the test helper that produces a
managed workspace) and assert
syncWorkspaceSpy.toHaveBeenCalledWith('/workspace/owner/repo', undefined, {
mode: 'reset' }) so the managed-clone branch is covered alongside the existing
fast-forward assertion.
🪄 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: 9f699614-0b74-44ff-9812-fd01ec863f3e

📥 Commits

Reviewing files that changed from the base of the PR and between 69b2c89 and 780ef50.

📒 Files selected for processing (17)
  • migrations/000_combined.sql
  • migrations/022_add_default_branch_to_codebases.sql
  • packages/core/src/db/adapters/sqlite.ts
  • packages/core/src/db/codebases.ts
  • packages/core/src/handlers/clone.ts
  • packages/core/src/orchestrator/orchestrator-agent.test.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/core/src/orchestrator/post-message-reminder.ts
  • packages/core/src/types/index.ts
  • packages/git/src/branch.ts
  • packages/git/src/git.test.ts
  • packages/git/src/index.ts
  • packages/git/src/repo.ts
  • packages/git/src/types.ts
  • packages/isolation/src/providers/worktree.test.ts
  • packages/isolation/src/providers/worktree.ts
  • packages/server/src/routes/schemas/codebase.schemas.ts

Comment thread packages/core/src/db/adapters/sqlite.ts
Comment thread packages/core/src/orchestrator/orchestrator-agent.ts
Comment thread packages/core/src/orchestrator/orchestrator-agent.ts Outdated
Comment thread packages/core/src/orchestrator/post-message-reminder.ts
Comment thread packages/isolation/src/providers/worktree.ts Outdated
Zolto added 7 commits May 2, 2026 13:47
Both
  PostgreSQL baseline and SQLite createSchema declared DEFAULT 'main', diverging from migration 022 which uses NULL. Fresh installs got 'main', migrated installs got NULL — same
  hard-reset-to-wrong-branch risk this PR is trying to remove.
…view)

Non-fatal  contract was broken when default_cwd is empty — toRepoPath throws RepoPath cannot be empty before the try block opens.
…k (CodeRabbit review)

startsWith(workspacesPath) matched siblings like workspaces-old/. Normalize trailing slashes and require an explicit / separator (or exact match).
…deRabbit review)

Without the branch check, a topic   branch that happens to be an ancestor of origin/<default_branch> would silently advance to origin's tip, violating the non-default-branches-preserved guarantee. Use  getCurrentBranch and noop unless HEAD == branchToSync.
…tached (CodeRabbit review)

dispatchOrchestratorWorkflow may have just persisted codebase_id (auto-attach on first turn), so the in-memory conversation can be stale. Re-read by platform id when  codebase_id isn't set in our snapshot.
@ztech-gthb
Copy link
Copy Markdown
Author

Tests + CodeRabbit review adressiert.

Test status

  • bun run type-check clean across all 10 packages
  • bun run test clean for @archon/git (143/0), @archon/isolation (251/0), @archon/core (815/0). Total ~1200 expects, 0 fails.

CodeRabbit findings — all five addressed

# Severity Finding Commit
1 🟠 Major SQLite createSchema declared default_branch TEXT DEFAULT 'main', diverging from migration 022's NULL. Fresh installs got 'main', migrated installs got NULL. Same hard-reset-to-wrong-branch risk this PR is trying to remove. Fixed in both PostgreSQL baseline and SQLite createSchema. 1db8f711
2 🟠 Major git merge --ff-only origin/<branch> ran without verifying HEAD was on branch. A topic branch that happened to be an ancestor of origin/<default> would silently advance to origin's tip — violating the "non-default branches preserved" guarantee. Now uses getCurrentBranch() and skips the merge unless HEAD is exactly on branchToSync. 70a39b10
3 🟡 Minor Post-message reminder read the in-memory conversation from handleMessage, but dispatchOrchestratorWorkflow may persist codebase_id mid-call (auto-attach on first turn). The reminder skipped exactly when it should start working. Now re-reads via db.getConversationByPlatformId when the snapshot lacks a codebase. ef76cd67
4 🟡 Minor toRepoPath(codebase.default_cwd) was outside the try/catch in reportUnpushedWorkInSource. An empty default_cwd would have thrown past the catch, breaking the documented non-fatal contract. Moved inside the try. 4c5fcd7a
5 🟠 Major startsWith(getArchonWorkspacesPath()) matched siblings like <root>/workspaces-old/... — locally-registered repos in such paths would be misclassified as managed and hard-reset on worktree creation. Now normalizes trailing slashes and requires === or startsWith(${root}/) boundary. 7c617811

Plus one self-spotted fix

Branch is at ef76cd67. CodeRabbit can re-review.

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/core/src/orchestrator/orchestrator-agent.ts`:
- Around line 1393-1403: getCurrentBranch can return the literal "HEAD" in a
detached HEAD state which currently gets stored as default_branch; change the
logic around getCurrentBranch in orchestrator-agent.ts so that after awaiting
getCurrentBranch(toRepoPath(projectPath)) you treat the returned value "HEAD" as
undefined (i.e., if detectedBranch === 'HEAD' set detectedBranch = undefined)
before calling codebaseDb.createCodebase, and update the inline comment to
reflect that detached HEAD yields undefined rather than relying on a thrown
error or a DB default; reference symbols: getCurrentBranch, detectedBranch,
codebaseDb.createCodebase, default_branch.
🪄 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: 8a794698-b9ca-4e36-8b16-ef05bf39ce01

📥 Commits

Reviewing files that changed from the base of the PR and between 5a309c2 and ef76cd6.

📒 Files selected for processing (7)
  • migrations/000_combined.sql
  • packages/core/src/db/adapters/sqlite.ts
  • packages/core/src/db/codebases.test.ts
  • packages/core/src/orchestrator/orchestrator-agent.ts
  • packages/core/src/orchestrator/post-message-reminder.ts
  • packages/git/src/repo.ts
  • packages/isolation/src/providers/worktree.ts
✅ Files skipped from review due to trivial changes (1)
  • migrations/000_combined.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/isolation/src/providers/worktree.ts
  • packages/core/src/orchestrator/post-message-reminder.ts
  • packages/git/src/repo.ts

Comment thread packages/core/src/orchestrator/orchestrator-agent.ts
Untracked files (e.g. .DS_Store on macOS, claude/skill  caches) are safe across all syncWorkspace paths — neither git merge --ff-only nor git reset --hard origin/<branch> ever touches untracked files. Treating them as 'dirty' was  blocking fast-forward sync on every checkout where the user happens to have local-only files, despite there being no destructive risk to defend against. Add --untracked-files=no to the porcelain check; tracked modifications still block ff-merge as before.
@ztech-gthb
Copy link
Copy Markdown
Author

Untracked-files refinement folded into the PR (518cefa5).

Why this is in the PR, not a follow-up

In the real-world verification (see Human Verification section in the PR body), the FF-banner that should have appeared on a state='behind' chat-tick didn't — because state='dirty' was reported as dominant due to a .DS_Store and a Claude Code .agents/skills/ cache being untracked in source/. That's a false positive: untracked files don't need protection from either of syncWorkspace's paths.

The reasoning, made explicit

Why untracked files are safe across both sync modes:

  • mode: 'reset' runs git reset --hard origin/<branch>. This rewrites HEAD, the index, and the tracked working tree — but never touches untracked files. (To remove untracked you would need git clean -fd separately, which is not in this codebase.)
  • mode: 'fast-forward' runs git merge --ff-only origin/<branch>. Same story — fast-forward merges only update tracked files; untracked are never affected.

So the only thing the previous "untracked counts as dirty" logic was defending against was an imaginary threat: a sync path that doesn't exist. Meanwhile, every macOS user with a .DS_Store and every developer with any tool that writes lock/cache files into the repo path would silently never see fast-forward sync work — exactly what surfaced in the manual test.

After this commit, --untracked-files=no makes state='dirty' reflect only tracked modifications. Tracked modifications continue to block ff-merge as before (the legitimate case to protect).

Worktree cleanup is unaffected

The change is local to repo.ts:isDirty. The broader branch.ts:hasUncommittedChanges helper — which is used for worktree cleanup safety to refuse deleting any worktree that has local files — keeps its pre-PR semantics (still includes untracked, deliberately conservative for cleanup contexts).

Tests

bun run type-check                 # clean across all 10 packages
bun --filter @archon/git test      # 143 pass / 0 fail

No fixture changes needed — existing tests don't exercise the untracked-files boundary explicitly. Branch is now at 518cefa5.

…RegisterProject (CodeRabbit round 2)

getCurrentBranch returns the literal string 'HEAD' on detached HEAD, not an exception. he catch block never fires for detached HEAD, so the codebase row ended up with default_branch='HEAD' — which later gets passed to syncWorkspace as a branch name and fails the fetch with 'Configured base branch HEAD not found on remote'. Mirror the existing guard in clone.ts:cloneRepository: only assign the branch when it's a real name.
@ztech-gthb
Copy link
Copy Markdown
Author

CodeRabbit round 2 actionable addressed.

What was missed in round 1

The handleRegisterProject branch-detect path used try/catch around getCurrentBranch(toRepoPath(projectPath)) with the comment "non-git directory or detached HEAD — fall back to DB default 'main'". That comment was wrong on the detached-HEAD case: git rev-parse --abbrev-ref HEAD returns the literal string "HEAD" rather than throwing, so the codebase row got default_branch='HEAD'. On the next chat-tick that gets passed to syncWorkspace as a branch name and the git fetch origin HEAD fails with the actionable error message — bad UX, easy to misdiagnose.

clone.ts:cloneRepository already had the right guard (if (branch && branch !== 'HEAD') { detectedBranch = branch; }); I'd missed copying that exact pattern into handleRegisterProject during the 1273-backport. CodeRabbit caught it.

Fix

Mirror the clone.ts guard in handleRegisterProject. Detached HEAD now leaves detectedBranch undefined → the codebase row gets NULL → first sync auto-detects via getDefaultBranch. Comment updated accordingly.

Tests

bun run type-check                    # clean across all 10 packages
bun --filter @archon/core test        # 815 pass / 0 fail

No fixture changes needed — existing register-project tests use mocks for getCurrentBranch and don't exercise the detached-HEAD path. Could add an explicit unit test for the detached-HEAD case if reviewers prefer; left out to keep the diff minimal and aligned with the surrounding test conventions.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented May 4, 2026

@ztech-gthb related to #1516 — non-destructive sync + default branch fix.

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

Labels

None yet

Projects

None yet

2 participants