Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .mise-tasks/gen-task-alias
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# gen-task-alias — generate skills/tasks/ (the /tasks trigger alias) from the
# canonical skills/task/ package. skills/task/ is the source of truth; skills/tasks/
# is identical except name/trigger-token/heading substitutions in SKILL.md.
#
# gen-task-alias regenerate skills/tasks/ in place
# gen-task-alias --check regenerate into a temp dir and diff; exit 1 on drift
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC="$ROOT/skills/task"
CHECK=0
[ "${1:-}" = "--check" ] && CHECK=1

build_into() {
dest="$1"
rm -rf "$dest"
mkdir -p "$dest"
# copy everything (references/, scripts/, etc.)
cp -R "$SRC/." "$dest/"
# rewrite SKILL.md: name, trigger token, H1 heading
skill="$dest/SKILL.md"
sed -e 's/^name: task$/name: tasks/' \
-e 's#"/task"#"/tasks"#g' \
-e 's/^# task$/# tasks/' \
"$skill" > "$skill.tmp"
mv "$skill.tmp" "$skill"
}

# Detect the stat variant once. GNU stat uses `-c FMT`; BSD/macOS stat uses
# `-f FMT`. We must NOT probe with `stat -f ... || stat -c ...` per file: on GNU,
# `-f` means --file-system, so `stat -f '%Lp' FILE` prints a filesystem block
# (which embeds FILE's path) to stdout AND exits non-zero — the `||` fallback
# then appends the real mode, and the path-bearing blocks never compare equal,
# yielding phantom "mode drift" on every file (works on BSD, fails on Linux CI).
if stat -c '%a' . >/dev/null 2>&1; then
_statmode() { stat -c '%a' "$1"; } # GNU
else
_statmode() { stat -f '%Lp' "$1"; } # BSD/macOS
fi

# Compare file modes (permission bits) between two trees. Portable across
# GNU/BSD stat. Emits "path old new" for each differing file; empty = in sync.
mode_diff() {
a="$1"; b="$2"
( cd "$a" && find . -type f | sort ) | while IFS= read -r rel; do
fa="$a/$rel"; fb="$b/$rel"
[ -e "$fb" ] || continue
ma="$(_statmode "$fa")"
mb="$(_statmode "$fb")"
[ "$ma" = "$mb" ] || printf '%s %s %s\n' "$rel" "$ma" "$mb"
done
}

if [ "$CHECK" -eq 1 ]; then
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
build_into "$tmp/tasks"
modes="$(mode_diff "$ROOT/skills/tasks" "$tmp/tasks")"
if ! diff -r "$ROOT/skills/tasks" "$tmp/tasks" >/dev/null 2>&1 || [ -n "$modes" ]; then
echo "FAIL: skills/tasks is out of sync with skills/task."
echo "Run: mise run skills:sync-task-alias"
diff -r "$ROOT/skills/tasks" "$tmp/tasks" || true
if [ -n "$modes" ]; then
echo "Mode drift (file expected-mode actual-mode):"
printf '%s\n' "$modes"
fi
exit 1
fi
echo "PASS: skills/tasks is in sync with skills/task (content + modes)."
else
build_into "$ROOT/skills/tasks"
echo "Generated skills/tasks/ from skills/task/."
fi
33 changes: 19 additions & 14 deletions docs/skills/task.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
# task

Unified task runner skill that auto-detects mise, just, task.dev, or package.json scripts and provides a consistent interface for listing and running tasks.
Unified task runner skill that auto-detects mise, just, task.dev, or package.json scripts and provides a consistent interface for listing and running tasks. All work runs in a single shell call, so it is fast even on hot paths. Invoke as `/task` (or the alias `/tasks`).

## Commands

### detectIdentify the task runner
### listList available tasks (default)

Probe the project for a task runner and report which one is active.
With no arguments, `/task` detects the runner and lists all available tasks in one fast call.

```bash
/tasks detect
/task # list all tasks (same as /task list)
/task list
```

### list — List available tasks
Alias: `/task ls`.

### run — Run a task (or just name it)

List all tasks with descriptions from the detected task runner.
Any first token that is not a known verb is treated as a task to run. Extra arguments are passed through to the runner.

```bash
/tasks list # list all tasks
/tasks # same as /tasks list
/task test # run the "test" task
/task run test # explicit form
/task build -- --release # pass extra args after --
```

Alias: `/tasks ls`.
The query is matched against task names by exact match, then case-insensitive exact, then unique prefix, then unique substring. If no task matches, `/task` offers to create a task-runner-compatible entry (a mise `[tasks.*]` block, a just recipe, a `Taskfile.yml` task, or a `package.json` script) — or, if no runner is configured, to scaffold a `mise.toml`. It only writes the change after you confirm.

### run — Run a task
Alias: `/task r`.

Run a named task via the detected task runner, with optional extra arguments.
### detect — Identify the task runner

Probe the project for a task runner and report which one is active.

```bash
/tasks run test # run the test task
/tasks run build -- --release # pass extra args after --
/task detect
```

Alias: `/tasks r`.
Alias: `/task d`.
7 changes: 7 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@ echo ""
echo "$count skills checked, $failed failed."
if [ "$failed" -gt 0 ]; then exit 1; fi
echo "All skills passed validation."
# Fail if the /tasks alias has drifted from the canonical /task package.
"$MISE_PROJECT_ROOT/.mise-tasks/gen-task-alias" --check
"""

[tasks."skills:sync-task-alias"]
description = "Regenerate skills/tasks/ (the /tasks trigger alias) from canonical skills/task/"
run = ".mise-tasks/gen-task-alias"

[tasks."skills:install"]
description = "Install/update all skills from remote (cloudvoyant/codevoyant)"
run = "npx skills add cloudvoyant/codevoyant -g --all -y"
103 changes: 76 additions & 27 deletions skills/task/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,116 @@ license: MIT
compatibility: Works on Claude Code, OpenCode, GitHub Copilot (VS Code), and Codex. No platform-specific features used.
---

# tasks
# task

Unified task runner skill dispatcher. Auto-detects the project's task runner and provides a consistent interface for listing, detecting, and running tasks. Enforces task runner usage, discourages ad-hoc bash usage.
Unified task runner skill dispatcher. Auto-detects the project's task runner and provides a consistent interface for listing, detecting, and running tasks. All detection, listing, and running is done by one shipped script (`scripts/task.sh`) invoked in a **single Bash call** — never by reading multiple markdown files.

## Critical Rules

- **Never execute workflow logic here** — this file only parses args and dispatches
- **Step 0 always runs first** — no exceptions
- **Unknown verb run `help.md`** — never error silently
- **Pass all remaining args through** — workflow receives `$REMAINING_ARGS` unchanged
- **Before running raw build/test/lint/format commands** (`tsc`, `vitest`, `eslint`, `prettier`, etc.) in any other workflow, first call `/tasks list` to check whether a task wraps the command. If a task exists, use it. Never bypass the task runner.
- **Do the work in one Bash call** — dispatch every verb to `scripts/task.sh`; never transcribe its logic into chat or re-read the workflow docs to run bash.
- **Step 0 always runs first** — no exceptions.
- **Unknown verb is a run query** — a first token that is not `detect`/`list`/`run` (or an alias) is treated as a task to run.
- **Pass all remaining args through** — everything after the task name is forwarded to the runner unchanged.
- **Before running raw build/test/lint/format commands** (`tsc`, `vitest`, `eslint`, `prettier`, etc.) in any other workflow, first call `/task list` (alias: `/tasks list`) to check whether a task wraps the command. If a task exists, use it. Never bypass the task runner.

## Step 0: Parse Arguments

```bash
VERB="[first non-flag argument, or empty]"
VERB="[first non-flag token, or empty]"
REMAINING_ARGS="[everything after VERB, preserving order and flags]"

# Aliases
case "$VERB" in
r) VERB="run" ;;
ls) VERB="list" ;;
d) VERB="detect" ;;
"") VERB="list" ;;
esac
```

## Step 1: Dispatch to Workflow

Read and execute `references/workflows/{VERB}.md`, passing `$REMAINING_ARGS` as the argument string.

If `references/workflows/{VERB}.md` does not exist, print the Help section below and note the unknown verb.

## Workflow Index

- **detect** (`references/workflows/detect.md`) — detect which task runner the project uses; sets `TASK_RUNNER` and `TASK_RUNNER_LIST_CMD`
- **list** (`references/workflows/list.md`) — list all available tasks with descriptions
- **run** (`references/workflows/run.md`) — run a named task with optional extra args
`SKILL_DIR` is the directory containing this SKILL.md (resolve it from this file's own path so the script is found from any working directory).

## Step 1: Dispatch to the script (one Bash call)

Run exactly one command based on `VERB`:

| Invocation | Command to run |
|---|---|
| no verb / no args | `bash "$SKILL_DIR/scripts/task.sh" list` |
| `list` (or `ls`) | `bash "$SKILL_DIR/scripts/task.sh" list` |
| `detect` (or `d`) | `bash "$SKILL_DIR/scripts/task.sh" detect` |
| `run <query> [args…]` (or `r …`) | `bash "$SKILL_DIR/scripts/task.sh" run <query> [args…]` |
| any other first token `<query> [args…]` | `bash "$SKILL_DIR/scripts/task.sh" run <query> [args…]` |

Stream the output. The script prints a `RUNNER: <name>` header and the task listing for `list`; runner variables for `detect`; and for `run` it `exec`s the matched task, forwarding extra args.

## Step 2: Handle the script's markers

The `run` subcommand may exit non-zero with a marker on the last line. Act on it:

- **`NO_MATCH:<query>`** — no task matched. Offer to create a task-runner-compatible entry for the detected runner, naming the exact file and showing the snippet, then create it only on user confirmation:
- **mise** → add to `mise.toml`:
```toml
[tasks.<name>]
description = "<what it does>"
run = "<command>"
```
- **just** → add to `justfile`:
```
<name>:
<command>
```
- **task** → add to `Taskfile.yml` under `tasks:`:
```yaml
<name>:
desc: "<what it does>"
cmds:
- <command>
```
- **package.json** → add to the `scripts` object: `"<name>": "<command>"`.
- **`AMBIGUOUS:<query>`** — 2+ tasks matched the query. The candidate names are printed above the marker. Do **not** offer to create anything — present the candidates and ask which one to run (or have the user retype a more specific query).
- **`ENUM_FAILED:<query>`** — a runner is configured but its task list could not be enumerated (e.g. an untrusted mise config on a fresh clone, or a runner error). The runner's error is printed above the marker. Do **not** offer to create a task — surface the error and, for mise, suggest `mise trust`, then retry. Never treat this as "no such task."
- **`NO_RUNNER`** — no runner configured. Offer to scaffold a minimal `mise.toml`:
```toml
[tasks.<name>]
run = "<command>"
```
- **`NO_QUERY`** — `run` was called with no task name; present the listing and ask which task to run.

Never silently edit build config — offer the snippet and target file, and only write it after the user confirms.

## Workflow Index (reference docs — not executed on the hot path)

- **detect** (`references/workflows/detect.md`) — describes the `task.sh detect` contract and variables.
- **list** (`references/workflows/list.md`) — describes the `task.sh list` contract.
- **run** (`references/workflows/run.md`) — describes the `task.sh run` contract and matching rules.

## Help

```
/tasks <verb> [args]
/task <verb|query> [args]

Verbs:
detect Detect which task runner this project uses
list List all available tasks (default when no verb given)
list List all available tasks (default when no args given)
run <task> [args] Run a named task via the detected runner

Any other first token is treated as a task to run:
/task test → run the "test" task
/task build -- --release

Aliases:
r → run
ls → list
d → detect

Examples:
/tasks list
/tasks run test
/tasks run build -- --release
/tasks detect
/task # list all tasks
/task list
/task test # run the test task
/task run build -- --release
/task detect
```

## Usage Note for Other Skills

Other workflows in the skills system should call `/tasks detect` at the start (or read its logic inline) and use the resulting `TASK_RUNNER` / `TASK_RUNNER_LIST_CMD` variables instead of hard-coding shell commands. This enforces a single source of truth for build/test/lint commands per project.
Other workflows should call `/task detect` at the start (or run `scripts/task.sh detect`) and use the resulting `RUNNER` / `LIST_CMD` variables instead of hard-coding shell commands. `/tasks …` is an accepted alias for `/task …` and resolves to the same script.
73 changes: 13 additions & 60 deletions skills/task/references/workflows/detect.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,19 @@
# detect
# detect (reference)

Detect the task runner used by this project and export `TASK_RUNNER`, `TASK_RUNNER_LIST_CMD`, and `TASK_RUNNER_NAME` for downstream use.
`/task detect` runs `scripts/task.sh detect` in a single Bash call. This file documents that contract; it is **not** transcribed or executed on the hot path.

## Detection Priority
## Detection order

1. `mise.toml` or `.mise.toml` → `mise run`
2. `justfile` or `Justfile` → `just`
3. `Taskfile.yml` or `Taskfile.yaml` → `task`
4. `package.json` with a `scripts` field → `pnpm run` (fallback to `npm run` if pnpm not on PATH)
5. None found → report and suggest adding a `mise.toml`
1. `mise.toml` or `.mise.toml` → mise (`mise run`, list: `mise tasks ls`)
2. `justfile` or `Justfile` → just (`just`, list: `just --list`)
3. `Taskfile.yml` or `Taskfile.yaml` → task (`task`, list: `task --list`)
4. `package.json` with a `scripts` field → `pnpm run` (or `npm run` if pnpm is absent)
5. None found → `none`; recommend adding a `mise.toml` with a `[tasks]` table

## Step 1: Detect in Order
## Output (machine-readable lines from `task.sh detect`)

```bash
if [ -f mise.toml ] || [ -f .mise.toml ]; then
TASK_RUNNER="mise run"
TASK_RUNNER_LIST_CMD="mise tasks ls"
TASK_RUNNER_NAME="mise"
elif [ -f justfile ] || [ -f Justfile ]; then
TASK_RUNNER="just"
TASK_RUNNER_LIST_CMD="just --list"
TASK_RUNNER_NAME="just"
elif [ -f Taskfile.yml ] || [ -f Taskfile.yaml ]; then
TASK_RUNNER="task"
TASK_RUNNER_LIST_CMD="task --list"
TASK_RUNNER_NAME="task"
elif [ -f package.json ] && jq -e '.scripts' package.json > /dev/null 2>&1; then
if command -v pnpm > /dev/null 2>&1; then
TASK_RUNNER="pnpm run"
TASK_RUNNER_LIST_CMD="pnpm run"
else
TASK_RUNNER="npm run"
TASK_RUNNER_LIST_CMD="npm run"
fi
TASK_RUNNER_NAME="package.json"
else
TASK_RUNNER=""
TASK_RUNNER_LIST_CMD=""
TASK_RUNNER_NAME="none"
fi
```
- `RUNNER_NAME=` — `mise` | `just` | `task` | `package.json` | `none`
- `RUNNER=` — the run prefix (e.g. `mise run`, `just`, `pnpm run`), empty when none
- `LIST_CMD=` — the command that lists tasks, empty when none

## Step 2: Report

If `TASK_RUNNER` is set, report:

```
Task runner: {TASK_RUNNER_NAME} ({TASK_RUNNER})
List command: {TASK_RUNNER_LIST_CMD}
```

If `TASK_RUNNER_NAME` is `none`, report:

```
No task runner found in this project.
Recommended: add a mise.toml with a [tasks] table to declare your build/test/lint commands.
```

## Output

When another workflow calls this one, expose the three variables:

- `TASK_RUNNER` — e.g. `mise run`, `just`, `task`, `pnpm run`, `npm run`
- `TASK_RUNNER_LIST_CMD` — command that lists tasks for this runner
- `TASK_RUNNER_NAME` — short identifier: `mise`, `just`, `task`, `package.json`, or `none`
Source of truth: `skills/task/scripts/task.sh`.
Loading
Loading