Repository navigation
v3.14.0
Highlights
- Added paging (
limit/offset) toquery_dependenciesedge listings and a 1000-node cap on
graph traversal — large dependency graphs no longer blow past response limits - Added
schemaWarningstomanage_project_configandPUT /roots/{rootId}/configresponses —
invalid note roles and other parse problems surface instead of being dropped - Added a Docker
HEALTHCHECKbacked by a readiness marker (READINESS_FILE) — startup failures
now exit non-zero and orchestrators can detect a half-started server - Added an SSE
sync.lostcontrol event on unreplayableLast-Event-ID— reconnecting clients
know when a full resync is required - Changed every boolean env var to one parser (
true/1/yes,false/0/no) —
USE_FLYWAY=1now selects Flyway;API_ENABLED/API_ALLOW_UNAUTHENTICATEDfail startup on a
bad value - Changed
API_JWKS_URLto requirehttps(loopbackhttponly with
API_JWKS_ALLOW_INSECURE_URL=true) and JWKS-mode tokens to requireexp - Changed start cascades to respect the parent's queue-note gate, reported via
gateBlocked/missingNotes - Fixed root- and tag-scoped REST reads, SSE fan-out, dependency and backlink listings that leaked
out-of-scope items — fail-closed everywhere now - Fixed
complete_treebypassing the advance pipeline (claims, dependency validation, audit rows,
cascades) - Fixed FTS pagination returning overlapping pages, unbounded REST request bodies (now
413), and
500-on-conflict forPATCH /items/{id}(now409) - Fixed recursive traversals hanging on cyclic rows, and shutdown cleanup racing registrations
- Bumped plugin version to 3.6.1 (hook and skill text fixes;
config-syncprints schema warnings)
Full per-item detail follows.
Changed
- Bumped plugin version to 3.6.1. The session-start hook and
quick-startskill drop the
stale depth-3 claim,config-syncprintsschemaWarningsreturned by the config push, and the
workflow-orchestratoroutput style gains the dispatch-contract paragraph for Parallel-tier
waves. No new skill, hook, or contract change. /implement's shared-worktree commit and review guidance now mandates path-scoped commits.
git commit --only -- <owned paths>plus agit show --stat HEADverification replaces a bare
git commit, and reviews scope by owned-file diff instead of SHA range. (#301)- Agent compile self-checks exit early on foreign-file errors. A self-check failing entirely
in files outside an agent's owned list is recorded undersession-trackingFriction and
committed as-is instead of retried three times; thegradle-locked.ps1invocation is pinned as a
comma-separated array literal to avoid the nested-PowerShell string-flattening bug. (#302) test-authorskill requires fixtures to satisfy domainvalidate()invariants. A new
"Fixture invariants" rule derives dependent fields from each other instead of defaulting them
independently, and thetest-manifestnote format gains an invariants-respected line. (#303)/implementadds a named planning seat for the Parallel tier. Oneopusagent per stream
verifies the diagnosis against source and fillstest-planbefore any implementer is
dispatched, returning a structureddiagnosis-corrections/cross-stream-file-overlaps/
missing-api-or-seam/test-plan-statusblock instead of free-form prose. (#304)- Test plans label scenarios
EXISTING-SURFACEorNEW-SURFACEwith a red-proof plan. Each
NEW-SURFACEscenario states a narrowest-possible-revert recipe (or a named substitute
verification), and the planning-seat return template gains ared-proof-shapefield. (#306) - Adopted process rules for Parallel-tier waves now ship through the file dispatch prompts
actually reference, not through skill prose. A controlled two-wave comparison showed
prose-only adoption let the same failure classes re-fire, while plan-file delivery drove
shared-index sweeps, dropped commits, and wasted gradle runs to zero. (#307) - Test-author blindness is now a structural, tool-enforced boundary. The dispatch contract's
Test author protocol supplies every public declaration inline, bars any tool from reading
src/main, requires an explicitkeys=filter on everyquery_notescall, and directs the
author to stop and ask rather than derive a missing declaration. (#310) - New dispatch-contract reference template.
.claude/skills/implement/references/dispatch-contract-template.md
gives every Parallel-tier wave 12 fixed slots (header, conflict rule, items, planning-seat
return, commit discipline, compile self-check, file ownership, test-author protocol,
contract-change sweep, docs, notes, review scoping) to copy into the run's plan file, plus an
"Adoption reach" subsection mapping each prior proposal to the slot that now carries it. - Contract-tightening changes get a post-wave sweep step. After a parameter becomes required,
avalidate()invariant is added, or a new accessor lands on a strict-mocked interface, the
orchestrator greps every call site and fixture and repairs them by construction — never by
relaxing the contract — declaring each repair in the contract's commit map. (82034e9a,
31a1abeb) containerand the newprojecttype carry apermanentlifecycle in this repo's dogfood
config. A container is a filing cabinet, not a work item, so cascades can no longer terminalize
the Bugs container or the project anchor root over dozens of queued children (both had sat in
terminal at the start of the September bug wave). Adopts proposal7b03dad6. (#300)- Plugin session-start hook and
quick-startskill no longer claim a maximum tree depth of 3.
Hierarchy depth has been unbounded since the V7 cycle-guard migration; both surfaces now say
trees nest to any depth. Because the plugin cache is version-keyed, the corrected hook text
reaches sessions only after this release's plugin version bump. (#317) /prepare-releaseregained itsserver.jsonstep and learned to fold[Unreleased]. The
"bumpserver.jsonversion + immutable OCI identifier" paragraph from #298 had been added to a
flat duplicate of the skill file that #321 later deleted, leaving the surviving skill unable to
satisfy the release workflow's ownserver.jsonguard. Step 8a now carries the paragraph, the
staging lists includeserver.json, and Step 8c folds an existing## [Unreleased]section into
the new version header instead of inserting a second section above it.- Bumped
sqlite-jdbcfrom 3.53.2.0 to 3.53.4.0 (bundles SQLite 3.53.4). Routine patch
refresh ahead of the release; no advisory is open against either version.
Fixed
-
REST tag-scope enforcement had gaps across read routes, and a re-parent could create a cycle.
A shared tag-scope predicate now guards every read surface atags_include-scoped principal can
reach — items, roots, breadcrumbs, search, notes/search, transitions, and?include=children—
andPATCH /items/{id}re-parent enforces scope on the new parent while rejecting self or
descendant re-parents with400instead of entering an infinite cascade loop. (ffa12a2f,
544ae4b9) -
complete_treebypassed the advance pipeline. Every transition it performs now runs through
the same path asadvance_item— claim ownership, dependency validation, cascade and unblock
reporting, actor-attributed audit rows, and per-root status labels — instead of calling the
transition handler directly. (3e455253) -
Startup failures exited
0and Docker had no health check. Startup now reports a
StartupOutcomeand exits non-zero on any failure (including a transport-start exception on
either the stdio or http branch, which previously still reportedStarted); a readiness marker
file (READINESS_FILE) is touched only after DB init, schema update, and transport bind all
succeed, cleared on shutdown, and backs a new DockerHEALTHCHECK. (56ac1690,56593660) -
Four incompatible env-var boolean parsers. One
EnvBooleanparser now handles every boolean
environment variable:true/1/yesandfalse/0/no(case-insensitive) are all recognized
—USE_FLYWAY=1now selects Flyway — and an unrecognized value either falls back to the default
with a warning or, forAPI_ENABLED/API_ALLOW_UNAUTHENTICATED, fails startup. (64f7b265) -
Per-root config push silently dropped schema parse warnings. Parse warnings (including an
invalid noterole) now surface asschemaWarningson both themanage_project_configand
PUT /roots/{rootId}/configresponses, and theconfig-synchook prints them. (40d755cc) -
SSE
?token=auth was unreachable, and unauthenticated mode returned401for every SSE
subscription. Both paths now behave as documented. (3a6c0e5a) -
An optimistic-lock conflict on
PATCH /items/{id}surfaced as500. It now maps to
409 version_conflict. (20ccc9fb) -
The MCP adapter coerced the strings
"true"/"false"to booleans for every parameter.
Coercion now applies only to boolean-typed parameters, so a string-typed field keeps its literal
value. (462931bb) -
Recursive item delete was not atomic. A cascade delete now runs in one transaction per root
id, so a mid-cascade failure leaves no half-deleted subtree. (c75085d3) -
A child
startcascaded a queued parent into WORK past its unfilled required queue notes.
Start cascades now respect the parent's note gate: the cascade is suppressed and reported with
gateBlocked/missingNotes, exactly like terminal cascades, before any resource lease is
acquired. Reopen cascades keep their documented bypass. (473e4f49) -
Dependency and backlink reads leaked out-of-scope items.
GET /items/{id}/backlinks
disclosed thefromTitleof items outside the caller's scope, and the same leak existed under
root scope and onGET /items/{id}/dependencies; both are now filtered fail-closed forrootIds
andtags_includeprincipals alike. (72911c9f) -
GET /api/v1/eventsignoredtags_include. SSE events are now filtered per event, fail-closed,
for tag-scoped principals on both the live stream andLast-Event-IDreplay; a tag-scoped
subscription the server cannot evaluate is refused with403 insufficient_scope. (ad2c23ea) -
The re-parent cycle guard was depth-bounded and failed open. It now tests full ancestor-chain
membership, and a lookup error returns500 db_errorbefore any write instead of allowing the
re-parent. (1a5ccf06) -
Recursive traversals could hang on cyclic rows. Descendant and ancestor traversals are now
cycle-guarded and bounded at depth 1000 on both SQLite (recursive CTE) and H2 (BFS with a visited
set): cascade deletes and re-parent depth recomputes fail loud at the boundary, search-scope
traversals bound-and-continue, and a new detailed ancestor-chain result reports whether a chain
was truncated by a cycle or a missing ancestor. (71bc3d09) -
A malformed
requestIdsilently disabled idempotency.manage_items,manage_notes,
manage_dependencies,advance_item,complete_tree, andcreate_work_treenow reject a
non-canonicalrequestId(blank, number, null, object, array, or non-36-char form) with a
validation error naming the field, matching whatclaim_itemalready did. (71dea46e) -
A shutdown-cleanup registration racing the drain threw
ConcurrentModificationException,
skipping every remaining cleanup while shutdown still reported clean. Registration is now
thread-safe, late registrations run immediately, and forward order is preserved. (eeba1b12) -
SSE reconnects with an unreplayable
Last-Event-IDgot no signal. Async.lostcontrol
event now leads the stream withreason=buffer_evictedorunknown_event_id(alongside the
existingqueue_overflow), carrying idoldestRetained - 1so reconnecting at the sentinel
yields a full replay; control events bypass thetypes=filter, andAPI_SSE_BUFFER_SIZE=0now
means retain nothing instead of crashing. (a3ebd108) -
MCP Registry record followed the mutable
:latestimage tag.server.jsonpinned
ghcr.io/jpicklyk/task-orchestrator:latestunder an immutable registry version, so the published
3.2.0 record resolved to whichever image later ownedlatest— and the file itself had not been
bumped since 3.2.0 despite/prepare-releaseinstructing it. Now pins the per-release tag
(:3.13.1),docker-publish.ymlfails the release ifserver.jsondisagrees with
version.properties, and every tagged release publishes its own registry record via
mcp-publisher(GitHub OIDC). The/prepare-releaseskill spells out the identifier bump. (#297) -
/prepare-releaserelease-notes extraction silently produced an empty body. The awk step built
a dynamic regex from the version string ($0 ~ "^## \\[" ver "\\]"); the bracket escaping is
interpreted twice in a string-built regex and degrades to a character class on gawk, so the header
never matched and the extraction yielded zero lines. Becausegh release edit --notes-fileaccepts
an empty file without complaint, the curated notes were replaced with nothing. Now matches the
header literally viaindex($0, hdr) == 1and fails loudly on an empty extraction instead of
publishing it. Observed live during the v3.13.1 release. -
API_JWKS_URLaccepted plaintexthttp, exposing JWKS key material to network interception.
The loader now requireshttps; plaintexthttpis accepted only for a loopback host
(localhost,127.x.x.x,::1) with the new opt-inAPI_JWKS_ALLOW_INSECURE_URL=true. Any
other scheme, anhttpURL on a non-loopback host, or an unparseable
API_JWKS_ALLOW_INSECURE_URLvalue fails startup. (231dd7f3) -
page/pageSizequery parameters on REST list endpoints could overflow the internalInt
offset.page/pageSizeare now validated and rejected with400 validation_errorwhen
non-integer,page < 1,page > 100000, orpageSize < 1— previously out-of-range values were
silently clamped.GET /transitions's underlying scan is now bounded at 1000 rows regardless of
the requested page. (c471607b) -
A claim survived an item reaching TERMINAL, so
reopencould resurrect a stale claim.
claimedBy/claimedAt/claimExpiresAt/originalClaimedAtare now cleared whenever a transition
reaches (or leaves) TERMINAL, acrossadvance_item,complete_tree, the REST advance route, and
cascades — a reopened item always starts unclaimed. (3785f37a) -
query_items/query_notesfull-text search pagination could return overlapping or duplicate
results across pages. The FTS candidate window is now fixed-size and offset-independent, ties
are broken deterministically by id, and the fused result list is capped before the page slice is
taken —totalHitsis now identical on every page of the same query andtruncatedcan fire on
page 1. (0ba7c92d) -
query_items'slimitparameter description was wrong for FTS search and scoped-overview
modes. TheparameterSchemadescription now states the correct per-mode defaults and caps
(20/FTS, 50/list and global-or-anchored overview, no cap on overview, ignored on scoped overview).
(97aa5855) -
query_notesvalidated ascope.rolevalue that was never used to filter results. The dead
validation (and its undeclared schema field) has been removed — note search has always been
role-less by role, and now the code and docs agree. (9ad250e3) -
SSE events with an unresolved root set were broadcast to every subscriber, including root-scoped
ones.ApiEventBus.publishnow carries arootsResolvedflag; an unresolved result (ancestor
cache miss, resolution failure) fails closed, reaching only unrestricted subscribers, on both the
live fan-out andLast-Event-IDreplay.GET /api/v1/eventsalso now rejects?root=values
entirely outside a root-scoped token'sscope.rootIdswith403 insufficient_scope, and a
?root=yielding no valid UUID with400 validation_error. (ffce70f6) -
JWTs with no
expclaim were accepted indefinitely, and the SSE expiry watchdog silently
skipped sessions without one. JWKS-mode tokens now requireexp; a token with noexpis
rejected with401 invalid_token(no max-lifetime opt-in). Theauth.expiredSSE watchdog now
runs for every JWKS-authenticated session as a direct consequence — only unauthenticated sessions
and bearer tokens with noexpires_atremain watchdog-free. (708063fa) -
Domain events were published inside the same DB transaction as the write, so a subscriber could
observe an event for a change that later rolled back. Events now publish after the enclosing
transaction commits, and are dropped entirely if it rolls back instead; ordering on the success
path (includingLast-Event-IDreplay) is unchanged. (0e9d5675) -
query_items's global and anchored overview modes acceptedlimit < 1. Alimitof0
silently returned an empty page, and a negativelimitcrashed with an uncaught
IllegalArgumentExceptionon the anchored path. Both arms now rejectlimit < 1with the same
validation error as thesearcharm; scoped overview (itemIdset) is unaffected and still
ignoreslimit. (1a04106a) -
Root-scoped WorkItem queries ignored the
root_idcolumn and expanded every subtree id into a
bound-variable list, hitting SQLite's ~32,766-parameter ceiling on large subtrees.
findByRole/findForNextItem/findClaimable/countByClaimStatus/findInScope/countInScope/countInScopeByRole
now filterroot_id = ?directly when every requested scope id is a stamped depth-0 root
(bindingO(|rootIds|)params instead of one per row); below-root or unstamped scopes still take
the original recursive-CTE path. No public signature or migration change. (09205394) -
dependency.added/dependency.removedSSE events were withheld from root-scoped subscribers
whenever the ancestor-root cache was cold. Eight ofDependencyRepository's ten methods are
nowsuspend(joining the caller's coroutine transaction), and the event decorator resolves
roots via a live database lookup instead of the cache-only path, so a cold-cache dependency write
now correctly reaches root-scoped subscribers as long as one is connected at write time.
createSuspendis removed (collapsed intocreate);findByFromItemId/findByToItemIdremain
non-suspend. (33e96efd) -
query_dependencies'sgetedge listing had no paging, issued twogetByIdcalls per edge for
item-info enrichment, and its BFS graph traversal was unbounded.getnow accepts optional
limit/offseton the edge listing (rejectinglimit < 1/offset < 0), enrichment is fetched
in one batched lookup instead of per-edge calls, and BFS traversal caps at
MAX_DEPENDENCY_GRAPH_NODES(1000) nodes, settinggraph.truncatedwhen the cap is hit.
Responses that omit bothlimitandoffsetare byte-identical to before. (6e2d8fc2) -
The REST idempotency cache held one process-wide write lock across the entire
compute-and-store operation, serializing unrelated requests, and read the request body inside
that lock.IdempotencyCachenow locks only the store read-check-write and per-key in-flight
computations — concurrent same-key callers coalesce onto one execution and different keys never
block each other — andPOST /items,PATCH /items/{id}, andPUT /items/{id}/notes/{key}read
the body before entering the idempotency path.POST /itemsandPUT .../notes/{key}also now
explicitly reject a non-JSONContent-Typewith415 unsupported_media_type(an absent header
is still accepted). Replay semantics are unchanged: same key + different body still replays the
first response verbatim. (c7751104) -
The Exposed-generated
work_itemstable (used by Direct-mode/dev databases) was missing the
role/previous_role/priorityCHECKconstraints and theclaim_expires_atindex that the
Flyway migration (V7) defines, so a Direct-mode database created today could accept an invalid
role/priority value the Flyway-migrated schema would reject.WorkItemsTablenow declares the
same three CHECK constraints and index; no migration file changed. (97f8632f) -
Every REST write route buffered the entire request body into heap before any size check
ran.POST /items,PATCH /items/{id},POST /items/{id}/advance,PUT /items/{id}/notes/{key}, andPOST /dependencieshad no size limit at all;PUT /roots/{rootId}/configandPUT /roots/{rootId}/plans/{slug}checked their existing limits only
after the full body had already been read. A sharedreceiveBounded()helper now rejects an
over-limitContent-Lengthbefore touching the body, and caps the actual channel read at
limit + 1bytes to catch a chunked or understatedContent-Length— no oversized body is ever
buffered in full. All seven routes now share one413 payload_too_largeshape; the two
pre-existing numeric limits (128 KiB, 64 KiB) are unchanged, and the five previously-uncapped
routes share a new 1 MiB limit. (e941c2c7)
Documentation
- Rewrote the README for first-time readers. Leads with what the server does and the shortest
path to a working setup, drops volatile tool counts in favor of the API reference, and removes
the brokenworkspaceFoldermount examples. (#315)