Skip to content

Skills: stop using chokidar globs for refresh#54927

Open
08820048 wants to merge 1 commit intoopenclaw:mainfrom
08820048:codex/skills-refresh-chokidar
Open

Skills: stop using chokidar globs for refresh#54927
08820048 wants to merge 1 commit intoopenclaw:mainfrom
08820048:codex/skills-refresh-chokidar

Conversation

@08820048
Copy link
Copy Markdown

Summary

  • Problem: skills refresh watched */SKILL.md glob targets that chokidar v4 no longer expands, so nested skill folders stopped triggering reloads.
  • Why it matters: editing workspace/skills/<name>/SKILL.md leaves the runtime on stale skill definitions until a manual refresh or restart.
  • What changed: watch the skill roots directly, cap traversal at depth 1, and filter events so only SKILL.md or <skill>/SKILL.md paths can trigger a refresh.
  • What did NOT change (scope boundary): no skill loading semantics, config schema, or non-skill filesystem watchers changed.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write N/A. If the cause is unclear, write Unknown.

  • Root cause: the watcher passed glob patterns like <skillsRoot>/*/SKILL.md into chokidar.watch, but chokidar v4 removed glob expansion support.
  • Missing detection / guardrail: the existing watcher test only asserted the configured watch targets and did not simulate a nested SKILL.md change event through the v4-compatible watch path.
  • Prior context (git blame, prior PR, issue, or refactor if known): Unknown.
  • Why this regressed now: once runtime moved onto a chokidar version without glob support, nested skill directories stopped being watched even though root-level SKILL.md still worked.
  • If unknown, what was ruled out: ruled out skills snapshot/version code and debounce handling by verifying the broken surface was specifically the watched path configuration.

Regression Test Plan (if applicable)

For bug fixes or regressions, name the smallest reliable test coverage that should have caught this. Otherwise write N/A.

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/agents/skills/refresh.test.ts
  • Scenario the test should lock in: nested skills/<name>/SKILL.md changes should trigger a refresh, while sibling files like notes.md should not.
  • Why this is the smallest reliable guardrail: the failure is in watcher configuration and event filtering, which can be deterministically exercised with a mocked watcher without needing a full filesystem integration test.
  • Existing test that already covers this (if any): the pre-existing watcher test covered ignore patterns only.
  • If no new test is added, why not:

User-visible / Behavior Changes

Editing SKILL.md inside a nested skill directory now refreshes workspace skills again without a restart when skills.load.watch is enabled.

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local Node 22 / pnpm workspace
  • Model/provider: N/A
  • Integration/channel (if any): N/A
  • Relevant config (redacted): default skills watcher with workspace skills enabled

Steps

  1. Configure a workspace skill under ~/.openclaw/workspace/skills/<skill>/SKILL.md.
  2. Start the watcher path via the normal runtime.
  3. Edit the nested SKILL.md.

Expected

  • The skills snapshot should bump and refresh the loaded skill definitions.

Actual

  • Before this change, only root-level skills/SKILL.md was watched; nested skill edits were ignored.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: ran pnpm test -- src/agents/skills/refresh.test.ts and pnpm tsgo; the updated unit test simulates a nested my-skill/SKILL.md change and confirms it bumps the snapshot, while notes.md does not.
  • Edge cases checked: root-level SKILL.md, nested <skill>/SKILL.md, ignored cache/build directories, and unrelated files under a skill directory.
  • What you did not verify: live end-to-end filesystem events against a real chokidar instance outside the unit test harness.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert this commit or disable skills.load.watch.
  • Files/config to restore: src/agents/skills/refresh.ts
  • Known bad symptoms reviewers should watch for: nested skill edits stop refreshing again, or unrelated files under skills/ begin triggering reload churn.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: filtering watched paths too aggressively could miss valid skill layouts.
    • Mitigation: the watcher now explicitly allows both supported layouts (SKILL.md and <skill>/SKILL.md) and tests both positive and negative cases.

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: M labels Mar 26, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR fixes a chokidar v4 compatibility regression where skill file watchers used glob patterns (*/SKILL.md) that chokidar v4 no longer expands, causing nested skill directory changes to be silently missed.\n\nThe fix watches skill root directories directly (depth: 1) and replaces the static ignored array with a function-based callback (shouldIgnoreSkillsWatchPath) that filters by relative path depth and filename. A defense-in-depth guard in schedule (isWatchedSkillFilePath) prevents non-SKILL.md events from bumping the snapshot even if the ignored filter misfires. The approach is clean and fits naturally alongside the existing debounce and listener infrastructure.\n\nKey changes:\n- resolveWatchTargets (glob-based) replaced by resolveWatchRoots (directory roots) + depth: 1 chokidar option\n- New shouldIgnoreSkillsWatchPath function handles the two supported layouts: SKILL.md (root) and <skill>/SKILL.md (nested)\n- New isWatchedSkillFilePath guard in schedule as a secondary filter\n- Test infrastructure upgraded to capture watcher instances and simulate event dispatch; new test locks in that only valid SKILL.md change paths bump the snapshot\n\nConcerns:\n- shouldIgnoreSkillsWatchPath returns true (ignore) when stats is undefined for a depth-1 directory entry. If chokidar calls ignored before it can provide stats (e.g., on a newly-created directory or a transient stat failure), the skill subdirectory is marked as ignored and its contents are never watched.\n- The new snapshot-bump test only exercises the change event; add and unlink handlers share the same schedule logic but have no test coverage for valid or invalid paths.

Confidence Score: 4/5

Safe to merge with one targeted fix recommended: the undefined stats fallback in shouldIgnoreSkillsWatchPath could silently prevent newly-created skill directories from being watched.

The core bug fix is correct and well-tested for the primary user path. One P1 edge case exists — stats === undefined for a depth-1 directory causes it to be ignored — but it only affects newly-created skill directories while the watcher is running, not the editing case the PR is focused on.

src/agents/skills/refresh.ts — specifically the stats?.isDirectory?.() !== true branch in shouldIgnoreSkillsWatchPath (line 134)

Important Files Changed

Filename Overview
src/agents/skills/refresh.ts Replaces glob-based chokidar watch targets with direct directory roots and a function-based ignored callback; one edge case with undefined stats for skill subdirectories.
src/agents/skills/refresh.test.ts Test infrastructure upgraded to capture watcher instances and simulate events; missing add/unlink coverage in the snapshot-bump test.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/skills/refresh.ts
Line: 130-134

Comment:
**Skill subdirectory silently ignored when `stats` is `undefined`**

When `shouldIgnoreSkillsWatchPath` is called by chokidar for a newly-created skill subdirectory (e.g. `skills/new-skill/`) and `stats` is `undefined` (chokidar can call `ignored` before completing a stat in some code paths, or stat can fail on a transient race), the expression `stats?.isDirectory?.() !== true` evaluates to `undefined !== true``true`, so the directory is **ignored**. Chokidar will then never traverse into it and will never surface `skills/new-skill/SKILL.md` add/change events.

For the watched-root (`relative === ""`) and known-file cases this is fine, but for the directory case a safe fallback should not treat "unknown stats" as "not a directory":

```suggestion
    return stats !== undefined && stats.isDirectory?.() !== true;
```

With this change, when `stats` is `undefined` (unknown), the function returns `false` (don't ignore), letting chokidar proceed to stat the entry and call `ignored` again with actual stats — which is the intended behaviour.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/skills/refresh.test.ts
Line: 113-150

Comment:
**`add`/`unlink` event paths not exercised in the snapshot test**

The new `"bumps the snapshot only for supported skill file changes"` test only fires a `change` event. The `add` and `unlink` handlers are registered separately (`watcher.on("add", …)`, `watcher.on("unlink", …)`) and both go through the same `schedule` guard, but they are never tested. A new skill being created (add) or deleted (unlink) are real user-facing triggers. Consider extending the test to call `handlers.get("add")` and `handlers.get("unlink")` for a valid SKILL.md path and assert the snapshot bumps, and also for an invalid path to confirm they are ignored — mirroring the `change` cases already written.

This is a minor coverage gap; the core logic is shared, so the risk is low, but the regression surface (newly created skill files not triggering a reload) is the kind of thing users would notice.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Skills: stop using chokidar globs for re..." | Re-trigger Greptile

Comment on lines +130 to +134
if (segments.length === 1) {
if (segments[0] === "SKILL.md") {
return false;
}
return stats?.isDirectory?.() !== true;
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.

P1 Skill subdirectory silently ignored when stats is undefined

When shouldIgnoreSkillsWatchPath is called by chokidar for a newly-created skill subdirectory (e.g. skills/new-skill/) and stats is undefined (chokidar can call ignored before completing a stat in some code paths, or stat can fail on a transient race), the expression stats?.isDirectory?.() !== true evaluates to undefined !== truetrue, so the directory is ignored. Chokidar will then never traverse into it and will never surface skills/new-skill/SKILL.md add/change events.

For the watched-root (relative === "") and known-file cases this is fine, but for the directory case a safe fallback should not treat "unknown stats" as "not a directory":

Suggested change
if (segments.length === 1) {
if (segments[0] === "SKILL.md") {
return false;
}
return stats?.isDirectory?.() !== true;
return stats !== undefined && stats.isDirectory?.() !== true;

With this change, when stats is undefined (unknown), the function returns false (don't ignore), letting chokidar proceed to stat the entry and call ignored again with actual stats — which is the intended behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/skills/refresh.ts
Line: 130-134

Comment:
**Skill subdirectory silently ignored when `stats` is `undefined`**

When `shouldIgnoreSkillsWatchPath` is called by chokidar for a newly-created skill subdirectory (e.g. `skills/new-skill/`) and `stats` is `undefined` (chokidar can call `ignored` before completing a stat in some code paths, or stat can fail on a transient race), the expression `stats?.isDirectory?.() !== true` evaluates to `undefined !== true``true`, so the directory is **ignored**. Chokidar will then never traverse into it and will never surface `skills/new-skill/SKILL.md` add/change events.

For the watched-root (`relative === ""`) and known-file cases this is fine, but for the directory case a safe fallback should not treat "unknown stats" as "not a directory":

```suggestion
    return stats !== undefined && stats.isDirectory?.() !== true;
```

With this change, when `stats` is `undefined` (unknown), the function returns `false` (don't ignore), letting chokidar proceed to stat the entry and call `ignored` again with actual stats — which is the intended behaviour.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +113 to 150
it("bumps the snapshot only for supported skill file changes", async () => {
vi.useFakeTimers();
const mod = await import("./refresh.js");
const events: Array<{ workspaceDir?: string; changedPath?: string }> = [];
const unregister = mod.registerSkillsChangeListener((event) => {
events.push(event);
});

mod.ensureSkillsWatcher({
workspaceDir: "/tmp/workspace",
config: {
skills: {
load: {
watchDebounceMs: 25,
},
},
},
});

const watcher = watcherInstances.at(-1);
expect(watcher).toBeDefined();

watcher?.handlers.get("change")?.("/tmp/workspace/skills/my-skill/SKILL.md");
await vi.advanceTimersByTimeAsync(25);
expect(events).toEqual([
{
workspaceDir: "/tmp/workspace",
reason: "watch",
changedPath: "/tmp/workspace/skills/my-skill/SKILL.md",
},
]);

watcher?.handlers.get("change")?.("/tmp/workspace/skills/my-skill/notes.md");
await vi.advanceTimersByTimeAsync(25);
expect(events).toHaveLength(1);

// Should NOT ignore normal skill files
expect(ignored.some((re) => re.test("/tmp/.hidden/skills/index.md"))).toBe(false);
expect(ignored.some((re) => re.test("/tmp/workspace/skills/my-skill/SKILL.md"))).toBe(false);
unregister();
});
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.

P2 add/unlink event paths not exercised in the snapshot test

The new "bumps the snapshot only for supported skill file changes" test only fires a change event. The add and unlink handlers are registered separately (watcher.on("add", …), watcher.on("unlink", …)) and both go through the same schedule guard, but they are never tested. A new skill being created (add) or deleted (unlink) are real user-facing triggers. Consider extending the test to call handlers.get("add") and handlers.get("unlink") for a valid SKILL.md path and assert the snapshot bumps, and also for an invalid path to confirm they are ignored — mirroring the change cases already written.

This is a minor coverage gap; the core logic is shared, so the risk is low, but the regression surface (newly created skill files not triggering a reload) is the kind of thing users would notice.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/skills/refresh.test.ts
Line: 113-150

Comment:
**`add`/`unlink` event paths not exercised in the snapshot test**

The new `"bumps the snapshot only for supported skill file changes"` test only fires a `change` event. The `add` and `unlink` handlers are registered separately (`watcher.on("add", …)`, `watcher.on("unlink", …)`) and both go through the same `schedule` guard, but they are never tested. A new skill being created (add) or deleted (unlink) are real user-facing triggers. Consider extending the test to call `handlers.get("add")` and `handlers.get("unlink")` for a valid SKILL.md path and assert the snapshot bumps, and also for an invalid path to confirm they are ignored — mirroring the `change` cases already written.

This is a minor coverage gap; the core logic is shared, so the risk is low, but the regression surface (newly created skill files not triggering a reload) is the kind of thing users would notice.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b647fb3ea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (segments[0] === "SKILL.md") {
return false;
}
return stats?.isDirectory?.() !== true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid ignoring top-level skill dirs when stats are absent

The ignored predicate currently returns true for first-level entries unless stats.isDirectory() is explicitly true, but chokidar evaluates ignored paths before stat-ing them in its traversal path (it calls _isIgnored(path) without stats in _addToNodeFs). For a directory like .../skills/my-skill, this line treats it as ignored on the pre-stat check, so chokidar never descends into that folder and nested SKILL.md changes still do not trigger refreshes.

Useful? React with 👍 / 👎.

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

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: skills glob file change watch via chokidar is not worked

1 participant