Skip to content

Commit 52c5b35

Browse files
jpicklykclaude
andauthored
feat(workflow): config-driven status labels with query response support (#71)
* feat: add session retrospective skill and run manifest tracking Introduces structured post-implementation analysis — a /session-retrospective skill that evaluates schema effectiveness, delegation alignment, note quality, plan-to-execution fit, and friction across five dimensions. Includes run manifest instructions in the shared orchestrator output style for durable session telemetry, and a retrospective nudge after implementation completions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: improve session-retrospective skill robustness Address issues found during skill review: remove hardcoded absolute paths, add early exit for empty sessions, expand trigger description, simplify terminal advancement, and add recency filter to fallback mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: upgrade MCP Kotlin SDK from 0.8.4 to 0.9.0 Co-Authored-By: Claude <noreply@anthropic.com> * feat: adopt ClientConnection and kotlin-sdk-testing from MCP SDK 0.9.0 Co-Authored-By: Claude <noreply@anthropic.com> * fix: enforce note schema gates on cascade-to-TERMINAL transitions Cascade auto-completion previously bypassed gate enforcement, allowing parent items with schema tags to reach TERMINAL with required notes unfilled. Now cascade-to-TERMINAL checks all required notes (matching the "complete" trigger behavior). Schema-free parents cascade freely. Also extracts buildMissingNotesArray() to NoteSchemaJsonHelpers and replaces inline filledKeys computation with buildFilledKeys() across all three gate-check paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: document local-first squash flow in implement skill and CLAUDE.md Update the /implement skill to reflect the local-first git workflow: branches stay local, squash-merge into local main, and PRs are batched. Align CLAUDE.md git workflow section to match. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: schema improvement — role validation, AdvanceItemTool consolidation - Add VALID_SCHEMA_ROLES validation in YamlNoteSchemaService.parseEntry() to catch typos and invalid role values at config load time (warns + skips) - Consolidate AdvanceItemTool response fields to use shared computePhaseNoteContext() instead of manual NoteSchemaJsonHelpers calls - Remove findGuidancePointer() and buildNoteProgress() from NoteSchemaJsonHelpers (now gate-check helpers only) - Add plugin-change schema to config.yaml (gitignored, local only) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(test): create shared test infrastructure — TestBase, fixtures, and helpers Add reusable test utilities under current/src/test/.../test/: - TestFixtures.kt: makeItem(), blocksDep(), makeNote(), JSON param helpers, response extractors - BaseRepositoryTest.kt: H2 in-memory DB base class with createPersistedItem/Note/Dependency - MockRepositoryProvider.kt: MockK-based RepositoryProvider factory with context() builder - TestNoteSchemaService.kt: In-memory NoteSchemaService with FEATURE_IMPLEMENTATION/BUG_FIX presets - TestInfrastructureTest.kt: 27 validation tests covering all shared infrastructure Migrated SQLiteWorkItemRepositoryTest to extend BaseRepositoryTest as proof of concept. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: broaden retrospective nudge to cover all terminal transition paths The retrospective nudge previously only fired after complete_tree, missing single-item runs that reach terminal via advance_item. Updated the nudge condition to trigger on any terminal transition during an /implement run, with single-item, multi-item, and fallback detection rules. Also: minor AdvanceItemTool optimization — derive existingKeys from notesByKey instead of a separate .map() pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace session manifest with distributed session-tracking notes The monolithic run manifest (stored as a CC session task) had a 100% creation failure rate. Replace it with distributed `session-tracking` notes on each work item, enforced by schema gates. - Add default schema fallback to YamlNoteSchemaService (one-line change) - Add session-tracking note to feature-implementation, bug-fix, and plugin-change schemas in config.yaml - Create new `default` schema as catch-all for untagged items - Rewrite session-retrospective skill to aggregate distributed notes - Remove manifest section from workflow-orchestrator output style - Add lightweight delegation-metadata pattern for orchestrator-side data - Simplify retrospective nudge (remove manifest references) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add feature-task schema for lighter child work item gates Split feature work into two schema tiers: feature-implementation for the parent container (full spec, holistic review with /simplify) and feature-task for child work items (task-scope, task-level review). Plugin skills updated to remain generic — they reference config.yaml for schema discovery rather than hardcoding specific tag names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(workflow): add config-driven status labels and surface in query responses Add StatusLabelService for trigger-to-label auto-mapping on role transitions. Labels are configured in .taskorchestrator/config.yaml (status_labels section) with hardcoded defaults as fallback. Label precedence: hardcoded resolution (cancel/reopen) > config-driven > null. Changes: - NEW: StatusLabelService interface + NoOpStatusLabelService defaults - NEW: YamlStatusLabelService reads config with AGENT_CONFIG_DIR support - Wire label resolution into AdvanceItemTool, CompleteTreeTool (including cascade) - Surface statusLabel in QueryItemsTool search, overview, and get responses - Add statusLabel to toMinimalJson and toFullJson serializers - 23 new tests: YamlStatusLabelService (11), AdvanceItemTool (6), CompleteTreeTool (2), QueryItemsTool (4) — covering config-driven labels, cascade labels, label precedence, block/resume, and query responses - Update workflow-guide.md and api-reference.md documentation - Enforce explicit model parameter in delegation table (plugin/skill update) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 77e0d3e commit 52c5b35

File tree

18 files changed

+805
-20
lines changed

18 files changed

+805
-20
lines changed

.claude/skills/implement/SKILL.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ UUID inclusion). The key decisions at this step are:
126126
- **Multiple child tasks, dependent:** dispatch sequentially — wait for each agent
127127
to return before dispatching the next.
128128

129+
**Model selection — always set `model` explicitly on every Agent dispatch:**
130+
131+
| Agent purpose | Model |
132+
|--------------|-------|
133+
| Implementation, code changes, test writing | `model="sonnet"` |
134+
| Architecture, complex multi-file synthesis | `model="opus"` |
135+
| MCP bulk ops, materialization | `model="haiku"` |
136+
137+
Omitting `model` causes the agent to inherit the orchestrator's model (typically
138+
opus), wasting tokens on sonnet-eligible implementation work.
139+
129140
**After implementation agents return:**
130141

131142
If agents used worktree isolation, the Agent tool result includes the **worktree

claude-plugins/task-orchestrator/output-styles/workflow-orchestrator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ If `get_context` returns no `noteSchema` for a tagged item, schemas may not be c
3333
| Code reading, implementation, test writing | `sonnet` |
3434
| Architecture, complex tradeoffs, multi-file synthesis | `opus` |
3535

36-
Set via the `model` parameter on the Agent tool. Default inherits orchestrator model — always override for haiku-eligible work.
36+
Set via the `model` parameter on the Agent tool. Default inherits orchestrator model — **always set `model` explicitly** on every Agent dispatch. Omitting it causes sonnet-eligible work to run on opus (wasting tokens) or opus-eligible work to run on a weaker model.
3737

3838
**Rule: Never make 3+ MCP write calls in a single turn.** Parallelized reads (e.g., `get_context` + `query_items overview`) are fine and encouraged. Use the Agent tool with `model: "haiku"` to delegate bulk MCP write work (multiple item/dependency/note creates) and keep the orchestrator context clean.
3939

current/docs/api-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ hierarchical overview.
212212
}
213213
```
214214

215-
Search returns minimal fields (`id`, `parentId`, `title`, `role`, `priority`, `depth`, `tags`).
215+
Search returns minimal fields (`id`, `parentId`, `title`, `role`, `statusLabel`, `priority`, `depth`, `tags`). `statusLabel` is only present when non-null.
216216
Use `get` for full item JSON including `description`, `summary`, `statusLabel`, timestamps, and
217217
`roleChangedAt`.
218218

@@ -228,7 +228,7 @@ Use `get` for full item JSON including `description`, `summary`, `statusLabel`,
228228
}
229229
```
230230

231-
Scoped overview returns the full item JSON in `item`, a count per role in `childCounts`, and a minimal JSON list of direct children in `children`. The global overview (no `itemId`) returns a flat `items` array of root items each with `id`, `title`, `role`, `priority`, and `childCounts`. In global mode `total` reflects the count of root items returned (not a total-in-DB count).
231+
Scoped overview returns the full item JSON in `item`, a count per role in `childCounts`, and a minimal JSON list of direct children in `children` (each child includes `statusLabel` when non-null). The global overview (no `itemId`) returns a flat `items` array of root items each with `id`, `title`, `role`, `statusLabel` (when non-null), `priority`, and `childCounts`. When `includeChildren` is true, each child object also includes `statusLabel` when non-null. In global mode `total` reflects the count of root items returned (not a total-in-DB count).
232232

233233
---
234234

current/docs/workflow-guide.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,53 @@ Use manage_notes(operation="upsert") with itemId="abc-123", key="design", role="
466466

467467
---
468468

469-
## 6. Dependency Blocking
469+
## 6. Status Labels
470+
471+
Status labels are human-readable strings automatically set on WorkItems during role transitions. They provide a display-friendly status name alongside the semantic role.
472+
473+
### Default Labels
474+
475+
| Trigger | Status Label | Description |
476+
|------------|------------------|-------------------------------------------------|
477+
| `start` | `"in-progress"` | Item has begun active work. |
478+
| `complete` | `"done"` | Item finished successfully. |
479+
| `block` | `"blocked"` | Item is paused due to a dependency or hold. |
480+
| `cancel` | `"cancelled"` | Item was explicitly cancelled. |
481+
| `cascade` | `"done"` | Item auto-completed via cascade from children. |
482+
| `resume` | *(null)* | Preserves the label from before the block. |
483+
| `reopen` | *(null/cleared)* | Clears the label when reopening a terminal item.|
484+
485+
### Label Precedence
486+
487+
1. **Resolution label** — hardcoded for `cancel` ("cancelled") and `reopen` (null/cleared). Always wins when non-null.
488+
2. **Config-driven label** — resolved from `StatusLabelService` for the trigger. Used when the resolution label is null.
489+
3. **Resume behavior**`applyTransition` preserves the pre-block label automatically.
490+
491+
### Customizing Labels
492+
493+
Override default labels in `.taskorchestrator/config.yaml`:
494+
495+
```yaml
496+
status_labels:
497+
start: "working"
498+
complete: "finished"
499+
block: "on-hold"
500+
cancel: "abandoned"
501+
cascade: "auto-completed"
502+
```
503+
504+
Triggers not listed in the config get no label override (null). If no `status_labels` section exists, the hardcoded defaults above are used.
505+
506+
### Where Labels Appear
507+
508+
- **`advance_item` response** — each successful result includes `"statusLabel"` when set.
509+
- **`complete_tree` response** — each completed/cancelled item includes `"statusLabel"` when set.
510+
- **Cascade events** — cascade results in both tools include `"statusLabel"` when set.
511+
- **`query_items` responses** — the `statusLabel` field is included in item JSON when non-null.
512+
513+
---
514+
515+
## 7. Dependency Blocking
470516

471517
### BLOCKS Edges
472518

@@ -573,7 +619,7 @@ Items in `unblockedItems` are now eligible to be started.
573619

574620
---
575621

576-
## 7. Efficient Queries
622+
## 8. Efficient Queries
577623

578624
### Role-Based Filtering
579625

@@ -649,7 +695,7 @@ get_next_item(parentId="feature-uuid")
649695

650696
---
651697

652-
## 8. Config Reference
698+
## 9. Config Reference
653699

654700
The full schema for `.taskorchestrator/config.yaml`:
655701

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.github.jpicklyk.mcptask.current.application.service
2+
3+
/**
4+
* Provides trigger-to-label mappings for status labels on role transitions.
5+
*
6+
* Labels are resolved from `.taskorchestrator/config.yaml` under the `status_labels` section.
7+
* When no config is present, hardcoded defaults are used:
8+
* - start -> "in-progress"
9+
* - complete -> "done"
10+
* - block -> "blocked"
11+
* - cancel -> "cancelled"
12+
* - cascade -> "done"
13+
* - resume -> null (preserves pre-block label)
14+
* - reopen -> null (clears label)
15+
*
16+
* Label precedence in AdvanceItemTool:
17+
* 1. resolution.statusLabel (hardcoded cancel/reopen) — if non-null, use it
18+
* 2. Config-driven label from resolveLabel(trigger) — if resolution was null
19+
* 3. For resume: existing applyTransition logic preserves pre-block label
20+
*/
21+
interface StatusLabelService {
22+
23+
/**
24+
* Returns the status label for the given trigger, or null if the trigger
25+
* should not set a label (e.g., resume, reopen).
26+
*/
27+
fun resolveLabel(trigger: String): String?
28+
}
29+
30+
/**
31+
* No-op implementation that returns hardcoded defaults.
32+
* Used when no config file is present or as a fallback.
33+
*/
34+
object NoOpStatusLabelService : StatusLabelService {
35+
private val defaults = mapOf(
36+
"start" to "in-progress",
37+
"complete" to "done",
38+
"block" to "blocked",
39+
"cancel" to "cancelled",
40+
"cascade" to "done"
41+
// resume and reopen intentionally absent — null means no label override
42+
)
43+
44+
override fun resolveLabel(trigger: String): String? = defaults[trigger]
45+
}

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/tools/EntityJsonSerializers.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fun WorkItem.toMinimalJson(): JsonObject = buildJsonObject {
5353
parentId?.let { put("parentId", JsonPrimitive(it.toString())) }
5454
put("title", JsonPrimitive(title))
5555
put("role", JsonPrimitive(role.toJsonString()))
56+
statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
5657
put("priority", JsonPrimitive(priority.toJsonString()))
5758
put("depth", JsonPrimitive(depth))
5859
tags?.let { put("tags", JsonPrimitive(it)) }

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/tools/ToolExecutionContext.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package io.github.jpicklyk.mcptask.current.application.tools
22

33
import io.github.jpicklyk.mcptask.current.application.service.NoteSchemaService
44
import io.github.jpicklyk.mcptask.current.application.service.NoOpNoteSchemaService
5+
import io.github.jpicklyk.mcptask.current.application.service.NoOpStatusLabelService
6+
import io.github.jpicklyk.mcptask.current.application.service.StatusLabelService
57
import io.github.jpicklyk.mcptask.current.application.service.WorkTreeExecutor
68
import io.github.jpicklyk.mcptask.current.domain.repository.DependencyRepository
79
import io.github.jpicklyk.mcptask.current.domain.repository.NoteRepository
@@ -22,7 +24,8 @@ import io.github.jpicklyk.mcptask.current.infrastructure.repository.RepositoryPr
2224
*/
2325
class ToolExecutionContext(
2426
val repositoryProvider: RepositoryProvider,
25-
private val noteSchemaService: NoteSchemaService = NoOpNoteSchemaService
27+
private val noteSchemaService: NoteSchemaService = NoOpNoteSchemaService,
28+
private val statusLabelService: StatusLabelService = NoOpStatusLabelService
2629
) {
2730

2831
/** Access to WorkItem CRUD and query operations. */
@@ -40,6 +43,9 @@ class ToolExecutionContext(
4043
/** Access to Note schema configuration service. */
4144
fun noteSchemaService(): NoteSchemaService = noteSchemaService
4245

46+
/** Access to the status label configuration service. */
47+
fun statusLabelService(): StatusLabelService = statusLabelService
48+
4349
/** Access to the atomic work-tree creation executor. */
4450
fun workTreeExecutor(): WorkTreeExecutor = repositoryProvider.workTreeExecutor()
4551
}

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/tools/compound/CompleteTreeTool.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,11 @@ Complete or cancel all descendants of a root item (or an explicit list of items)
283283
continue
284284
}
285285

286-
// Apply transition
286+
// Apply transition with config-driven status label
287+
val configLabel = context.statusLabelService().resolveLabel(trigger)
288+
val effectiveLabel = resolution.statusLabel ?: configLabel
287289
val applyResult = handler.applyTransition(
288-
item, resolution.targetRole, trigger, null, resolution.statusLabel,
290+
item, resolution.targetRole, trigger, null, effectiveLabel,
289291
context.workItemRepository(),
290292
context.roleTransitionRepository()
291293
)
@@ -310,6 +312,7 @@ Complete or cancel all descendants of a root item (or an explicit list of items)
310312
put("title", JsonPrimitive(item.title))
311313
put("applied", JsonPrimitive(true))
312314
put("trigger", JsonPrimitive(trigger))
315+
applyResult.item?.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
313316
})
314317
}
315318

@@ -371,8 +374,10 @@ Complete or cancel all descendants of a root item (or an explicit list of items)
371374
return
372375
}
373376

377+
val configLabel = context.statusLabelService().resolveLabel(trigger)
378+
val effectiveLabel = resolution.statusLabel ?: configLabel
374379
val applyResult = handler.applyTransition(
375-
item, resolution.targetRole, trigger, null, resolution.statusLabel,
380+
item, resolution.targetRole, trigger, null, effectiveLabel,
376381
context.workItemRepository(),
377382
context.roleTransitionRepository()
378383
)
@@ -395,6 +400,7 @@ Complete or cancel all descendants of a root item (or an explicit list of items)
395400
put("title", JsonPrimitive(item.title))
396401
put("applied", JsonPrimitive(true))
397402
put("trigger", JsonPrimitive(trigger))
403+
applyResult.item?.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
398404
})
399405
}
400406

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/tools/items/QueryItemsTool.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ Operations: get, search, overview
518518
put("id", JsonPrimitive(item.id.toString()))
519519
put("title", JsonPrimitive(item.title))
520520
put("role", JsonPrimitive(item.role.toJsonString()))
521+
item.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
521522
put("priority", JsonPrimitive(item.priority.toJsonString()))
522523
put("childCounts", roleCountToJson(childCounts))
523524
if (includeChildren) {
@@ -530,6 +531,7 @@ Operations: get, search, overview
530531
put("id", JsonPrimitive(child.id.toString()))
531532
put("title", JsonPrimitive(child.title))
532533
put("role", JsonPrimitive(child.role.toJsonString()))
534+
child.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
533535
put("depth", JsonPrimitive(child.depth))
534536
}
535537
}))

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/tools/workflow/AdvanceItemTool.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
162162
// Phase 1: Resolve — schema-driven review phase detection
163163
val hasReviewPhase = noteSchemaService.hasReviewPhase(itemTags)
164164
val resolution = handler.resolveTransition(item, trigger, hasReviewPhase)
165+
val configLabel = context.statusLabelService().resolveLabel(trigger)
165166
if (!resolution.success || resolution.targetRole == null) {
166167
failCount++
167168
resultsList.add(buildErrorResult(itemId, trigger, resolution.error ?: "Failed to resolve transition"))
@@ -249,8 +250,9 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
249250
}
250251

251252
// Phase 3: Apply
253+
val effectiveLabel = resolution.statusLabel ?: configLabel
252254
val applyResult = handler.applyTransition(
253-
item, targetRole, trigger, summary, resolution.statusLabel,
255+
item, targetRole, trigger, summary, effectiveLabel,
254256
context.workItemRepository(),
255257
context.roleTransitionRepository()
256258
)
@@ -316,7 +318,8 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
316318

317319
val cascadeApply = handler.applyTransition(
318320
parentItem, event.targetRole, "cascade",
319-
"Auto-cascaded from child completion", null,
321+
"Auto-cascaded from child completion",
322+
context.statusLabelService().resolveLabel("cascade"),
320323
context.workItemRepository(),
321324
context.roleTransitionRepository()
322325
)
@@ -327,6 +330,7 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
327330
put("previousRole", JsonPrimitive(event.currentRole.toJsonString()))
328331
put("targetRole", JsonPrimitive(event.targetRole.toJsonString()))
329332
put("applied", JsonPrimitive(cascadeApply.success))
333+
cascadeApply.item?.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
330334
})
331335

332336
if (!cascadeApply.success || cascadeApply.item == null) break
@@ -421,6 +425,7 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
421425
put("previousRole", JsonPrimitive(previousRole.toJsonString()))
422426
put("newRole", JsonPrimitive(targetRole.toJsonString()))
423427
put("trigger", JsonPrimitive(trigger))
428+
applyResult.item?.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
424429
put("applied", JsonPrimitive(true))
425430
if (summary != null) put("summary", JsonPrimitive(summary))
426431
put("cascadeEvents", JsonArray(cascadeJsonList))
@@ -475,7 +480,7 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
475480

476481
val cascadeApply = handler.applyTransition(
477482
parentItem, event.targetRole, "cascade",
478-
summary, null,
483+
summary, context.statusLabelService().resolveLabel("cascade"),
479484
context.workItemRepository(),
480485
context.roleTransitionRepository()
481486
)
@@ -486,6 +491,7 @@ Trigger-based role transitions for WorkItems with validation, cascade detection,
486491
put("previousRole", JsonPrimitive(event.currentRole.toJsonString()))
487492
put("targetRole", JsonPrimitive(event.targetRole.toJsonString()))
488493
put("applied", JsonPrimitive(cascadeApply.success))
494+
cascadeApply.item?.statusLabel?.let { put("statusLabel", JsonPrimitive(it)) }
489495
})
490496
}
491497
}

0 commit comments

Comments
 (0)