Skip to content

fix(core): watch for skills/ inside existing .agents and .claude roots - #55280

Open
Hona wants to merge 1 commit into
anomalyco:devfrom
Hona:skills-watcher-wait
Open

Hona wants to merge 1 commit into
anomalyco:devfrom
Hona:skills-watcher-wait

Conversation

@Hona

@Hona Hona commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Fixes the flaky config plugin reloads > loads skills when <root> appears after startup test.

Failing runs:

error: Timed out waiting for config plugin reloads
(fail) config plugin reloads > loads skills when .agents appears after startup [1217.60ms]

Cause

  • Product bug: ConfigCompatibilityPlugin only watched <root>/skills when it already existed. If .agents or .claude existed without skills/, a later skills/ was never loaded until restart.
  • Same race in the test: Bun.write(".agents/skills/probe/SKILL.md") creates the directories one by one. When the refresh ran between mkdir .agents and mkdir .agents/skills, the skill was lost.
  • Second gap: each refresh cleared every watch and reattached it, and the scan did not wait for the new watch. Writes made before the watch attached were lost.
  • Test timing: each wait polled for about 1 second of wall-clock time, so slow CI runners also failed.
sequenceDiagram
  participant T as Test / user
  participant C as Config parent watch
  participant P as Compatibility plugin
  T->>C: mkdir .agents
  C->>P: change event, refresh
  P->>P: realPath(.agents/skills) fails, no watch (before)
  T->>T: mkdir .agents/skills, write SKILL.md
  Note over P: nothing is watching, skill is never loaded
Loading

Fix

  • compatibility.ts
    • Watches <root>/skills as a file target when it is missing, so its creation triggers a refresh. This is the same approach as ConfigSkillPlugin.
    • Passes a readiness callback to watcher.subscribe, so a newly attached watch triggers one rescan. Config uses the same pattern.
    • Removes only the stale watches. Live watches stay attached, so readiness does not loop.
  • reload.test.ts
    • Replaces the 1-second polling with settle(events, action, condition). It subscribes to config.updated or skill.updated before the action and checks the condition on each event. The test runner timeout is the only time limit.
    • Creates the root before skills/, which covers the product bug. Recreating the whole tree in one write still covers the concurrent case.

Not in this PR

  • Exit code 139 in the dev run: turbo crashed, not Bun. The log shows error: Failed to run "turbo" due to signal SIGSEGV after bun test exited normally with code 1. In this window it happened in 2 of 19 failed linux unit jobs, and only after a task failed. That matches a known turbo 2.10.x crash when a run ends with a failed task. The repo pins turbo 2.10.2.

Added by the opencode bot (AI-generated).

Related: #54114, #53393, #42478

Copilot AI balanced review requested due to automatic review settings October 14, 2026 01:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fix looks right to me, and I found nothing that should block a merge.

How I checked: I ran the updated reload.test.ts against the base. All four loads skills when <root> appears after startup cases time out at the step that creates the root before skills/, so the bug is real. With this PR the file passes 12/12 on three runs (it uses the real native watcher). The rest of test/config passes (238 tests) and typecheck in packages/core is clean.

Code: readiness can't loop, because FiberMap.run(..., { onlyIfMissing: true }) only consumes the stream (and fires onReady) for a newly attached key. Watches that died drop out of the map and get reattached on the next refresh. The file watch on a missing <root>/skills matches what ConfigSkillPlugin does.

Public API: unchanged.

Note on #54114 / #53393: dropping FiberMap.clear means a refresh no longer rebuilds the native watches. But refresh() still calls watcher.subscribe for every root, and that writes a watcher subscribe log line on every refresh. If the data directory is the same as the config directory, those writes can still feed back into config.changes(), so I've listed both as related rather than fixed. #54450 is an outside PR that also reconciles these watches.

Related: #54114, #53393, #42478

This branch has not been deployed

No deployments
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.

2 participants