Skip to content

Commit c9f41f6

Browse files
jpicklykclaude
andauthored
release: v2.5.2 — consistent schema fields, test coverage, container schema (#92)
* feat(plugin): add tiered execution model (Direct/Delegated/Parallel) Output style now classifies work into three tiers with proportional process. Direct tier (1-2 files, known fix) implements inline — no subagent dispatch, no plan mode, no separate review. Delegated and Parallel tiers preserve the full pipeline. Changes: - workflow-orchestrator.md: tier classification section, conditional principles, Direct Tier Workflow section, frontmatter added, removed project-specific references (session-retrospective, quick-fix schema, gradlew), removed redundant delegation templates - implement skill: tier-conditional Steps 1/3/4/5, two-dimensional classification (tier + interaction mode), resume-with-tier-awareness - pre-plan.mjs: Direct tier safety net message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(plugin): trim workflow-orchestrator to universally valuable content Remove project-specific details, redundant templates, and niche operational guidance. Condense worktree dispatch to 4 universal principles, session tasks to one line, completion format to inline. Drop schema tag row, delegation metadata section, return format templates, and guidancePointer rendering convention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(plugin): minor cleanup — schema context in implement skill, analyst zone updates - Implement skill: add schema context to session-tracking note reference - Workflow-analyst Zone 2: add Delegation Metadata section, tier-aware Parallel Dispatch reference - Workflow-analyst Zone 3: replace stale manifest monitoring with tier classification monitoring, connect analysis layer to /session-retrospective Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(plugin): strengthen planning hooks and enforce WHAT/HOW separation Planning hooks now use imperative gate language (PREREQUISITE/MUST/NEXT STEP) instead of suggestive phrasing that lost attention competition with plan mode. Output style trimmed to WHAT-level principles — procedural HOW detail moved to skills where it belongs. Worktree Dispatch section removed (prescriptive technique, not a principle). Skills get explicit handoff sections for clean control flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: enforce IS_BLOCKED_BY deps in advance_item and unblock detection IS_BLOCKED_BY dependencies were checked by get_blocked_items and get_next_item but silently ignored by advance_item (validateTransition) and CascadeDetector (findUnblockedItems/isFullyUnblocked). This meant agents could advance items past IS_BLOCKED_BY gates that the read-only tools correctly reported as blocked. - RoleTransitionHandler.validateTransition now queries findByFromItemId for outgoing IS_BLOCKED_BY edges in addition to findByToItemId for incoming BLOCKS edges - CascadeDetector.findUnblockedItems discovers IS_BLOCKED_BY targets via findByToItemId (incoming IS_BLOCKED_BY on the transitioning item) - CascadeDetector.isFullyUnblocked checks outgoing IS_BLOCKED_BY edges on the target item via findByFromItemId - All existing tests updated with explicit dep mocks (no relaxed defaults) - Added IS_BLOCKED_BY coverage for GetBlockedItemsTool, GetNextItemTool, and GetNextStatusTool (satisfied, unsatisfied, mixed, custom unblockAt) - Fixed reversed dependency example in quick-start.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(plugin): improve manage-schemas templates and guidance generation - Updated feature-implementation and bug-fix schema templates to align with spec-quality and review-quality frameworks - Added guidance generation rules (lead with consumer, structure over prose, concrete over generic, session-tracking prompt) - Added cross-schema duplication check when editing guidance values - Companion template now preserves markdown formatting in guidance text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(cascade): add DetectReopenCascades coverage + transaction-gap KDoc Add 5 test cases for CascadeDetector.detectReopenCascades (previously zero coverage) and document the intentional detect/apply transaction split with KDoc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(config): add structured warning collection to YamlNoteSchemaService Add SchemaLoadResult with warnings list, surface via getLoadWarnings() on NoteSchemaService interface. Warnings collected for: missing key, missing role, non-boolean required, missing note_schemas key, parse errors. Log summary at INFO on startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tools): always include expectedNotes and schemaMatch in create response manage_items create now returns expectedNotes (empty array when no schema matches) and schemaMatch (boolean) on every item. Previously expectedNotes was omitted when no schema matched, making it ambiguous for consumers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: deduplicate WorkTreeService insert/upsert with repository helpers Extract insertRow() and upsertRow() internal helpers on SQLiteWorkItemRepository and SQLiteNoteRepository. SQLiteWorkTreeService now delegates to these instead of duplicating column-mapping logic. Helpers return Result types; WorkTreeService checks for Result.Error explicitly to trigger rollback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(service): extract SchemaEntryJsonBuilder — unify schema-to-JSON across 4 tools Add buildExpectedNotesJson() and buildSchemaResponseFields() to replace 6 duplicated schema-entry-to-JSON mapping blocks across CreateItemHandler, CreateWorkTreeTool, AdvanceItemTool, and GetContextTool. Supports 3 shape variants (creation/transition/context) via parameters. CreateWorkTreeTool now consistently includes expectedNotes and schemaMatch fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: close H1/H2/M5 gaps — WorkTreeService rollback, happy path with notes, note ID preservation H1: duplicate item UUID causes full transaction rollback H2: items + deps + notes all committed atomically on happy path M5: upsert preserves original note ID and createdAt on update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: close H3/H4/M3/M4 gaps — expectedNotes inspection, reopen cascade, blank body, noteProgress H3: expectedNotes array contents verified after queue-to-work transition H4: reopen child under terminal parent cascades parent back to work M3: exists=true + blank body → filled=false in GetContextTool M4: noteProgress filled/remaining/total directly asserted Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: close M1/M2/M6/M7/M8 gaps — lazy init, wrong YAML type, empty schema, exists=false, getLoadWarnings M1: lazy init triggered by first getSchemaForTags call (bad role skipped) M2: note_schemas as YAML list returns null without crashing M6: schemaMatch=true with empty schema list, expectedNotes is empty array M7: exists=false explicitly asserted on CreateWorkTreeTool expectedNotes M8: getLoadWarnings() default method returns empty list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(plugin): add container schema example to manage-schemas skill Teaches the gate-free pattern for organizational root containers that should not be gated on implementation-oriented notes like session-tracking. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * release: bump to v2.5.2 — plugin v2.7.3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent eee3919 commit c9f41f6

29 files changed

Lines changed: 1494 additions & 233 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"plugins": [
1111
{
1212
"name": "task-orchestrator",
13-
"version": "2.7.2",
13+
"version": "2.7.3",
1414
"description": "Skills, hooks, and workflows for MCP Task Orchestrator. Schema-aware context, note-driven workflow, and session hooks.",
1515
"author": {
1616
"name": "Jeff Picklyk",

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.5.2] - 2026-04-03 (Plugin v2.7.3)
9+
10+
### Fixed
11+
- Fixed `manage_items` create response to always include `expectedNotes` and `schemaMatch` fields — agents no longer need a separate `get_context` call after item creation
12+
13+
### Improved
14+
- Unified schema-to-JSON serialization across `manage_items`, `create_work_tree`, `advance_item`, and `get_context` via shared `SchemaEntryJsonBuilder`
15+
- Added structured warning collection to schema config loading — config issues are surfaced programmatically via `getLoadWarnings()` instead of silently logged
16+
- Deduplicated WorkTreeService insert/upsert logic with shared repository helpers
17+
- Expanded test coverage across 12 identified gaps — WorkTreeService rollback, cascade detection, schema loading edge cases, note preservation, and gate lifecycle
18+
- Bumped plugin version to 2.7.3 — added `container` schema example to manage-schemas skill
19+
20+
---
21+
822
## [2.5.1] - 2026-03-31 (Plugin v2.7.2)
923

1024
### Fixed

claude-plugins/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ removing and re-adding the marketplace in Claude Code. No version bump is needed
1010

1111
| Plugin | Directory | Current Version |
1212
|--------|-----------|-----------------|
13-
| `task-orchestrator` | `claude-plugins/task-orchestrator/` | `2.7.2` |
13+
| `task-orchestrator` | `claude-plugins/task-orchestrator/` | `2.7.3` |
1414

1515
> Updated automatically by `/prepare-release`. Do not bump manually.
1616

claude-plugins/task-orchestrator/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-orchestrator",
3-
"version": "2.7.2",
3+
"version": "2.7.3",
44
"description": "Claude Code integration for MCP Task Orchestrator — schema-aware context, note-driven workflow",
55
"skills": "./skills",
66
"hooks": "./hooks/hooks-config.json",

claude-plugins/task-orchestrator/skills/manage-schemas/references/example-schemas.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ bug-fix:
9191
guidance: "Run tests and report results. Note if a new test was added to cover the fix."
9292
```
9393

94+
## `container` (gate-free organizational grouping)
95+
96+
Schema for root-level category containers (Features, Bugs, Tech Debt, etc.) that exist only to group child work items. No required notes at any phase — containers advance freely without gate friction. Without this, containers fall back to the `default` schema and get gated on notes like `session-tracking` that make no sense for organizational items.
97+
98+
```yaml
99+
container:
100+
- key: container-summary
101+
role: queue
102+
required: false
103+
description: "Optional high-level description of what this container organizes."
104+
guidance: "Brief description of the container's purpose and scope. Not required — containers exist to group work items, not to carry implementation detail."
105+
```
106+
107+
Tag root containers with `container` so they match this schema instead of `default`.
108+
94109
## `agent-observation` (queue only, minimal)
95110

96111
Single-note schema for lightweight tracking — no work phase gates.

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/CascadeDetector.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ data class UnblockedItem(
4040
*
4141
* 2. **Unblock detection** -- when a WorkItem transitions, find any downstream items
4242
* whose incoming blocking dependencies are now fully satisfied.
43+
*
44+
* ## Transaction-gap caveat
45+
*
46+
* Detection and application are intentionally **separate transactions**. The detect phase
47+
* reads a snapshot of DB state, computes which cascade events are warranted, and returns
48+
* them to the caller. The caller then applies each event in its own transaction. No single
49+
* transaction spans both detect and apply.
50+
*
51+
* In theory this creates a window where concurrent writers could observe stale state
52+
* (e.g. a sibling's role changes between detect and apply). In practice SQLite's
53+
* single-writer model means only one write can proceed at a time, which eliminates
54+
* the concurrency risk for the common deployment scenario.
55+
*
56+
* For multi-level hierarchies (child → parent → grandparent), [AdvanceItemTool]
57+
* compensates with an **iterative detect-apply loop**: after each cascade event is
58+
* applied and persisted, detection is re-run on the newly advanced item with fresh
59+
* DB state. This ensures each level's decision is based on accurate, up-to-date data.
60+
* The split-transaction design is intentional — not a bug.
4361
*/
4462
class CascadeDetector {
4563
companion object {

current/src/main/kotlin/io/github/jpicklyk/mcptask/current/application/service/NoteSchemaService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ interface NoteSchemaService {
2525
* Returns false when no schema matches (schema-free mode — skip REVIEW).
2626
*/
2727
fun hasReviewPhase(tags: List<String>): Boolean = getSchemaForTags(tags)?.any { it.role == Role.REVIEW } ?: false
28+
29+
/**
30+
* Returns any warnings collected during schema loading (e.g., malformed entries,
31+
* missing required fields). Returns an empty list if no warnings were generated or
32+
* if this implementation does not support warning collection.
33+
*/
34+
fun getLoadWarnings(): List<String> = emptyList()
2835
}
2936

3037
/**
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.github.jpicklyk.mcptask.current.application.service
2+
3+
import io.github.jpicklyk.mcptask.current.application.tools.toJsonString
4+
import io.github.jpicklyk.mcptask.current.domain.model.NoteSchemaEntry
5+
import io.github.jpicklyk.mcptask.current.domain.model.Role
6+
import kotlinx.serialization.json.*
7+
8+
/**
9+
* Result of schema lookup + JSON serialization for tool responses.
10+
*
11+
* @property schemaMatch True when the item's tags matched a configured note schema.
12+
* @property expectedNotes JSON array of schema entries, empty when no schema matched.
13+
*/
14+
data class SchemaResponseFields(
15+
val schemaMatch: Boolean,
16+
val expectedNotes: JsonArray
17+
)
18+
19+
/**
20+
* Build expectedNotes JSON array from schema entries.
21+
*
22+
* Supports three shapes:
23+
* - Shape 1 (creation): exists=false for all, no filled field
24+
* - Shape 2 (transition): exists checked against existingNoteKeys
25+
* - Shape 3 (context): exists + filled checked against notes
26+
*
27+
* @param schema Schema entries, or null if no schema matches
28+
* @param existingNoteKeys Keys of notes that exist (empty = all false)
29+
* @param filledNoteKeys Keys of notes with non-blank body, or null to omit "filled" field
30+
* @param filterRole If non-null, only include entries matching this role
31+
*/
32+
fun buildExpectedNotesJson(
33+
schema: List<NoteSchemaEntry>?,
34+
existingNoteKeys: Set<String> = emptySet(),
35+
filledNoteKeys: Set<String>? = null,
36+
filterRole: Role? = null
37+
): JsonArray {
38+
if (schema == null) return JsonArray(emptyList())
39+
val entries = if (filterRole != null) schema.filter { it.role == filterRole } else schema
40+
return JsonArray(
41+
entries.map { entry ->
42+
buildJsonObject {
43+
put("key", JsonPrimitive(entry.key))
44+
put("role", JsonPrimitive(entry.role.toJsonString()))
45+
put("required", JsonPrimitive(entry.required))
46+
put("description", JsonPrimitive(entry.description))
47+
entry.guidance?.let { put("guidance", JsonPrimitive(it)) }
48+
put("exists", JsonPrimitive(entry.key in existingNoteKeys))
49+
if (filledNoteKeys != null) {
50+
put("filled", JsonPrimitive(entry.key in filledNoteKeys))
51+
}
52+
}
53+
}
54+
)
55+
}
56+
57+
/**
58+
* Build both schemaMatch and expectedNotes for tool responses.
59+
* Convenience wrapper for creation responses (exists=false, no filled).
60+
*
61+
* @param schema Schema entries, or null if no schema matches
62+
*/
63+
fun buildSchemaResponseFields(
64+
schema: List<NoteSchemaEntry>?
65+
): SchemaResponseFields =
66+
SchemaResponseFields(
67+
schemaMatch = schema != null,
68+
expectedNotes = buildExpectedNotesJson(schema)
69+
)

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

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.github.jpicklyk.mcptask.current.application.tools.compound
22

33
import io.github.jpicklyk.mcptask.current.application.service.TreeDepSpec
44
import io.github.jpicklyk.mcptask.current.application.service.WorkTreeInput
5+
import io.github.jpicklyk.mcptask.current.application.service.buildSchemaResponseFields
56
import io.github.jpicklyk.mcptask.current.application.tools.*
67
import io.github.jpicklyk.mcptask.current.domain.model.*
78
import io.github.jpicklyk.mcptask.current.domain.repository.Result
@@ -351,68 +352,36 @@ Atomically create a hierarchical work tree: root item, child items, dependencies
351352
val idToRef = treeResult.refToId.entries.associate { (ref, id) -> id to ref }
352353

353354
val rootResultItem = treeResult.items.first()
355+
val rootSchemaFields = buildSchemaResponseFields(
356+
context.noteSchemaService().getSchemaForTags(rootResultItem.tagList())
357+
)
354358
val rootJson =
355359
buildJsonObject {
356360
put("id", JsonPrimitive(rootResultItem.id.toString()))
357361
put("title", JsonPrimitive(rootResultItem.title))
358362
put("role", JsonPrimitive(rootResultItem.role.toJsonString()))
359363
put("depth", JsonPrimitive(rootResultItem.depth))
360364
rootResultItem.tags?.let { put("tags", JsonPrimitive(it)) }
361-
// Add expectedNotes from schema
362-
rootResultItem.tags?.let { tags ->
363-
val schemaEntries = context.noteSchemaService().getSchemaForTags(rootResultItem.tagList())
364-
if (!schemaEntries.isNullOrEmpty()) {
365-
put(
366-
"expectedNotes",
367-
JsonArray(
368-
schemaEntries.map { entry ->
369-
buildJsonObject {
370-
put("key", JsonPrimitive(entry.key))
371-
put("role", JsonPrimitive(entry.role.toJsonString()))
372-
put("required", JsonPrimitive(entry.required))
373-
put("description", JsonPrimitive(entry.description))
374-
entry.guidance?.let { put("guidance", JsonPrimitive(it)) }
375-
put("exists", JsonPrimitive(false))
376-
}
377-
}
378-
)
379-
)
380-
}
381-
}
365+
put("schemaMatch", JsonPrimitive(rootSchemaFields.schemaMatch))
366+
put("expectedNotes", rootSchemaFields.expectedNotes)
382367
}
383368

384369
val childrenJson =
385370
JsonArray(
386371
treeResult.items.drop(1).map { item ->
387372
val ref = idToRef[item.id] ?: "unknown"
373+
val childSchemaFields = buildSchemaResponseFields(
374+
context.noteSchemaService().getSchemaForTags(item.tagList())
375+
)
388376
buildJsonObject {
389377
put("ref", JsonPrimitive(ref))
390378
put("id", JsonPrimitive(item.id.toString()))
391379
put("title", JsonPrimitive(item.title))
392380
put("role", JsonPrimitive(item.role.toJsonString()))
393381
put("depth", JsonPrimitive(item.depth))
394382
item.tags?.let { put("tags", JsonPrimitive(it)) }
395-
// Add expectedNotes from schema
396-
item.tags?.let {
397-
val schemaEntries = context.noteSchemaService().getSchemaForTags(item.tagList())
398-
if (!schemaEntries.isNullOrEmpty()) {
399-
put(
400-
"expectedNotes",
401-
JsonArray(
402-
schemaEntries.map { entry ->
403-
buildJsonObject {
404-
put("key", JsonPrimitive(entry.key))
405-
put("role", JsonPrimitive(entry.role.toJsonString()))
406-
put("required", JsonPrimitive(entry.required))
407-
put("description", JsonPrimitive(entry.description))
408-
entry.guidance?.let { put("guidance", JsonPrimitive(it)) }
409-
put("exists", JsonPrimitive(false))
410-
}
411-
}
412-
)
413-
)
414-
}
415-
}
383+
put("schemaMatch", JsonPrimitive(childSchemaFields.schemaMatch))
384+
put("expectedNotes", childSchemaFields.expectedNotes)
416385
}
417386
}
418387
)

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

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.jpicklyk.mcptask.current.application.tools.items
22

33
import io.github.jpicklyk.mcptask.current.application.service.ItemHierarchyValidator
4+
import io.github.jpicklyk.mcptask.current.application.service.buildSchemaResponseFields
45
import io.github.jpicklyk.mcptask.current.application.tools.ResponseUtil
56
import io.github.jpicklyk.mcptask.current.application.tools.ToolExecutionContext
67
import io.github.jpicklyk.mcptask.current.application.tools.ToolValidationException
@@ -138,6 +139,7 @@ class CreateItemHandler(
138139
?.filter { it.isNotBlank() }
139140
?: emptyList()
140141
val schemaEntries = context.noteSchemaService().getSchemaForTags(tagList)
142+
val schemaFields = buildSchemaResponseFields(schemaEntries)
141143
createdItems.add(
142144
buildJsonObject {
143145
put("id", JsonPrimitive(result.data.id.toString()))
@@ -151,23 +153,8 @@ class CreateItemHandler(
151153
} else {
152154
put("tags", JsonNull)
153155
}
154-
if (!schemaEntries.isNullOrEmpty()) {
155-
put(
156-
"expectedNotes",
157-
JsonArray(
158-
schemaEntries.map { entry ->
159-
buildJsonObject {
160-
put("key", JsonPrimitive(entry.key))
161-
put("role", JsonPrimitive(entry.role.toJsonString()))
162-
put("required", JsonPrimitive(entry.required))
163-
put("description", JsonPrimitive(entry.description))
164-
entry.guidance?.let { put("guidance", JsonPrimitive(it)) }
165-
put("exists", JsonPrimitive(false))
166-
}
167-
}
168-
)
169-
)
170-
}
156+
put("schemaMatch", JsonPrimitive(schemaFields.schemaMatch))
157+
put("expectedNotes", schemaFields.expectedNotes)
171158
}
172159
)
173160
}

0 commit comments

Comments
 (0)