diff --git a/docs/architecture.md b/docs/architecture.md index 77dcca8..c695214 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -80,38 +80,46 @@ repos_base_paths = [ ## Cycle Flow -The service uses a **two-phase batch workflow**: +The service uses a **per-repo atomic workflow**: ``` -Phase 1: Commit All Phase 2: Push All -───────────────────── ───────────────────── -repo-a → commit ✓ repo-a → push ✓ -repo-b → commit ✓ repo-b → push ✓ -repo-c → no changes repo-c → (skip) -repo-d → commit ✓ repo-d → push ✓ - ↓ ↓ - All commits done All pushes done +┌─────────────────────────────────────────┐ +│ CYCLE LOOP │ +├─────────────────────────────────────────┤ +│ repo-a: commit → push → done │ +│ repo-b: commit → push → done │ +│ repo-c: no changes → skip │ +│ repo-d: commit → push → done │ +│ ↓ │ +│ All repos processed │ +│ ↓ │ +│ Sleep X seconds │ +│ ↓ │ +│ Next cycle │ +└─────────────────────────────────────────┘ ``` -### Phase 1: Sloppy-Atomic Commits +### Per-Repo Processing For each repo: 1. Check `git status --porcelain` 2. Skip if no changes 3. Get diff and send to llama-service for commit message 4. Stage all changes (`git add -A`) 5. Commit with generated message -6. **Do NOT push yet** - return `COMMITTED` status +6. **Push immediately** to remote +7. Move to next repo -### Phase 2: Batch Push -Only after ALL commits complete: -1. For each repo with `COMMITTED` status -2. Push to remote (with retry + rebase on rejection) -3. Update status to `SUCCESS` or `ERROR` +### Cycle Completion +When all repos processed: +- Log summary (committed, failed, unchanged) +- Persist commit history +- Sleep for `cycle_interval_seconds` (default: 60) +- Start next cycle -### Why Two Phases? -- **Atomic batch**: All commits happen before any push -- **Fail-safe**: If commit fails mid-way, no partial pushes -- **Consistent state**: Remote only sees complete batch updates +### Why Per-Repo Atomic? +- **Sloppy-atomic**: Each repo is self-contained (commit+push) +- **Progress visible**: Changes appear on remote as processed +- **Fail-isolated**: One repo failing doesn't block others ## API Endpoints diff --git a/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc b/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc index a3b5b8a..f114649 100644 Binary files a/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc and b/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc differ diff --git a/src/auto_commit_service/scheduler/daemon.py b/src/auto_commit_service/scheduler/daemon.py index 92a5f6a..66e3d0b 100644 --- a/src/auto_commit_service/scheduler/daemon.py +++ b/src/auto_commit_service/scheduler/daemon.py @@ -538,48 +538,40 @@ class CommitDaemon: self._total_cycles += 1 return cycle_result - # Phase 1: Commit all repos with changes - logger.info(f"[{cycle_id}] Phase 1: Committing changes") - commit_results: dict[str, RepoProcessResult] = {} + # Process each repo: commit → push → next + results: list[RepoProcessResult] = [] for repo in self.repos: if not force and not self._running: logger.info("Daemon stopped, aborting cycle") break + # Step 1: Commit result = await self.processor.commit_repo(repo) - commit_results[repo.name] = result - if result.status == ProcessStatus.COMMITTED: - logger.info(f"[{cycle_id}] {repo.name}: Committed {result.commit_hash}") - elif result.status == ProcessStatus.NO_CHANGES: + if result.status == ProcessStatus.NO_CHANGES: logger.debug(f"[{cycle_id}] {repo.name}: No changes") - elif result.status == ProcessStatus.ERROR: - logger.error(f"[{cycle_id}] {repo.name}: {result.error}") + results.append(result) + continue - # Phase 2: Push all committed repos - committed_count = sum(1 for r in commit_results.values() if r.status == ProcessStatus.COMMITTED) - if committed_count > 0: - logger.info(f"[{cycle_id}] Phase 2: Pushing {committed_count} repos") + if result.status == ProcessStatus.ERROR: + logger.error(f"[{cycle_id}] {repo.name}: Commit failed - {result.error}") + results.append(result) + continue - for repo in self.repos: - if not force and not self._running: - logger.info("Daemon stopped, aborting push phase") - break + # Step 2: Push (only if committed) + if result.status == ProcessStatus.COMMITTED: + logger.info(f"[{cycle_id}] {repo.name}: Committed {result.commit_hash}, pushing...") + result = await self.processor.push_repo(repo, result) - result = commit_results[repo.name] - if result.status == ProcessStatus.COMMITTED: - result = await self.processor.push_repo(repo, result) - commit_results[repo.name] = result + if result.status == ProcessStatus.SUCCESS: + logger.info(f"[{cycle_id}] {repo.name}: Pushed successfully") + elif result.status == ProcessStatus.RECOVERED: + logger.info(f"[{cycle_id}] {repo.name}: Recovered by Claude") + elif result.status == ProcessStatus.ERROR: + logger.error(f"[{cycle_id}] {repo.name}: Push failed - {result.error}") - if result.status == ProcessStatus.SUCCESS: - logger.info(f"[{cycle_id}] {repo.name}: Pushed successfully") - elif result.status == ProcessStatus.RECOVERED: - logger.info(f"[{cycle_id}] {repo.name}: Recovered by Claude") - elif result.status == ProcessStatus.ERROR: - logger.error(f"[{cycle_id}] {repo.name}: Push failed - {result.error}") - - results = list(commit_results.values()) + results.append(result) # Build cycle result completed_at = datetime.now()