tt-workflow-run
A thin, tasktracker-native orchestrator that turns an autonomous Claude Code build loop into a visible, auditable, resumable, projectable workflow_run. It is the session layer between the loop (which executes) and the tasktracker spine (project → phase → task → requirement+AC → time-log → insight).
It does not merely record the loop — it enforces the lifecycle through three backend gates that already exist (shipped in P99–P101), measures real build-time per slice, auto-logs insights, verifies architecture is followed each slice, and re-projects remaining effort from measured time only as an append-only versioned series.
Design source of truth: brainstorm
92898896→ "Solution Description (v1)" docba161e43-7697-4167-9fc7-f314ca68c3af. The four forks are LOCKED (A: new entity; B: loose coupling; C: lean v1 dogfooded, UI v2; D: getTimeLog preview folded in). Re-read it before changing this skill.
How this differs from /tt-implement-plan
| | /tt-implement-plan | /tt-workflow-run (this skill) |
|---|---|---|
| Unit driven | the plan's phase tasks, in author order | a first-class workflow_run over the next-ready backlog |
| Slice selection | next pending phase | tasktracker_getNextReadyTask(projectId) — dependency-free, ranked |
| Pre-conditions | readiness plan row satisfied | Gate 1 (lifecycle completeness) + Gate 2 (zero-defects-in), enforced server-side as 412 |
| Per-slice gates | none | Gate 3 architecture-followed (drift hash-diff) + Gate 2 defect-delta, every slice |
| Time / projection | none | getTimeSummary measured ms → workflow_recordIteration appends a by-layer projection version |
| Run entity | none | workflow_run row with status machine + projection series; resumable across sessions |
| Insight logging | per-phase, via /tt-implement-phase | per-phase plus the run's OWN insights (no-data friction, patterns, new-defect) |
If you just want to execute phases in order with no measured run, use /tt-implement-plan. If you want the run entity + measured projection + per-slice gates on top, use this skill. Both delegate the actual code work to /tt-implement-phase.
CRITICAL: Orchestrator pattern (kept from /tt-implement-plan)
THIS SESSION IS THE RUN ORCHESTRATOR + the SINGLE writer of run state. IT NEVER IMPLEMENTS CODE DIRECTLY — it delegates each slice to
/tt-implement-phase. That holds in every environment. One level down,/tt-implement-phasewrites code via subagents when a subagent-dispatch tool exists, or in-context itself when none does (its graceful-degradation mode). Run-state writes (workflow_* MCP calls) are always yours and always main-context, regardless of mode.
tt-workflow-run (this session — RUN ORCHESTRATOR + the SINGLE writer of run state)
│ ⛔ NEVER writes code ⛔ NEVER uses Write/Edit (in EVERY mode)
└── /tt-implement-phase (per-slice executor)
├── orchestrated mode → Subagents write code / create files / run tests
└── in-context mode → tt-implement-phase does it directly (no subagent tool)
| DO (this session) | DO NOT |
|---|---|
| Create + drive the workflow_run (start/record/pause/complete) | Write code or edit files |
| setActiveTask / pauseActiveTask / clearActiveTask | Edit a locked phase body (HTTP 422) |
| Pick the next slice via getNextReadyTask | Invent a new backend Workflow API (none exists — Fork B) |
| Delegate each slice to /tt-implement-phase | Estimate a slice's duration (principle #11) |
| Run the two start gates + three per-slice gates | Advance past a blocked gate without resolving it |
| Log the run's own defects/learnings/frictions | Bury insights in chat narrative |
This skill is main-loop-only: it calls sibling skills via the Skill tool (which stays in the main loop), never via an Agent dispatch. The tasktracker_workflow_* MCP tools are module-cached at MCP boot — if one isn't visible, the MCP needs a restart (mcp-server/CLAUDE.md), not a workaround. This restart caveat applies ONLY to those MCP tools. The built-in Workflow and Agent tools are top-level main-loop tools — never MCP, never in ToolSearch, never affected by an MCP restart; don't conflate the two.
The MCP surface this skill uses (all already shipped, P100/P101)
Exact tool names — note the tasktracker_ prefix + workflow_ infix:
| Tool | Role in the loop | Active-task |
|---|---|---|
| tasktracker_workflow_startRun | Create + start a run; runs Gate 1 + Gate 2; 412 on failure (run persists as enforcing). acknowledgeBaseline: true pins the current open-defect count as the run baseline (bootstrap fallback only). | EXEMPT (first-contact, like createProject) |
| tasktracker_getNextReadyTask | Pick the next dependency-free slice. null ⇒ backlog drained ⇒ complete. | EXEMPT (read) |
| tasktracker_workflow_setCurrentTask | Point the run at the picked slice (no iteration recorded). | requires active task (heartbeats) |
| tasktracker_scanArchitectureDrift | Baseline (slice start) + re-scan (slice end). Returns bucketed missing / stale / orphaned. | EXEMPT (read) |
| tasktracker_getDefectStats | Re-read .open for the Gate 2 delta vs baseline. | EXEMPT (read) |
| tasktracker_getTimeSummary | Measured (stopped-segment-only) ms for the slice task. | EXEMPT (read) |
| tasktracker_workflow_recordIteration | Runs the advance gates (Gate 3 reqs-link + architecture-followed, Gate 2 defect-delta), records the iteration, appends a projection version, and auto-logs the "no data" friction when hadHeartbeatData: false. 412 on a blocked advance (run → blocked_on_gate). | requires active task (heartbeats) |
| tasktracker_workflow_pauseRun / resumeRun | Pause before a human gate / resume after. | requires active task |
| tasktracker_workflow_completeRun / abandonRun | Terminal. | requires active task |
| tasktracker_workflow_getRun / listRuns | Inspect run + projection series. | EXEMPT (read) |
recordIteration key inputs (get these right — they drive the gates + projection):
runId,taskId,layer(required) — layer ∈backend|frontend|fullstack|docs|infra(project-estimate.py taxonomy).measuredMs— fromgetTimeSummary(bigint → pass as a string for large values; defaults to 0).measurementSource—'timer'(summed stopped segments) or'wallclock'(manual fallback).hadHeartbeatData— false when the slice produced no heartbeat-backed time-log data → triggers the run's own "no data" friction (R3 / principle #11).baselineDrift/drift— the slice-START and slice-ENDscanArchitectureDriftoutputs as{missing:[file...], stale:["componentId:file"...], orphaned:["componentId:file"...]}key arrays. Net-new at end vs baseline blocks the advance + logs a defect.remainingByLayer— per-layer count of the run's remaining pending dependency-free slices: the projection multiplier (projected = Σ_layer remaining×mean). Omit the whole object on the final slice and the projection trends to ~0.projectionTrigger— defaults'slice_complete'; use'scope_change'/'defect_baseline_shift'for re-projections not driven by a finished slice.
Workflow
Step 0 — Locate the project + pre-flight the gates (this session, inline)
1. tasktracker_listProjects({search}) — or use the argument hint → projectId.
2. tasktracker_getProject({projectId}) — confirm solutionDescription is non-null (Gate 1 input).
3. tasktracker_getProjectReadiness({projectId}) — the requirements row must read `satisfied`
and unlinkedTaskCount must be 0 for the run's subtree (Gate 1).
4. tasktracker_getDefectStats({projectId}) — note `.open`. This is the Gate 2 input AND the run baseline.
Do not try to pre-satisfy a gate by editing data behind the run's back. If Gate 1 will fail (no solution description, requirements not satisfied, unlinked tasks), STOP — the project needs /tt-create-plan / requirement linking first. If Gate 2 will fail (open defects), see the Bootstrap nuance below — the strict posture refuses to start; the documented fallback is acknowledgeBaseline.
Step 1 — Start the run (Gates 1 + 2)
tasktracker_workflow_startRun({
projectId,
name: "<run name>",
acknowledgeBaseline: <true ONLY for the documented bootstrap case — see below>,
brainstormId?: <originating brainstorm>,
sessionId?: <current session>,
})
- Strict (default): with zero open defects + a passing lifecycle, the run transitions to
runningwithbaselineDefectCount = 0. Strict zero-defects-in now holds for the whole run. - 412: the start gate failed. The run row persists in status
enforcingso you can inspect the structured body (unlinked task ids / open-defect count). Fix the cause (link tasks, resolve defects, or acknowledge the baseline) and retry — do not route around the gate.
startRun is active-task-EXEMPT (it CREATES the run). Right after a successful start, the first setActiveTask (Step 2) begins heartbeating.
Bootstrap nuance (read once — this is the inaugural-run case)
A project that already has an open defect cannot pass strict Gate 2 — but that open defect is often exactly the first thing the loop should fix. The documented bootstrap path:
- Start with
acknowledgeBaseline: true. The run pinsbaselineDefectCount = N(the current open count). Only NEW defects above N block advance; the N pre-existing ones don't. - Make the inaugural slice a fix-task for one of those baseline defects (
tasktracker_createFixTask({defectInsightId})— stampsmetadata.fixesInsightIdso the fix-time rolls intogetDefectStats.timeOnDefects). Running it resolves the defect, dropping.openback toward 0. - After the baseline defects are cleared, strict zero-defects-in holds for every subsequent slice — a new defect above the (now lower) baseline blocks advance and is attributed as "ours" (principle #14: pre-existing is not a thing).
acknowledgeBaseline is a one-time bootstrap concession, not a routine knob. Once the backlog is clean, future runs start strict (baseline 0).
Step 2 — The loop (per-slice protocol)
Loop while getNextReadyTask returns a slice. Each iteration is the brainstorm's per-slice protocol, one step per call:
loop:
slice = tasktracker_getNextReadyTask({projectId})
if slice == null: break # backlog drained → Step 3
# 2a. Focus + point the run at the slice
tasktracker_setActiveTask(slice.id) # heartbeats start; digest carries principles + body
tasktracker_workflow_setCurrentTask({runId, taskId: slice.id})
# 2b. Pre-slice drift baseline (Gate 3 input — baseline-subtracted so only the slice's delta is charged)
baselineDrift = tasktracker_scanArchitectureDrift({projectId}) # capture missing/stale/orphaned key arrays
# 2c. Delegate the actual work to /tt-implement-phase
Skill(skill="tt-implement-phase"): Execute the slice.
Context: project, phase/slice task id = slice.id, sub-tasks (getTask), linked requirements + ACs,
principles from the setActiveTask digest.
/tt-implement-phase: does the code (via subagents), registers the architecture delta in the SAME
slice (principle #8 — so the slice's own drift re-scan stays clean), runs verification-loop +
code-review + ADR compliance, and auto-logs defects/learnings/frictions with relatedTaskId = slice.id.
# 2d. Slice-end gate inputs
endDrift = tasktracker_scanArchitectureDrift({projectId}) # Gate 3 delta
defects = tasktracker_getDefectStats({projectId}) # Gate 2 delta (.open vs run baseline)
time = tasktracker_getTimeSummary({taskId: slice.id}) # measured (stopped-only) ms
layer = classify(slice) # backend|frontend|fullstack|docs|infra
# 2e. Record the iteration → appends a projection version (runs the advance gates server-side)
tasktracker_workflow_recordIteration({
runId, taskId: slice.id, layer,
measuredMs: time.totalMs (as string if large),
measurementSource: time had data ? 'timer' : 'wallclock',
hadHeartbeatData: <true iff getTimeSummary returned measured data>,
baselineDrift, drift: endDrift,
remainingByLayer: <per-layer count of remaining pending dependency-free slices>, # omit on last slice
})
# 412 here ⇒ the advance was BLOCKED (Gate 3 net-new drift / missing req link, or Gate 2 new defect).
# The run is now blocked_on_gate. Resolve the cause (re-register the arch delta, link the requirement,
# or fix the new defect) within /tt-implement-phase, then re-record. Do NOT skip the slice.
# 2f. Pause before any human gate
tasktracker_pauseActiveTask() # before any message that waits on the human (their think-time isn't work)
The run's OWN insights (R3 — log these, don't narrate them)
/tt-implement-phase logs the slice's insights. ON TOP of that, this skill logs the run's own:
| Condition | Tool | Notes |
|---|---|---|
| The slice produced no heartbeat-backed time data | recordIteration({hadHeartbeatData:false}) auto-logs it, OR tasktracker_logFriction | "no data" — never estimate the duration (principle #11). The auto-log stamps run_id + iteration_id. |
| A notable cross-slice pattern emerged | tasktracker_logLearning | E.g., "drift baseline-subtraction means an unrelated stale component never blocks an unrelated slice." |
| .open rose above the run baseline | tasktracker_logDefect | The new error is "ours" (principle #14). recordIteration also blocks the advance. |
Per R3, auto-logged slice insights carry relatedTaskId = slice.id AND their metadata is stamped with run_id + iteration_id (no new column). When you log the run's own insight manually, set relatedTaskId to the slice and reference the run/iteration in the body.
Classifying the slice layer
Map the slice to the project-estimate.py taxonomy: frontend (Angular templates/components/styles), backend (NestJS services/controllers/entities), fullstack (both in one slice), docs (markdown/skill files), infra (CI, compose, migrations-only). When a slice touches both backend + frontend, classify fullstack (don't split the iteration).
Step 3 — Complete the run
When getNextReadyTask returns null:
tasktracker_workflow_completeRun({runId}) # terminal; sets ended_at
tasktracker_workflow_getRun({runId}) # confirm the projection series is populated
tasktracker_getDefectStats({projectId}) # confirm .open == baseline (no nets introduced)
tasktracker_clearActiveTask()
Then emit the closing summary (counts, the projection series, insights logged this run, manual verification still owed) in chat — not as a file. If the run ended deliberately short, use abandonRun instead and say why.
Optional outer cadence — /loop
Loose coupling (Fork B) means /loop is orthogonal: you can wrap the whole skill in /loop <interval> /tt-workflow-run <project> for an unattended cadence, but the run does not depend on it. The run entity + gates + projection are pure tasktracker data; /loop just re-invokes the skill, which resumes the same run (the active-task pointer + run row persist across sessions).
Handling a blocked gate
A 412 is the system working, not a failure to route around.
| Where | Cause | Resolution |
|---|---|---|
| startRun 412 | Gate 1: no solution description / requirements not satisfied / unlinked tasks | Fix the lifecycle (/tt-create-plan, link requirements), then retry. |
| startRun 412 | Gate 2: open defects, no ack | Resolve them, OR start with acknowledgeBaseline: true and clear them as inaugural slices. |
| recordIteration 412 | Gate 3: net-new drift (a new file no component cites; an edited component whose driftHash wasn't refreshed) | Register/refresh the architecture component in-slice (principle #8), then re-record. |
| recordIteration 412 | Gate 3: the slice task has no requirement link in its hierarchy | Link it to a requirement, then re-record. |
| recordIteration 412 | Gate 2: .open rose above baseline | The new defect is ours — fix it (a follow-up slice), then re-record. |
On any 412 the run sits in enforcing (start) or blocked_on_gate (advance). pauseActiveTask before surfacing the blocker to the user; resume from the blocked step, not from Step 0.
Anti-patterns to avoid
- ❌ Writing code in this session. Delegate every slice to
/tt-implement-phase. - ❌ Inventing a backend Workflow API. Fork B is loose coupling — the loop is a skill over existing MCP tools only.
- ❌ Estimating a slice's duration when heartbeating was off. The answer is "no data" → friction (principle #11). Never synthesize a
measuredMs. - ❌ Skipping
setActiveTaskbefore a slice — no heartbeats, no measured time, the projection goes blind. - ❌ Routing around a gate (editing data to dodge a 412, or skipping a blocked slice). Resolve the cause.
- ❌ Editing a locked phase/slice body. Design notes go to a sub-task; the body is the contract.
- ❌
acknowledgeBaseline: trueas a routine convenience. It is a one-time bootstrap concession; once clean, start strict. - ❌ Re-deriving scope from the brainstorm mid-run. The slice's task body (from
setActiveTask) is the locked contract. - ❌ Burying the run's own defects/learnings/frictions in chat. Log them via
logDefect/logLearning/logFriction. - ❌ Running this as an
Agent-dispatched subagent — invoke it in the main loop; call siblings viaSkill.
Quality checklist
- [ ] Step 0: project located; Gate 1 inputs (solution description, requirements satisfied, zero unlinked) checked; Gate 2 baseline (
.open) noted. - [ ] Step 1:
startRunran the gates; strict by default;acknowledgeBaselineused ONLY for the documented bootstrap case, with the inaugural slice resolving a baseline defect. - [ ] Step 2: every slice did
setActiveTask+setCurrentTask, captured a pre-slice drift baseline, delegated to/tt-implement-phase, re-scanned drift + re-read defect stats, readgetTimeSummary, andrecordIteration'd with the right layer + drift arrays +remainingByLayer. - [ ] R3: slice insights carry
relatedTaskId+ run_id/iteration_id; the run logged its OWN "no data" friction whenever heartbeating was off. - [ ]
pauseActiveTaskbefore every human-gate message. - [ ] No locked-body edits; no invented backend API; no synthesized durations.
- [ ] Step 3:
completeRun; projection series populated;.openback at baseline;clearActiveTask. - [ ] No code written in this session (Write/Edit unused); all slice code came from
/tt-implement-phase(via its subagents, or its in-context mode when no subagent tool exists).
Resources
This skill reuses the tasktracker references already shipped with the sibling skills:
/tt-implement-phasereferences/insight-cookbook.md— when/how tologDefect/logLearning/logFriction./tt-workflow-auditreferences/workflow-tasktracker-contract.md— the sharedtt-workflow-*contract (parent owns writes, no Date/RNG in any workflow script, MCP reachability, prod safety). This skill is sequential (single active task) — it does NOT use the parallelWorkflowtool — but the active-task / locked-body / prod-write rules in that contract still apply. Namespace note: thetasktracker_workflow_*MCP tools this skill uses (workflow_startRun,workflow_recordIteration, …) are run-TRACKING tools — NOT the built-inWorkfloworchestration tool; they only share the word "workflow". The siblings/tt-workflow-auditand/tt-workflow-buildare the ones that invoke the built-inWorkflowtool.
Related skills
/tt-implement-phase— per-slice executor. This skill delegates every slice to it./tt-implement-plan— the no-run-entity alternative: executes phases in order without projection or per-slice gates./tt-create-plan— upstream: produces the requirements + linked tasks Gate 1 checks for./loop— optional outer cadence; orthogonal to the run./code-review,/verification-loop,/adr— quality gates inside/tt-implement-phase.
Key principles
- Orchestrate, never implement.
/tt-implement-phase(→ subagents) does the code; this session drives the run. - Loose coupling (Fork B). The run is pure tasktracker data over existing MCP tools; no backend Workflow API;
/loopoptional. - Enforce, don't record. The three gates (lifecycle, zero-defects-in, architecture-followed) are 412s, not advisories.
- Measured time only (principle #11). Projection consumes
getTimeSummary(stopped segments); no heartbeat data ⇒ "no data" friction, never an estimate. - Zero-error invariant (principle #14). A defect above baseline is ours and blocks advance.
acknowledgeBaselineis a one-time bootstrap. - Active-task discipline.
setActiveTaskper slice,pauseActiveTaskbefore human waits,clearActiveTaskon completion. - The run logs its own insights. Friction / learning / defect for the loop itself — in tasktracker, not chat.
- Locked bodies are sacred. Slice scope is the contract; notes go to sub-tasks.