A Claude Code plugin that fetches Teamwork.com tasks (single task or whole
tasklist) by URL, pulls every bit of context that already lives in Teamwork
(tasklist description, task & comment attachments, file comments), implements
each task in the current repository, moves the card across the Kanban board as
the work progresses, commits per task using the TYPE(scope)[<task-id>]: Message
convention, and logs spent time back to Teamwork as sequential, non-overlapping
entries. Push to remote is intentionally left to the user.
Part of the wame Claude Code plugin marketplace.
Current version: 1.5.0 — see CHANGELOG.md for the full release history.
- Comments are actually read again. Up to 1.4.2 the plugin could report "no comments" on a task that had eight — four independent defects each produced that same silent result (reported in a colleague's "Štyri cesty k nule" analysis): an invalid sort key that made the comments endpoint answer HTTP 400, a gate on a
commentsCountfield Teamwork v3 never returns, a wrong timestamp field name, andecho "$JSON" | jqpipes that break in zsh (the macOS default shell Claude Code runs every snippet in). All four are fixed; failures now print a⚠line naming the endpoint and the fallback instead of continuing with empty context. - The newest comment is always read.
fetch_comments_mode: when_needednow runs one cheap probe per task that returns the comment count and the newest comment. Whenever a task has comments, the newest one is read — it is the freshest truth and can overrule the description. The old heuristics (thin description, "viď komentár") now only decide whether the full thread is fetched.alwaysandneverkeep their meaning. A v1 fallback covers a failing v3 endpoint. - Board moves, the tasklist filter and subtasks work again. v3 task objects carry no
projectId; the plugin now reads.tasklist.meta.projectId, so workflow detection, board moves (also for subtasks) and the tasklist filter get a real project. The filter reads each task's column fromworkflowStages(the?include=cards,stageslookup always came back empty). - The new WAME board. Work starts in any of the start columns —
tasklist_filter.todo_stages, default Ready for Development and To Do — and a finished task moves to Done - Local (fallbacks Internal testing → Testing, so the older per-project boards keep landing where they did). In progress still resolves In Progress (case-insensitive). A task that is not on the board is now analyse-only ("not on the board") instead of being implemented — only the start columns are greenlit work; promote it from the plan when it is yours. See Board workflow and the migration rule. - A completed task behind a single-task URL is not reworked silently. The skill asks first — Skip (recommended) or Process anyway, with the warning that processing moves the card out of its done column.
skip_completed_taskskeeps skipping completed tasks silently in tasklists and subtasks. - Explicit
falsein the config is respected. jq's// truetreatedfalseas missing, sotasklist_filter.enabled,only_assigned_to_me,board_workflow.enabled,skip_completed_tasks,is_billable_by_defaultand other switches could not be turned off (the migration even rewrote them totrue). - Attachments come from the right place. Task files are resolved via
GET /tasks/{id}.json?include=attachments; the old fallback endpoint ignored its task filter and returned the whole workspace's files. - Build-time quality rules. Every task is planned (Step 6.2), implemented (Step 6.3) and self-checked (new Step 6.5.5) against the same five dimensions
/teamwork-task-testreviews at QA time — UI/UX & accessibility, performance, security, page reachability (a new screen gets its menu entry and inbound links from related screens in the same commit) and framework best practices (new or changed code uses the current idioms and built-in features of the framework / language versions the project actually has installed). What stays open is listed per task in the final summary and handed to/teamwork-task-test. Configure withbuild_quality.dimensionsor--dimensions=<csv>|none. - Framework versions are detected, not remembered. Once per run the plugin reads
composer.lock,composer.json(require.php/config.platform.php),package.json+node_modules/ lock,browserslistand.nvmrcinto/tmp/tw_job_<id>/framework_versions.tsv, and looks APIs up in current docs (Laravel Boostsearch-docs→ context7 → official docs). Guardrails: project conventions win over a newer idiom, no drive-by rewrites of untouched code (such opportunities are only listed as advisory tips), nothing deprecated in — or newer than — the installed versions and the browserslist target./teamwork-task-testtreatsframeworkas advisory: it recommends, it never fails an acceptance criterion on it. - zsh-safe snippets. Every snippet in the skill follows a documented shell portability contract (no echo pipes on API data, no swallowed parse errors, no unquoted
forloops, no bash-only arrays, run state in files under/tmp/tw_job_<id>/instead of shell variables that die with each Bash call).
The 1.5.0 migration is automatic and idempotent — your config gains build_quality.dimensions (all five) on the next run. A list written by a pre-release 1.5.0 build that holds exactly the four original keys gains framework once; any list you customised stays as it is (see the migration rule). The board keys move to the new WAME board only where you kept the old defaults: todo_stages is added (["Ready for Development", "To Do"], or [<your column>] when you had customised todo_stage), and done_stage: "Internal testing" becomes "Done - Local" with ["Internal testing", "Testing"] as fallbacks; a customised done_stage is never touched (see the board migration rule).
- Subtasks are first-class tasks. When a parent task has subtasks (the common "container parent + N children" pattern — e.g. Project bootstrap with 8 subtasks for CI / auth / DB / …), the plugin now expands the parent in the working set and runs each subtask through the full pipeline: own description / acceptance criteria / comments / attachments fetch, own plan entry, own commit (
TYPE(scope)[<subtaskId>]: …), own board move (In progress → Internal testing at the time; Done - Local with fallbacks since 1.5.0), own time log. The parent itself stays put on the board — it is just the container — and gets no commit and no time log. - Tasklist filter applies to subtasks. The v1.3.0 To Do + assigned to me rules (since 1.5.0: start columns + me) run on the post-expansion set: a subtask in the wrong column or assigned to a teammate is dropped to analyse-only exactly like a top-level task. The single-task URL bypass still applies — paste the parent's URL, get all its subtasks processed regardless of column.
- Parent context in the plan. Each subtask's plan entry is prefixed with a one-line
Parent context:showing the parent's name and a short description excerpt, so you can scan what the container is meant to deliver before reading the subtask. Disable with"subtasks": {"include_parent_context": false}. - Recursive expansion up to
subtasks.max_depth(default2) — covers parent → subtask → sub-subtask. Set to1to expand only direct children; raise for deeper hierarchies. - API shape tolerance. Both v3 response shapes (
.tasks[]and.subtasks[]) and both endpoints (/tasks/{id}/subtasks.jsonprimary,/tasks.json?parentTaskId={id}fallback) are supported transparently. - Backward-compatible default-off path — set
"subtasks": {"enabled": false}to recover v1.3 behaviour entirely (parents stay in the working set, subtasks invisible). Existing single-task and tasklist runs that did not involve subtasks behave exactly the same as in v1.3.
The 1.4.0 migration is automatic and idempotent. Your existing config gains the subtasks block on the next run with safe defaults.
- Tasklist filter (To Do + me) — for tasklist URLs, only tasks in the board column
To Do(exact, case-sensitive; since 1.5.0 any of the start columns, default Ready for Development + To Do) AND assigned to the current Teamwork user are actually implemented. The rest of the tasklist is still fetched and rendered in the plan with a 1–2 sentence Quick read opinion, but the worker loop skips every mutation (no commit, no time log, no board move). Solves the common multi-repo case where a single Teamwork project holds a Laravel backend + an Ionic frontend owned by different developers — without the filter, running/teamwork-taskfrom the backend repo would implement the frontend developer's tasks in the wrong codebase. - Single-task URLs deliberately bypass the filter. When the user opens a specific task by ID, we honour that intent regardless of which column the task sits in or who it is assigned to (since 1.5.0 a completed task asks first).
- Three new CLI flags for per-run overrides:
--tasklist-filter=true|false,--tasklist-todo-stage=<name>,--tasklist-only-mine=true|false. - Plan-approval gate gains two new options: Promote an analyse-only task to implement (flip one or more skipped tasks back to the implement set) and Disable the tasklist filter for this run (process everything regardless of stage/assignee).
- Step 7 final summary now lists the filter activity and the analyse-only task IDs so the run report is honest about what was and was not implemented.
- Honest time logs in both directions (
round_threshold_minutes, default4). Up to v1.2.0 every entry rounded up to ≥ 5 min — a 30-second README typo fix got billed as 5, an 11-min hotfix as 15 (a 36 % over-bill). v1.3.0 splits the decision into two zones: elapsed below the threshold logs as raw minutes (1, 2, 3); elapsed at or above the threshold rounds to the nearest 5-min step. Concretely: 4 → 5, 5 → 5, 6 → 5, 7 → 5, 8 → 10, 9 → 10, 11 → 10, 12 → 10, 13 → 15, 14 → 15. Set the threshold equal totime_rounding_minutes(e.g. both5) to recover the pre-1.3.0 always-round-up behaviour. - Empty-result message for the tasklist filter. If the v1.3.0 "To Do" + me filter ends up with 0 tasks to implement, the skill explains which rules produced the empty set, per-task reasons, and most common causes, then asks a focused prompt: disable the filter, pick analyse-only tasks to promote, change the stage name, toggle the assignee check off, or cancel. No more silent "skill did nothing" runs.
- Worktree handoff at end of run (Step 9.5). When the skill executes inside a git worktree, every commit lands on the worktree's branch and never reaches
mainon its own. v1.3.0 asks at the end of the run what to do with those commits: Merge into a target branch (default — FF if possible, fall back to merge commit; target asked separately as parent / main / custom), Push the branch for a PR, Leave as-is, or Cherry-pick specific commits. On a successful merge the worktree branch + directory are removed by default. Disable with--worktree-handoff=leaveor"worktree_handoff": {"enabled": false}for runs where you want the v1.2.x "leave it all in the worktree" behaviour back.
The 1.3.0 migration is automatic and idempotent — your existing config gains the tasklist_filter block on the next run, with safe defaults.
- Tasklist context — tasklist description is shown on top of the plan so the whole batch is framed before per-task planning.
- Attachments — task files, comment files, and file comments are pulled into
./teamwork-task-<id>/and used as inline context..gitignoreis auto-updated; the folder is cleaned up after the time log succeeds. - Conditional comments —
fetch_comments_mode: when_needed(default) only fetches comments when the description is thin or explicitly references them. (Superseded in 1.5.0: the count now comes from a real probe — thecommentsCountfield this relied on is never returned by Teamwork v3 — and the newest comment is always read; see Fetching comments.) - Smart auto-commit gate — UI/template files or diffs over 100 lines trigger a confirmation prompt; trivial textual changes commit and log automatically.
- Auto-proposed tests — when a task does not specify how to verify the change, the skill sniffs
composer.json/package.jsonfor installed test frameworks (Pest, PHPUnit, Dusk, Vitest, Jest, Playwright, Cypress, Selenium) and proposes the right kind of test alongside the implementation. Visual changes default to a browser test (Dusk for Laravel, Playwright/Cypress for JS); backend changes to feature / unit tests. - Smarter time cursor — picks up from the end of today's most recent timelog (or skill start time for the day's first log). No overlap, no cross-midnight bleed, no future timestamps.
- Board workflow — task auto-moves to
In progresson start andInternal testingon finish (with fallback toTesting). Missing workflow/columns degrade silently with a warning in the plan. (1.5.0: the done target isDone - Local, fallbacksInternal testing→Testing; see Board workflow.) - Commit hash in time-log description — every Teamwork timelog now ends with
(commit: <short-hash>)for direct traceability. - CHANGELOG.md — proper Keep-a-Changelog file for the project.
Full migration notes are in CHANGELOG.md. The 1.1.0 migration is automatic and idempotent.
/plugin marketplace add wamesk/claude-code
/plugin install teamwork-task@wame
- A Teamwork.com account with API access enabled.
- A personal Teamwork API Key. Create one here:
https://<workspace>.teamwork.com/launchpad/apikey/manage→ Create API Key. See the Teamwork API authentication docs for details. curlandjqavailable in your shell (preinstalled on macOS; on Linux:apt install jq/brew install jq).gitinstalled and configured.
On the first invocation the plugin will:
- Create
~/.claude/plugins/data/teamwork-task-wamesk/config.json(copied from the bundledconfig.example.json) with permissions0600. - Prompt you (via
AskUserQuestion) for your Teamwork base URL (prefilled from the URL you passed in) and API token. - Save the values back into that config file.
- Append
/teamwork-task-*/to your project's.gitignore(if not already covered) so downloaded attachments cannot accidentally land in a commit. You will be asked whether to commit that change first.
The config lives outside the plugin cache, so reinstalling or updating the plugin will not wipe your API token. Existing v1.0.0 configs are migrated to the v1.1.0 schema automatically on the next run — no manual action required.
/teamwork-task <teamwork-url>
Supported URL shapes:
- Single task —
https://<workspace>.teamwork.com/app/tasks/<taskId> - Tasklist —
https://<workspace>.teamwork.com/app/tasklists/<tasklistId>
Examples:
/teamwork-task https://acme.teamwork.com/app/tasks/12345
/teamwork-task https://acme.teamwork.com/app/tasklists/678 --time-mode=ask
/teamwork-task https://acme.teamwork.com/app/tasklists/678 --branching=new_feature_branch
/teamwork-task https://acme.teamwork.com/app/tasks/12345 --auto-commit=never
Optional flags (override the saved config for this run only, not persisted):
--plan-mode=overview— generate a single tasklist-wide plan and ask for approval before any work (default).--plan-mode=per_task— render and approve a plan before each individual task.--plan-mode=none— skip planning approval entirely.--time-mode=real_rounded_5m— measure actual elapsed time and round up to the nearest 5 minutes (default).--time-mode=ask— ask after each task how many minutes to log; the measured value is suggested as the default.--branching=current_branch— commit on the current branch (default).--branching=new_feature_branch— createfeature/teamwork-tasklist-<id>(orfeature/teamwork-task-<id>) and commit there.--auto-commit=always— commit and log without prompting, no matter how risky the diff looks.--auto-commit=when_safe— ask only for UI/template files or large diffs (default).--auto-commit=never— always prompt before committing.--tasklist-filter=true|false— v1.3.0, tasklist URLs only: filter implementation set to the start columns + assigned to current user (defaulttrue). Single-task URLs ignore this flag.--tasklist-todo-stage=<name>[,<name>…]— override the start columns used by the tasklist filter for this run, comma-separated (default"Ready for Development,To Do", each matched pertodo_stage_match_mode— case-sensitive by default).--tasklist-only-mine=true|false— toggle the assignee check independently of the stage check (defaulttrue).--worktree-handoff=ask|merge|push|leave— v1.3.0, only when running inside a git worktree: what to do with the worktree's commits at end of run (defaultask).merge= fast-forward / merge into a target branch;push= push the branch for a PR;leave= no-op.--worktree-target=ask|parent|main|<branch>— when--worktree-handoff=merge, decide the target branch (defaultask).--subtasks=true|false— v1.4.0, expand parent tasks into their subtasks (defaulttrue).--dimensions=<csv>|none— v1.5.0, which build-time quality dimensions to plan and self-check: any subset ofui_ux,performance,security,reachability,framework, ornone(default:build_quality.dimensions, all five). See Build-time quality.
- Parses the Teamwork URL → workspace, kind (task/tasklist), entity ID.
- Loads or creates the persistent config; prompts for credentials on first run; migrates v1.0.0 configs to the v1.1.0 schema.
- Fetches the task(s) via Teamwork REST API v3, including:
- the tasklist description (for tasklist runs) — surfaced as "Tasklist context" in the plan,
- comments per task — the newest one whenever the task has any, the full thread when
fetch_comments_modesays so, - task attachments into
./teamwork-task-<id>/, - comment attachments into
./teamwork-task-<id>/comments/, - file comments (when
fetch_file_comments=true), - the description split into acceptance criteria and final summary (treated as the authoritative goal) — in the canonical WAME format the criteria are the block under
## Akceptačné kritériá/## Acceptance criteriaup to the next horizontal rule,### Prierezové požiadavkyincluded; otherwise the description is split on the first horizontal rule (above / below), - for each unique project, the workflow + stages so the card can be moved to the right column,
- for a single-task URL pointing at a completed task, a question whether to process it at all (default: skip — processing would move its card out of the done column).
- Renders a plan (per
plan_mode):overview(default) — single tasklist-wide plan with goal / acceptance / approach / comments digest / attachments / board target per task → one approval.per_task— same content but rendered and approved one task at a time inside the loop.none— no plan approval, jump straight to work. You can Approve / Skip tasks / Reorder / Add context / Cancel before the work starts. Plan time is not logged to Teamwork.
- For each task (in the approved order):
- Starts a timer.
- Moves the card to "In progress" on the project's board (if a workflow is configured).
- Plans the implementation internally using the final summary + acceptance criteria + comments + attachments, including which of the five build-time quality dimensions the change touches (and, once per run, the framework versions the project has installed). If a business decision or missing context is required, it stops and asks via
AskUserQuestion. - Implements the change in the current repo, following the quality rules of every dimension that applies.
- Runs relevant tests, if any.
- Runs Pint formatting for PHP changes.
- Self-checks its own diff against the applicable dimensions, fixes what is cheap, and records what stays open.
- Stops the timer, rounds the duration up to the configured minute step (default 5).
- Safety gate — if the diff touches UI/template/styling files or exceeds the line threshold, asks whether to commit, inspect first, or abort the task. Trivial edits skip this prompt.
- Commits using the
TYPE(scope)[<task-id>]: Messageconvention (noCo-Authored-Bylines). - Logs time back to Teamwork via the API as a sequential, non-overlapping entry (see Time logging behaviour) with a business-oriented Slovak description ending in
(commit: <short-hash>). - Moves the card to "Done - Local" (fallbacks "Internal testing" → "Testing") once the time log succeeds.
- Cleans up the attachment folder.
- Optionally marks the task as completed in Teamwork.
- Prints a summary table (task ID, title, minutes, commit hash, TW status, board stage, build quality) with the open quality items per task, hands them to
/teamwork-task-testwhen it is installed, and reminds you togit pushmanually.
File: ~/.claude/plugins/data/teamwork-task-wamesk/config.json
| Key | Default | Notes |
|---|---|---|
teamwork.base_url |
https://<workspace>.teamwork.com |
Your Teamwork workspace URL. Asked on first run. |
teamwork.api_token |
(empty) | Personal API key. Asked on first run. Stored at chmod 0600. |
plan_mode |
overview |
overview (one approval for the whole tasklist), per_task (approval before each task), or none. |
fetch_comments_mode |
when_needed |
always, when_needed (default), or never. when_needed always reads the newest comment and fetches the full thread only when the description is thin. See Fetching comments. |
fetch_attachments |
true |
If true, task and comment file attachments are downloaded into ./teamwork-task-<id>/. |
fetch_file_comments |
true |
If true, comments attached to file objects are fetched and surfaced in the plan. |
max_attachment_size_mb |
25 |
Files larger than this are skipped (listed in the plan + final summary, not downloaded). |
attachments_cleanup |
after_timelog |
after_commit, after_timelog (default), or never. Recommended after_timelog keeps files available for debugging if the time log POST fails. |
auto_commit_mode |
when_safe |
always (auto-commit, never ask), when_safe (ask only if the diff looks risky — see Smart auto-commit), or never (always ask). |
auto_commit_risky_patterns |
UI/template/styling regex | Regex (one per array entry) applied to changed file paths. Match ⇒ the safety gate prompts. Default covers .vue, .jsx, .tsx, .svelte, .blade.php, .css/.scss/.sass/.less, .html. |
auto_commit_max_diff_lines |
100 |
Total insertions + deletions over this threshold triggers the safety gate. |
auto_propose_tests |
true |
If true, when the task description doesn't mention testing, propose tests appropriate for the detected stack. See Auto-proposed tests. |
test_frameworks.php_unit_preference |
auto |
auto, pest, or phpunit. auto picks Pest if installed, otherwise PHPUnit. |
test_frameworks.php_browser_preference |
auto |
auto, dusk, or selenium. auto picks Dusk in Laravel projects, otherwise Selenium standalone. |
test_frameworks.js_unit_preference |
auto |
auto, vitest, or jest. auto follows what package.json already declares. |
test_frameworks.js_browser_preference |
auto |
auto, playwright, or cypress. auto follows what package.json already declares. |
test_visual_file_patterns |
UI / template regex | Regex list applied to changed paths. A match classifies the task as a visual change (browser test preferred); no match means backend change (unit/feature test preferred). |
test_opt_out_keywords |
["no tests", "skip tests", "without tests", "bez testov", "netreba testy"] |
Phrases in the task description that disable auto-propose for that task. |
time_cursor_strategy |
last_teamwork_timelog |
last_teamwork_timelog (default — pick up from today's last timelog) or floor_now (always start at the rounding boundary of the skill's launch time). |
include_commit_hash_in_log_description |
true |
Append (commit: <short-hash>) to every Teamwork time-log description. |
board_workflow.enabled |
true |
Master toggle for board moves (in progress / done column). |
board_workflow.in_progress_stage |
In progress |
Name of the column to move the card to when work starts. Match is case-insensitive, so it also resolves In Progress on the WAME board. |
board_workflow.done_stage |
Done - Local |
1.5.0 default (was Internal testing). Name of the column to move the card to when the time log succeeds — the WAME board's column for work finished locally. |
board_workflow.done_stage_fallbacks |
["Internal testing", "Testing"] |
Tried in order if done_stage does not exist in the project's workflow (the older boards). First match wins. |
board_workflow.done_stage_schema |
2 |
v1.5.0 — migration marker, do not edit: records that the done-target migration ran, so choosing Internal testing again later is never undone. See the board migration rule. |
board_workflow.match_mode |
case_insensitive |
Reserved for future variants; today it is always case-insensitive. |
time_mode |
real_rounded_5m |
real_rounded_5m measures actual time; ask prompts after every task. |
time_rounding_minutes |
5 |
Rounding step (minutes). Applies to both real_rounded_5m durations and the session cursor alignment. |
round_threshold_minutes |
4 |
v1.3.0 — elapsed < threshold logs as raw minutes (1, 2, 3); elapsed >= threshold rounds to the nearest time_rounding_minutes step. Set equal to time_rounding_minutes to recover the pre-1.3.0 always-round-up behaviour. Old name round_up_threshold_minutes is auto-migrated. |
min_log_minutes |
1 |
v1.2.0 — smallest entry the skill will write. Lower than round_threshold_minutes enables the raw-minutes sub-round zone. |
branching_mode |
current_branch |
current_branch or new_feature_branch. |
default_language |
sk |
Language for the Teamwork time-log description (Slovak by default). |
auto_complete_finished_tasks |
false |
If true, the plugin marks each task as completed in TW after the commit + time log. Independent from board moves. |
skip_completed_tasks |
true |
If true, tasks already marked as completed in TW are skipped silently when iterating a tasklist or subtasks. A single-task URL pointing at a completed task always asks first (default: skip). |
is_billable_by_default |
true |
Sets isbillable on every logged time entry. |
tasklist_filter.enabled |
true |
v1.3.0 — for tasklist URLs only, filter to tasks in one of tasklist_filter.todo_stages AND assigned to the current user. Single-task URLs always bypass. |
tasklist_filter.todo_stages |
["Ready for Development", "To Do"] |
v1.5.0 — the start columns: a task is implementable when its column is any of them (order = display only). Each matched per todo_stage_match_mode. Tasks not on the board are never implementable. An empty list falls back to todo_stage, then the default. |
tasklist_filter.todo_stage |
(legacy) | Pre-1.5.0 single column name. Still read when todo_stages is absent or empty; the migration turns it into todo_stages once ("To Do" → the default pair, anything else → [<name>]) and leaves the key in place for older sibling plugins. |
tasklist_filter.todo_stage_match_mode |
case_sensitive |
case_sensitive (default, strict match per start column — to do does not match To Do) or case_insensitive (loose match). |
tasklist_filter.only_assigned_to_me |
true |
Also require the current user to be in task.assignees. When false, only the stage check applies. |
tasklist_filter.analyze_all_tasks |
true |
When true, tasks that fail the filter are rendered in the plan as analyse-only with a Quick read line. When false, they are dropped entirely. |
tasklist_filter.skip_reason_render |
inline |
inline (default) renders the skip reason next to each analyse-only task in the plan. Other modes reserved for future variants. |
subtasks.enabled |
true |
v1.4.0 — expand every parent task with subtasks into the working set so each subtask is implemented, committed, board-moved and time-logged independently. false recovers v1.3 (parent stays, subtasks invisible). |
subtasks.max_depth |
2 |
v1.4.0 — recursion ceiling for subtask expansion. 1 = direct children only, 2 = also sub-subtasks, 3+ = deeper hierarchies. Safety cap against runaway recursion. |
subtasks.include_parent_context |
true |
v1.4.0 — when expanding a subtask, render a Parent context: line (parent name + ≤ 300-char description excerpt) in the plan entry so the LLM understands the broader scope. |
worktree_handoff.enabled |
true |
v1.3.0 — master toggle for the end-of-run worktree handoff. When false, the skill never auto-merges or pushes from a worktree. |
worktree_handoff.default_action |
ask |
ask (default — render an AskUserQuestion), merge, push, or leave. |
worktree_handoff.default_target |
ask |
ask (default), parent (the branch the worktree was created from), main (origin/HEAD / main / master / trunk fallback), or a literal branch name. |
worktree_handoff.merge_strategy |
ff_else_merge |
ff_else_merge (try fast-forward, fall back to merge commit), ff_only, no_ff, or squash. |
worktree_handoff.delete_branch_after_merge |
true |
If true, after a successful merge the worktree's branch is deleted with git branch -d (refuses if branch has unmerged commits, as a sanity belt). |
worktree_handoff.delete_worktree_after_merge |
true |
If true, after a successful merge the worktree directory is removed with git worktree remove. The cwd may shift back to the main checkout. |
worktree_handoff.push_remote |
origin |
Remote name used by the "push branch for PR" path and the dirty-main fallback. |
worktree_handoff.skip_if_no_commits |
true |
If true, Step 9.5 is skipped silently when HEAD has not advanced this run (no new commits to hand off). |
build_quality.dimensions |
["ui_ux", "performance", "security", "reachability", "framework"] |
v1.5.0 — build-time quality dimensions planned (Step 6.2), followed (Step 6.3) and self-checked (Step 6.5.5) per task. Same keys /teamwork-task-test reviews (framework there as advisory only). [] = none. Per run: --dimensions=. |
build_quality.dimensions_schema |
2 |
v1.5.0 — migration marker, do not edit: records that the list was reconciled with the five-key set, so removing framework is never undone by a later run. See the migration rule. |
To change a setting, edit the file directly and rerun the command.
When you run /teamwork-task inside a git worktree — typically because Claude Code launched a background job in .claude/worktrees/<name> or because you started the session from a worktree manually — every commit produced by the worker loop lands on the worktree's branch (e.g. claude/teamwork-task-12345). Up to v1.2.0 those commits stayed there forever; you had to remember to merge them by hand. v1.3.0 closes that loop in Step 9.5, right after the final summary and the optional verification handoff.
At the end of the run, the skill renders an AskUserQuestion:
Run finished in worktree
.claude/worktrees/fooon branchclaude/foowith N new commits this session: ⟨short list⟩. What do you want to do with them?
The default options are:
- Merge into
<parent>— fast-forward into the parent branch (the one the worktree was created from) when possible, fall back to a merge commit otherwise. After a successful merge, the worktree's branch and directory are removed (configurable). Recommended for most flows. - Merge into a different branch… — opens a follow-up question with
main, the parent branch, and Other (free text) so you can point at any local branch. - Push the branch to
originfor a PR —git push -u origin <branch>; the branch and worktree stay, so you can open a PR manually. - Leave as-is — I will handle the handoff manually — no-op.
- Cherry-pick specific commits into a target branch — power option; multi-select the commits, pick the target, the skill does the cherry-picks.
Edit ~/.claude/plugins/data/teamwork-task-wamesk/config.json:
worktree_handoff.default_action = "merge"— skip the first question.worktree_handoff.default_target = "parent"— skip the target question.worktree_handoff.merge_strategy = "ff_else_merge"— default.worktree_handoff.delete_branch_after_merge = true— default.worktree_handoff.delete_worktree_after_merge = true— default.
Combined, this gives you a fully hands-off pipeline: the skill implements, commits, time-logs, moves the board, runs verification, and merges everything back into the parent branch, all in one command.
- The main checkout must be clean. If
git statuson the main repo shows uncommitted changes, Step 9.5 refuses to switch branches there (could lose your work) and falls back to push-only — your commits go toorigin/<branch>and you finish the merge manually. - Merge conflicts pause the run. The skill never auto-resolves merge conflicts. On failure the worktree is left intact, the error is surfaced, and you can
cdin and finish manually. - Cherry-pick conflicts pause the run. Same rule.
skip_if_no_commits = true(default) — if the worker loop did not commit anything (every task aborted / skipped / timelog failed), Step 9.5 is silently skipped. Nothing to merge.
- Per run:
--worktree-handoff=leave(skip entirely) or--worktree-handoff=push(skip the merge question). - Persistently:
"worktree_handoff": {"enabled": false}in your config. - Single-task runs from the main checkout never trigger this step — the detection in Step 5.1 marks
WT_RUN_IN_WORKTREE=0and Step 9.5 is a no-op.
For tasklist URLs only, the plugin filters which tasks get actually implemented:
- Only tasks in one of the start columns —
tasklist_filter.todo_stages, defaultReady for DevelopmentandTo Do(the two start columns of the shared WAME board; an older board without Ready for Development simply uses To Do), each matched case-sensitively by default —to do,TO DO,ToDodo not matchTo Do— AND - assigned to the current Teamwork user
are run through the worker loop. The rest of the tasklist's tasks — including every task that is not on the board — are still fetched and shown in the plan with a short Quick read sanity-check line, but the plugin never commits, time-logs, or moves the board card for them.
Single-task URLs (/teamwork-task https://…/tasks/12345) deliberately bypass the filter — when you point at a specific task by ID, the plugin honours that intent regardless of the task's column or assignee. The one exception is a completed task: the plugin asks first whether to process it (default: skip), because processing moves its card out of the done column. Completed tasks inside a tasklist or among subtasks are skipped silently (skip_completed_tasks).
The motivation is the standard multi-repo Kanban setup: a single Teamwork project commonly contains both a Laravel backend and an Ionic / iOS / Vue frontend, owned by different developers working in different repositories. Without the filter, running /teamwork-task <tasklist-url> from the Laravel repo would happily start implementing the frontend developer's tasks in the wrong codebase. The filter narrows the implementation set to what you actually own and is greenlit to work on, while still surfacing teammates' tasks in the same plan so you can sanity-check them in standup.
- Per run:
--tasklist-filter=false(process all fetched tasks),--tasklist-only-mine=false(skip the assignee check), or--tasklist-todo-stage="Backlog"/--tasklist-todo-stage="Ready for Development,To Do,Backlog"(other start columns, comma-separated). - From the plan-approval prompt: Promote an analyse-only task to implement (flip specific task IDs back to the implement set — also the way to take a task that is not on the board) or Disable the tasklist filter for this run.
- Persistently: edit
~/.claude/plugins/data/teamwork-task-wamesk/config.jsonand set"tasklist_filter": {"enabled": false}, or changetodo_stages/only_assigned_to_meto whatever your team's convention is.
- Task has no card / is not on the board → v1.5.0 analyse-only with reason
no_card, shown as "not on the board" — a backlog item nobody moved into a start column is not greenlit work. Promote it from the plan when it is yours. (1.3.0–1.4.2 documented the opposite, process.) - Project has no workflow at all → every task is not on the board, so the whole tasklist is analyse-only and the plan asks how to proceed (disable the filter for this run, promote tasks, change the start columns, or cancel). A one-line note names the project.
- Board has only some of the start columns (the older per-project boards have To Do but no Ready for Development) → the missing name never matches; nothing else changes. A board with none of them is named once in the plan.
- Task is on the board but its column cannot be read (the workflow lookup failed, e.g. rate limit) → v1.5.0 keeps it analyse-only with reason
stage_unresolved(<stageId>)and a⚠line — an unreadable column is never treated as a start column. /me.jsonis unreachable (token withoutusers.readscope, network blip) → assignee check is skipped so you are never silently locked out of your own work; the stage check still applies.
Teamwork's data model allows a task to have subtasks — child tasks under a parent with their own description, acceptance criteria, comments, attachments, assignee, and stage on the board. A common pattern is a "container" parent task like Project bootstrap with 8 subtasks splitting the work by area (CI, auth, DB, …). Up to v1.3.0 the plugin ignored subtasks entirely: it planned, committed and time-logged against the empty parent, leaving the actual N units of work invisible. v1.4.0 closes that loop in Step 3.42.
After the initial fetch and the tasklist-context lookup (Step 3.4), the plugin calls GET /projects/api/v3/tasks/{id}/subtasks.json (fallback when it fails: GET /tasks.json?parentTaskId={id} — singular, v3 ignores the plural parentTaskIds — filtered client-side) for every task in the working set. Each subtask's board column comes from its own workflowStages, its project from .tasklist.meta.projectId. When subtasks come back, the parent is removed from the implementation working set and each subtask takes its place. From that point on every subtask goes through the full pipeline as if it were a top-level task:
- Step 3.5 — own comments probe (newest comment always, full thread when needed).
- Step 3.6 — own description split (acceptance criteria above HR, final summary below).
- Step 3.7 — own attachment folder
./teamwork-task-<subtaskId>/. - Step 3.9 — own file-comments digest.
- Step 3.10 — local working-tree discovery.
- Step 3.45 — tasklist filter (when applicable) operates on the expanded set; a subtask outside the start columns (not on the board included) or assigned to a teammate is dropped to analyse-only by the same rules as a top-level task.
- Step 4 — own plan entry, prefixed with a
Parent context:line (parent name + ≤ 300-char description excerpt) whensubtasks.include_parent_context = true. - Step 5 — own timer; the same sequential, non-overlapping 5-min cursor.
- Step 6 — own implementation, own
TYPE(scope)[<subtaskId>]: …commit, own In progress → Done - Local board move (fallbacks Internal testing → Testing). - Step 5/6 timelog — own
POST /projects/api/v3/tasks/{subtaskId}/time.jsonentry.
- Parent stays put. The parent is never moved across the workflow and gets no time log — it is a container, not a work item. When all its subtasks finish, the parent is not auto-clicked complete; close it manually if your team uses that convention.
- Per-subtask commits and time logs. No aggregation into one parent timelog. Every subtask gets its own commit with its own ID in square brackets and its own 5-min-aligned timelog.
- Per-subtask board moves. Each subtask has its own card and goes through In progress → Done - Local (or its fallback) independently.
- Filter applies to subtasks. A subtask outside the start columns or assigned to a teammate is dropped to
analyse_onlyby Step 3.45 with the same start columns + me rules as a standalone task.
URL_KIND=task and the URL points at a parent: Step 3.42 expands it into its 8 (or however many) subtasks. The tasklist filter is skipped (single-task URL bypass), so every open subtask is process_mode=process regardless of column or assignee (completed subtasks are skipped silently with skip_completed_tasks; a completed parent URL asks first) — you effectively get the same behaviour as feeding the plugin a tasklist URL with 8 children, minus the filter.
Paste the subtask's URL directly: Step 3.42 finds no further subtasks (leaf) and the pipeline runs against that single subtask. Useful for hand-picking one specific child to work on.
Edit ~/.claude/plugins/data/teamwork-task-wamesk/config.json:
subtasks.enabled = false— recover v1.3 behaviour (parent stays in the working set, subtasks invisible). The whole rest of v1.4 is no-op.subtasks.max_depth = 1— expand only direct children; sub-subtasks stay attached to their parent subtask.subtasks.max_depth = 3(or higher) — go deeper for nested hierarchies.subtasks.include_parent_context = false— drop theParent context:line from subtask plan entries (saves a few tokens per task; useful when the parent name is enough).
- Subtask in a different project than parent → the per-project workflow cache (Step 3.3) picks up the extra project; the subtask's board move targets its own project's workflow.
- API shape variance → both
.tasks[]and.subtasks[]response shapes are accepted; both/tasks/{id}/subtasks.json(primary) and/tasks.json?parentTaskId={id}(fallback) are tried. skip_completed_tasks = true→ applies per subtask. A completed subtask is dropped from the expanded set the same way a completed top-level task is.- Subtask has no card / not on the board → v1.5.0 analyse-only (
no_card) in a tasklist run, like a top-level task; promote it from the plan when it is yours. Under a single-task URL the filter is bypassed and it is processed. subtasks.enabled = false→ step is a complete no-op; v1.3 behaviour.
overview(default) — after fetching tasks and comments, the plugin prints a single markdown plan covering every task in the run (goal, acceptance criteria, planned approach, comments digest, attachments, board target) and asks for one approval. You can Approve / Skip tasks / Reorder / Add context / Cancel before any code is touched.per_task— the same plan shape is rendered and approved before each task, right before its timer starts. Safest for high-stakes work; noisier for long tasklists.none— no plan approval gate; the plugin starts working as soon as the task list is fetched. Use only when you fully trust the task descriptions.
The plan generation itself is not counted in any task's time log — the timer for a task starts after planning, when implementation begins.
When the description is in the canonical WAME format written by teamwork-task-analyze, -from-desk, -from-session and -from-dnr ([preamble] → --- → ## Akceptačné kritériá → --- → ## Cieľ → --- → ## Technický popis), the acceptance criteria are the block from the ## Akceptačné kritériá / ## Acceptance criteria heading to the next horizontal rule — its ### Prierezové požiadavky / ### Cross-cutting requirements items included, which Step 6.2 treats as binding — and the preamble is context. Any other description is parsed by splitting on the first horizontal rule (<hr> in HTML or --- / *** / ___ on its own line in Markdown):
Acceptance criteria:
- Must allow PDF export
- Must respect tenant theme
- Must work offline
---
Final summary: implement an `InvoicePdfExporter` action that uses the Dompdf engine,
respects the active tenant theme via the existing `ThemeResolver` service, and queues
the job for offline support. Reuse the existing `Invoice::toArray()` shape.
- Content above the HR is treated as the acceptance criteria (the checklist).
- Content below the HR is the final summary — the authoritative goal — and is used as the basis for the commit message body and the Teamwork time-log description.
- If no HR is present, the whole description is treated as acceptance criteria and the plan will warn that no final summary was provided.
When URL_KIND=tasklist, the tasklist's own description is rendered on top of the plan as "Tasklist context" to frame the whole batch before per-task planning.
fetch_comments_mode controls when the plugin pulls comments:
always— fetch comments for every task (legacy v1.0 behavior, more API calls).when_needed(default) — one probe per task (GET …/comments.json?pageSize=1&orderBy=date&orderMode=desc) returns the comment count and the newest comment in a single call:- no comments → nothing to read;
- one or more → the newest comment is always read, because the last comment is the freshest truth and can change the spec even on a well-described task;
- more than one → the full thread is fetched as well when at least one of these holds:
- The description has no horizontal rule (no final summary), or
- The acceptance criteria above the HR is shorter than 100 characters, or
- The description text explicitly references comments (e.g. "viď komentár", "see comments", "viz nižšie"), or
- The newest comment refers back to earlier ones or contradicts the description.
never— skip entirely (no probe either), even when the description is sparse. Useful when the tasks are known to be fully described.
Comments are sorted chronologically by postedDateTime (orderBy=date). If the v3 endpoint fails, the plugin prints a ⚠ line and falls back to the v1 endpoint (/tasks/{id}/comments.json, sorted by datetime); an unknown count means the full thread is read rather than assuming zero. The plan's Comments context line says exactly what was read (e.g. "newest of 8 read — full thread not fetched"). The last comment is the freshest truth when comments contradict each other or the description.
/teamwork-task-test reviews five cross-cutting dimensions at QA time. Since 1.5.0 this plugin applies the same five while building, so QA confirms instead of discovers:
| Key | Applies when the change … | Build rule (short form) |
|---|---|---|
ui_ux |
touches templates / components / views (test_visual_file_patterns) or a class feeding them |
copy the sibling screen's patterns; accessible names; disabled controls say why; labels and alt; loading / empty / error states; every string through the translation layer with a resolving English key |
performance |
adds queries, migrations, loops over records, batches, imports / exports | no query in a loop, eager loads, pagination, chunkById, indexes on new foreign keys and filtered columns, filter in SQL, slow work to a queue |
security |
adds routes, actions, buttons, endpoints, inputs, raw queries | same gate as the neighbours plus an object-scoped policy (no IDOR), tenant scope intact, FormRequest validation, explicit $fillable, handled errors instead of 500s, no internals or secrets in messages |
reachability |
adds, renames or removes a screen (page, view route, Nova resource / lens / dashboard / tool, SPA route) | the screen ships in the same commit with its menu entry and inbound links from related screens (relation tab on the parent, detail link, action, breadcrumb); a deliberately URL-only page is named as an allow_orphans candidate; renames leave no dead links; menu visibility matches route authorization |
framework |
writes or changes code in PHP, Laravel, Nova, Livewire, Inertia, Pest, JS / TS, Vue, React, CSS or Tailwind — almost every code task | new or changed code uses the current idiom / built-in feature of the installed version (looked up in current docs, not remembered) instead of a dated or hand-rolled pattern; project conventions win; no drive-by rewrites; nothing deprecated in, or newer than, the installed version / PHP floor / browserslist target; no new dependency for a built-in |
How it runs per task:
- Plan (Step 6.2) — decide from the shape of the change which dimensions apply; a pure backend task records
ui_ux: not_applicableinstead of growing UI boilerplate. - Implement (Step 6.3) — follow the rules of every dimension that applies.
- Self-check (Step 6.5.5) — walk the task's own diff, fix what is cheap now, and record one status per dimension:
checked,not_applicable,skipped(--dimensions)oropen(<item> @ <file:line>). Forframeworkit also records up to three advisorysuggest(<opportunity> @ <file:line>)tips for code the task read but did not change. A dimension is never reported as checked when it was not read. - Report & hand off — the final summary shows a Build quality cell per task plus every open item, the detected framework versions and the advisory framework tips; open
security/reachabilityitems make the commit safety gate ask first (defaultauto_commit_mode=when_safe) —frameworkrows never do; the list is handed to/teamwork-task-testbefore it runs, with theframeworklines marked advisory. Nothing is blocked silently.
Turn it off per run with --dimensions=none, or persistently with "build_quality": {"dimensions": []}.
The fifth dimension asks for the idioms and built-in features of the versions the project actually has installed, not the ones a model remembers from older releases:
- Detect the versions once per run (Step 6.2) into
/tmp/tw_job_<id>/framework_versions.tsv—laravel/framework,laravel/nova,livewire/livewire,inertiajs/inertia-laravel,pestphp/pest,laravel/boostfromcomposer.lock; the PHP constraint fromcomposer.json(require.php,config.platform.php);vue,react,nuxt,vite,typescript,tailwindcss,@inertiajs/*,@ionic/*fromnode_modules→package-lock.json→yarn.lock→ the declared range; thebrowserslisttarget; the Node version (.nvmrc/.node-version/engines.node). The first task of the run detects, later tasks reuse (a task that changes a manifest or lock file drops the cache, so the next one detects again). PHP language features are bounded by the lowest PHP the project admits (config.platform.php, else the floor ofrequire.php), not by your localphp -v. - Look the API up in current docs — Laravel Boost
search-docswhen the project haslaravel/boost, otherwise the context7 MCP, otherwise the official docs; per major version skim the upgrade guide for what is new and deprecated. - Build within the guardrails — the project's
CLAUDE.mdand sibling conventions win over a newer idiom; no second pattern next to an established one (unless the task is the refactor); no drive-by rewrites of untouched code (those become advisory tips in the summary); never a deprecated API; never a feature newer than the installed version, the PHP floor or the browserslist target (that one is a bug and is fixed before the commit); no new dependency for something the framework ships.
On the QA side /teamwork-task-test (1.2.0+) treats framework as advisory: recommendations only, no code edits, never a failed or downgraded acceptance criterion, never a blocker (only code that will not run on the installed versions / browserslist target is a real finding, filed under the other keys). Sibling plugins that write task descriptions (teamwork-task-analyze, -from-desk, -from-session, -from-dnr) put framework notes into the technical plan, never into the acceptance criteria.
build_quality.dimensions_schema records which key set your list was last reconciled with (absent = a four-key list from a pre-release 1.5.0 build; 2 = five keys). The Step 2.6 migration adds framework once, and only when the marker is absent and the list is exactly ui_ux, performance, security, reachability (each once, any order) — such a list cannot be told apart from the untouched old default. Every other list counts as customised and is left untouched: a subset, [], a list that already names framework, or one with other keys. A missing list gets the five-key default. Afterwards the marker is 2, so if you remove framework from the list it stays removed on every later run.
When fetch_attachments=true (default), the plugin downloads:
- Task files →
./teamwork-task-<task-id>/ - Comment files →
./teamwork-task-<task-id>/comments/ - (Optionally) File comments → surfaced as a one-line digest per file in the plan, not downloaded as separate files.
Each downloaded file is prefixed with its Teamwork file ID (e.g. 987_mockup.png) so name collisions never overwrite each other.
A few hard rules:
- The first run in a repo appends
/teamwork-task-*/to your project's.gitignoreand asks whether to commit that.gitignorechange before starting work. Subsequent runs only touch.gitignoreif the pattern is not already there. - Files larger than
max_attachment_size_mb(default 25 MB) are skipped — listed in the plan and the final summary as "skipped: file (size)", never downloaded. - After the time log succeeds, the attachment folder is removed per
attachments_cleanup(defaultafter_timelog). If the time log POST fails, the folder is intentionally kept so you can inspect what was being worked on. - Text-based files (
.md,.txt,.json,.yaml,.csv,.log, source files, etc.) are read inline by Claude for additional context. Binary files (images, PDFs, archives) are listed in the plan but not opened.
The auto_commit_mode setting decides whether a commit is auto-approved or gated on a confirmation prompt:
always— every commit is auto-approved.when_safe(default) — auto-commit unless the diff trips a heuristic that suggests human review:- A changed file matches one of
auto_commit_risky_patterns(UI / template / styling by default), or - The diff size is greater than
auto_commit_max_diff_lines(default 100 lines of insertions + deletions). If any of those fires, the plugin asks viaAskUserQuestion: - Approve commit → commit and continue.
- Inspect first → pause; you
git diff/ open the editor; the plugin re-asks afterwards. - Abort task → leave the working tree as-is, do not commit, do not log time, do not move the board card. The next task (if any) still runs.
- A changed file matches one of
never— always ask before committing, regardless of diff content.
auto_commit_risky_patterns is a JSON array of regex strings applied to changed file paths via grep -iE. Adjust it to match your codebase: e.g. add \\.tpl$ for Smarty templates, or ^database/migrations/ if you want database changes gated even when small.
If a Teamwork task forgets to describe how the change should be verified, the plugin proposes tests for you instead of skipping the topic. The behaviour is controlled by auto_propose_tests (default true).
How the skill decides what to do
For each task, the plugin classifies the description as one of:
described— text mentions any of test, unit test, feature test, browser test, Pest, PHPUnit, Dusk, Selenium, Playwright, Cypress, e2e, cover with tests, napíš testy, etc. The skill uses your description as-is.opted_out— text matches any phrase intest_opt_out_keywords(defaults include "no tests", "skip tests", "bez testov", "netreba testy"). The skill writes a note in the plan and writes no tests.missing— neither of the above. Auto-propose mode kicks in.
How the framework is chosen
For missing, the skill sniffs composer.json and package.json for installed frameworks:
| Project signal | Visual change (.vue / .tsx / .blade.php / resources/views/…) |
Backend change |
|---|---|---|
| Laravel + Dusk installed | Laravel Dusk in tests/Browser/ |
Pest if installed, else PHPUnit |
| Laravel without Dusk | Ask to install Dusk or write manual checklist | Pest if installed, else PHPUnit |
| PHP without Laravel | Selenium standalone PHPUnit or manual checklist | Pest if installed, else PHPUnit |
| JS with Playwright | Playwright in tests/e2e/ |
Vitest if installed, else Jest |
| JS with Cypress | Cypress in cypress/e2e/ |
Vitest if installed, else Jest |
| JS without any browser | Ask to install Playwright or manual checklist | Vitest if installed, else Jest |
| Nothing detected | Manual checklist | Manual checklist |
test_frameworks.*_preference lets you pin a specific choice (e.g. force pest even if PHPUnit is also present, or force playwright over a coexisting cypress). The default "auto" follows the table.
Missing tool — ask, never auto-install
When the chosen framework is not installed (e.g. a visual change in a Laravel project without Dusk), the skill never runs composer require or npm install silently. It asks via AskUserQuestion:
- Install
<package>now — runs the install command and continues. - Skip browser tests, write a manual checklist — proceeds without a test file; the checklist is emitted into the plan.
- Abort the task — leaves the working tree as-is.
What gets committed
When the skill auto-proposes tests, the implementation and the test file land in the same commit, so the time log line (commit: <hash>) covers both. The next board move to the done column (Done - Local, or Internal testing / Testing on older boards) signals that a human can pick up where automated verification stopped.
When board_workflow.enabled=true (default), the plugin moves the Teamwork card across the project's Kanban board as work progresses:
- At the start of each task (right after the timer starts) → move to
board_workflow.in_progress_stage(default"In progress"). - After the time log succeeds → move to
board_workflow.done_stage(default"Done - Local"since 1.5.0). If that column does not exist in the project, the plugin tries the names inboard_workflow.done_stage_fallbacks(default["Internal testing", "Testing"]) in order; first match wins.
Stage matching is case-insensitive — "DONE - LOCAL", "Done - Local", and "done - local" all match the same column, and the default "In progress" matches In Progress on the WAME board.
Both board generations work with the defaults (verified with read-only requests on 2026-09-24):
| Board | Start columns (tasklist filter) | In progress | Done target |
|---|---|---|---|
| Shared WAME workflow (Planned tasks · … · Ready for Development · To Do · In Progress · On Hold · Done - Local · Deployed to DEV · …) | Ready for Development, To Do | In Progress | Done - Local |
| Older per-project boards (Next Sprint · Waiting for approval · To Do · In progress · On hold · Testing · Done - ready to deploy · …) | To Do | In progress | Testing (fallback) |
What happens if a stage is missing:
- Both start and done stages found — both moves happen.
- Only
in_progress_stagefound — start move happens; done move is skipped with a warning in the plan and the final summary. The task stays in In progress after completion. - Only the done stage found — start move is skipped; done move still happens after the time log.
- No workflow on the project, or neither stage found — board moves are disabled for that project's tasks; a single warning is shown in the plan, and the Board stage column in the final summary shows
—.
Failures of the move POST itself (HTTP non-2xx) are non-fatal: the plugin logs a warning and continues. Board moves are a nice-to-have, not a gating dependency. There are no retries — the most likely cause is a permissions or naming mismatch which retrying will not fix.
Interaction with auto_complete_finished_tasks: both run independently. If you enable task completion and board moves, the task ends up completed=true and in the done stage simultaneously, which Teamwork handles fine.
If the safety gate at commit time results in Abort task, the card stays in In progress (semantically correct — the task was not completed).
The shared WAME board starts work in Ready for Development or To Do and ends it in Done - Local; older boards end in Internal testing or Testing. The Step 2.6 migration moves your config to the new board only where you kept the old defaults, idempotently:
- Start columns —
tasklist_filter.todo_stagesis added once, only when it is missing (ornull):["Ready for Development", "To Do"]whentodo_stagewas missing or the old default"To Do",[<your column>]when you had customised it ("Backlog"→["Backlog"]). An existingtodo_stagesis never overwritten; the legacytodo_stagekey stays in the file. - Done target —
board_workflow.done_stagemoves from the old default"Internal testing"to"Done - Local", and the fallbacks become["Internal testing", …your other fallbacks except "Done - Local"…](deduplicated,"Testing"kept). A customiseddone_stage(anything but"Internal testing") is never touched.board_workflow.done_stage_schema: 2records that the check ran, so if you set"Internal testing"again afterwards it stays.
Examples: the 1.4.2 default ("Internal testing" + ["Testing"]) and a config that already listed "Done - Local" as a fallback ("Internal testing" + ["Done - Local", "Testing"]) both end as "Done - Local" + ["Internal testing", "Testing"]; "QA review" + ["Testing"] stays as it is. in_progress_stage is not migrated — "In progress" already matches In Progress case-insensitively.
Hard rules every entry the plugin writes to Teamwork obeys:
- Sequential, non-overlapping entries. Even though implementation work can overlap in real time (parallel tool calls, interleaved investigations), the time logged to Teamwork is laid out strictly back-to-back. The plugin maintains a single session cursor that advances by exactly the logged duration after each entry. Result: if task A is logged as
start 10:15, 20 min, task B will be logged asstart 10:35, …— never10:32, never overlapping10:25–10:45. - Round-to-nearest beyond the threshold, raw below it (default 5 min step, 4 min threshold — v1.3.0). The duration decision is split into two zones controlled by
round_threshold_minutes(default4):- Elapsed at or above the threshold rounds to the nearest
time_rounding_minutesstep using half-up integer rounding. With defaults (threshold=4,ROUND=5): 4 → 5, 5 → 5, 6 → 5, 7 → 5, 8 → 10, 9 → 10, 10 → 10, 11 → 10, 12 → 10, 13 → 15, 14 → 15, 15 → 15. This is honest in both directions — 7-min work bills as 5, 8-min work bills as 10. Over many tasks the bias averages out. - Elapsed strictly below the threshold but at least
min_log_minutes(default 1) logs as raw minutes. With defaults: 1 → 1, 2 → 2, 3 → 3. The cursor advances by the exact raw amount, so the next log starts off the 5-min grid (e.g.:17). This keeps a trivial-edit log honest at 1 min instead of padding it to 5. - Set
round_threshold_minutesequal totime_rounding_minutes(e.g. both5) to recover the pre-1.3.0 "always round up to ROUND" behaviour where every entry is ≥ 5 min and the cursor never falls off the 5-min grid.
- Elapsed at or above the threshold rounds to the nearest
- Plan time is never logged. The session cursor starts only after the plan is approved. Time spent on planning, reading the user's clarifications, or waiting for an
AskUserQuestionresponse is not billed.
The session cursor's starting position is controlled by time_cursor_strategy:
last_teamwork_timelog(default) — pick up where you left off. The plugin asks the Teamwork API for the most recent timelog from today (viaGET /projects/api/v3/time.json?assignedToUserIds=<you>&startDate=<today>&orderBy=date&orderMode=desc&pageSize=1) and uses its end timestamp (rounded up to the 5-minute boundary). If there is no log today, the cursor starts atfloor(skill-launch-time, 5min). Future timestamps (clock skew) and cross-midnight cases fall back tofloor(now).floor_now— always start atfloor(skill-launch-time, 5min). Classic v1.0 behavior, useful if you keep parallel timesheets manually and don't want the plugin to discover them.
The final summary prints which source was used (last_timelog @ 10:50 or skill start (first log of day)), so you can always verify which strategy actually fired.
If a POST to Teamwork fails (network blip, 5xx), the cursor does not advance — the next successful log keeps the same start time so your timesheet stays contiguous, and the corresponding move to the done column (Done - Local or its fallback) is skipped (the task is not yet billed).
The text that lands in Teamwork's time-log description is written from the perspective of someone reading the timesheet for billing or status (PM, client, accountant) — not a developer reading a code review. State what was delivered for the user or the business. Include a technical detail only when it materially helps identify the work (a specific module name, a feature flag, a migration ID). Avoid variable names, line counts, library versions, and diff stats. Keep it to 1–2 sentences in default_language (Slovak by default), followed by (commit: <short-hash>) if include_commit_hash_in_log_description=true (default).
Good (business):
- „Pridaná možnosť exportu faktúr do PDF s podporou témy nájomcu. (commit: a1b2c3d)"
- „Opravený výpadok prihlasovania pri súbežnom obnovení relácie. (commit: 9f4e2b1)"
Avoid (technical):
- „Refactored InvoiceController::export() to use new DompdfRenderer, added 3 tests."
- „Updated 8 files, +142 −37 lines, bumped filament/filament to 5.2."
real_rounded_5m(default) — the plugin measures wall-clock time between starting the task and the commit, then rounds up to the nearesttime_rounding_minutes. Best for billable work where you trust the measurement.ask— after each commit, the plugin asks you how many minutes to log, with the measured value prefilled. Use this when you frequently take long breaks, multitask, or want explicit control.
Either mode still feeds into the sequential, 5-minute-aligned session cursor above — ask only lets you override the measured duration, not the start time.
current_branch(default) — every commit lands on whatever branch is currently checked out. The plugin verifies the working tree is clean before starting (after the.gitignoreauto-update step).new_feature_branch— the plugin creates (or switches to)feature/teamwork-tasklist-<tasklistId>for tasklist runs, orfeature/teamwork-task-<taskId>for single-task runs, and commits there.
The plugin extends the conventions in your ~/.claude/CLAUDE.md with the Teamwork task ID in square brackets right after the scope, so every commit is traceable back to its source task:
TYPE(scope)[<task-id>]: Short summary
Optional detailed description on next lines (based on the task's final summary).
TYPE∈CREATE,UPDATE,EDIT,FIX,REMOVE,MOVE,UPGRADE,DELETE, …scopeis the module / model / area touched (lowercase, e.g.user,auth,invoice).<task-id>is the numeric Teamwork task ID, no#prefix — sogit log --grep '\[123456\]'finds every commit related to that task.- The plugin never adds
Co-Authored-Bylines.
Examples:
CREATE(user)[123456]: Create user module
UPDATE(invoice)[123789]: Add PDF export action
FIX(auth)[124001]: Resolve session timeout race condition
When something genuinely cannot be decided (ambiguous spec, missing context, business call), the plugin stops and asks via AskUserQuestion. It will not invent decisions for product/business-shaped questions, and it will not skip the task silently.
Non-essential side-effects are never blockers — board move failures, attachment download failures, file comments fetch failures, and auto_complete_finished_tasks POST failures all degrade silently with a warning. The core task → commit → time log pipeline still runs end-to-end.
- The Teamwork API token is stored in plaintext at
~/.claude/plugins/data/teamwork-task-wamesk/config.jsonwith file permissions0600. - The token is never echoed to stdout, never written to commit messages, and never passed to
curlvia the URL — only via-u "$TOKEN:xxx". - The plugin's own
.gitignoreexcludesconfig.jsonfrom the repo itself as a safety net. - Downloaded attachments may contain confidential customer material. The auto-added
/teamwork-task-*/entry in your project's.gitignoreprevents accidental commits, and the post-timelog cleanup removes the folder once the work is logged.
If you suspect your token has leaked, revoke it in Teamwork (/launchpad/apikey/manage) and rerun the plugin — it will re-prompt.
- Branch per task (currently: tasklist-level branch or current branch).
- Automatic
git pushafter the run. - Open a Pull Request per task or per tasklist.
- Multi-repo orchestration (when a tasklist spans multiple repositories).
- Resume after interruption — recover from the last successful timelog / commit when a run was killed mid-way (today only the time cursor handles part of this).
MIT — see LICENSE.