Skip to content

v3.14.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 19:39
· 107 commits to main since this release
074130f

Highlights

  • Added paging (limit/offset) to query_dependencies edge listings and a 1000-node cap on
    graph traversal — large dependency graphs no longer blow past response limits
  • Added schemaWarnings to manage_project_config and PUT /roots/{rootId}/config responses —
    invalid note roles and other parse problems surface instead of being dropped
  • Added a Docker HEALTHCHECK backed by a readiness marker (READINESS_FILE) — startup failures
    now exit non-zero and orchestrators can detect a half-started server
  • Added an SSE sync.lost control event on unreplayable Last-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=1 now selects Flyway; API_ENABLED/API_ALLOW_UNAUTHENTICATED fail startup on a
    bad value
  • Changed API_JWKS_URL to require https (loopback http only with
    API_JWKS_ALLOW_INSECURE_URL=true) and JWKS-mode tokens to require exp
  • 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_tree bypassing 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 for PATCH /items/{id} (now 409)
  • 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-sync prints schema warnings)

Full per-item detail follows.

Changed

  • Bumped plugin version to 3.6.1. The session-start hook and quick-start skill drop the
    stale depth-3 claim, config-sync prints schemaWarnings returned by the config push, and the
    workflow-orchestrator output 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 a git show --stat HEAD verification 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 under session-tracking Friction and
    committed as-is instead of retried three times; the gradle-locked.ps1 invocation is pinned as a
    comma-separated array literal to avoid the nested-PowerShell string-flattening bug. (#302)
  • test-author skill requires fixtures to satisfy domain validate() invariants. A new
    "Fixture invariants" rule derives dependent fields from each other instead of defaulting them
    independently, and the test-manifest note format gains an invariants-respected line. (#303)
  • /implement adds a named planning seat for the Parallel tier. One opus agent per stream
    verifies the diagnosis against source and fills test-plan before any implementer is
    dispatched, returning a structured diagnosis-corrections / cross-stream-file-overlaps /
    missing-api-or-seam / test-plan-status block instead of free-form prose. (#304)
  • Test plans label scenarios EXISTING-SURFACE or NEW-SURFACE with a red-proof plan. Each
    NEW-SURFACE scenario states a narrowest-possible-revert recipe (or a named substitute
    verification), and the planning-seat return template gains a red-proof-shape field. (#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 explicit keys= filter on every query_notes call, 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,
    a validate() 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)
  • container and the new project type carry a permanent lifecycle 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 proposal 7b03dad6. (#300)
  • Plugin session-start hook and quick-start skill 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-release regained its server.json step and learned to fold [Unreleased]. The
    "bump server.json version + 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 own server.json guard. Step 8a now carries the paragraph, the
    staging lists include server.json, and Step 8c folds an existing ## [Unreleased] section into
    the new version header instead of inserting a second section above it.
  • Bumped sqlite-jdbc from 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 a tags_include-scoped principal can
    reach — items, roots, breadcrumbs, search, notes/search, transitions, and ?include=children —
    and PATCH /items/{id} re-parent enforces scope on the new parent while rejecting self or
    descendant re-parents with 400 instead of entering an infinite cascade loop. (ffa12a2f,
    544ae4b9)

  • complete_tree bypassed the advance pipeline. Every transition it performs now runs through
    the same path as advance_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 0 and Docker had no health check. Startup now reports a
    StartupOutcome and exits non-zero on any failure (including a transport-start exception on
    either the stdio or http branch, which previously still reported Started); 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 Docker HEALTHCHECK. (56ac1690, 56593660)

  • Four incompatible env-var boolean parsers. One EnvBoolean parser now handles every boolean
    environment variable: true/1/yes and false/0/no (case-insensitive) are all recognized
    — USE_FLYWAY=1 now selects Flyway — and an unrecognized value either falls back to the default
    with a warning or, for API_ENABLED/API_ALLOW_UNAUTHENTICATED, fails startup. (64f7b265)

  • Per-root config push silently dropped schema parse warnings. Parse warnings (including an
    invalid note role) now surface as schemaWarnings on both the manage_project_config and
    PUT /roots/{rootId}/config responses, and the config-sync hook prints them. (40d755cc)

  • SSE ?token= auth was unreachable, and unauthenticated mode returned 401 for every SSE
    subscription.
    Both paths now behave as documented. (3a6c0e5a)

  • An optimistic-lock conflict on PATCH /items/{id} surfaced as 500. 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 start cascaded 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 the fromTitle of items outside the caller's scope, and the same leak existed under
    root scope and on GET /items/{id}/dependencies; both are now filtered fail-closed for rootIds
    and tags_include principals alike. (72911c9f)

  • GET /api/v1/events ignored tags_include. SSE events are now filtered per event, fail-closed,
    for tag-scoped principals on both the live stream and Last-Event-ID replay; a tag-scoped
    subscription the server cannot evaluate is refused with 403 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 returns 500 db_error before 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 requestId silently disabled idempotency. manage_items, manage_notes,
    manage_dependencies, advance_item, complete_tree, and create_work_tree now reject a
    non-canonical requestId (blank, number, null, object, array, or non-36-char form) with a
    validation error naming the field, matching what claim_item already 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-ID got no signal. A sync.lost control
    event now leads the stream with reason = buffer_evicted or unknown_event_id (alongside the
    existing queue_overflow), carrying id oldestRetained - 1 so reconnecting at the sentinel
    yields a full replay; control events bypass the types= filter, and API_SSE_BUFFER_SIZE=0 now
    means retain nothing instead of crashing. (a3ebd108)

  • MCP Registry record followed the mutable :latest image tag. server.json pinned
    ghcr.io/jpicklyk/task-orchestrator:latest under an immutable registry version, so the published
    3.2.0 record resolved to whichever image later owned latest — and the file itself had not been
    bumped since 3.2.0 despite /prepare-release instructing it. Now pins the per-release tag
    (:3.13.1), docker-publish.yml fails the release if server.json disagrees with
    version.properties, and every tagged release publishes its own registry record via
    mcp-publisher (GitHub OIDC). The /prepare-release skill spells out the identifier bump. (#297)

  • /prepare-release release-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. Because gh release edit --notes-file accepts
    an empty file without complaint, the curated notes were replaced with nothing. Now matches the
    header literally via index($0, hdr) == 1 and fails loudly on an empty extraction instead of
    publishing it. Observed live during the v3.13.1 release.

  • API_JWKS_URL accepted plaintext http, exposing JWKS key material to network interception.
    The loader now requires https; plaintext http is accepted only for a loopback host
    (localhost, 127.x.x.x, ::1) with the new opt-in API_JWKS_ALLOW_INSECURE_URL=true. Any
    other scheme, an http URL on a non-loopback host, or an unparseable
    API_JWKS_ALLOW_INSECURE_URL value fails startup. (231dd7f3)

  • page/pageSize query parameters on REST list endpoints could overflow the internal Int
    offset.
    page/pageSize are now validated and rejected with 400 validation_error when
    non-integer, page < 1, page > 100000, or pageSize < 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 reopen could resurrect a stale claim.
    claimedBy/claimedAt/claimExpiresAt/originalClaimedAt are now cleared whenever a transition
    reaches (or leaves) TERMINAL, across advance_item, complete_tree, the REST advance route, and
    cascades — a reopened item always starts unclaimed. (3785f37a)

  • query_items/query_notes full-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 — totalHits is now identical on every page of the same query and truncated can fire on
    page 1. (0ba7c92d)

  • query_items's limit parameter description was wrong for FTS search and scoped-overview
    modes.
    The parameterSchema description 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_notes validated a scope.role value 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.publish now carries a rootsResolved flag; an unresolved result (ancestor
    cache miss, resolution failure) fails closed, reaching only unrestricted subscribers, on both the
    live fan-out and Last-Event-ID replay. GET /api/v1/events also now rejects ?root= values
    entirely outside a root-scoped token's scope.rootIds with 403 insufficient_scope, and a
    ?root= yielding no valid UUID with 400 validation_error. (ffce70f6)

  • JWTs with no exp claim were accepted indefinitely, and the SSE expiry watchdog silently
    skipped sessions without one.
    JWKS-mode tokens now require exp; a token with no exp is
    rejected with 401 invalid_token (no max-lifetime opt-in). The auth.expired SSE watchdog now
    runs for every JWKS-authenticated session as a direct consequence — only unauthenticated sessions
    and bearer tokens with no expires_at remain 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 (including Last-Event-ID replay) is unchanged. (0e9d5675)

  • query_items's global and anchored overview modes accepted limit < 1. A limit of 0
    silently returned an empty page, and a negative limit crashed with an uncaught
    IllegalArgumentException on the anchored path. Both arms now reject limit < 1 with the same
    validation error as the search arm; scoped overview (itemId set) is unaffected and still
    ignores limit. (1a04106a)

  • Root-scoped WorkItem queries ignored the root_id column 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 filter root_id = ? directly when every requested scope id is a stamped depth-0 root
    (binding O(|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.removed SSE events were withheld from root-scoped subscribers
    whenever the ancestor-root cache was cold.
    Eight of DependencyRepository's ten methods are
    now suspend (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.
    createSuspend is removed (collapsed into create); findByFromItemId/findByToItemId remain
    non-suspend. (33e96efd)

  • query_dependencies's get edge listing had no paging, issued two getById calls per edge for
    item-info enrichment, and its BFS graph traversal was unbounded.
    get now accepts optional
    limit/offset on the edge listing (rejecting limit < 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, setting graph.truncated when the cap is hit.
    Responses that omit both limit and offset are 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.
    IdempotencyCache now 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 — and POST /items, PATCH /items/{id}, and PUT /items/{id}/notes/{key} read
    the body before entering the idempotency path. POST /items and PUT .../notes/{key} also now
    explicitly reject a non-JSON Content-Type with 415 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_items table (used by Direct-mode/dev databases) was missing the
    role/previous_role/priority CHECK constraints and the claim_expires_at index 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. WorkItemsTable now 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}, and POST /dependencies had no size limit at all; PUT /roots/{rootId}/config and PUT /roots/{rootId}/plans/{slug} checked their existing limits only
    after the full body had already been read. A shared receiveBounded() helper now rejects an
    over-limit Content-Length before touching the body, and caps the actual channel read at
    limit + 1 bytes to catch a chunked or understated Content-Length — no oversized body is ever
    buffered in full. All seven routes now share one 413 payload_too_large shape; 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 broken workspaceFolder mount examples. (#315)