/detect-task
Internal skill for commands. Detect the active task (any tracker adapter) from the task-state file / git branch name. Do not trigger on user conversation - only when commands need task detection.
Claude Code
$ npx -y skills add restarter/lets-workflow --skill detect-task --agent claude-code
Codex
$ npx skills add restarter/lets-workflow --skill detect-task -a codex -y
Cursor
$ npx skills add restarter/lets-workflow --skill detect-task -a cursor -y
Gemini CLI
$ npx skills add restarter/lets-workflow --skill detect-task -a gemini-cli -y
GitHub Copilot
$ npx skills add restarter/lets-workflow --skill detect-task -a github-copilot -y
OpenCode
$ npx skills add restarter/lets-workflow --skill detect-task -a opencode -y
Windsurf
$ npx skills add restarter/lets-workflow --skill detect-task -a windsurf -y
SKILL.md
detect-task.SKILL.mdname: detect-task
description: Internal skill for commands. Detect the active task (any tracker adapter) from the task-state file / git branch name. Do not trigger on user conversation - only when commands need task detection.
user-invocable: false
Detect Active Task
Parse the task-state file / current git branch to find the active task ID (tracker-adapter-aware). Used by commands that need to know which task is in progress.
> **IMPORTANT:** If the spec below invokes any deferred tool (e.g. `AskUserQuestion`), you MUST load and call it as specified. Never skip the call, never substitute a default answer of your own — the tool invocation is part of the contract. This is critical.
Why This Exists
10+ commands need to detect the active task from branch name. This skill centralizes the logic so branch format changes are updated in one place.
Detection Flow
Step 1: Parse Branch Name
Go reads the branch against the active tracker's convention (the adapter's `## Worktree` `id:` / `branch:` / `worktree-branch:`, overridable per key by the user's board file) - markdown never matches an id regex by eye:
lets worktree info --json --task-candidate --plugin-root "${CLAUDE_PLUGIN_ROOT}"Take `task_candidate.id` when `task_candidate.source` is `created` - a `branch:` / `worktree-branch:` match is this branch's task by construction. `reason=no_match` or `detached_head` -> the name carries no id; go on (Step 2 fallback). For the `branch=<ref>` variant, add `--ref-file` (see Optional arguments) - Go reads the ref from that file, so an untrusted ref is never typed into a shell.
**`reason=convention_undeclared` / `convention_declaration_invalid`, or no `lets` binary** - parse the branch name (`git branch --show-current`) by the shapes below, unchanged. Formats:
- `feature/<task-id>-<slug>` - standard LETS branches (main repo)
- `worktree-<task-id>-<slug>` - worktree branches created via `/lets:worktree create` in new-branch mode (the LETS convention)
- `worktree-<custom-name>` - worktree branch without an embedded task ID; use fallback
- any other shape (e.g. `feature/foo`, `bugfix/bar`) - attached existing branch via `/lets:worktree create --attach`; no task ID in the name; use fallback
The `<task-id>` shape is TRACKER-DEPENDENT (the id sits immediately after `feature/` or `worktree-`, up to the first `-<slug>` boundary):
- beads: `<prefix>-<alphanum>[.<number>]` - e.g. `lets-abc`, `lets-abc.1`, `proj-xyz.42`
- a numeric-id tracker: a pure-numeric id - e.g. `48647`, so `feature/48647-<slug>`
- other adapters: the tracker's own id shape
Whatever this parse yields is a CANDIDATE, not an answer: it goes to **the id gate** below, like every other path. On the `branch=<ref>` variant the name being parsed is a pull request's head, which its author chose.
**Do NOT apply the beads `<prefix>-<alphanum>` regex on a non-beads project** - it false-positives on a slug word: `feature/48647-lifecycle-test` makes the beads regex capture `lifecycle-test`, NOT the numeric id `48647`. Match using the ACTIVE tracker's id shape (`{LETS_TRACKER}` from LETS Config). When the branch shape is ambiguous, the `.task-<slug>` file (Step 1.5, authoritative) and the Step 2 search-and-confirm fallback are safer than a branch-name guess.
Step 1.5: Task-State File (fills the gap when the branch name carries no id)
The `.task-<branch-slug>` file (written by `take-task`) records the CURRENT task - authoritative over the branch name, because a worktree branch is frozen at create time and may host several tasks in sequence (the branch name is only the worktree's home task). Read it after the explicit-arg short-circuit, before the branch-name parse. `{LETS_MERGE_BRANCH}` is from LETS Config:
LETS_PROJECT_ROOT=$(git rev-parse --show-toplevel)
BRANCH=$(git branch --show-current); BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
TASK_FILE="$LETS_PROJECT_ROOT/.lets/sessions/.task-${BRANCH_SLUG}"
CANDIDATE=$(sed -n 's/^task: //p' "$TASK_FILE" 2>/dev/null | head -1)
ORIGIN=$(sed -n 's/^origin: //p' "$TASK_FILE" 2>/dev/null | head -1)
[ -n "$CANDIDATE" ] && printf 'candidate=%s on_merge_branch=%s origin=%s\n' "$CANDIDATE" \
"$([ "$BRANCH" = "{LETS_MERGE_BRANCH}" ] && echo yes || echo no)" "${ORIGIN:-none}"This prints a CANDIDATE, not an answer - it is a value read off disk, and nothing may use it before **the id gate** below. `on_merge_branch` only decides whether the liveness probe runs; `origin` says whether a person or a guess wrote it.
**`origin=branch` or `origin=dir`** (checked first) - `lets worktree adopt` derived this id from an `accept:` branch shape or the directory name of a worktree Orca or a teammate created; nobody confirmed it. Gate it, then probe once:
**Tracker protocol:** `Skill(skill: "lets:protocol-tracker")` before this and any later `lets-tracker` block here, unless its text is in your current context.
show task=<TASK_ID from the gate> # returns {id,title,status,url}; read status- `in_progress` or `open` -> use the id and add one line: "task <id> was derived from the branch name - `/lets:start <id>` confirms it".
- `closed`, any other status, a failed lookup, or `show` absent (the none adapter) -> discard the candidate, do NOT re-parse the branch name, and continue at Step 2 (search-and-confirm for picker callers, None for the no-picker callers in Step 3).
- Cost bound: `take-task` rewrites the file without `origin:`, so a claimed worktree never pays this probe.
**`on_merge_branch=no`** - take this candidate to the gate and stop looking. The branch corroborates the file, and a just-closed id in a worktree is low-severity (the next claim overwrites it).
**`on_merge_branch=yes`** - the file alone cannot tell a live trunk claim from a stale `.task-main` left by a closed task or a main-mode session, so verify it. Run the candidate through **the id gate FIRST**: this probe is itself a tracker verb, so a raw candidate here would cross the bound
Read more
name: detect-task description: Internal skill for commands. Detect the active task (any tracker adapter) from the task-state file / git branch name. Do not trigger on user conversation - only when commands need task detection. user-invocable: false
Detect Active Task
Parse the task-state file / current git branch to find the active task ID (tracker-adapter-aware). Used by commands that need to know which task is in progress.
> **IMPORTANT:** If the spec below invokes any deferred tool (e.g. `AskUserQuestion`), you MUST load and call it as specified. Never skip the call, never substitute a default answer of your own — the tool invocation is part of the contract. This is critical.
Why This Exists
10+ commands need to detect the active task from branch name. This skill centralizes the logic so branch format changes are updated in one place.
Detection Flow
Step 1: Parse Branch Name
Go reads the branch against the active tracker's convention (the adapter's `## Worktree` `id:` / `branch:` / `worktree-branch:`, overridable per key by the user's board file) - markdown never matches an id regex by eye:
lets worktree info --json --task-candidate --plugin-root "${CLAUDE_PLUGIN_ROOT}"Take `task_candidate.id` when `task_candidate.source` is `created` - a `branch:` / `worktree-branch:` match is this branch's task by construction. `reason=no_match` or `detached_head` -> the name carries no id; go on (Step 2 fallback). For the `branch=<ref>` variant, add `--ref-file` (see Optional arguments) - Go reads the ref from that file, so an untrusted ref is never typed into a shell.
**`reason=convention_undeclared` / `convention_declaration_invalid`, or no `lets` binary** - parse the branch name (`git branch --show-current`) by the shapes below, unchanged. Formats:
- `feature/<task-id>-<slug>` - standard LETS branches (main repo)
- `worktree-<task-id>-<slug>` - worktree branches created via `/lets:worktree create` in new-branch mode (the LETS convention)
- `worktree-<custom-name>` - worktree branch without an embedded task ID; use fallback
- any other shape (e.g. `feature/foo`, `bugfix/bar`) - attached existing branch via `/lets:worktree create --attach`; no task ID in the name; use fallback
The `<task-id>` shape is TRACKER-DEPENDENT (the id sits immediately after `feature/` or `worktree-`, up to the first `-<slug>` boundary):
- beads: `<prefix>-<alphanum>[.<number>]` - e.g. `lets-abc`, `lets-abc.1`, `proj-xyz.42`
- a numeric-id tracker: a pure-numeric id - e.g. `48647`, so `feature/48647-<slug>`
- other adapters: the tracker's own id shape
Whatever this parse yields is a CANDIDATE, not an answer: it goes to **the id gate** below, like every other path. On the `branch=<ref>` variant the name being parsed is a pull request's head, which its author chose.
**Do NOT apply the beads `<prefix>-<alphanum>` regex on a non-beads project** - it false-positives on a slug word: `feature/48647-lifecycle-test` makes the beads regex capture `lifecycle-test`, NOT the numeric id `48647`. Match using the ACTIVE tracker's id shape (`{LETS_TRACKER}` from LETS Config). When the branch shape is ambiguous, the `.task-<slug>` file (Step 1.5, authoritative) and the Step 2 search-and-confirm fallback are safer than a branch-name guess.
Step 1.5: Task-State File (fills the gap when the branch name carries no id)
The `.task-<branch-slug>` file (written by `take-task`) records the CURRENT task - authoritative over the branch name, because a worktree branch is frozen at create time and may host several tasks in sequence (the branch name is only the worktree's home task). Read it after the explicit-arg short-circuit, before the branch-name parse. `{LETS_MERGE_BRANCH}` is from LETS Config:
LETS_PROJECT_ROOT=$(git rev-parse --show-toplevel)
BRANCH=$(git branch --show-current); BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
TASK_FILE="$LETS_PROJECT_ROOT/.lets/sessions/.task-${BRANCH_SLUG}"
CANDIDATE=$(sed -n 's/^task: //p' "$TASK_FILE" 2>/dev/null | head -1)
ORIGIN=$(sed -n 's/^origin: //p' "$TASK_FILE" 2>/dev/null | head -1)
[ -n "$CANDIDATE" ] && printf 'candidate=%s on_merge_branch=%s origin=%s\n' "$CANDIDATE" \
"$([ "$BRANCH" = "{LETS_MERGE_BRANCH}" ] && echo yes || echo no)" "${ORIGIN:-none}"This prints a CANDIDATE, not an answer - it is a value read off disk, and nothing may use it before **the id gate** below. `on_merge_branch` only decides whether the liveness probe runs; `origin` says whether a person or a guess wrote it.
**`origin=branch` or `origin=dir`** (checked first) - `lets worktree adopt` derived this id from an `accept:` branch shape or the directory name of a worktree Orca or a teammate created; nobody confirmed it. Gate it, then probe once:
**Tracker protocol:** `Skill(skill: "lets:protocol-tracker")` before this and any later `lets-tracker` block here, unless its text is in your current context.
show task=<TASK_ID from the gate> # returns {id,title,status,url}; read status- `in_progress` or `open` -> use the id and add one line: "task <id> was derived from the branch name - `/lets:start <id>` confirms it".
- `closed`, any other status, a failed lookup, or `show` absent (the none adapter) -> discard the candidate, do NOT re-parse the branch name, and continue at Step 2 (search-and-confirm for picker callers, None for the no-picker callers in Step 3).
- Cost bound: `take-task` rewrites the file without `origin:`, so a claimed worktree never pays this probe.
**`on_merge_branch=no`** - take this candidate to the gate and stop looking. The branch corroborates the file, and a just-closed id in a worktree is low-severity (the next claim overwrites it).
**`on_merge_branch=yes`** - the file alone cannot tell a live trunk claim from a stale `.task-main` left by a closed task or a main-mode session, so verify it. Run the candidate through **the id gate FIRST**: this probe is itself a tracker verb, so a raw candidate here would cross the bound
A development workflow plugin for Claude Code Stop babysitting your AI. Start shipping with it.
Other skills on lets-workflow.
goal-test
Local sandbox for experimenting with Claude Code's /goal command (stop-hook condition + LLM-evaluator driving autonomous work…
loop-bb-pr
Compose a /loop that monitors a Bitbucket PR for activity (new comments, review state changes, merge, decline) via the bb-api…
loop-gh-pr
Compose a /loop that watches all open GitHub PRs for activity (new PRs, new commits, new comments) and auto-prepares a READ-ONLY…
loop-test
Local sandbox for experimenting with Claude Code's /loop command (interval + dynamic/self-paced scheduling) applied to LETS…
actor-fetch-personalit…
Internal skill for commands. Fetch and validate personality from URL or file path for Actor agent. Do not trigger on user…
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about detect-task.
- Which coding agents can use detect-task?
- detect-task installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

