Skip to content

feat: implement manifest watcher#108

Open
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-74-manifest-watcher
Open

feat: implement manifest watcher#108
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-74-manifest-watcher

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 27, 2026

Summary

  • Add src/server/ws/watcher.ts with dual-source manifest change detection
  • fs.watch on manifest.json with 300ms debounce → broadcasts manifest:updated events
  • Status polling at 3s interval using checkAgentStatus() with batch pane fetching → broadcasts agent:status events on change
  • Tracks previous agent statuses to detect and emit granular change events
  • Graceful error handling for in-flight writes, corrupted JSON, and tmux unavailability
  • Clean shutdown via stop() method
  • 8 tests covering debounce, status change detection, error resilience, and cleanup

Closes #74

Test plan

  • npm run typecheck passes (no new errors)
  • npm test — all 226 tests pass (8 new watcher tests)
  • Manual: wire broadcast to WS handler and verify events fire on manifest changes

Summary by CodeRabbit

Release Notes

  • New Features
    • Manifest changes are now detected and broadcast in real-time
    • Agent status is periodically monitored and broadcast when changes occur
    • Debouncing and polling intervals are configurable for performance tuning

Add fs.watch on manifest.json (300ms debounce) and status polling
(3s interval) that broadcast events to connected WebSocket clients.
Tracks previous agent statuses to emit granular agent:status change
events. Gracefully handles in-flight writes and tmux unavailability.

Closes #74
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 928e0a7 and a997446.

📒 Files selected for processing (4)
  • src/commands/spawn.test.ts
  • src/server/ws/watcher.test.ts
  • src/server/ws/watcher.ts
  • src/test-fixtures.ts
📝 Walkthrough

Walkthrough

Introduces a new WebSocket watcher module that monitors manifest.json for changes using fs.watch with 300ms debounce and periodically polls agent statuses at 3-second intervals, broadcasting manifest:updated and agent:status events. Includes comprehensive test suite validating debounce behavior, status change detection, error handling, and cleanup mechanisms.

Changes

Cohort / File(s) Summary
Manifest Watcher Implementation
src/server/ws/watcher.ts
New WebSocket watcher module that monitors manifest.json using fs.watch with configurable debounce (default 300ms), polls agent statuses at configurable intervals (default 3000ms), broadcasts manifest:updated events on file changes and agent:status events when status changes, and provides graceful error handling for manifest read failures and tmux unavailability. Exports WsEvent, BroadcastFn, ManifestWatcher interfaces and startManifestWatcher function.
Watcher Test Suite
src/server/ws/watcher.test.ts
Comprehensive test suite covering debounce behavior verification, manifest read failure handling, status polling with change detection, tmux unavailability scenarios, and cleanup/timer clearing on stop. Mocks fs.watch, manifest reading, agent status checking, and tmux operations to validate control flow and event emission patterns.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant FSWatch as fs.watch
    participant Watcher as startManifestWatcher
    participant ManifestFile as manifest.json
    participant Tmux as tmux
    participant Broadcast as broadcast()

    rect rgba(100, 150, 200, 0.5)
        Note over User,Broadcast: File Change Flow
        User->>FSWatch: Modify manifest.json
        activate FSWatch
        FSWatch->>Watcher: file change event
        Watcher->>Watcher: Debounce (300ms default)
        deactivate FSWatch
        Watcher->>ManifestFile: Read manifest
        ManifestFile-->>Watcher: manifest content
        Watcher->>Broadcast: manifest:updated
        Broadcast-->>User: Event broadcasted
    end

    rect rgba(150, 200, 100, 0.5)
        Note over User,Broadcast: Status Polling Flow
        User->>Watcher: Poll interval (3000ms default)
        activate Watcher
        Watcher->>ManifestFile: Read manifest
        ManifestFile-->>Watcher: manifest content
        Watcher->>Tmux: List session panes
        Tmux-->>Watcher: Pane list
        Watcher->>Watcher: Check agent status
        alt Status Changed
            Watcher->>Broadcast: agent:status
            Broadcast-->>User: Event broadcasted
        else No Change
            Watcher->>Watcher: Skip broadcast
        end
        deactivate Watcher
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A watcher awakens, fs.watch in paw,
Manifest changes trigger without a flaw,
Polling the agents on interval beat,
Broadcasting status—the websocket feat!
Timers debounce, errors are caught,
Real-time updates, exactly as sought! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement manifest watcher' clearly summarizes the main change, which is the implementation of a new manifest watcher module.
Description check ✅ Passed The PR description provides comprehensive coverage of the implementation, testing status, and includes all required template sections (What, Why, How, Checklist) with most items checked.
Linked Issues check ✅ Passed The PR fulfills all acceptance criteria from issue #74: fs.watch with debounce, status polling, change tracking, error handling, cleanup, and comprehensive test coverage including debounce and status change detection.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #74 requirements: new watcher.ts module, new watcher.test.ts tests, and no unrelated modifications to other files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/issue-74-manifest-watcher

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.

Copy link

@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: 3

🧹 Nitpick comments (1)
src/server/ws/watcher.test.ts (1)

245-270: Add a regression test for stop() during an in-flight poll.

Line 246 validates timer cleanup, but not a poll already awaiting checkAgentStatus(). Add one deferred-promise case to ensure no late agent:status emits after stop.

🧪 Suggested test addition
 describe('cleanup', () => {
   test('stop should clear all timers and close watcher', async () => {
@@
   });
+
+  test('given in-flight poll, should not broadcast after stop', async () => {
+    const agent = makeAgent({ id: 'ag-aaa11111', status: 'running' });
+    const wt = makeWorktree({ id: 'wt-abc123', agents: { [agent.id]: agent } });
+    const manifest = makeManifest({ worktrees: { [wt.id]: wt } });
+    mockedReadManifest.mockResolvedValue(manifest);
+
+    let resolveStatus: ((v: { status: 'idle' }) => void) | null = null;
+    mockedCheckAgentStatus.mockReturnValueOnce(
+      new Promise((resolve) => {
+        resolveStatus = resolve as (v: { status: 'idle' }) => void;
+      }),
+    );
+
+    const events: WsEvent[] = [];
+    const watcher = startManifestWatcher(PROJECT_ROOT, (e) => events.push(e), {
+      pollIntervalMs: 1000,
+    });
+
+    await vi.advanceTimersByTimeAsync(1000); // starts poll
+    watcher.stop();
+    resolveStatus?.({ status: 'idle' });
+    await vi.advanceTimersByTimeAsync(0);
+
+    expect(events).toHaveLength(0);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/ws/watcher.test.ts` around lines 245 - 270, Add a regression test
to ensure watcher.stop() prevents late emits from an in-flight poll: in the
existing describe('cleanup') test block that calls startManifestWatcher,
stub/mock the module/function that performs the poll (checkAgentStatus or the
internal function invoked during polling) to return a deferred promise (resolve
it after calling watcher.stop()), trigger the polling path (e.g., via
triggerFsWatch or advancing timers to start the poll), call watcher.stop() while
the promise is still pending, then resolve the deferred promise and advance
timers—assert that no new events (especially an `agent:status` event) are pushed
to the events array and that fsWatcher.close was still called; use existing
helpers like makeManifest, mockedReadManifest, mockedFsWatch and inspect
mockedFsWatch.mock.results to locate the watcher instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/ws/watcher.ts`:
- Around line 82-86: The watcher currently assumes
listSessionPanes(manifest.sessionName) will reject when tmux is unavailable, but
tmux.ts swallows errors and returns an empty Map, causing false "gone"
transitions; update watcher.ts to detect tmux unavailability by catching and/or
inspecting the error type from listSessionPanes (use the PpgError hierarchy) and
specifically handle PpgError with code TMUX_NOT_FOUND (or any TMUX-related error
code) to return/skip the cycle; locate the call to listSessionPanes and the
paneMap handling and change the try/catch to examine the thrown error (or the
returned Map result) and treat PpgError.code === 'TMUX_NOT_FOUND' as the skip
case while preserving normal empty-Map behavior for legitimate empty sessions.
- Around line 57-64: The onManifestFileChange handler can start while a prior
poll is running and can still broadcast after stop(); fix by serializing polls
and preventing late emits: add a module-scoped boolean or Promise lock (e.g.,
currentPoll) and a stopped flag; in onManifestFileChange check and await
currentPoll (or set/clear the running flag) so only one poll runs at a time, and
before calling broadcast or mutating previousStatuses verify stopped is false;
in stop() set stopped=true and await currentPoll to finish (or clear the lock)
so no in-flight cycle can emit after shutdown, and keep the existing try/catch
around readManifest to swallow transient read errors.
- Around line 39-55: The current watcher uses manifestPath(projectRoot) (mPath)
directly which breaks when the file is atomically replaced; change the watcher
to watch the manifest directory (use path.dirname(mPath)) instead of the file
inode and filter events by path.basename(mPath) inside the callback, keeping the
existing debounce logic (debounceTimer, debounceMs) and stopped guard, and
retain watcher.on('error') and the try/catch around creating watcher; update any
references to fs.watch(mPath, ...) to fs.watch(dir, ...) with a filename check
before calling onManifestFileChange().

---

Nitpick comments:
In `@src/server/ws/watcher.test.ts`:
- Around line 245-270: Add a regression test to ensure watcher.stop() prevents
late emits from an in-flight poll: in the existing describe('cleanup') test
block that calls startManifestWatcher, stub/mock the module/function that
performs the poll (checkAgentStatus or the internal function invoked during
polling) to return a deferred promise (resolve it after calling watcher.stop()),
trigger the polling path (e.g., via triggerFsWatch or advancing timers to start
the poll), call watcher.stop() while the promise is still pending, then resolve
the deferred promise and advance timers—assert that no new events (especially an
`agent:status` event) are pushed to the events array and that fsWatcher.close
was still called; use existing helpers like makeManifest, mockedReadManifest,
mockedFsWatch and inspect mockedFsWatch.mock.results to locate the watcher
instance.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34deb69 and 928e0a7.

📒 Files selected for processing (2)
  • src/server/ws/watcher.test.ts
  • src/server/ws/watcher.ts

Comment on lines 82 to 86
try {
paneMap = await listSessionPanes(manifest.sessionName);
} catch {
return; // tmux unavailable — skip
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

tmux unavailable handling currently relies on a rejection path that may not happen.

Line 83 expects listSessionPanes() to reject, but src/core/tmux.ts (Lines 202-232) catches failures and returns an empty Map. That can convert tmux outages into synthetic gone transitions instead of skipping the cycle.

As per coding guidelines: Use PpgError hierarchy with typed error codes: TMUX_NOT_FOUND, NOT_GIT_REPO, NOT_INITIALIZED, MANIFEST_LOCK, WORKTREE_NOT_FOUND, AGENT_NOT_FOUND, MERGE_FAILED, INVALID_ARGS, AGENTS_RUNNING, WAIT_TIMEOUT, AGENTS_FAILED, NO_SESSION_ID.

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

In `@src/server/ws/watcher.ts` around lines 82 - 86, The watcher currently assumes
listSessionPanes(manifest.sessionName) will reject when tmux is unavailable, but
tmux.ts swallows errors and returns an empty Map, causing false "gone"
transitions; update watcher.ts to detect tmux unavailability by catching and/or
inspecting the error type from listSessionPanes (use the PpgError hierarchy) and
specifically handle PpgError with code TMUX_NOT_FOUND (or any TMUX-related error
code) to return/skip the cycle; locate the call to listSessionPanes and the
paneMap handling and change the try/catch to examine the thrown error (or the
returned Map result) and treat PpgError.code === 'TMUX_NOT_FOUND' as the skip
case while preserving normal empty-Map behavior for legitimate empty sessions.

P1: Replace inline import() type with top-level import type for PaneInfo
P1: Add multi-agent/multi-worktree test covering nested loop + worktreeId mapping
P2: Replace WsEvent payload: unknown with discriminated union for type-safe consumers
P2: Add directory watcher fallback when manifest.json doesn't exist at startup
P2: Add test for agent removed between polls (stale entry handling)
P3: Use Promise.all for parallel agent status checks (consistent with refreshAllAgentStatuses)
P3: Add polling overlap guard to prevent duplicate events from slow polls
P3: Fix cleanup test silent if-guard — assert watchResults.length > 0
P3: Move makeManifest factory to shared test-fixtures.ts
P3: Add optional onError callback for error observability
P3: Document manifest:updated vs agent:status event gap in JSDoc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement manifest watcher

1 participant