Skip to content

v3.16.0

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Oct 14:07
· 45 commits to main since this release
c7df00b

Plugin

  • Bumped plugin version to 3.9.0 — the init skill and personal root, bundled process rules,
    the project-level client.json, and the run-wave front door with the implement-wave,
    review-wave, audit and retro-analysis workflows (new skill, workflows and agents: minor).
  • Project init and personal root. New /task-orchestrator:init skill sets up a project root and config for the current directory, or with --user a personal root (anchor-only; reads stay unscoped) in the user-level ~/.taskorchestrator/config.yaml. Session start now reports the setup state (not set up, config without a root, personal scope, project scope), shows the Config: file the skills should read and edit, and, when an orchestrator MCP server is registered, suggests init at most once per directory per day (TASK_ORCHESTRATOR_SETUP_HINT=off silences it). Config discovery also checks the main checkout of a linked worktree and the user-level file, with TASK_ORCHESTRATOR_HOME and TASK_ORCHESTRATOR_CEILING for isolation, and the REST URL resolves from TASK_ORCHESTRATOR_API_URL, then a project-level client.json beside the located config or, for a linked worktree, in the main checkout (written by project-mode init, honoured only as a bare loopback origin with no path, query or fragment), then the user-level client.json (written by init --user). The hooks ignore a project.rootId that is not 1 to 64 letters, digits or hyphens, or that starts with a hyphen. Config-sync pushes the plugin's bundled process rules by manifest policy, shares one time budget across its requests and reports unfinished work as deferred to the next session, and plan dispatch sends multi-item work to run-wave. Skills that scope reads by root (including quick-start, ralph and session-retrospective) scope only by a project root; a personal root anchors new items and reads stay unscoped.
  • run-wave Method B: envelope schema in seat prompts, blind test-author default (#393). run-planner.mjs prompt appends a METHOD B RETURN block for plans marked meta.method: "B" (no StructuredOutput tool exists for a dispatched Agent; the envelope is the last JSON object of the final message; the exact envelope schema is inlined, and planner prompts get a line superseding the core StructuredOutput sentence), so Method B stages no longer cost a re-emit retry each. next defaults a test-author seat whose dispatch names no agent to task-orchestrator:test-author under Method B. Method A prompt bytes are unchanged (S6 parity); recorded Method B hand-backs are replayed as conformance fixtures.
  • Test-author red-proof runs only in a scratch copy (#387). The served test-author rule, the task-orchestrator:test-author agent and the project test-author skill now state that a seat in a shared worktree never runs git stash, git checkout --, git restore or git reset; the revert that observes red runs in a detached scratch copy that is removed afterwards, and test-manifest records where and at which commit it ran. An in-place revert is not red evidence.
  • Seat-owned notes are never rewritten by the orchestrator (#384). run-wave post-run and both output styles state that re-review after fixes goes to a fresh task-orchestrator:reviewer seat and that orchestrator confirmations go under their own orchestrator-confirmation key; the enforce-actor-attribution hook adds a best-effort, non-blocking warning when an orchestrator actor upserts a note the server stores under a subagent actor (fires only where note attribution is not redacted).
  • implement-wave workflow (B1). New Claude Code Workflow script workflows/implement-wave.js
    that runs one work item's seats in order (planner, implementer, declarations extractor, blind test
    author) and several items concurrently from a run plan the front door builds: settle-once milestones
    for in-run dependency edges, per-file lock chains in a shared worktree, entry-result mapping
    (dependency_blocked/resource_unavailable/config_unavailable → deferred, never dropped), rerun-safe
    seats, rule text fetched by key via query_rules (never embedded), and a scanDeclarations step between
    the extractor and the author. Never call it bare — it returns {started:false}; the run-wave front door
    (B2) builds its args. Ships with two new agent definitions, task-orchestrator:planner and
    task-orchestrator:test-author, and a fake-agent test harness (scripts/tests/workflow-harness.mjs).
  • audit workflow (Phase C / C2). New Claude Code Workflow script workflows/audit.js, generalized from the
    September architecture review: presets quick|standard|full, barrier-per-phase reviewers → gap critic → merge →
    adversarial verification (1–3 lenses by severity) → synthesis → triage. It never writes MCP items; its result carries a
    findings proposal that /task-orchestrator:create-item materializes after a human confirms (new section
    "From a workflow findings proposal"). Cost is projected up front and refused above maxAgents unless allowLarge.
  • retro-analysis workflow (Phase C / C3b). Sharded trend matching and adjudication over prior retrospectives and
    observations, opt-in from /session-retrospective --deep (user-invoked only; the retrospective hooks stay the trigger).
  • session-retrospective: Step 4.2/4.3 now read trend ids and titles from the list-mode search and fetch summaries with
    get (list mode never returned summary); the 24 h window uses modifiedAfter.
  • Test harness extension scripts/tests/workflow-harness-ext.mjs (loadCoreNamed, autoAgent, assertBarrier,
    freeRuntimeIds, ruleWindowHits) for workflow scripts beyond implement-wave.
  • review-wave workflow (Phase C / C1). New Claude Code Workflow script workflows/review-wave.js that runs an
    item's review-phase notes as independent lanes (this repo derives reviewer, security, api-compat and plugin-impact
    lanes plus an advisory simplify lane feeding the reviewer; the script itself is lane-agnostic), each on the
    owned-file diff with rules fetched by key, and aggregates a verdict in args order (pass / pass-with-observations /
    fail-blocking). Lanes arrive as validated review-wave/args-v1 stages, hand-built by the orchestrator until the
    run-wave front door wires them (C1-productize); the script never derives lanes from config and refuses a reviewer
    whose actor matches an implementer or test author.
  • run-wave front door (Phase B / B2+B3). New plugin skill /task-orchestrator:run-wave that plans a wave of ready
    items with the run-planner helper (scripts/run-planner.mjs: probe|plan|explain|validate over a pure planner
    that derives per-item seat stages from the resolved schema, classifies dependency edges as in-run or cross-run,
    defers on exclusive resources and file overlaps, and assembles the implement-wave args), launches the workflow
    (Method A) or dispatches the same run plan with the Agent tool (Method B: next|prompt|stage-result| scan-declarations), and on the result verifies commit ownership, audits seat actors, writes the structured
    delegation-metadata provenance line and advances items (verify|actors|provenance|review-prompt). Run plans and
    state persist as run/<runId> plan documents so a run survives turns and compaction.
  • /implement now routes Delegated waves with two or more ready items, and every Parallel wave, through run-wave;
    the dispatch-contract template fetches its rule slots by key (new served rules declarations-extractor and
    contract-change-sweep); WORKTREE.md dispatch examples read the resolved seat profile; .taskorchestrator/ run-profile.json carries this repo's verify commands and search scope; workflowSizeGuideline: large is set.
  • session-retrospective reads the structured per-seat provenance line (seats=<seat:model,...>) alongside the
    legacy one-model-per-item form.
  • Fixed config-sync.mjs pushing all .taskorchestrator/rules/*.md PUTs in parallel (Promise.all), which raced
    the server's SQLite writer and produced one success plus N-1 db_error failures per session; rule PUTs now go
    sequentially, one at a time. run-planner's validateSnapshot now requires a top-level probe object (the
    verbatim probe subcommand output), matching chooseEntry's and plan's existing reliance on it; a snapshot
    missing probe previously planned silently pre-entered instead of failing validation. references/snapshot.md
    documents probe next to profile.
  • Fixed four implement-wave.js/run-exec-lib.mjs bugs found in the B1d live
    acceptance runs (L1–L4): (1) lockKeysFor no longer disables locking outright when a shared-mode writer's
    derived file set comes back empty (no planner output, or the planner reported no files for that
    role) — it now takes a single worktree:<path> key instead of none. (2) mapStageResult now
    catches a planner-v1 envelope that reports proceed:true with mainFiles/docFiles/testFiles
    all empty while the stage owns required notes, mapping it to stopped/envelope-mismatch instead
    of silently proceeding with nothing for downstream seats to act on; the planner's seat prompt also
    now says to emit StructuredOutput exactly once. (3) An implementer stage on an item with no
    test-author-v1 seat now has the planner's testFiles/existingTestEdits[].file folded into both
    its lock keys and its "owned files" set in run-exec-lib.mjs's verify — previously those tests
    were locked and owned by nobody, so verify reported them as unowned writes. (4) The
    missingDeclaration/breachDisclosure "nothing to disclose" sentinel is no longer a literal
    === 'none' compare (so "None." or "none - pre-fix tree extracted via git archive" incorrectly
    failed the gate) — a new isNoneSentinel helper matches the trimmed value against /^none\b/i,
    used by both fields; the test-author output schema and seat prompt now state the accepted format.
    run-exec-lib.mjs's auditActors gained an optional opts.result (and the actors CLI subcommand
    a --result flag) so a Step-2 actor audit no longer flags notes from stages that never ran (deferred
    or missing from the result) as missing — those items instead report skipped: true.
  • Subagents are told to leave nothing running. The SubagentStart hook's
    Subagent Discipline gains item 4, "Leave nothing running" (no background command may outlive the
    return; filesystem searches bounded to the repo/worktree and known caches, never / or a drive root),
    and the implementer and reviewer agent definitions carry the same rule (#356).

Added

  • Seats (A1): explicit seats: declarations on schemas and traits. A schema or trait can now
    declare a seats: list — named orchestration roles within a phase, each with an optional
    enters (the seat that transitions the item into that phase), after (an ordering hint), and
    reads_exclude (a blindness hint, parsed and served only — enforcement is a later phase). Note
    entries gain seat (the owning seat) and independent_of (also parsed/served only). A trait's
    dispatch.<phase>: block can carry a seats: sub-key with per-seat overrides
    (dispatch.<phase>.seats.<seat>: {agent?, model?, effort?}, an explicit agent: null clearing
    the phase default for that seat only). Seats are pure orchestration signals — the server parses,
    validates, merges, and serves them, but enforces nothing new. See
    config-format.md
    → "Seats (Trait & Schema Dimension)" for the full syntax, merge rules, and per-root layering.

    • Served as seats and dispatchBySeat on get_context (current phase only, flat per-seat
      profiles) and query_items(operation="schema") (all phases, per-phase-nested profiles), as
      seat/independentOf on note-schema entries, and as missingBySeat (required notes
      bucketed by owning seat, with a reserved unowned bucket for a note with no seat owner) on
      get_context's gateStatus, advance_item's gate-blocked failures, REST GET /items/{id}/gate, and REST POST /items/{id}/advance's 422 gate_blocked response. The
      seat field appears on every note-schema entry when the resolved schema is seat-aware
      (declares seats: on itself or a merged trait) — JSON null for an entry with no declared
      seat — and is omitted entirely for a seat-less schema; independentOf appears only on an
      entry that declares a non-empty list.
    • New REST route GET /api/v1/items/{id}/schema — the item's resolved schema view, identical to
      query_items(operation="schema", itemId=...)'s response body (both built by the same shared
      function). See api-rest.md → "GET /items/{id}/schema".
    • New features array (currently ["seats", "dispatchBySeat"]) on query_items(schema), GET /api/v1/info, and the .well-known service descriptor — advertised unconditionally so a
      client can tell an old server (which never emits these fields) apart from a new server whose
      current config simply declares no seats.
    • Two enters: true seats in the same phase, a duplicate seat name, a seat named the reserved
      unowned, or an after cycle now fail config load (global startup) or a per-root config push
      outright — nothing is stored. Malformed seat/dispatch-override entries and unknown keys warn
      and skip rather than failing the load.
    • Byte-identity preserved. A config with no seats: anywhere produces byte-identical
      MCP/REST responses to a pre-A1 server, with exactly one additive exception: the features
      field described above.
    • Upgrade note. A per-root config row stored before this upgrade that would now fail one of
      the new fatal seat checks (e.g. two enters: true seats in one phase) is not rejected
      retroactively — it is not re-validated just because the server upgraded. The server logs a
      WARN and falls back to the global config for that root until the row is re-pushed (at which
      point the new checks apply and the push is rejected if still invalid).
  • Independence attestation gate (A2): independent_of is now enforced, not just served. A
    top-level independence: {mode: off|warn|reject, require_verified: bool} config block (default
    warn/false) turns a note-schema entry's independent_of (A1) into a gate check: at
    start/complete (and the terminal/start cascade gates), the server compares the actor who wrote
    a declaring note against the actor(s) who wrote the seat(s) it names, and reports same_actor /
    missing_actor (fail-closed on an actor-less note) / unverified (with require_verified: true)
    findings. warn mode reports without blocking; reject mode blocks on any non-waived finding. A
    note may waive its own same_actor findings with a first body line of exactly
    independence: temporal-only, when the note's createdAt is strictly after every same-identity
    conflicting note's createdAt — the waiver never covers missing_actor/unverified. Findings
    are actor-free by construction ({key, seat?, constraint, conflictingSeat?, waived?} — never an
    actor id, proof, or claim) and are per-root honorable: a per-root independence block replaces
    the global one wholesale. See
    config-format.md
    → "Independence (A2)" for the full semantics, including an honest-limits section on what this
    gate does and does not catch.

    • Surfaced as violations on get_context's gateStatus, advance_item's success/failure
      results and cascadeEvents, complete_tree's applied/gate-failed entries, REST GET /items/{id}/gate's GateStatusDto, REST POST /items/{id}/advance's AdvanceResponseDto/
      CascadeEventDto/422 gate_blocked details.violations, and the plugin's SubagentStop
      phase-guard hook (which blocks only when the gate's missing notes are empty and the block is a
      reject-mode, non-waived, own-seat finding — warn-mode findings never block). Omitted
      entirely (not []/null) wherever the resolved schema declares no independent_of or mode is
      off; on every surface OTHER than gateStatus, also omitted (not sent as []) whenever the
      check ran and simply found nothing — gateStatus alone reports an explicit [] for that case.
    • No tool description or parameterSchema change on advance_item/get_context/complete_tree.
  • schema_resolution: legacy | layered | isolated — an opt-in, per-document top-level config key
    (AR-39) controlling how a root's per-root and global config layers combine for schema/tag
    lookup. Settable in the global config, in a per-root pushed document, or both; the effective mode
    per root is the per-root document's own key, else the global file's key (isolated in the
    global file is treated as layered, with a load warning), else legacy. Absent everywhere is
    legacy — no default flip, no behavior change for existing deployments beyond the tag-matching
    fix below. See config-format.md
    → "Global vs Per-Project Config".

  • GET /api/v1/roots/{rootId}/config/effective — a new, purely additive REST route returning every
    registered type's RESOLVED base schema against rootId's layered (per-root-over-global) config
    in one response, plus resolved traits, the status graph, and the effective schemaResolution
    mode. Uses a new "eff-<fingerprint>" ETag prefix, distinct from /config*'s and
    /roots/{rootId}/config's "cfg-" prefix.

  • Added an opt-in (iss, jti) replay cache for actor-authentication JWTs
    (actor_authentication.verifier.jti_replay_protection, default false). A per-MCP-call memo
    keeps a proof that is legitimately re-verified multiple times within one call (idempotency-key
    lookups, multi-transition batches) from tripping the cache as a false replay. (#366)

  • One-time startup compaction after the V17 actor-proof scrub. On the first Flyway-mode start
    after upgrading, the server now runs a one-time VACUUM + FTS5 shadow-table rebuild (with
    integrity checks) + WAL checkpoint, gated on PRAGMA user_version so it runs exactly once per
    database file. This reclaims free-page copies of pre-upgrade actor_proof values that V17's
    migration could not reach. Runs before the readiness marker is written; failures are WARN-logged
    and retried on the next boot, never fail startup. Opt out with DB_COMPACT_ON_UPGRADE=false to
    keep using the offline compaction runbook instead. (#365)

  • Rule text per project (A3): query_rules MCP tool + REST rules routes. Git-tracked,
    client-neutral operating rules (blind-authorship protocol, commit discipline, forbidden test
    patterns, review scoping, and more) are now servable as text, stored per project root as
    rule/<key> plan documents via the existing manage_plan_documents tool -- no new table, no
    migration. A note-schema entry's skill pointer doubles as a rule key: the new query_rules
    tool reads a rule directly (rootId+key) or resolves it through an item's effective,
    trait-merged schema (itemId+noteKey); query_items(operation="schema") stays pointer-only
    and never inlines rule bodies. rulesVersion on every response is the backing plan document's
    contentHash.

    • New REST routes GET /api/v1/roots/{rootId}/rules/{key} and GET /api/v1/roots/{rootId}/rules, converging on the same RuleService as the MCP tool (no REST
      item/skill-pointer mode). See api-reference.md
      and api-rest.md section 19a.
    • A rule/-prefixed plan-document body is capped tighter than the general 64 KiB plan-document
      limit, at 16384 bytes (16 KiB, UTF-8), enforced at the same PlanDocumentService.stash
      validation point both manage_plan_documents and the REST plan PUT route already share (MCP
      VALIDATION_ERROR / REST 413 payload_too_large).
    • create_work_tree now refuses a docRef.slug starting with rule/ -- rule documents are
      read-only via query_rules and cannot be adopted into a work item.
    • This repository's own initial rule set ships git-tracked under .taskorchestrator/rules/.
      The plugin's config-sync hook pushes those files into the per-root store automatically at
      session start (plugin 3.8.0, #375). See
      config-format.md
      → "Rule text".

Changed

  • features (on query_items(operation="schema"), GET /api/v1/info, and the .well-known
    descriptor) now also lists "rules", so MCP and REST-only clients can detect per-root rule text
    (query_rules, GET /api/v1/roots/{rootId}/rules[/{key}]) with one check.

  • create_work_tree's docRef and manage_plan_documents' body/bodyFromFile parameter
    descriptions now state the rule/ limits: a rule/ docRef slug is rejected, and a rule/<key>
    body is capped at 16 KiB.

  • features gained "independent_of" (A2): query_items(operation="schema"), REST GET /items/{id}/schema, GET /api/v1/info, and the .well-known service descriptor now advertise
    ["seats", "dispatchBySeat", "independent_of"] — independent_of was parsed and served but not
    enforced in A1, so it was deliberately absent from features then; A2 (above) now enforces it, so
    it joins the advertised list.

  • REST and the MCP tools now read per-root config through the SAME EffectiveConfigResolver
    instance (one per-root last-known-good cache, built once in ServerComposition) instead of each
    maintaining its own — REST previously built a separate PerRootConfigService/cache. This makes a
    transient per-root DB read failure less likely to surface as 503 config_unavailable, since a
    cache warmed by one surface now serves the other.

  • Each MCP tool invocation and REST config-resolving request now reads per-root config against one
    per-request/per-call config snapshot (ConfigSession) instead of re-reading it on every
    individual resolution step within that call. The snapshot also remembers a failed read: if a
    root's per-root config cannot be read cold (no last-known-good yet), every later lookup for that
    root within the SAME call (e.g. the rest of an advance_item/complete_tree batch) reports
    config_unavailable without re-reading; the next call reads fresh.

  • The global .taskorchestrator/config.yaml file is now parsed exactly ONCE per process, into a
    single shared GlobalConfigFile — the schema service, the status-label service, and the
    actor-authentication service all read from this one parsed document instead of each
    independently re-reading and re-parsing the file.

  • The legacy per-root and global-file YAML loaders (YamlStatusLabelService,
    YamlActorAuthenticationConfigService) now construct SnakeYAML's Yaml with an explicit
    SafeConstructor, matching the parser already used for manage_project_config/PUT /roots/{rootId}/config pushes. Defense in depth, not a vulnerability fix: SnakeYAML 2.x's
    default TagInspector already rejects arbitrary !!-tagged global tags, so this closes no
    exploitable gap in the shipped version — it removes reliance on that default holding across a
    future SnakeYAML upgrade.

  • YamlWorkItemSchemaService.getSchemaForType/getSchemaForTags (the library-level schema-service
    API) are now EXACT — they no longer fold an unmatched lookup onto a default schema themselves.
    The default fallback moved to LayeredConfig, which now applies it per the root's effective
    schema_resolution mode instead of unconditionally. Direct callers of this service (outside the
    config-resolution path) that relied on the implicit default fold must call getSchemaForType/
    getSchemaForTags on "default" themselves.

  • Actor-authentication and REST API JWTs are now capped at a maximum lifetime, default 24 hours
    (actor_authentication.verifier.max_token_lifetime_seconds / API_JWKS_MAX_TOKEN_LIFETIME_SECONDS).
    Upgrade note: a token whose exp - iat (or remaining exp - now) exceeds 24 hours is now
    REJECTED where it previously verified; raise the new setting before or immediately after
    upgrading if your issuer intentionally mints longer-lived tokens. (#366)

Fixed

  • manage_items: the top-level type and properties parameters were declared in the schema but
    never read on any operation, so a caller setting them got a silent no-op reported as success. Their
    schema descriptions now state they are ignored at the top level and must be set on each item instead
    (no behaviour change). A new CI guard (ToolParameterReadCoverageTest) fails when any registered
    tool declares a parameter that its source never reads, unless that parameter's own description
    explicitly qualifies it as ignored (#290).

  • The LEGACY global tag-matching step could report the first tag in an item's tag list as the
    "matched" tag whenever the global config defined a default schema — even when that tag had no
    schema of its own — because the probe folded a default-schema hit into "this tag matched". The
    probe now requires an EXACT tag match before reporting a match; this fix applies in every
    schema_resolution mode, not just legacy. Narrow, gate-relevant behavior change: an untyped or
    unmatched-type item with two or more tags, whose first tag has no schema of its own but whose
    config defines a global default, now resolves the first tag that DOES have an exact schema
    (previously it resolved default).

  • GET /api/v1/roots/{rootId}/config/effective's root lookup now returns 500 db_error for a
    repository failure other than not-found, instead of folding every lookup error into 404 not_found.

  • Fixed a malformed JWT claims set (e.g. a non-numeric exp/iat) being misreported as
    UNAVAILABLE/failureKind: network under DID trust, or REJECTED/failureKind: internal in
    static-JWKS mode; both now report REJECTED/failureKind: claims. (#366)

  • Fixed SQLITE_BUSY_SNAPSHOT on concurrent writers: every SQLite transaction now begins IMMEDIATE
    (writer lock acquired at BEGIN) instead of the driver default DEFERRED, so a transaction that reads
    then writes (e.g. the plan-document upsert behind PUT /roots/{rootId}/plans/{slug}) queues on
    DATABASE_BUSY_TIMEOUT_MS instead of failing immediately when another connection commits a write
    after its snapshot started. The JDBC URL is unchanged.

Security

  • Bumped Ktor 3.3.3 to 3.4.3 and the MCP Kotlin SDK 0.12.0 to 0.13.0 (the SDK is compiled against
    one Ktor minor, so the two move together). This closes CVE-2026-68762 (JetBrains Ktor before
    3.4.1, potential denial of service via WebSocket decompression; fixed upstream in 3.4.1): the
    server jar previously bundled ktor-websockets and ktor-server-websockets 3.3.3, pulled in
    transitively by the SDK, and now bundles 3.4.3. The server never installs the Ktor WebSockets
    plugin, so the vulnerable path is not believed to have been reachable. SDK 0.13.0 adds its own
    DNS-rebinding guard on /mcp, on by default and loopback-only; it is turned off in favour of the
    server's existing Host allowlist (MCP_ALLOWED_HOSTS), which stays the single Host authority for
    /mcp, /api/v1 and /.well-known. No behaviour change for clients.

Removed

  • BREAKING (REST): ?include=proof and API_REDACT_ACTOR_PROOF, deprecated in 3.15.0.
    ?include=proof is now ignored like any unknown include value (no more Warning: 299 header);
    API_REDACT_ACTOR_PROOF is no longer read (no startup WARN). actor.proof is removed from the
    ActorClaimDto schema (it was already never sent). Admins read proof evidence via
    verification.proof. (#362)