From d56c89f873b892e85642c9a3fae5d0e881dc182d Mon Sep 17 00:00:00 2001 From: Siddharth Kapoor Date: Sun, 19 Jul 2026 15:34:59 -0400 Subject: [PATCH] perf(task): deterministic single-script runner dispatch + /tasks alias Model-driven dispatch (SKILL.md -> detect.md -> list.md -> run.md) made `/task detect` and `/task list` take ~2 minutes: every verb walked several reference docs and ran ad-hoc bash the model assembled per call. Replace that hot path with one shipped POSIX-sh script so detection, listing, running, and the no-match offer happen in a single deterministic process. **Single shipped dispatcher** - Add scripts/task.sh: one source of truth for runner detection (mise -> just -> Taskfile -> package.json -> none), listing, and running. - No argument lists tasks; an unrecognised first token is treated as a run query and matched against the runner's task names; a query with no match returns NO_MATCH so the skill can offer to create a runner-native task entry instead of failing. - SKILL.md maps every verb to a single Bash call; the three workflow docs shrink to thin references pointing at the script, so no executable bash remains on the hot path. **Correct lookup signals** - List names via `mise tasks ls --no-header` so the first task is no longer dropped by a header-skipping awk (mise 2026.3.x emits no header); dropping it made `/task ` wrongly offer to create an existing task. - Capture enumeration stderr: a runner present but unable to enumerate (e.g. untrusted mise on a fresh clone) is reported as ENUM_FAILED, surfacing the trust error rather than offering to create the task. - Two or more prefix/substring candidates return AMBIGUOUS with the candidate list so the query is disambiguated, not treated as no-match. - Match literally via `grep -F` and a lowercased case-glob prefix; the query is never interpreted as a regex. **/tasks alias with a drift guard** - Ship skills/tasks/ as a generated mirror of canonical skills/task/ (identical but for the name, `/tasks` trigger token, and SKILL.md H1), so both triggers share the same fast script. - Add .mise-tasks/gen-task-alias to regenerate it and a skills:sync-task-alias task; fold a --check drift guard (content and exec-bit modes) into skills:validate so CI fails if the two diverge. - Update docs/skills/task.md for no-arg list, bare-query run, and the create-offer. Closes #33 --- .mise-tasks/gen-task-alias | 74 +++++++++ docs/skills/task.md | 33 ++-- mise.toml | 7 + skills/task/SKILL.md | 103 ++++++++---- skills/task/references/workflows/detect.md | 73 ++------- skills/task/references/workflows/list.md | 64 +------- skills/task/references/workflows/run.md | 75 ++------- skills/task/scripts/task.sh | 170 ++++++++++++++++++++ skills/tasks/SKILL.md | 120 ++++++++++++++ skills/tasks/references/workflows/detect.md | 19 +++ skills/tasks/references/workflows/list.md | 12 ++ skills/tasks/references/workflows/run.md | 19 +++ skills/tasks/scripts/task.sh | 170 ++++++++++++++++++++ 13 files changed, 717 insertions(+), 222 deletions(-) create mode 100755 .mise-tasks/gen-task-alias create mode 100755 skills/task/scripts/task.sh create mode 100644 skills/tasks/SKILL.md create mode 100644 skills/tasks/references/workflows/detect.md create mode 100644 skills/tasks/references/workflows/list.md create mode 100644 skills/tasks/references/workflows/run.md create mode 100755 skills/tasks/scripts/task.sh diff --git a/.mise-tasks/gen-task-alias b/.mise-tasks/gen-task-alias new file mode 100755 index 0000000..9935445 --- /dev/null +++ b/.mise-tasks/gen-task-alias @@ -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 diff --git a/docs/skills/task.md b/docs/skills/task.md index 7f50f96..80d8f38 100644 --- a/docs/skills/task.md +++ b/docs/skills/task.md @@ -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 -### detect — Identify the task runner +### list — List 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`. diff --git a/mise.toml b/mise.toml index aa38e7a..1c8b7a1 100644 --- a/mise.toml +++ b/mise.toml @@ -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" diff --git a/skills/task/SKILL.md b/skills/task/SKILL.md index 6079fb8..ce9d72f 100644 --- a/skills/task/SKILL.md +++ b/skills/task/SKILL.md @@ -5,22 +5,22 @@ 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 @@ -28,44 +28,93 @@ 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 [args…]` (or `r …`) | `bash "$SKILL_DIR/scripts/task.sh" run [args…]` | +| any other first token ` [args…]` | `bash "$SKILL_DIR/scripts/task.sh" run [args…]` | + +Stream the output. The script prints a `RUNNER: ` 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:`** — 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.] + description = "" + run = "" + ``` + - **just** → add to `justfile`: + ``` + : + + ``` + - **task** → add to `Taskfile.yml` under `tasks:`: + ```yaml + : + desc: "" + cmds: + - + ``` + - **package.json** → add to the `scripts` object: `"": ""`. +- **`AMBIGUOUS:`** — 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:`** — 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.] + run = "" + ``` +- **`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 [args] +/task [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 [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. diff --git a/skills/task/references/workflows/detect.md b/skills/task/references/workflows/detect.md index c9c7e73..fcf9590 100644 --- a/skills/task/references/workflows/detect.md +++ b/skills/task/references/workflows/detect.md @@ -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`. diff --git a/skills/task/references/workflows/list.md b/skills/task/references/workflows/list.md index a5f6ab6..dfb139f 100644 --- a/skills/task/references/workflows/list.md +++ b/skills/task/references/workflows/list.md @@ -1,62 +1,12 @@ -# list +# list (reference) -List all available tasks in the project using the detected task runner. +`/task list` (and the no-arg default `/task`) runs `scripts/task.sh list` in a single Bash call. This file documents that contract; it is **not** transcribed or executed on the hot path. -## Step 1: Detect Runner +## Behaviour -Run the detection logic from `detect.md` inline: +- Detects the runner (same order as `detect`), prints a `RUNNER: ()` header, then the runner's native task listing. +- If no runner is found, prints a short "no task runner" message recommending a `mise.toml` with a `[tasks]` table. -```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 -``` +Native listings are shown as-is (mise: name+description columns; just: recipes grouped by module; task: tasks with `desc`; package.json: the `scripts` object). -## Step 2: List Tasks - -If `TASK_RUNNER_NAME` is `none`: - -``` -No task runner found in this project. -Recommended: add a mise.toml with a [tasks] table. -``` - -Otherwise: - -```bash -$TASK_RUNNER_LIST_CMD -``` - -Present output as-is — each runner has good built-in formatting: - -- **mise** — `mise tasks ls` shows name + description columns -- **just** — `just --list` shows recipes grouped by `mod` with descriptions -- **task** — `task --list` shows tasks with their `desc` field -- **package.json** — `pnpm run` / `npm run` shows scripts; for package.json fall back to `jq '.scripts' package.json` if the bare command does not list scripts - -## Output - -Report the listing as final agent text. Mention the detected runner at the top, then the list. +Source of truth: `skills/task/scripts/task.sh`. diff --git a/skills/task/references/workflows/run.md b/skills/task/references/workflows/run.md index c823f30..bb782f6 100644 --- a/skills/task/references/workflows/run.md +++ b/skills/task/references/workflows/run.md @@ -1,72 +1,19 @@ -# run +# run (reference) -Run a named task using the project's detected task runner. +`/task run [args…]` (and any unrecognised first token) runs `scripts/task.sh run [args…]` in a single Bash call. This file documents that contract; it is **not** transcribed or executed on the hot path. -## Step 1: Detect Runner +## Matching -Run the detection logic from `detect.md` inline: +`` is resolved against the runner's task names in order: exact → case-insensitive exact → unique case-insensitive prefix → unique case-insensitive substring. The first unique match wins. -```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_NAME="none" -fi -``` +## Argument pass-through -If `TASK_RUNNER_NAME` is `none`, abort and tell the user no task runner is configured (suggest `mise.toml`). +Everything after `` is forwarded to the runner verbatim (including `--` and quoting). -## Step 2: Resolve Task Name +## Exit markers (last output line) -Parse `$REMAINING_ARGS`: +- `NO_MATCH:` (exit 3) — no unique match; the listing is printed. The skill then offers to create a runner-native task entry (see SKILL.md Step 2). +- `NO_QUERY` (exit 2) — `run` called with no task name; the skill presents the listing and asks which task. +- `NO_RUNNER` (exit 4) — no runner configured; the skill offers to scaffold a `mise.toml`. -- First token is `TASK_NAME` -- Everything after the first token is `EXTRA_ARGS` (preserve `--` and quoting as-is) - -If `REMAINING_ARGS` is empty: - -1. Run `$TASK_RUNNER_LIST_CMD` to enumerate available tasks -2. Ask the user via `AskUserQuestion`: "Which task?" — present up to 4 task names as options plus "Other" (let the user type a name) -3. Set `TASK_NAME` to the chosen value; `EXTRA_ARGS` remains empty - -## Step 3: Execute - -Run: - -```bash -$TASK_RUNNER $TASK_NAME $EXTRA_ARGS -``` - -Stream output live. - -## Step 4: Handle Failure - -If the exit code is non-zero: - -1. Show the last 20 lines of output -2. Suggest checking the task definition file (`mise.toml`, `justfile`, `Taskfile.yml`, or `package.json`) -3. If the task name was not recognized by the runner, suggest running `/tasks list` to see available tasks - -## Output - -Report success or failure as final agent text — do not call any notification command. +Source of truth: `skills/task/scripts/task.sh`. diff --git a/skills/task/scripts/task.sh b/skills/task/scripts/task.sh new file mode 100755 index 0000000..56549d0 --- /dev/null +++ b/skills/task/scripts/task.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# task.sh — single-process task-runner dispatcher for the `task`/`tasks` skill. +# One source of truth for detection, listing, running, and the no-match offer. +# Usage: +# task.sh [list] detect runner and list tasks (default) +# task.sh detect print machine-readable runner variables + report +# task.sh run [..] match to a task and run it (extra args passed through) +set -u + +# Scratch file for the last enumeration's stderr (see list_task_names). +ENUM_ERR="$(mktemp "${TMPDIR:-/tmp}/task-enum.XXXXXX")" +trap 'rm -f "$ENUM_ERR"' EXIT + +# ---- Detection (single place; order: mise -> just -> Taskfile -> package.json -> none) ---- +detect_runner() { + if [ -f mise.toml ] || [ -f .mise.toml ]; then + RUNNER_NAME="mise"; RUNNER="mise run"; LIST_CMD="mise tasks ls" + elif [ -f justfile ] || [ -f Justfile ]; then + RUNNER_NAME="just"; RUNNER="just"; LIST_CMD="just --list" + elif [ -f Taskfile.yml ] || [ -f Taskfile.yaml ]; then + RUNNER_NAME="task"; RUNNER="task"; LIST_CMD="task --list" + elif [ -f package.json ] && jq -e '.scripts' package.json >/dev/null 2>&1; then + if command -v pnpm >/dev/null 2>&1; then RUNNER="pnpm run"; else RUNNER="npm run"; fi + RUNNER_NAME="package.json"; LIST_CMD="$RUNNER" + else + RUNNER_NAME="none"; RUNNER=""; LIST_CMD="" + fi +} + +# ---- Enumerate task names, one per line, for matching ---- +# Writes the runner's enumeration error (if any) to $ENUM_ERR (a temp file) so +# callers can distinguish "couldn't enumerate" from "no such task". Returns the +# underlying command's exit status; a non-zero status OR a non-empty error means +# enumeration failed and MUST NOT be treated as "the task does not exist". +list_task_names() { + : > "$ENUM_ERR" + case "$RUNNER_NAME" in + mise) mise tasks ls --no-header 2>"$ENUM_ERR" | awk 'NF{print $1}' ;; + just) just --summary 2>"$ENUM_ERR" | tr ' ' '\n' ;; + task) task --list 2>"$ENUM_ERR" | awk '/^\* /{print $2}' | sed 's/:$//' ;; + package.json) jq -r '.scripts | keys[]' package.json 2>"$ENUM_ERR" ;; + *) : ;; + esac +} + +print_listing() { + if [ "$RUNNER_NAME" = "none" ]; then + printf 'No task runner found in this project.\n' + printf 'Recommended: add a mise.toml with a [tasks] table to declare your build/test/lint commands.\n' + return 0 + fi + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + # shellcheck disable=SC2086 + $LIST_CMD 2>/dev/null || { + # package.json fallback when the bare runner does not print scripts + if [ "$RUNNER_NAME" = "package.json" ]; then jq '.scripts' package.json 2>/dev/null; fi + } +} + +cmd_detect() { + detect_runner + printf 'RUNNER_NAME=%s\n' "$RUNNER_NAME" + printf 'RUNNER=%s\n' "$RUNNER" + printf 'LIST_CMD=%s\n' "$LIST_CMD" + if [ "$RUNNER_NAME" = "none" ]; then + printf '\nNo task runner found. Recommended: add a mise.toml with a [tasks] table.\n' + else + printf '\nTask runner: %s (%s)\nList command: %s\n' "$RUNNER_NAME" "$RUNNER" "$LIST_CMD" + fi +} + +cmd_list() { + detect_runner + print_listing +} + +# Resolve $1 (query) against available task names. +# return 0 — unique match; echoes the resolved name. +# return 1 — enumeration failed (runner present but could not list tasks); +# echoes the runner's error. NOT the same as "no such task". +# return 2 — ambiguous (2+ candidates); echoes the candidate names. +# return 3 — no candidates matched a query that could be enumerated. +# All matching is literal (grep -F / case) — the query is never treated as a pattern. +resolve_task() { + query="$1" + names="$(list_task_names)" + enum_status=$? + # A runner is configured but enumeration errored or produced nothing: this is + # "couldn't enumerate," not "no such task." Surface it rather than offering create. + if [ "$enum_status" -ne 0 ] || [ -s "$ENUM_ERR" ] || [ -z "$names" ]; then + cat "$ENUM_ERR" 2>/dev/null + return 1 + fi + # 1. exact (case-sensitive, literal) + if printf '%s\n' "$names" | grep -qxF -- "$query"; then printf '%s\n' "$query"; return 0; fi + # 2. case-insensitive exact (literal) + ci="$(printf '%s\n' "$names" | grep -ixF -- "$query")" + if [ "$(printf '%s\n' "$ci" | grep -c .)" = "1" ]; then printf '%s\n' "$ci"; return 0; fi + # 3. prefix (case-insensitive, literal): lowercase both sides and compare via case. + lq="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]')" + pre="$(printf '%s\n' "$names" | while IFS= read -r n; do + ln="$(printf '%s' "$n" | tr '[:upper:]' '[:lower:]')" + case "$ln" in "$lq"*) printf '%s\n' "$n" ;; esac + done)" + npre=$(printf '%s\n' "$pre" | grep -c .) + if [ "$npre" = "1" ]; then printf '%s\n' "$pre"; return 0; fi + if [ "$npre" -gt 1 ]; then printf '%s\n' "$pre"; return 2; fi + # 4. substring (case-insensitive, literal via grep -iF) + sub="$(printf '%s\n' "$names" | grep -iF -- "$query")" + nsub=$(printf '%s\n' "$sub" | grep -c .) + if [ "$nsub" = "1" ]; then printf '%s\n' "$sub"; return 0; fi + if [ "$nsub" -gt 1 ]; then printf '%s\n' "$sub"; return 2; fi + return 3 +} + +cmd_run() { + detect_runner + if [ "$RUNNER_NAME" = "none" ]; then + printf 'NO_RUNNER\n' + printf 'No task runner is configured. Add a mise.toml with a [tasks] table, then retry.\n' + return 4 + fi + if [ "$#" -eq 0 ]; then + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + print_listing + printf '\nNO_QUERY\n' + return 2 + fi + query="$1"; shift + resolved="$(resolve_task "$query")" + case $? in + 0) : ;; # unique match — fall through to exec + 1) # enumeration failed: runner present but tasks couldn't be listed + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + printf 'Could not enumerate tasks for %s:\n' "$RUNNER_NAME" + [ -n "$resolved" ] && printf '%s\n' "$resolved" + if [ "$RUNNER_NAME" = "mise" ]; then + printf 'If this is a fresh clone, the config may be untrusted — run: mise trust\n' + fi + printf '\nENUM_FAILED:%s\n' "$query" + return 5 ;; + 2) # ambiguous — 2+ candidates; disambiguate, do NOT offer to create + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + printf 'Multiple tasks match "%s":\n' "$query" + printf '%s\n' "$resolved" + printf '\nAMBIGUOUS:%s\n' "$query" + return 6 ;; + *) # genuine no-match against a runner whose tasks enumerated cleanly + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + print_listing + printf '\nNO_MATCH:%s\n' "$query" + return 3 ;; + esac + # shellcheck disable=SC2086 + exec $RUNNER "$resolved" "$@" +} + +main() { + verb="${1:-list}" + case "$verb" in + detect) shift; cmd_detect ;; + list) shift; cmd_list ;; + run) shift; cmd_run "$@" ;; + "") cmd_list ;; + *) # unknown first token => treat as a run query + cmd_run "$@" ;; + esac +} + +main "$@" diff --git a/skills/tasks/SKILL.md b/skills/tasks/SKILL.md new file mode 100644 index 0000000..b73dd51 --- /dev/null +++ b/skills/tasks/SKILL.md @@ -0,0 +1,120 @@ +--- +name: tasks +description: 'Unified task runner dispatcher: auto-detects mise, just, task.dev, or package.json scripts and provides commands to run, list, or detect the project task runner. Triggers on: "/tasks", "run task", "list tasks", "detect task runner", "what tasks", "mise run", "just run", "pnpm run", "package.json scripts".' +license: MIT +compatibility: Works on Claude Code, OpenCode, GitHub Copilot (VS Code), and Codex. No platform-specific features used. +--- + +# tasks + +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 + +- **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 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" ;; +esac +``` + +`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 [args…]` (or `r …`) | `bash "$SKILL_DIR/scripts/task.sh" run [args…]` | +| any other first token ` [args…]` | `bash "$SKILL_DIR/scripts/task.sh" run [args…]` | + +Stream the output. The script prints a `RUNNER: ` 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:`** — 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.] + description = "" + run = "" + ``` + - **just** → add to `justfile`: + ``` + : + + ``` + - **task** → add to `Taskfile.yml` under `tasks:`: + ```yaml + : + desc: "" + cmds: + - + ``` + - **package.json** → add to the `scripts` object: `"": ""`. +- **`AMBIGUOUS:`** — 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:`** — 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.] + run = "" + ``` +- **`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 + +``` +/task [args] + +Verbs: + detect Detect which task runner this project uses + list List all available tasks (default when no args given) + run [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: + /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 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. diff --git a/skills/tasks/references/workflows/detect.md b/skills/tasks/references/workflows/detect.md new file mode 100644 index 0000000..fcf9590 --- /dev/null +++ b/skills/tasks/references/workflows/detect.md @@ -0,0 +1,19 @@ +# detect (reference) + +`/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 order + +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 + +## Output (machine-readable lines from `task.sh detect`) + +- `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 + +Source of truth: `skills/task/scripts/task.sh`. diff --git a/skills/tasks/references/workflows/list.md b/skills/tasks/references/workflows/list.md new file mode 100644 index 0000000..dfb139f --- /dev/null +++ b/skills/tasks/references/workflows/list.md @@ -0,0 +1,12 @@ +# list (reference) + +`/task list` (and the no-arg default `/task`) runs `scripts/task.sh list` in a single Bash call. This file documents that contract; it is **not** transcribed or executed on the hot path. + +## Behaviour + +- Detects the runner (same order as `detect`), prints a `RUNNER: ()` header, then the runner's native task listing. +- If no runner is found, prints a short "no task runner" message recommending a `mise.toml` with a `[tasks]` table. + +Native listings are shown as-is (mise: name+description columns; just: recipes grouped by module; task: tasks with `desc`; package.json: the `scripts` object). + +Source of truth: `skills/task/scripts/task.sh`. diff --git a/skills/tasks/references/workflows/run.md b/skills/tasks/references/workflows/run.md new file mode 100644 index 0000000..bb782f6 --- /dev/null +++ b/skills/tasks/references/workflows/run.md @@ -0,0 +1,19 @@ +# run (reference) + +`/task run [args…]` (and any unrecognised first token) runs `scripts/task.sh run [args…]` in a single Bash call. This file documents that contract; it is **not** transcribed or executed on the hot path. + +## Matching + +`` is resolved against the runner's task names in order: exact → case-insensitive exact → unique case-insensitive prefix → unique case-insensitive substring. The first unique match wins. + +## Argument pass-through + +Everything after `` is forwarded to the runner verbatim (including `--` and quoting). + +## Exit markers (last output line) + +- `NO_MATCH:` (exit 3) — no unique match; the listing is printed. The skill then offers to create a runner-native task entry (see SKILL.md Step 2). +- `NO_QUERY` (exit 2) — `run` called with no task name; the skill presents the listing and asks which task. +- `NO_RUNNER` (exit 4) — no runner configured; the skill offers to scaffold a `mise.toml`. + +Source of truth: `skills/task/scripts/task.sh`. diff --git a/skills/tasks/scripts/task.sh b/skills/tasks/scripts/task.sh new file mode 100755 index 0000000..56549d0 --- /dev/null +++ b/skills/tasks/scripts/task.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# task.sh — single-process task-runner dispatcher for the `task`/`tasks` skill. +# One source of truth for detection, listing, running, and the no-match offer. +# Usage: +# task.sh [list] detect runner and list tasks (default) +# task.sh detect print machine-readable runner variables + report +# task.sh run [..] match to a task and run it (extra args passed through) +set -u + +# Scratch file for the last enumeration's stderr (see list_task_names). +ENUM_ERR="$(mktemp "${TMPDIR:-/tmp}/task-enum.XXXXXX")" +trap 'rm -f "$ENUM_ERR"' EXIT + +# ---- Detection (single place; order: mise -> just -> Taskfile -> package.json -> none) ---- +detect_runner() { + if [ -f mise.toml ] || [ -f .mise.toml ]; then + RUNNER_NAME="mise"; RUNNER="mise run"; LIST_CMD="mise tasks ls" + elif [ -f justfile ] || [ -f Justfile ]; then + RUNNER_NAME="just"; RUNNER="just"; LIST_CMD="just --list" + elif [ -f Taskfile.yml ] || [ -f Taskfile.yaml ]; then + RUNNER_NAME="task"; RUNNER="task"; LIST_CMD="task --list" + elif [ -f package.json ] && jq -e '.scripts' package.json >/dev/null 2>&1; then + if command -v pnpm >/dev/null 2>&1; then RUNNER="pnpm run"; else RUNNER="npm run"; fi + RUNNER_NAME="package.json"; LIST_CMD="$RUNNER" + else + RUNNER_NAME="none"; RUNNER=""; LIST_CMD="" + fi +} + +# ---- Enumerate task names, one per line, for matching ---- +# Writes the runner's enumeration error (if any) to $ENUM_ERR (a temp file) so +# callers can distinguish "couldn't enumerate" from "no such task". Returns the +# underlying command's exit status; a non-zero status OR a non-empty error means +# enumeration failed and MUST NOT be treated as "the task does not exist". +list_task_names() { + : > "$ENUM_ERR" + case "$RUNNER_NAME" in + mise) mise tasks ls --no-header 2>"$ENUM_ERR" | awk 'NF{print $1}' ;; + just) just --summary 2>"$ENUM_ERR" | tr ' ' '\n' ;; + task) task --list 2>"$ENUM_ERR" | awk '/^\* /{print $2}' | sed 's/:$//' ;; + package.json) jq -r '.scripts | keys[]' package.json 2>"$ENUM_ERR" ;; + *) : ;; + esac +} + +print_listing() { + if [ "$RUNNER_NAME" = "none" ]; then + printf 'No task runner found in this project.\n' + printf 'Recommended: add a mise.toml with a [tasks] table to declare your build/test/lint commands.\n' + return 0 + fi + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + # shellcheck disable=SC2086 + $LIST_CMD 2>/dev/null || { + # package.json fallback when the bare runner does not print scripts + if [ "$RUNNER_NAME" = "package.json" ]; then jq '.scripts' package.json 2>/dev/null; fi + } +} + +cmd_detect() { + detect_runner + printf 'RUNNER_NAME=%s\n' "$RUNNER_NAME" + printf 'RUNNER=%s\n' "$RUNNER" + printf 'LIST_CMD=%s\n' "$LIST_CMD" + if [ "$RUNNER_NAME" = "none" ]; then + printf '\nNo task runner found. Recommended: add a mise.toml with a [tasks] table.\n' + else + printf '\nTask runner: %s (%s)\nList command: %s\n' "$RUNNER_NAME" "$RUNNER" "$LIST_CMD" + fi +} + +cmd_list() { + detect_runner + print_listing +} + +# Resolve $1 (query) against available task names. +# return 0 — unique match; echoes the resolved name. +# return 1 — enumeration failed (runner present but could not list tasks); +# echoes the runner's error. NOT the same as "no such task". +# return 2 — ambiguous (2+ candidates); echoes the candidate names. +# return 3 — no candidates matched a query that could be enumerated. +# All matching is literal (grep -F / case) — the query is never treated as a pattern. +resolve_task() { + query="$1" + names="$(list_task_names)" + enum_status=$? + # A runner is configured but enumeration errored or produced nothing: this is + # "couldn't enumerate," not "no such task." Surface it rather than offering create. + if [ "$enum_status" -ne 0 ] || [ -s "$ENUM_ERR" ] || [ -z "$names" ]; then + cat "$ENUM_ERR" 2>/dev/null + return 1 + fi + # 1. exact (case-sensitive, literal) + if printf '%s\n' "$names" | grep -qxF -- "$query"; then printf '%s\n' "$query"; return 0; fi + # 2. case-insensitive exact (literal) + ci="$(printf '%s\n' "$names" | grep -ixF -- "$query")" + if [ "$(printf '%s\n' "$ci" | grep -c .)" = "1" ]; then printf '%s\n' "$ci"; return 0; fi + # 3. prefix (case-insensitive, literal): lowercase both sides and compare via case. + lq="$(printf '%s' "$query" | tr '[:upper:]' '[:lower:]')" + pre="$(printf '%s\n' "$names" | while IFS= read -r n; do + ln="$(printf '%s' "$n" | tr '[:upper:]' '[:lower:]')" + case "$ln" in "$lq"*) printf '%s\n' "$n" ;; esac + done)" + npre=$(printf '%s\n' "$pre" | grep -c .) + if [ "$npre" = "1" ]; then printf '%s\n' "$pre"; return 0; fi + if [ "$npre" -gt 1 ]; then printf '%s\n' "$pre"; return 2; fi + # 4. substring (case-insensitive, literal via grep -iF) + sub="$(printf '%s\n' "$names" | grep -iF -- "$query")" + nsub=$(printf '%s\n' "$sub" | grep -c .) + if [ "$nsub" = "1" ]; then printf '%s\n' "$sub"; return 0; fi + if [ "$nsub" -gt 1 ]; then printf '%s\n' "$sub"; return 2; fi + return 3 +} + +cmd_run() { + detect_runner + if [ "$RUNNER_NAME" = "none" ]; then + printf 'NO_RUNNER\n' + printf 'No task runner is configured. Add a mise.toml with a [tasks] table, then retry.\n' + return 4 + fi + if [ "$#" -eq 0 ]; then + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + print_listing + printf '\nNO_QUERY\n' + return 2 + fi + query="$1"; shift + resolved="$(resolve_task "$query")" + case $? in + 0) : ;; # unique match — fall through to exec + 1) # enumeration failed: runner present but tasks couldn't be listed + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + printf 'Could not enumerate tasks for %s:\n' "$RUNNER_NAME" + [ -n "$resolved" ] && printf '%s\n' "$resolved" + if [ "$RUNNER_NAME" = "mise" ]; then + printf 'If this is a fresh clone, the config may be untrusted — run: mise trust\n' + fi + printf '\nENUM_FAILED:%s\n' "$query" + return 5 ;; + 2) # ambiguous — 2+ candidates; disambiguate, do NOT offer to create + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + printf 'Multiple tasks match "%s":\n' "$query" + printf '%s\n' "$resolved" + printf '\nAMBIGUOUS:%s\n' "$query" + return 6 ;; + *) # genuine no-match against a runner whose tasks enumerated cleanly + printf 'RUNNER: %s (%s)\n\n' "$RUNNER_NAME" "$RUNNER" + print_listing + printf '\nNO_MATCH:%s\n' "$query" + return 3 ;; + esac + # shellcheck disable=SC2086 + exec $RUNNER "$resolved" "$@" +} + +main() { + verb="${1:-list}" + case "$verb" in + detect) shift; cmd_detect ;; + list) shift; cmd_list ;; + run) shift; cmd_run "$@" ;; + "") cmd_list ;; + *) # unknown first token => treat as a run query + cmd_run "$@" ;; + esac +} + +main "$@"