Repository: go-task/task

Issue:
Multiple matches with wildcards
> 1. I think that adding this restriction is fine for now. It keeps things simple. However, I can definitely see a world where someone with a complex setup asks for the current behaviour as it would allow you to do something like this:
> 
>     ```yml
>     version: 3
> 
>     tasks:
>       foo-*-*:
>         ..
>       foo-*:
>         ..
>     ```
> 
>     This allows Task to do something different depending on if the user does `task foo-bar` vs `task foo-bar-baz`. This would not be possible if we errored when multiple tasks match since `task-bar-baz` matches both tasks in the file.
> 
>     Happy to add the error for now and revisit this later though.

_Originally posted by @pd93 in https://github.com/go-task/task/pull/1489#discussion_r1499811332_
            

Discussion:
@MetalArend: Running `task do:1:2` with the following file:

```
tasks:
  do:*:
    vars:
      VAR1: '{{index .MATCH 0}}'
    cmds:
      - echo "{{.VAR1}}"
  do:*:*:
    vars:
      VAR1: '{{index .MATCH 0}}'
      VAR2: '{{index .MATCH 1}}'
    cmds:
      - echo "{{.VAR1}}"
      - echo "{{.VAR2}}"
```

results in

> task: Found multiple tasks (do:*, do:*:*) that match "do:1:2"

---

Running it with the following file:

```
tasks: 
  do:*:
    aliases:
      - do:*:*
    vars:
      VAR1: '{{index .MATCH 0}}'
      VAR2: '{{index .MATCH 1}}'
    cmds:
      - echo "{{.VAR1}}"
      - echo "{{.VAR2}}"
```

results in

> template: :1:2: executing "" at <index .MATCH 1>: error calling index: reflect: slice index out of range

---

I'm not sure how this can be solved currently.


Solution patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 13728403..5defec36 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -47,7 +47,7 @@ jobs:
       - uses: actions/checkout@v4
       - name: Get changed files in the docs folder
         id: changed-files-specific
-        uses: tj-actions/changed-files@v45
+        uses: tj-actions/changed-files@v46
         with:
           files: website/versioned_docs/**
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2eb54e9c..75b88607 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.23.x
+          go-version: 1.24.x
 
       - name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v6
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 870cd246..03de2992 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,25 @@
 
 ## Unreleased
 
+- Significant improvements were made to the watcher. We migrated from
+  [watcher](https://github.com/radovskyb/watcher) to
+  [fsnotify](https://github.com/fsnotify/fsnotify). The former library used
+  polling, which means Task had a high CPU usage when watching too many files.
+  `fsnotify` uses proper the APIs from each operating system to watch files,
+  which means a much better performance. The default interval changed from 5
+  seconds to 100 milliseconds, because now it configures the wait time for
+  duplicated events, instead of the polling time (#2048 by @andreynering, #1508,
+  #985, #1179).
+- The [Map Variables experiment](#1585) was made generally available so you can
+  now
+  [define map variables in your Taskfiles!](https://taskfile.dev/next/usage/#variables)
+  (#1585, #1547, #2081 by @pd93).
 - The default taskfile (output when using the `--init` flag) is now an embedded
   file in the binary instead of being stored in the code (#2112 by @pd93).
+- Fixed a bug where sorting tasks by "none" would use the default sorting
+  instead of leaving tasks in the order they were defined (#2124, #2125 by
+  @trulede).
+- Fixed Fish completion on newer Fish versions (#2130 by @atusy).
 
 #### Package API
 
diff --git a/Taskfile.yml b/Taskfile.yml
index e8ce66f1..ee9bcf9a 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -18,6 +18,11 @@ tasks:
       - task: lint
       - task: test
 
+  run:
+    desc: Runs Task
+    cmds:
+      - go run ./cmd/task {{.CLI_ARGS}}
+
   install:
     desc: Installs Task
     aliases: [i]
@@ -104,6 +109,12 @@ tasks:
     cmds:
       - go test ./...
 
+  test:watch:
+    desc: Runs test suite with watch tests included
+    deps: [sleepit:build]
+    cmds:
+      - go test ./... -tags 'watch'
+
   test:all:
     desc: Runs test suite with signals and watch tests included
     deps: [sleepit:build]
diff --git a/completion/fish/task.fish b/completion/fish/task.fish
index a38a2130..cfe6ca61 100644
--- a/completion/fish/task.fish
+++ b/completion/fish/task.fish
@@ -1,6 +1,6 @@
 set GO_TASK_PROGNAME task
 
-function __task_get_tasks --description "Prints all available tasks with their description"
+function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
   # Read the list of tasks (and potential errors)
   $GO_TASK_PROGNAME --list-all 2>&1 | read -lz rawOutput
 
diff --git a/executor.go b/executor.go
index 6b8d991f..c7d5f64c 100644
--- a/executor.go
+++ b/executor.go
@@ -7,6 +7,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/puzpuzpuz/xsync/v3"
 	"github.com/sajari/fuzzy"
 
 	"github.com/go-task/task/v3/internal/logger"
@@ -65,6 +66,7 @@ type (
 		mkdirMutexMap        map[string]*sync.Mutex
 		executionHashes      map[string]context.Context
 		executionHashesMutex sync.Mutex
+		watchedDirs          *xsync.MapOf[string, bool]
 	}
 	TempDir struct {
 		Remote      string
@@ -77,7 +79,6 @@ type (
 func NewExecutor(opts ...ExecutorOption) *Executor {
 	e := &Executor{
 		Timeout:              time.Second * 10,
-		Interval:             time.Second * 5,
 		Stdin:                os.Stdin,
 		Stdout:               os.Stdout,
 		Stderr:               os.Stderr,
@@ -254,8 +255,8 @@ func ExecutorWithConcurrency(concurrency int) ExecutorOption {
 	}
 }
 
-// ExecutorWithInterval sets the interval at which the [Executor] will check for
-// changes when watching tasks.
+// ExecutorWithInterval sets the interval at which the [Executor] will wait for
+// duplicated events before running a task.
 func ExecutorWithInterval(interval time.Duration) ExecutorOption {
 	return func(e *Executor) {
 		e.Interval = interval
diff --git a/go.mod b/go.mod
index 3699d7d0..bc5fda63 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/dominikbraun/graph v0.23.0
 	github.com/elliotchance/orderedmap/v3 v3.1.0
 	github.com/fatih/color v1.18.0
+	github.com/fsnotify/fsnotify v1.8.0
 	github.com/go-git/go-billy/v5 v5.6.2
 	github.com/go-git/go-git/v5 v5.14.0
 	github.com/go-task/slim-sprig/v3 v3.0.0
@@ -19,7 +20,7 @@ require (
 	github.com/mattn/go-zglob v0.0.6
 	github.com/mitchellh/hashstructure/v2 v2.0.2
 	github.com/otiai10/copy v1.14.1
-	github.com/radovskyb/watcher v1.0.7
+	github.com/puzpuzpuz/xsync/v3 v3.5.1
 	github.com/sajari/fuzzy v1.0.0
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
@@ -45,7 +46,6 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/otiai10/mint v1.6.3 // indirect
 	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -54,9 +54,7 @@ require (
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	golang.org/x/crypto v0.35.0 // indirect
-	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/net v0.35.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/tools v0.27.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 )
diff --git a/go.sum b/go.sum
index 9a8afcda..c9898452 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,6 @@ github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6h
 github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
 github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
@@ -23,14 +21,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
 github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
-github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
-github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
 github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
 github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
-github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
-github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
-github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
-github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
+github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
+github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -40,14 +34,16 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA
 github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
 github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
-github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
-github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
+github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
 github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
 github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -56,8 +52,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
-github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
 github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
 github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
@@ -66,12 +60,10 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
 github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -98,8 +90,6 @@ github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
 github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
 github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
 github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
-github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
-github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
@@ -112,17 +102,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
-github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
+github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
 github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
-github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
 github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
 github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
@@ -141,22 +129,13 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
 github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
 github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
 golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
-golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
-golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
 golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
 golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -168,22 +147,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
 golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
-golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
-golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -194,7 +166,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
-mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
 mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
 mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go
index 6235a039..ab676022 100644
--- a/internal/experiments/experiments.go
+++ b/internal/experiments/experiments.go
@@ -44,7 +44,7 @@ func init() {
 	GentleForce = New("GENTLE_FORCE", 1)
 	RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
 	AnyVariables = New("ANY_VARIABLES")
-	MapVariables = New("MAP_VARIABLES", 1, 2)
+	MapVariables = New("MAP_VARIABLES")
 	EnvPrecedence = New("ENV_PRECEDENCE", 1)
 }
 
diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go
index f564eff2..d9e70135 100644
--- a/internal/fingerprint/glob.go
+++ b/internal/fingerprint/glob.go
@@ -12,28 +12,20 @@ import (
 )
 
 func Globs(dir string, globs []*ast.Glob) ([]string, error) {
-	fileMap := make(map[string]bool)
+	resultMap := make(map[string]bool)
 	for _, g := range globs {
-		matches, err := Glob(dir, g.Glob)
+		matches, err := glob(dir, g.Glob)
 		if err != nil {
 			continue
 		}
 		for _, match := range matches {
-			fileMap[match] = !g.Negate
+			resultMap[match] = !g.Negate
 		}
 	}
-	files := make([]string, 0)
-	for file, includePath := range fileMap {
-		if includePath {
-			files = append(files, file)
-		}
-	}
-	sort.Strings(files)
-	return files, nil
+	return collectKeys(resultMap), nil
 }
 
-func Glob(dir string, g string) ([]string, error) {
-	files := make([]string, 0)
+func glob(dir string, g string) ([]string, error) {
 	g = filepathext.SmartJoin(dir, g)
 
 	g, err := execext.Expand(g)
@@ -46,6 +38,8 @@ func Glob(dir string, g string) ([]string, error) {
 		return nil, err
 	}
 
+	results := make(map[string]bool, len(fs))
+
 	for _, f := range fs {
 		info, err := os.Stat(f)
 		if err != nil {
@@ -54,7 +48,18 @@ func Glob(dir string, g string) ([]string, error) {
 		if info.IsDir() {
 			continue
 		}
-		files = append(files, f)
+		results[f] = true
+	}
+	return collectKeys(results), nil
+}
+
+func collectKeys(m map[string]bool) []string {
+	keys := make([]string, 0, len(m))
+	for k, v := range m {
+		if v {
+			keys = append(keys, k)
+		}
 	}
-	return files, nil
+	sort.Strings(keys)
+	return keys
 }
diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go
index 91cb6e38..16a98c16 100644
--- a/internal/fingerprint/sources_checksum.go
+++ b/internal/fingerprint/sources_checksum.go
@@ -56,7 +56,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
 			if g.Negate {
 				continue
 			}
-			generates, err := Glob(t.Dir, g.Glob)
+			generates, err := glob(t.Dir, g.Glob)
 			if os.IsNotExist(err) {
 				return false, nil
 			}
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 7d437537..8c21193e 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -184,7 +184,7 @@ func WithExecutorOptions() task.ExecutorOption {
 		var sorter sort.Sorter
 		switch TaskSort {
 		case "none":
-			sorter = nil
+			sorter = sort.NoSort
 		case "alphanumeric":
 			sorter = sort.AlphaNumeric
 		}
diff --git a/internal/fsnotifyext/fsnotify_dedup.go b/internal/fsnotifyext/fsnotify_dedup.go
new file mode 100644
index 00000000..ef9fa9cb
--- /dev/null
+++ b/internal/fsnotifyext/fsnotify_dedup.go
@@ -0,0 +1,56 @@
+package fsnotifyext
+
+import (
+	"math"
+	"sync"
+	"time"
+
+	"github.com/fsnotify/fsnotify"
+)
+
+type Deduper struct {
+	w        *fsnotify.Watcher
+	waitTime time.Duration
+	mutex    sync.Mutex
+}
+
+func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
+	return &Deduper{
+		w:        w,
+		waitTime: waitTime,
+	}
+}
+
+func (d *Deduper) GetChan() chan fsnotify.Event {
+	channel := make(chan fsnotify.Event)
+	timers := make(map[string]*time.Timer)
+
+	go func() {
+		for {
+			event, ok := <-d.w.Events
+			switch {
+			case !ok:
+				return
+			case event.Op == fsnotify.Chmod:
+				continue
+			}
+
+			d.mutex.Lock()
+			timer, ok := timers[event.String()]
+			d.mutex.Unlock()
+
+			if !ok {
+				timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
+				timer.Stop()
+
+				d.mutex.Lock()
+				timers[event.String()] = timer
+				d.mutex.Unlock()
+			}
+
+			timer.Reset(d.waitTime)
+		}
+	}()
+
+	return channel
+}
diff --git a/internal/sort/sorter.go b/internal/sort/sorter.go
index 404bc84a..14195973 100644
--- a/internal/sort/sorter.go
+++ b/internal/sort/sorter.go
@@ -9,6 +9,11 @@ import (
 // A Sorter is any function that sorts a set of tasks.
 type Sorter func(items []string, namespaces []string) []string
 
+// NoSort leaves the tasks in the order they are defined.
+func NoSort(items []string, namespaces []string) []string {
+	return items
+}
+
 // AlphaNumeric sorts the JSON output so that tasks are in alpha numeric order
 // by task name.
 func AlphaNumeric(items []string, namespaces []string) []string {
diff --git a/task.go b/task.go
index 8d54a065..40f7661a 100644
--- a/task.go
+++ b/task.go
@@ -412,7 +412,6 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
 		return matchingTasks
 	}
 	// Attempt a wildcard match
-	// For now, we can just nil check the task before each loop
 	for _, value := range e.Taskfile.Tasks.All(nil) {
 		if match, wildcards := value.WildcardMatch(call.Task); match {
 			matchingTasks = append(matchingTasks, &MatchingTask{
@@ -430,23 +429,12 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
 func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
 	// Search for a matching task
 	matchingTasks := e.FindMatchingTasks(call)
-	switch len(matchingTasks) {
-	case 0: // Carry on
-	case 1:
+	if len(matchingTasks) > 0 {
 		if call.Vars == nil {
 			call.Vars = ast.NewVars()
 		}
 		call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
 		return matchingTasks[0].Task, nil
-	default:
-		taskNames := make([]string, len(matchingTasks))
-		for i, matchingTask := range matchingTasks {
-			taskNames[i] = matchingTask.Task.Task
-		}
-		return nil, &errors.TaskNameConflictError{
-			Call:      call.Task,
-			TaskNames: taskNames,
-		}
 	}
 
 	// If didn't find one, search for a task with a matching alias
diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go
index abe5157d..0806e423 100644
--- a/taskfile/ast/var.go
+++ b/taskfile/ast/var.go
@@ -1,12 +1,9 @@
 package ast
 
 import (
-	"strings"
-
 	"gopkg.in/yaml.v3"
 
 	"github.com/go-task/task/v3/errors"
-	"github.com/go-task/task/v3/internal/experiments"
 )
 
 // Var represents either a static or dynamic variable.
@@ -19,82 +16,26 @@ type Var struct {
 }
 
 func (v *Var) UnmarshalYAML(node *yaml.Node) error {
-	if experiments.MapVariables.Enabled() {
-
-		// This implementation is not backwards-compatible and replaces the 'sh' key with map variables
-		if experiments.MapVariables.Value == 1 {
-			var value any
-			if err := node.Decode(&value); err != nil {
-				return errors.NewTaskfileDecodeError(err, node)
-			}
-			// If the value is a string and it starts with $, then it's a shell command
-			if str, ok := value.(string); ok {
-				if str, ok = strings.CutPrefix(str, "$"); ok {
-					v.Sh = &str
-					return nil
-				}
-				if str, ok = strings.CutPrefix(str, "#"); ok {
-					v.Ref = str
-					return nil
-				}
-			}
-			v.Value = value
-			return nil
-		}
-
-		// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
-		if experiments.MapVariables.Value == 2 {
-			switch node.Kind {
-			case yaml.MappingNode:
-				key := node.Content[0].Value
-				switch key {
-				case "sh", "ref", "map":
-					var m struct {
-						Sh  *string
-						Ref string
-						Map any
-					}
-					if err := node.Decode(&m); err != nil {
-						return errors.NewTaskfileDecodeError(err, node)
-					}
-					v.Sh = m.Sh
-					v.Ref = m.Ref
-					v.Value = m.Map
-					return nil
-				default:
-					return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
-				}
-			default:
-				var value any
-				if err := node.Decode(&value); err != nil {
-					return errors.NewTaskfileDecodeError(err, node)
-				}
-				v.Value = value
-				return nil
-			}
-		}
-	}
-
 	switch node.Kind {
-
 	case yaml.MappingNode:
 		key := node.Content[0].Value
 		switch key {
-		case "sh", "ref":
+		case "sh", "ref", "map":
 			var m struct {
 				Sh  *string
 				Ref string
+				Map any
 			}
 			if err := node.Decode(&m); err != nil {
 				return errors.NewTaskfileDecodeError(err, node)
 			}
 			v.Sh = m.Sh
 			v.Ref = m.Ref
+			v.Value = m.Map
 			return nil
 		default:
-			return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
+			return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
 		}
-
 	default:
 		var value any
 		if err := node.Decode(&value); err != nil {
diff --git a/watch.go b/watch.go
index 3b290b8b..76bab9ae 100644
--- a/watch.go
+++ b/watch.go
@@ -6,18 +6,22 @@ import (
 	"os"
 	"os/signal"
 	"path/filepath"
+	"slices"
 	"strings"
 	"syscall"
 	"time"
 
-	"github.com/radovskyb/watcher"
+	"github.com/fsnotify/fsnotify"
+	"github.com/puzpuzpuz/xsync/v3"
 
 	"github.com/go-task/task/v3/errors"
+	"github.com/go-task/task/v3/internal/filepathext"
 	"github.com/go-task/task/v3/internal/fingerprint"
+	"github.com/go-task/task/v3/internal/fsnotifyext"
 	"github.com/go-task/task/v3/internal/logger"
 )
 
-const defaultWatchInterval = 5 * time.Second
+const defaultWaitTime = 100 * time.Millisecond
 
 // watchTasks start watching the given tasks
 func (e *Executor) watchTasks(calls ...*Call) error {
@@ -32,34 +36,48 @@ func (e *Executor) watchTasks(calls ...*Call) error {
 	for _, c := range calls {
 		c := c
 		go func() {
-			if err := e.RunTask(ctx, c); err != nil && !isContextError(err) {
+			err := e.RunTask(ctx, c)
+			if err == nil {
+				e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task)
+			} else if !isContextError(err) {
 				e.Logger.Errf(logger.Red, "%v\n", err)
 			}
 		}()
 	}
 
-	var watchInterval time.Duration
+	var waitTime time.Duration
 	switch {
 	case e.Interval != 0:
-		watchInterval = e.Interval
+		waitTime = e.Interval
 	case e.Taskfile.Interval != 0:
-		watchInterval = e.Taskfile.Interval
+		waitTime = e.Taskfile.Interval
 	default:
-		watchInterval = defaultWatchInterval
+		waitTime = defaultWaitTime
 	}
 
-	e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v\n", watchInterval)
-
-	w := watcher.New()
+	w, err := fsnotify.NewWatcher()
+	if err != nil {
+		cancel()
+		return err
+	}
 	defer w.Close()
-	w.SetMaxEvents(1)
+
+	deduper := fsnotifyext.NewDeduper(w, waitTime)
+	eventsChan := deduper.GetChan()
 
 	closeOnInterrupt(w)
 
 	go func() {
 		for {
 			select {
-			case event := <-w.Event:
+			case event, ok := <-eventsChan:
+				switch {
+				case !ok:
+					cancel()
+					return
+				case event.Op == fsnotify.Chmod:
+					continue
+				}
 				e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event)
 
 				cancel()
@@ -70,35 +88,58 @@ func (e *Executor) watchTasks(calls ...*Call) error {
 				for _, c := range calls {
 					c := c
 					go func() {
-						if err := e.RunTask(ctx, c); err != nil && !isContextError(err) {
+						t, err := e.GetTask(c)
+						if err != nil {
+							e.Logger.Errf(logger.Red, "%v\n", err)
+							return
+						}
+						baseDir := filepathext.SmartJoin(e.Dir, t.Dir)
+						files, err := fingerprint.Globs(baseDir, t.Sources)
+						if err != nil {
+							e.Logger.Errf(logger.Red, "%v\n", err)
+							return
+						}
+						if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) {
+							relPath, _ := filepath.Rel(baseDir, event.Name)
+							e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath)
+							return
+						}
+						err = e.RunTask(ctx, c)
+						if err == nil {
+							e.Logger.Errf(logger.Green, "task: task \"%s\" finished running\n", c.Task)
+						} else if !isContextError(err) {
 							e.Logger.Errf(logger.Red, "%v\n", err)
 						}
 					}()
 				}
-			case err := <-w.Error:
-				switch err {
-				case watcher.ErrWatchedFileDeleted:
+			case err, ok := <-w.Errors:
+				switch {
+				case !ok:
+					cancel()
+					return
 				default:
 					e.Logger.Errf(logger.Red, "%v\n", err)
 				}
-			case <-w.Closed:
-				cancel()
-				return
 			}
 		}
 	}()
 
+	e.watchedDirs = xsync.NewMapOf[string, bool]()
+
 	go func() {
-		// re-register every 5 seconds because we can have new files, but this process is expensive to run
+		// NOTE(@andreynering): New files can be created in directories
+		// that were previously empty, so we need to check for new dirs
+		// from time to time.
 		for {
-			if err := e.registerWatchedFiles(w, calls...); err != nil {
+			if err := e.registerWatchedDirs(w, calls...); err != nil {
 				e.Logger.Errf(logger.Red, "%v\n", err)
 			}
-			time.Sleep(watchInterval)
+			time.Sleep(5 * time.Second)
 		}
 	}()
 
-	return w.Start(watchInterval)
+	<-make(chan struct{})
+	return nil
 }
 
 func isContextError(err error) bool {
@@ -106,73 +147,66 @@ func isContextError(err error) bool {
 		err = taskRunErr.Err
 	}
 
-	return err == context.Canceled || err == context.DeadlineExceeded
+	return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
 }
 
-func closeOnInterrupt(w *watcher.Watcher) {
+func closeOnInterrupt(w *fsnotify.Watcher) {
 	ch := make(chan os.Signal, 1)
 	signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
 	go func() {
 		<-ch
 		w.Close()
+		os.Exit(0)
 	}()
 }
 
-func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...*Call) error {
-	watchedFiles := w.WatchedFiles()
-
-	var registerTaskFiles func(*Call) error
-	registerTaskFiles = func(c *Call) error {
+func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error {
+	var registerTaskDirs func(*Call) error
+	registerTaskDirs = func(c *Call) error {
 		task, err := e.CompiledTask(c)
 		if err != nil {
 			return err
 		}
 
 		for _, d := range task.Deps {
-			if err := registerTaskFiles(&Call{Task: d.Task, Vars: d.Vars}); err != nil {
+			if err := registerTaskDirs(&Call{Task: d.Task, Vars: d.Vars}); err != nil {
 				return err
 			}
 		}
 		for _, c := range task.Cmds {
 			if c.Task != "" {
-				if err := registerTaskFiles(&Call{Task: c.Task, Vars: c.Vars}); err != nil {
+				if err := registerTaskDirs(&Call{Task: c.Task, Vars: c.Vars}); err != nil {
 					return err
 				}
 			}
 		}
 
-		globs, err := fingerprint.Globs(task.Dir, task.Sources)
+		files, err := fingerprint.Globs(task.Dir, task.Sources)
 		if err != nil {
 			return err
 		}
 
-		for _, s := range globs {
-			files, err := fingerprint.Glob(task.Dir, s)
-			if err != nil {
-				return fmt.Errorf("task: %s: %w", s, err)
+		for _, f := range files {
+			d := filepath.Dir(f)
+			if isSet, ok := e.watchedDirs.Load(d); ok && isSet {
+				continue
 			}
-			for _, f := range files {
-				absFile, err := filepath.Abs(f)
-				if err != nil {
-					return err
-				}
-				if ShouldIgnoreFile(absFile) {
-					continue
-				}
-				if _, ok := watchedFiles[absFile]; ok {
-					continue
-				}
-				if err := w.Add(absFile); err != nil {
-					return err
-				}
-				e.Logger.VerboseOutf(logger.Green, "task: watching new file: %v\n", absFile)
+			if ShouldIgnoreFile(d) {
+				continue
+			}
+			if err := w.Add(d); err != nil {
+				return err
 			}
+			e.watchedDirs.Store(d, true)
+			relPath, _ := filepath.Rel(e.Dir, d)
+			w.Events <- fsnotify.Event{Name: f, Op: fsnotify.Create}
+			e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath)
 		}
 		return nil
 	}
 
 	for _, c := range calls {
-		if err := registerTaskFiles(c); err != nil {
+		if err := registerTaskDirs(c); err != nil {
 			return err
 		}
 	}
diff --git a/website/docs/experiments/map_variables.mdx b/website/docs/experiments/map_variables.mdx
deleted file mode 100644
index 477714d0..00000000
--- a/website/docs/experiments/map_variables.mdx
+++ /dev/null
@@ -1,245 +0,0 @@
----
-slug: /experiments/map-variables/
----
-
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
-
-# Map Variables (#1585)
-
-:::caution
-
-All experimental features are subject to breaking changes and/or removal _at any
-time_. We strongly recommend that you do not use these features in a production
-environment. They are intended for testing and feedback only.
-
-:::
-
-Currently, Task supports all variable types except for maps. This experiment
-adds two different proposals for map variables. Click on the tabs below to
-switch between them.
-
-<Tabs defaultValue="1" queryString="proposal"
-  values={[
-    {label: 'Proposal 1', value: '1'},
-    {label: 'Proposal 2', value: '2'}
-  ]}>
-
-<TabItem value="1">
-
-:::warning
-
-This experiment proposal breaks the following functionality:
-
-- Dynamically defined variables (using the `sh` keyword)
-
-:::
-
-:::info
-
-To enable this experiment, set the environment variable:
-`TASK_X_MAP_VARIABLES=1`. Check out [our guide to enabling experiments
-][enabling-experiments] for more information.
-
-:::
-
-This proposal removes support for the `sh` and `ref` keywords in favour of a new
-syntax for dynamically defined variables and references. This allows you to
-define a map directly as you would for any other type:
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      FOO: {a: 1, b: 2, c: 3} # <-- Directly defined map on the `FOO` key
-    cmds:
-      - 'echo {{.FOO.a}}'
-```
-
-## Migration
-
-Taskfiles with dynamically defined variables via the `sh` subkey or references
-defined with `ref` will no longer work with this experiment enabled. In order to
-keep using these features, you will need to migrate your Taskfile to use the new
-syntax.
-
-### Dynamic Variables
-
-Previously, you had to define dynamic variables using the `sh` subkey. With this
-experiment enabled, you will need to remove the `sh` subkey and define your
-command as a string that begins with a `$`. This will instruct Task to interpret
-the string as a command instead of a literal value and the variable will be
-populated with the output of the command. For example:
-
-<Tabs defaultValue="2"
-  values={[
-    {label: 'Before', value: '1'},
-    {label: 'After', value: '2'}
-  ]}>
-
-<TabItem value="1">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      CALCULATED_VAR:
-        sh: 'echo hello'
-    cmds:
-      - 'echo {{.CALCULATED_VAR}}'
-```
-
-</TabItem>
-<TabItem value="2">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      CALCULATED_VAR: '$echo hello' # <-- Prefix dynamic variable with a `$`
-    cmds:
-      - 'echo {{.CALCULATED_VAR}}'
-```
-
-</TabItem></Tabs>
-
-### References
-
-<Tabs defaultValue="2"
-  values={[
-    {label: 'Before', value: '1'},
-    {label: 'After', value: '2'}
-  ]}>
-
-<TabItem value="1">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      VAR: 42
-      VAR_REF:
-        ref: '.FOO'
-    cmds:
-      - 'echo {{.VAR_REF}}'
-```
-
-</TabItem>
-<TabItem value="2">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      VAR: 42
-      VAR_REF: '#.FOO' # <-- Prefix reference with a `#`
-    cmds:
-      - 'echo {{.VAR_REF}}'
-```
-
-</TabItem></Tabs>
-
-If your current Taskfile contains a string variable that begins with a `$` or a
-`#`, you will now need to escape it with a backslash (`\`) to stop Task from
-interpreting it as a command or reference.
-
-</TabItem>
-<TabItem value="2">
-
-:::info
-
-To enable this experiment, set the environment variable:
-`TASK_X_MAP_VARIABLES=2`. Check out [our guide to enabling experiments
-][enabling-experiments] for more information.
-
-:::
-
-This proposal maintains backwards-compatibility and the `sh` subkey and adds
-another new `map` subkey for defining map variables:
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      FOO:
-        map: {a: 1, b: 2, c: 3} # <-- Defined using the `map' subkey instead of directly on 'FOO'
-      BAR: true # <-- Other types of variables are still defined directly on the key
-      BAZ:
-        sh: 'echo Hello Task' # <-- The `sh` subkey is still supported
-      QUX:
-        ref: '.BAZ' # <-- The `ref` subkey is still supported
-    cmds:
-      - 'echo {{.FOO.a}}'
-```
-
-</TabItem></Tabs>
-
-## Looping over maps
-
-This experiment also adds support for looping over maps using the `for` keyword,
-just like arrays. In addition to the `{{.ITEM}}` variable being populated when
-looping over a map, we also make an additional `{{.KEY}}` variable available
-that holds the string value of the map key.
-
-<Tabs defaultValue="1" queryString="proposal"
-  values={[
-    {label: 'Proposal 1', value: '1'},
-    {label: 'Proposal 2', value: '2'}
-  ]}>
-
-<TabItem value="1">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      MAP: {a: 1, b: 2, c: 3}
-    cmds:
-      - for:
-          var: MAP
-        cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
-```
-
-</TabItem>
-<TabItem value="2">
-
-```yaml
-version: 3
-
-tasks:
-  foo:
-    vars:
-      map:
-        MAP: {a: 1, b: 2, c: 3}
-    cmds:
-      - for:
-          var: MAP
-        cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
-```
-
-:::note
-
-Remember that maps are unordered, so
-the order in which the items are looped over is random.
-
-:::
-
-</TabItem></Tabs>
-
-{/* prettier-ignore-start */}
-[enabling-experiments]: ./experiments.mdx#enabling-experiments
-{/* prettier-ignore-end */}
diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx
index 0bfa5086..6c480902 100644
--- a/website/docs/usage.mdx
+++ b/website/docs/usage.mdx
@@ -1113,53 +1113,38 @@ variable types are supported:
 - `int`
 - `float`
 - `array`
+- `map`
 
 :::note
 
-Maps are not supported by default, but there is an
-[experiment][map-variables] that can be enabled to add support. If
-you're interested in this functionality, we would appreciate your feedback.
-:pray:
+Defining a map requires that you use a special `map` subkey (see example below).
 
-In the meantime, it is technically possible to define a map using a `ref` resolver and a templating function. For example:
-
-```yaml
-version: '3'
-
-tasks:
-  task-with-map:
-    vars:
-      FOO:
-        ref: dict "a" "1" "b" "2" "c" "3"
-    cmds:
-      - echo {{.FOO}}
-```
-
-```txt
-map[a:1 b:2 c:3]
-```
-
-OR by using the same technique with JSON:
+:::
 
 ```yaml
-version: '3'
+version: 3
 
 tasks:
-  task-with-map:
+  foo:
     vars:
-      JSON: '{"a": 1, "b": 2, "c": 3}'
-      FOO:
-        ref: "fromJson .JSON"
-    cmds:
-      - echo {{.FOO}}
+      STRING: 'Hello, World!'
+      BOOL: true
+      INT: 42
+      FLOAT: 3.14
+      ARRAY: [1, 2, 3]
+      MAP:
+        map: {A: 1, B: 2, C: 3}
+    cmds:
+      - 'echo {{.STRING}}'  # Hello, World!
+      - 'echo {{.BOOL}}'    # true
+      - 'echo {{.INT}}'     # 42
+      - 'echo {{.FLOAT}}'   # 3.14
+      - 'echo {{.ARRAY}}'   # [1 2 3]
+      - 'echo {{.ARRAY.0}}' # 1
+      - 'echo {{.MAP}}'     # map[A:1 B:2 C:3]
+      - 'echo {{.MAP.A}}'   # 1
 ```
 
-```txt
-map[a:1 b:2 c:3]
-```
-
-:::
-
 Variables can be set in many places in a Taskfile. When executing
 [templates][templating-reference], Task will look for variables in the order
 listed below (most important first):
@@ -1360,6 +1345,29 @@ tasks:
       - 'echo {{.FOO}}' # <-- FOO is just the letter 'A'
 ```
 
+### Parsing JSON/YAML into map variables
+
+If you have a raw JSON or YAML string that you want to process in Task, you can
+use a combination of the `ref` keyword and the `fromJson` or `fromYaml`
+templating functions to parse the string into a map variable. For example:
+
+```yaml
+version: '3'
+
+tasks:
+  task-with-map:
+    vars:
+      JSON: '{"a": 1, "b": 2, "c": 3}'
+      FOO:
+        ref: "fromJson .JSON"
+    cmds:
+      - echo {{.FOO}}
+```
+
+```txt
+map[a:1 b:2 c:3]
+```
+
 ## Looping over values
 
 Task allows you to loop over certain values and execute a command for each.
@@ -1508,7 +1516,7 @@ tasks:
         cmd: cat {{.ITEM}}
 ```
 
-You can also loop over arrays directly and maps:
+You can also loop over arrays and maps directly:
 
 ```yaml
 version: 3
@@ -1674,36 +1682,45 @@ clear what they contain:
 version: '3'
 
 tasks:
-  echo-*:
+  start:*:*:
     vars:
-      TEXT: '{{index .MATCH 0}}'
+      SERVICE: "{{index .MATCH 0}}"
+      REPLICAS: "{{index .MATCH 1}}"
     cmds:
-      - echo {{.TEXT}}
+      - echo "Starting {{.SERVICE}} with {{.REPLICAS}} replicas"
 
-  run-*-*:
+  start:*:
     vars:
-      ARG_1: '{{index .MATCH 0}}'
-      ARG_2: '{{index .MATCH 1}}'
+      SERVICE: "{{index .MATCH 0}}"
     cmds:
-      - echo {{.ARG_1}} {{.ARG_2}}
+      - echo "Starting {{.SERVICE}}"
 ```
 
+This call matches the `start:*` task and the string "foo" is captured by the
+wildcard and stored in the `.MATCH` variable. We then index the `.MATCH` array
+and store the result in the `.SERVICE` variable which is then echoed out in the
+cmds:
+
 ```shell
-# This call matches the "echo-*" task and the string "hello" is captured by the
-# wildcard and stored in the .MATCH variable. We then index the .MATCH array and
-# store the result in the .TEXT variable which is then echoed out in the cmds.
-$ task echo-hello
-hello
-# You can use whitespace in your arguments as long as you quote the task name
-$ task "echo-hello world"
-hello world
-# And you can pass multiple arguments
-$ task run-foo-bar
-foo bar
-```
-
-If multiple matching tasks are found, an error occurs. If you are using included
-Taskfiles, tasks in parent files will be considered first.
+$ task start:foo
+Starting foo
+```
+
+You can use whitespace in your arguments as long as you quote the task name:
+
+```shell
+$ task "start:foo bar"
+Starting foo bar
+```
+
+If multiple matching tasks are found, the first one listed in the Taskfile will
+be used. If you are using included Taskfiles, tasks in parent files will be
+considered first.
+
+```shell
+$ task start:foo:3
+Starting foo with 3 replicas
+```
 
 ## Doing task cleanup with `defer`
 
@@ -2285,9 +2302,11 @@ With the flags `--watch` or `-w` task will watch for file changes and run the
 task again. This requires the `sources` attribute to be given, so task knows
 which files to watch.
 
-The default watch interval is 5 seconds, but it's possible to change it by
-either setting `interval: '500ms'` in the root of the Taskfile or by passing it
-as an argument like `--interval=500ms`.
+The default watch interval is 100 milliseconds, but it's possible to change it
+by either setting `interval: '500ms'` in the root of the Taskfile or by passing
+it as an argument like `--interval=500ms`.
+This interval is the time Task will wait for duplicated events. It will only run
+the task again once, even if multiple changes happen within the interval.
 
 Also, it's possible to set `watch: true` in a given task and it'll automatically
 run in watch mode:
@@ -2317,6 +2336,5 @@ if called by another task, either directly or as a dependency.
 
 {/* prettier-ignore-start */}
 [gotemplate]: https://golang.org/pkg/text/template/
-[map-variables]: ./experiments/map_variables.mdx
 [templating-reference]: ./reference/templating.mdx
 {/* prettier-ignore-end */}
diff --git a/website/static/schema.json b/website/static/schema.json
index a44df7a8..1aaeaa90 100644
--- a/website/static/schema.json
+++ b/website/static/schema.json
@@ -276,7 +276,7 @@
         "^.*$": {
           "anyOf": [
             {
-              "type": ["boolean", "integer", "null", "number", "string", "object", "array"]
+              "type": ["boolean", "integer", "null", "number", "string", "array"]
             },
             {
               "$ref": "#/definitions/var_subkey"
@@ -733,7 +733,7 @@
           "$ref": "#/definitions/run"
         },
         "interval": {
-          "description": "Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
+          "description": "Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
           "type": "string",
           "pattern": "^[0-9]+(?:m|s|ms)$"
         }



Test patch:
diff --git a/internal/sort/sorter_test.go b/internal/sort/sorter_test.go
index e55f0f0f..50a0071e 100644
--- a/internal/sort/sorter_test.go
+++ b/internal/sort/sorter_test.go
@@ -79,3 +79,35 @@ func TestAlphaNumeric_Sort(t *testing.T) {
 		})
 	}
 }
+
+func TestNoSort_Sort(t *testing.T) {
+	t.Parallel()
+
+	item1 := "a-item1"
+	item2 := "m-item2"
+	item3 := "ns1:item3"
+	item4 := "ns2:item4"
+	item5 := "z-item5"
+	item6 := "ns3:item6"
+
+	tests := []struct {
+		name  string
+		items []string
+		want  []string
+	}{
+		{
+			name:  "all items in order of definition",
+			items: []string{item3, item2, item5, item1, item4, item6},
+			want:  []string{item3, item2, item5, item1, item4, item6},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			NoSort(tt.items, nil)
+			assert.Equal(t, tt.want, tt.items)
+		})
+	}
+}
diff --git a/task_test.go b/task_test.go
index 0a2c9821..94109c32 100644
--- a/task_test.go
+++ b/task_test.go
@@ -3196,9 +3196,9 @@ func TestWildcard(t *testing.T) {
 			wantErr: true,
 		},
 		{
-			name:    "multiple matches",
-			call:    "wildcard-foo-bar",
-			wantErr: true,
+			name:           "multiple matches",
+			call:           "wildcard-foo-bar",
+			expectedOutput: "Hello foo-bar\n",
 		},
 	}
 
diff --git a/testdata/watcher_interval/.gitignore b/testdata/watch/.gitignore
similarity index 100%
rename from testdata/watcher_interval/.gitignore
rename to testdata/watch/.gitignore
diff --git a/testdata/watch/Taskfile.yaml b/testdata/watch/Taskfile.yaml
new file mode 100644
index 00000000..813ff45d
--- /dev/null
+++ b/testdata/watch/Taskfile.yaml
@@ -0,0 +1,10 @@
+# https://taskfile.dev
+
+version: '3'
+
+tasks:
+  default:
+    sources:
+      - "src/*"
+    cmds:
+      - echo "Task running!"
diff --git a/testdata/watcher_interval/Taskfile.yaml b/testdata/watcher_interval/Taskfile.yaml
deleted file mode 100644
index 110eff27..00000000
--- a/testdata/watcher_interval/Taskfile.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-# https://taskfile.dev
-
-version: '3'
-
-vars:
-  GREETING: Hello, World!
-
-interval: "500ms"
-
-tasks:
-  default:
-    sources:
-      - "src/*"
-    cmds:
-      - echo "{{.GREETING}}"
-    silent: false
diff --git a/watch_test.go b/watch_test.go
index be15ff2d..447c910a 100644
--- a/watch_test.go
+++ b/watch_test.go
@@ -17,71 +17,72 @@ import (
 
 	"github.com/go-task/task/v3"
 	"github.com/go-task/task/v3/internal/filepathext"
-	"github.com/go-task/task/v3/taskfile/ast"
 )
 
-func TestFileWatcherInterval(t *testing.T) {
-	const dir = "testdata/watcher_interval"
+func TestFileWatch(t *testing.T) {
+	t.Parallel()
+
+	const dir = "testdata/watch"
+	_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
+	_ = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
+
 	expectedOutput := strings.TrimSpace(`
 task: Started watching for tasks: default
-task: [default] echo "Hello, World!"
-Hello, World!
-task: [default] echo "Hello, World!"
-Hello, World!
+task: [default] echo "Task running!"
+Task running!
+task: task "default" finished running
+task: Task "default" is up to date
+task: task "default" finished running
 	`)
 
 	var buff bytes.Buffer
-	e := &task.NewExecutor(
-		task.WithDir(dir),
-		task.WithStdout(&buff),
-		task.WithStderr(&buff),
-		task.WithWatch(true),
+	e := task.NewExecutor(
+		task.ExecutorWithDir(dir),
+		task.ExecutorWithStdout(&buff),
+		task.ExecutorWithStderr(&buff),
+		task.ExecutorWithWatch(true),
 	)
 
 	require.NoError(t, e.Setup())
 	buff.Reset()
 
-	err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755)
+	dirPath := filepathext.SmartJoin(dir, "src")
+	filePath := filepathext.SmartJoin(dirPath, "a")
+
+	err := os.MkdirAll(dirPath, 0755)
 	require.NoError(t, err)
 
-	err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644)
-	if err != nil {
-		t.Fatal(err)
-	}
+	err = os.WriteFile(filePath, []byte("test"), 0644)
+	require.NoError(t, err)
 
-	ctx := context.Background()
-	ctx, cancel := context.WithCancel(ctx)
+	ctx, cancel := context.WithCancel(context.Background())
 
-	go func(ctx context.Context) {
+	go func() {
 		for {
 			select {
 			case <-ctx.Done():
 				return
 			default:
-				err := e.Run(ctx, &ast.Call{Task: "default"})
+				err := e.Run(ctx, &task.Call{Task: "default"})
 				if err != nil {
-					return
+					panic(err)
 				}
 			}
 		}
-	}(ctx)
+	}()
 
 	time.Sleep(10 * time.Millisecond)
-	err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644)
-	if err != nil {
-		t.Fatal(err)
-	}
-	time.Sleep(700 * time.Millisecond)
+	err = os.WriteFile(filePath, []byte("test updated"), 0644)
+	require.NoError(t, err)
+
+	time.Sleep(150 * time.Millisecond)
 	cancel()
 	assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
-	buff.Reset()
-	err = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
-	require.NoError(t, err)
-	err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
-	require.NoError(t, err)
 }
 
 func TestShouldIgnoreFile(t *testing.T) {
+	t.Parallel()
+
 	tt := []struct {
 		path   string
 		expect bool