Skip to content

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

Closed
ztech-gthb wants to merge 13 commits into
coleam00:devfrom
ztech-gthb:fix/issue-1516-soft-sync-workspace
Closed

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

Conversation

@ztech-gthb

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

Copy link
Copy Markdown
Contributor

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.

@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown

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
📝 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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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
@ztech-gthb

Copy link
Copy Markdown
Contributor 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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
@ztech-gthb

Copy link
Copy Markdown
Contributor 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.

@ztech-gthb

Copy link
Copy Markdown
Contributor 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

Wirasm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

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

@ztech-gthb

Copy link
Copy Markdown
Contributor Author

All Round-1 + Round-2 CodeRabbit findings have been addressed across the commit history (drop DEFAULT 'main' from sqlite schema, FF-merge HEAD-on-target-branch guard, conversation re-fetch before reminder, toRepoPath inside try/catch, managed-clone path-boundary check, detached-HEAD treated as undetected branch). The 6 review-comments still showing are the original line-level comments — they're inline, not 'Resolved' in CodeRabbit's UI, but the underlying issues are gone in code. Tests + type-check + lint + format clean. Ready for review.

Zolto added 12 commits May 4, 2026 20:14
…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
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.
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.
…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 ztech-gthb force-pushed the fix/issue-1516-soft-sync-workspace branch from be9dcb7 to 90bcf0d Compare May 4, 2026 18:18
@Wirasm

Wirasm commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Review Summary

Verdict: blocking-issues

This PR makes a good structural improvement — replacing the boolean resetAfterFetch with a typed mode: 'fast-forward' | 'reset', persisting detected branch, and surfacing an unpushed-work advisory. The core design is sound. However, two regressions introduced by this PR require fixes before merge: a syncWorkspace call still using the old API (silently defeats the PR's intent), and two empty catch {} blocks swallowing errors. Additionally, three user-visible behavioural changes have no documentation.

Blocking issues

  • packages/core/src/orchestrator/orchestrator-agent.ts:559: syncWorkspace call still uses the old resetAfterFetch: isManagedClone API instead of the new mode: 'fast-forward' with default_branch. This regression means codebase-attached conversations don't benefit from the PR's main fix.

    Fix: Replace with the new API, passing default_branch directly:

    syncResult = await syncWorkspace(
      toRepoPath(codebase.default_cwd),
      codebase.default_branch ? toBranchName(codebase.default_branch) : undefined
    );

    Remove the now-unnecessary isManagedClone variable and the resetAfterFetch option entirely.

  • packages/core/src/handlers/clone.ts:289-292 + orchestrator-agent.ts:1394-1399: Two empty catch {} blocks silently discard all errors during branch detection in clone and register paths. Distinguishes neither expected cases (detached HEAD) from actionable failures.

    Fix: Log at debug level, distinguishing expected vs unexpected:

    } catch (err) {
      const error = err as Error & { stderr?: string };
      const msg = (error.message + (error.stderr ?? '')).toLowerCase();
      if (msg.includes('head') || msg.includes('reference')) {
        getLog().debug({ path: targetPath }, 'branch_detection_detached_head');
      } else {
        getLog().debug({ err: error, path: targetPath }, 'branch_detection_failed');
      }
    }

Documentation required before merge

Three user-visible changes are entirely undocumented — these are also blocking:

  1. Fast-forward sync default: Chat-tick sync now preserves local commits and uncommitted changes in the canonical workspace. Add one paragraph to overview.md and isolation.md near the server-sync discussion explaining this.
  2. state field SSE messages: WorkspaceSyncResult.state (in_sync / behind / ahead / diverged / dirty) surfaces user-visible SSE messages. Document the five states and which trigger messages. Add a troubleshooting entry for "diverged from origin."
  3. Unpushed-work advisory: Users receive an SSE system message when source/ has local-only commits or uncommitted changes. Add one sentence in overview and isolation safety notes.

Also worth updating: docs that say "server only sees changes after commit + push" — this is no longer true for chat-mode (only worktree-creation resets managed clones). And the $BASE_BRANCH variable resolution docs should note the new codebase-row path alongside the YAML config key.

Suggested fixes

  • packages/git/src/git.test.ts:1316, 1376: Two hard-reset tests pass { mode: 'reset' } to syncWorkspace and expect git reset --hard to run. After this change the default mode is 'fast-forward'reset is never called unless mode: 'reset' is passed. Tests pass by accident because the mock makes the ancestor check fall through. Either update tests to pass { mode: 'reset' } explicitly, or assert on the absence of reset calls and comment the new contract.

  • packages/git/src/branch.ts:getCurrentBranch: Public export, no dedicated unit test. Test happy path and detached HEAD case.

  • packages/git/src/branch.ts:countCommitsAhead: Public export, no dedicated unit test. Test: unpushed commits → count, post-push → 0, origin absent → -1.

  • packages/core/src/orchestrator/post-message-reminder.ts: Entire reportUnpushedWorkInSource untested. Mock hasUncommittedChanges and countCommitsAhead. Cover all six branches.

  • packages/git/src/repo.ts:153-154: currentBranch has type string | undefined but is compared against BranchName. Add explicit type annotation for type safety.

Minor / nice-to-have

  • post-message-reminder.ts:57-59: String(aheadCount) produces "-1" for the sentinel. Use an explicit -1 check with a clear message instead.
  • orchestrator-agent.ts:764: "match the previous 'no message if not updated' behaviour" — temporal reference. Replace with "silent when nothing changed."
  • orchestrator-agent.ts:927: References dispatchOrchestratorWorkflow function name — implementation detail. Genericize to "codebase_id may have been set after conversation load."
  • post-message-reminder.ts:6: Module docstring references PR number #1516 — remove it; the rationale paragraph is clear without it.
  • git/src/branch.ts:80: JSDoc restates the function name — remove it per CLAUDE.md guidelines.
  • CHANGELOG.md: No entry for this change. Add a [Unreleased] entry under ### Changed for the fast-forward default and unpushed advisory.
  • configuration.md: Add a note distinguishing worktree.baseBranch (YAML, workflow/worktree creation) from default_branch (codebase DB row, chat-mode sync).

Compliments

  • The SyncMode type and WorkspaceSyncResult.state field design is excellent — the five-state machine gives callers exactly the context needed for appropriate UI.
  • The migration 022 comment explaining why existing rows get NULL (to avoid triggering unwanted hard-resets on non-main branches) is exactly the kind of "why" that prevents future regressions.
  • The isDirty function's docstring explaining why untracked files are excluded (--untracked-files=no) with concrete examples is exemplary.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality, docs-impact.

- Replace two empty catch{} blocks during branch detection with debug
  logging that distinguishes detached-HEAD / non-git-dir from actionable
  failures (packages/core/src/handlers/clone.ts,
  packages/core/src/orchestrator/orchestrator-agent.ts).
- Document the three user-visible changes the PR introduces: fast-forward
  chat-tick sync as the new default with the five-state machine
  (in_sync/behind/ahead/diverged/dirty), the unpushed-work SSE advisory,
  and the distinction between worktree.baseBranch (YAML override) and
  the per-codebase default_branch DB column. Updates the previously stale
  "server only sees pushed changes" caveat in the overview.
- CHANGELOG entry under [Unreleased].

Note on the third reported blocker (syncWorkspace still using
resetAfterFetch in orchestrator-agent.ts): not reproducible against
PR HEAD 90bcf0d. The orchestrator call site already passes the new
mode-based API; isManagedClone now lives only in worktree.ts where it
controls mode: 'reset' for managed-clone worktree creation by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ztech-gthb

Copy link
Copy Markdown
Contributor Author

Thanks @Wirasm — addressed in 3e80374:

Blocker 2 — empty catch {}
Both branch-detection catches now log at debug with detached-HEAD / non-git-dir distinguished from actionable failures (your snippet, adapted):

  • packages/core/src/handlers/clone.ts (post-clone rev-parse --abbrev-ref HEAD)
  • packages/core/src/orchestrator/orchestrator-agent.ts (getCurrentBranch in handleRegisterProject)

Blocker 3 — Docs

  • book/isolation.md — new "Workspace Sync in Chat Mode" section with the five-state table (in_sync / behind / ahead / diverged / dirty) and a diverged from origin troubleshooting note.
  • getting-started/overview.md — replaced the stale "server only sees pushed changes" caveat and added the unpushed-work SSE advisory.
  • reference/configuration.mddefault_branch DB-row added to the \$BASE_BRANCH resolution chain; explicit paragraph distinguishing worktree.baseBranch (YAML, worktree-creation) from default_branch (DB row, chat-mode sync); new "Chat-mode workspace sync" paragraph cross-linking the state table.
  • CHANGELOG.md[Unreleased] / Changed entry.

Blocker 1 — syncWorkspace still using old API ⚠️ Couldn't reproduce
Against PR HEAD (90bcf0dd before this commit), orchestrator-agent.ts:436 already calls the new API exactly as you proposed:

```ts
syncResult = await syncWorkspace(
toRepoPath(codebase.default_cwd),
codebase.default_branch ? toBranchName(codebase.default_branch) : undefined
);
```

No resetAfterFetch or isManagedClone in orchestrator-agent.ts at all. isManagedClone is only referenced at packages/isolation/src/providers/worktree.ts:789-791, where it controls mode: 'reset' for the worktree-creation path — which is exactly the design (managed clones get a hard reset before branching, live checkouts get fast-forward).

Could you double-check whether the review ran against an older snapshot? If you can point me at the specific line/commit you read I'll happily re-look.

Suggested fixes (Phase B, follow-up commit) — will tackle next:

  • git.test.ts:1316, 1376 — explicit { mode: 'reset' } in both hard-reset tests.
  • Unit tests for getCurrentBranch, countCommitsAhead, reportUnpushedWorkInSource.
  • repo.ts:153-154BranchName | undefined type annotation.

Nits (Phase C) — separate commit:

  • post-message-reminder.ts:57-59 String(-1) → explicit sentinel check.
  • Remove #1516 reference from post-message-reminder.ts:6 module docstring.
  • Strip JSDoc that restates function name in git/src/branch.ts:80.
  • Genericize temporal/implementation comments in orchestrator-agent.ts:764, 927.

@Wirasm

Wirasm commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Review Summary

Verdict: minor-fixes-needed

Your PR changes workspace sync from destructive (git reset --hard) to non-destructive fast-forward by default and persists the detected default branch per codebase. The implementation is solid — error handling is consistent, the new git utilities are well-structured, and the docs updates are well-targeted. Two categories of gaps remain before merge: five new functions (including two exported utilities) have zero test coverage, and two user-facing documentation gaps around destructive worktree behavior need addressing.

Blocking issues

  • packages/git/src/branch.tsgetCurrentBranch: no tests (5 exported/internal functions affected)
    • Add describe('getCurrentBranch'): happy path, detached HEAD ('HEAD'), invalid path.
    • Add describe('countCommitsAhead'): returns n≥0, returns -1 when never-pushed, returns 0 on unexpected errors (non-throwing contract).
    • Add describe('isAncestor'): true when ancestor reachable, false on bad refs.
    • Add describe('isDirty'): true/false for dirty/clean, false for untracked-only, false on error.
    • Add describe('reportUnpushedWorkInSource') in post-message-reminder.ts: SSE on dirty/ahead, silent when clean, non-fatal on git failure.

Suggested fixes

  • orchestrator-agent.ts:943-955: db.getConversationByPlatformId uses catch(() => null) — unexpected DB failures are silently swallowed. Add a debug log:

    catch((err) => { logger.warn({ err }, 'post_message_reminder.db_read_failed'); return null; })
  • isolation.md: Worktree creation hard-resets source/ — document this with a callout: "Before creating a worktree, commit anything you want to keep first."

  • database.md: Add default_branch TEXT to the remote_agent_codebases schema, noting it drives fast-forward branch targeting.

  • authoring-workflows.md: Update the "CLI vs Server" callout to reflect source/ as the canonical path and fast-forward sync.

  • syncWorkspace fast-forward state machine: Add test cases for state='behind' triggering merge --ff-only, state='behind' on wrong branch (no-op), and state='dirty' (blocked, no-op).

  • discoverAllWorkflows SSE events: Assert platform.sendStructuredEvent is called with the diverged message.

  • handleMessage reminder: Test the reportUnpushedWorkInSource call and the DB re-read of codebase_id.

Minor / nice-to-have

  • orchestrator-agent.ts:306-309, 336-339, 370-377: Shorten multi-line comment blocks — keep only WHY context, drop WHAT restatements and caller-specific notes.
  • branch.ts:79-81, 84-103: Trim getCurrentBranch and countCommairsAhead docstrings — the function names and return types already convey the contract.
  • isolation.md: Add a "If you see the unpushed-work advisory" callout explaining how to resolve it.
  • isolation.md: Explain in the state table what sync does per state: ahead = no-op + advisory only; dirty = sync skipped entirely.
  • CLAUDE.md: Add a note: "Chat-tick sync of source/ is non-destructive (fast-forward only); worktree creation hard-resets managed clones."
  • worktree.ts: Add mode: 'reset' test for managed clones and detectedBranch persistence test in handleRegisterProject.

Compliments

  • The isDirty comment in repo.ts:1011-1020 explaining why --untracked-files=no is used is excellent — it captures a non-obvious design decision that would otherwise be easy to "fix" incorrectly.
  • The error handling review found zero issues — the non-fatal advisory contract is applied consistently across all new code paths.
  • The SyncMode type introduction cleanly replaces resetAfterFetch?: boolean with a typed union, and all call sites are updated.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality, docs-impact.

@Wirasm

Wirasm commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Hi @ztech-gthb — your recent commit addressed the review findings, but the PR has since gone DIRTY from dev moving on. Could you rebase onto current dev? Once it's clean I'll re-verify and merge. Ping me if you hit any blockers.

@Wirasm

Wirasm commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Superseded by #1864 (merged). #1864 lands the same non-destructive-sync direction you proposed here (fast-forward-only default, surface divergence to the user instead of hard-resetting) and closes both #1273 and #1516. Thanks @ztech-gthb — your reporting + the non-destructive framing drove this. Closing as superseded by the merged fix.

@Wirasm Wirasm closed this Jun 8, 2026
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