tk-studio-track
Tracking verbs on the bound Jira project — `block` moves a story the loop cannot progress, and each dependent story, to "Blocked - Awaiting User" with a comment that @mentions the owner (or, with `--engagement`, one engagement's questions issue, which `close` later moves to "Done"); `reply` posts the owner's answer (or "Done.") with the owner's host-only token and moves the issue to "Block Resolved"; `validate` checks that resolution and passes the issue back to work or re-blocks it with a follow-up. Use when the user says "block this story", "tk track block", "put it in the owner's inbox", "reply to the blocked issue", "validate the resolution", or a supervisor or engagement payload names this skill.
- License 许可
- MIT
- 最近更新
- 2026-11-10
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- tk-studio-track
- description
- Tracking verbs on the bound Jira project — `block` moves a story the loop cannot progress, and each dependent story, to "Blocked - Awaiting User" with a comment that @mentions the owner (or, with `--engagement`, one engagement's questions issue, which `close` later moves to "Done"); `reply` posts the owner's answer (or "Done.") with the owner's host-only token and moves the issue to "Block Resolved"; `validate` checks that resolution and passes the issue back to work or re-blocks it with a follow-up. Use when the user says "block this story", "tk track block", "put it in the owner's inbox", "reply to the blocked issue", "validate the resolution", or a supervisor or engagement payload names this skill.
03 SKILL.md 正文
tk-studio-track
The owner's one inbox starts here (§6.9 items 1 and 2, §7.4): a story the
loop cannot progress — a dev session's blocked end, a paused run
awaiting a ruling, an engagement question — sits in Jira's
Blocked - Awaiting User with its reason, and so does every story that
depends on it. The sync-back then derives _bmad-output/tracking/inbox.json
from those statuses. Every Atlassian write goes through the planning adapter
(lib/jirabackend.py, AD-4); nothing else is promoted (D9).
Behavior (both modes)
-
Run the verb through the plugin root:
uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" block --directory <project-root> \ --story <ST-NNN | N-M-slug> --reason "<why it cannot progress>" \ --resolves "<what resolves it>" [--source run|engagement] [--dry-run]blockresolves the story's plan entity (an entity id, or a loop story keyN-M-…matched by the entity'ssource: epics.md#story-N.M) and itsexternal.jira.key, plus each dependent story — the story entities whosedepends_onnames it (a dependent alreadydoneordroppedis skipped, named innotes[]). Each issue moves toBlocked - Awaiting Userby the transition's target name (never its category:Block Resolvedshares it), skipped for an issue already there, and gets one agent comment (REST v2 wiki mention):[~accountid:<owner>] Blocked: <reason> Resolves when: <resolves> Blocked by: <story's key> (a dependent's comment only)The comment is skipped only when the issue was already in
Blocked - Awaiting Userbefore the run (no transition made) and its newest agent comment (read live,GET /issue/<KEY>/comment; agent is anyone not the owner) has the same body — a re-block afterBlock Resolvedalways comments. The result is{ok, dry_run, story, source, issues:[{issue, entity, role: story|dependent, transitioned, commented, skipped?, error?}], notes[], written[], retry?}with the status block last; no local file is written.--source engagementfixes the shape for the engagement loop's questions (EP-32); the supervisor's paused-run pass calls it with--source run.An engagement's questions (Story 32.3) block one issue per engagement instead of a story's; the supervisor's questions pass calls both verbs on the host:
uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" block --directory <project-root> \ --engagement <ENG-NNN> [--dry-run] uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" close --directory <project-root> \ --engagement <ENG-NNN> [--dry-run]--engagementis exclusive with--story, implies--source engagementand takes no--reasonor--resolves.blockfinds the issue by its exact summaryENG-NNN: questionsinplanning.jira.project_key(GET /search/jql) or creates it (POST /issue, typeTask, labeltk-studio-engagement, the request's first line as its description), moves it toBlocked - Awaiting Userby name with the skip rules above, and comments the record's open questions:[~accountid:<owner>] Blocked: ENG-NNN needs <n> answer(s) Resolves when: reply with one "<id>: <option>" line per question <id>: <text> Impact: <impact> - <label> (default) - <label>The result is
{ok, dry_run, story: null, engagement, source: "engagement", issues: [{issue, created, transitioned, commented, skipped?}], notes[], written: [], retry?}; its refusals addEngagementNotFound,NoOpenQuestions,IssueCreateRefused(a 400 on the create, e.g. noTasktype) andEngagementLockUnwritable.close(the agent seat) moves the issue by name toBlock Resolved(unless it is there or atDone), then toDone— no validation leg: the planner's next turn judges the answers; a move already in place is named inskipped[], an absent issue isNoIssue, a move no transition makesNoTransition(an issue never blocked, e.g. atTo Do, skipsBlock Resolvedwhen no transition reaches it, named inskipped[]); the result is{ok, dry_run, engagement, issue, moved[], skipped[], notes[], written: [], retry?}. Both verbs hold_bmad-output/engagements/<id>/track.lock(exclusive create, stale after 300 s; a dry run takes none): a lock held endspartialwithretry: true, so two callers never create two issues. A re-block after an owner reply that answered nothing posts the comment again, even with the same body.Story 32.6 adds the engagement block's kind; the supervisor's engagement-run pass calls it on the host:
uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" block --directory <project-root> \ --engagement <ENG-NNN> [--kind questions|cap|ask] [--reason "<text>"] [--dry-run]--kind questions(the default) is the block above and takes no--reason.--kind cap(the engagement reached its quote's cap) and--kind ask(a story landed underaskautonomy) need--reasonand no open questions; they block the sameENG-NNN: questionsissue with the 31.5 prefix kept —[~accountid:<owner>] Blocked: ENG-NNN <the reason's first line>, thenResolves when: reply with `go {cap}` from the web or the API, or `cancel`(cap) orResolves when: reply `continue` to run the next story(ask), then the reason's other lines (the figures). The result carrieskind; more refusals by name before any call:UnknownKindandReasonMissing, and--kindis refused with--story. Payload mapping:story,reason,resolvesmap to their flags;kind(withengagement) adds--kind <kind>;sourceadds--source <source>;dry_runadds--dry-run(reads only, writes nothing).The
unblockverb (Story 36.3) closes a block that was resolved by the operator's resume, or by the owner'sgoorcontinue; the supervisor calls it on the host, with the agent seat:uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" unblock --directory <project-root> \ --story <ST-NNN | N-M-slug> --reason "<text>" [--source run] [--dry-run] uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" unblock --directory <project-root> \ --engagement <ENG-NNN> --kind cap|ask --reason "<text>" [--dry-run]An issue in
Blocked - Awaiting Usergets oneResolved: <reason>agent comment with no account mention and moves by name toBlock Resolved; any other status is named inskipped[], with no move and no comment.--storyalso moves the dependentsblockmoved (one blocked by something else is skipped).--engagementmoves its issue toBlock Resolved, neverDone. The result is{ok, dry_run, story, source, issues: [{issue, entity, role, from, to, transitioned, commented, skipped?, error?}], skipped[], notes[], written: [], retry?}, plusengagementandkindon the engagement form. Refusals endblockedasblock's do, withNoIssue,UnknownKindandReasonMissingby name; a temporary failure endspartialwithok: falseandretry: true.The
dropverb (DW-51) retires the issues a superseded plan promoted; the supervisor's approve pass calls it on the host, with the agent seat, when an engagement is re-approved afterrevisewith its earlier approve PR still open:uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" drop --directory <project-root> \ --engagement <ENG-NNN> --issue <KEY> [--issue <KEY> …] --reason "<text>" [--dry-run]Before any write, every issue not already dropped must have a transition reaching
DNI, elseNoTransitionwith nothing moved or commented. Each issue then moves by name toDNIand gets oneDropped: <reason>agent comment with no account mention; an issue already in a dropped status is not moved, and gets the comment only when its newest agent comment is not that same body, else it is named inskipped[](a retry never comments twice). The result is{ok, dry_run, engagement, issues: [{issue, transitioned, commented}], skipped[], notes[], written: [], retry?}. Refusals endblockedby name:EngagementNotFound,NoIssue,BadIssue,ReasonMissing, the agent seat's gaps,NoTransitionandJiraRefused: <key>: <error>(any other non-transient Jira error); a temporary failure endspartialwithok: falseandretry: true. It holds the engagement'strack.lockasclosedoes.The
replyverb (Story 31.6) is the owner's answer from the web's Issue screen; only the supervisor'sPOST /inbox/{key}/replycalls it, on the host:uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" reply --directory <project-root> \ --issue <KEY> (--body-file <path> | --done) [--dry-run]It posts the comment (
--donewritesDone.; the body comes from a UTF-8 file, never the command line) with the owner seat's client (TK_STUDIO_ATLASSIAN_OWNER_EMAILandTK_STUDIO_ATLASSIAN_OWNER_TOKEN, host-only — outside theTK_ATLASSIAN_prefix the sandbox receives — through theTK_ATLASSIAN_CLOUD_IDgateway, REST v2, plain text), then moves the issue by name toBlock Resolved; an issue already there skips the transition, named inskipped[]. The comment is skipped — named inskipped[], withcommented: trueand the existingcomment_id— only when an owner comment with the same body already stands after the issue's newest agent comment (read live); a reply to a later re-block always posts. The result is{ok, dry_run, issue, commented, comment_id, transitioned, notes[], skipped[], written[], retry?}with the status block last; no local file is written and nothing else is promoted. Payload mapping:issuemaps to--issue;bodyis written to a temp file passed as--body-file;done: trueadds--done;dry_runadds--dry-run. Its refusals (exit 2), each named: an absent owner variable or cloud id, or noplanning.jirabinding (no Atlassian call made — the agent's token never stands in); no recordedplanning.jira.owner_account_id; a created comment whose author is not the owner (another seat's token: nothing moved); no transition reachingBlock Resolved(the comment stays,commented: true). A temporary failure endspartialwithok: falseandretry: true.The
validateverb (Story 31.7, §6.9 items 4 and 5) checks a resolution before the story continues, with the agent seat only (never the owner's variables, neverreply):uv run "${CLAUDE_PLUGIN_ROOT}/lib/track.py" validate --directory <project-root> \ --issue <KEY> (--check | --pass --to "In Progress"|"To Do" \ [--ruling-file <path> --spec <paused spec>] \ | --fail --follow-up-file <path>) [--dry-run]--checkreads the changelog:owner_resolvedis true only when the newest transition intoBlock Resolvedcarries the recorded owner accountId (an older owner transition never validates a later one; an absent or empty id never matches). It readsreason_commentandreplies[](the owner's comments after the newest agent comment;Done.is a valid answer) from local memory only, and marks the inbox itemvalidating. The result is{ok, dry_run, issue, mode, entity, story, owner_resolved, transition, reason_comment, replies[], notes[], written[], retry?}with the status block last.--passrefuses whenowner_resolvedis false; it checks a transition reaches--tobefore writing anything, writes the ruling when given (**Ruling (owner, <KEY> comment <id>, <date>):** <text>just before the spec's</intent-contract>line, else</frozen-after-approval>; neverstatus:), then moves the issue by name to--to.--fail(whateverowner_resolvedsays) re-blocks the issue with the follow-up as the reason (a newBlocked:agent comment — back in the Inbox); a retried--failwhose move intoBlocked - Awaiting Useralready landed posts the missing comment without moving the issue again, and a retried--passwhose move into--toalready landed (newer than the owner's newest move intoBlock Resolved) writes the ruling (idempotent) and the line without moving it again. A ruling the pass wrote is taken back out when the pass then ends refused. Each outcome appends one line to_bmad-output/tracking/<KEY>/validation.jsonl(the supervisor's evidence for the resume), named inwritten[]. Refusals (exit 2), each named: a bad key; noplanning.jirabinding or an absent agent variable (no Atlassian call made); no recorded owner accountId; a live status other thanBlock Resolved; a--passwhenowner_resolvedis false; no transition reaching the target; an absent spec or block for the ruling. A temporary failure endspartialwithok: falseandretry: true, appends no line — call again.Session procedure for
payload.verb: validate(the shippedvalidate-resolutionjob; the payload is{verb, project_root, issues:[{issue, story, paused_run, spec}]}): for each payload issue, run--checkwith--directory <project_root>. Onowner_resolved: false, take--failwith a follow-up asking the owner to resolve it from the Inbox. Otherwise readreason_commentandreplies[]: when the reason names something runnable on the host — a command, a path, a sign-in — test it; else judge whether the replies are enough. Pass →--pass --to "In Progress"(--to "To Do"when the story's run never started), adding--ruling-file <a temp file holding the reply verbatim> --spec <spec>when the reply carries a ruling (the block's resolves text names "a ruling in the spec") and the payload names aspec. Fail →--fail --follow-up-file <a temp file>saying what is still missing. Onretry: true, call the same verb again. End on the status block; commit nothing. -
Blocking conditions of
block(exit 2), each named inreason:- no
planning.jirabinding (runplansync.py setupfirst), or an absent agent-seat variable (TK_ATLASSIAN_AGENT_EMAIL,TK_ATLASSIAN_AGENT_TOKEN,TK_ATLASSIAN_CLOUD_ID) — no Atlassian call made; - no recorded
planning.jira.owner_account_id— the mention needs it; an absent or empty accountId never matches; - a story with no plan entity, or never promoted (no
external.jira.key); - no transition reaching
Blocked - Awaiting Useron the story's issue, or its block comment refused by name after the transition (the entry keepstransitioned: trueand namesblock comment refused); - the 401/403 split of the adapter (the token, its email, its expiry or the org toggle; the sandbox proxy's network approval step; a missing scope).
A temporary failure (an exhausted 429, a 5xx, the site unreachable) on the story's own issue, or on a dependent's after the story's issue was blocked (the other dependents still run), is no refusal: it ends
partialwithok: falseandretry: true(exit 0), and the caller calls again on its next pass. A dependent that fails otherwise while the story's issue was blocked endspartial, naming it; a dry run that would move or comment endspartialtoo. - no
-
Report:
-
Attended: show each issue moved or skipped and the comment text.
-
Headless: no prompts (AD-11). Relay the core's
status,reasonandartifactsas they are — never infer the status from the exit code (partialexits 0):{"status": "complete", "intent": "tk-studio-track", "artifacts": [], "reason": null}
-
If the deterministic core is unrunnable — a tool call denied by permissions, uv/python unavailable — end blocked with the status block naming the unrunnable core as reason: never a question, never a headless run that ends without the block (AD-11).
Rules
- No skill other than this one and
tk-studio-plan-syncwrites to Jira, and both only throughlib/jirabackend.py(AD-4). - Only the status move and the block comment (or the owner's reply) are written; entity fields stay canonical (AD-5, D9) — the sync-back pulls the new status back.
- All paths resolve through
${CLAUDE_PLUGIN_ROOT}.
04 同仓库更多 Skills
tk-studio-session
Session discipline as a surface — land a boundary handoff (optionally carrying knowledge deltas) and end the session, or resume a fresh session from a run workspace alone. Use when the user says "hand off this run", "tk session", "resume the run", or a boundary/budget trigger says the session must split.
tk-studio-research
Run a chartered research pass — survey the topics a charter scopes, grade every finding by evidence, land the findings in the run workspace, and emit observation events for recommendation-worthy ones. Use when the user says "run the ecosystem watch", "tk research", or a research job's invoke-skill directive names this skill.
tk-studio-report
One-verb human defect report — "the skill did the wrong thing" — recorded as a report event in the local measurement ledger. Use when the user says "tk report", "report a defect", "that skill misbehaved", or "log a studio issue".
tk-studio-playable-judge
The playable-verify leg's screenshot judge (Story 34.3) — reads only a playable run's report.json and its frames, answers the fixed rubric (scene loaded, player visible and framed, no magenta or error overlay, HUD reads, the named moments shown) with pass or fail and findings each naming a frame and a rubric line. Driven headless by the supervisor's host leg on the project's sandbox tier; use when the payload names a playable `media` folder, or the user says "judge the playable frames".
tk-studio-plan-sync
Planning adapter — canonicalize BMad-native planning artifacts (normalize pass, sole id authority) and sync them with the bound planning backend. Use when the user says "sync the plan", "normalize planning artifacts", "tk plan sync", or after stock BMad planning flows produce or change epics/stories.
tk-studio-orchestrator
The studio's front door — resolve who you are (role, per-user store), what this project needs (confirmed working_set.<role>), and route to resources by name, statelessly. Use when the user says "enter the studio", "tk studio", "orchestrate", "convene the council", or asks what to work on through the studio.