Repository navigation
Plugin
- Bumped plugin version to 3.9.0 — the
initskill and personal root, bundled process rules,
the project-levelclient.json, and therun-wavefront door with theimplement-wave,
review-wave,auditandretro-analysisworkflows (new skill, workflows and agents: minor). - Project init and personal root. New
/task-orchestrator:initskill sets up a project root and config for the current directory, or with--usera 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 theConfig: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=offsilences it). Config discovery also checks the main checkout of a linked worktree and the user-level file, withTASK_ORCHESTRATOR_HOMEandTASK_ORCHESTRATOR_CEILINGfor isolation, and the REST URL resolves fromTASK_ORCHESTRATOR_API_URL, then a project-levelclient.jsonbeside 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-levelclient.json(written byinit --user). The hooks ignore aproject.rootIdthat 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 torun-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 promptappends a METHOD B RETURN block for plans markedmeta.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.nextdefaults atest-authorseat whose dispatch names no agent totask-orchestrator:test-authorunder 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-authorrule, thetask-orchestrator:test-authoragent and the projecttest-authorskill now state that a seat in a shared worktree never runsgit stash,git checkout --,git restoreorgit reset; the revert that observes red runs in a detached scratch copy that is removed afterwards, andtest-manifestrecords 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-wavepost-run and both output styles state that re-review after fixes goes to a freshtask-orchestrator:reviewerseat and that orchestrator confirmations go under their ownorchestrator-confirmationkey; theenforce-actor-attributionhook 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-waveworkflow (B1). New Claude Code Workflow scriptworkflows/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 viaquery_rules(never embedded), and ascanDeclarationsstep between
the extractor and the author. Never call it bare — it returns{started:false}; therun-wavefront door
(B2) builds itsargs. Ships with two new agent definitions,task-orchestrator:plannerand
task-orchestrator:test-author, and a fake-agent test harness (scripts/tests/workflow-harness.mjs).auditworkflow (Phase C / C2). New Claude Code Workflow scriptworkflows/audit.js, generalized from the
September architecture review: presetsquick|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-itemmaterializes after a human confirms (new section
"From a workflow findings proposal"). Cost is projected up front and refused abovemaxAgentsunlessallowLarge.retro-analysisworkflow (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 returnedsummary); the 24 h window usesmodifiedAfter.- Test harness extension
scripts/tests/workflow-harness-ext.mjs(loadCoreNamed,autoAgent,assertBarrier,
freeRuntimeIds,ruleWindowHits) for workflow scripts beyond implement-wave. review-waveworkflow (Phase C / C1). New Claude Code Workflow scriptworkflows/review-wave.jsthat 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 validatedreview-wave/args-v1stages, hand-built by the orchestrator until the
run-wavefront 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-wavefront door (Phase B / B2+B3). New plugin skill/task-orchestrator:run-wavethat plans a wave of ready
items with therun-plannerhelper (scripts/run-planner.mjs:probe|plan|explain|validateover 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 theimplement-waveargs), 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-metadataprovenance line and advances items (verify|actors|provenance|review-prompt). Run plans and
state persist asrun/<runId>plan documents so a run survives turns and compaction./implementnow routes Delegated waves with two or more ready items, and every Parallel wave, throughrun-wave;
the dispatch-contract template fetches its rule slots by key (new served rulesdeclarations-extractorand
contract-change-sweep);WORKTREE.mddispatch examples read the resolved seat profile;.taskorchestrator/ run-profile.jsoncarries this repo's verify commands and search scope;workflowSizeGuideline: largeis set.session-retrospectivereads the structured per-seat provenance line (seats=<seat:model,...>) alongside the
legacy one-model-per-item form.- Fixed
config-sync.mjspushing all.taskorchestrator/rules/*.mdPUTs in parallel (Promise.all), which raced
the server's SQLite writer and produced one success plus N-1db_errorfailures per session; rule PUTs now go
sequentially, one at a time.run-planner'svalidateSnapshotnow requires a top-levelprobeobject (the
verbatimprobesubcommand output), matchingchooseEntry's andplan's existing reliance on it; a snapshot
missingprobepreviously planned silently pre-entered instead of failing validation.references/snapshot.md
documentsprobenext toprofile. - Fixed four
implement-wave.js/run-exec-lib.mjsbugs found in the B1d live
acceptance runs (L1–L4): (1)lockKeysForno 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 singleworktree:<path>key instead of none. (2)mapStageResultnow
catches aplanner-v1envelope that reportsproceed:truewithmainFiles/docFiles/testFiles
all empty while the stage owns required notes, mapping it tostopped/envelope-mismatchinstead
of silently proceeding with nothing for downstream seats to act on; the planner's seat prompt also
now says to emitStructuredOutputexactly once. (3) An implementer stage on an item with no
test-author-v1seat now has the planner'stestFiles/existingTestEdits[].filefolded into both
its lock keys and its "owned files" set inrun-exec-lib.mjs'sverify— previously those tests
were locked and owned by nobody, soverifyreported 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 newisNoneSentinelhelper 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'sauditActorsgained an optionalopts.result(and theactorsCLI subcommand
a--resultflag) so a Step-2 actor audit no longer flags notes from stages that never ran (deferred
or missing from the result) asmissing— those items instead reportskipped: 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 theimplementerandrevieweragent definitions carry the same rule (#356).
Added
-
Seats (A1): explicit
seats:declarations on schemas and traits. A schema or trait can now
declare aseats: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 gainseat(the owning seat) andindependent_of(also parsed/served only). A trait's
dispatch.<phase>:block can carry aseats:sub-key with per-seat overrides
(dispatch.<phase>.seats.<seat>: {agent?, model?, effort?}, an explicitagent: nullclearing
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
seatsanddispatchBySeatonget_context(current phase only, flat per-seat
profiles) andquery_items(operation="schema")(all phases, per-phase-nested profiles), as
seat/independentOfon note-schema entries, and asmissingBySeat(required notes
bucketed by owning seat, with a reservedunownedbucket for a note with no seat owner) on
get_context'sgateStatus,advance_item's gate-blocked failures, RESTGET /items/{id}/gate, and RESTPOST /items/{id}/advance's422 gate_blockedresponse. The
seatfield appears on every note-schema entry when the resolved schema is seat-aware
(declaresseats:on itself or a merged trait) — JSONnullfor an entry with no declared
seat — and is omitted entirely for a seat-less schema;independentOfappears 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). Seeapi-rest.md→ "GET /items/{id}/schema". - New
featuresarray (currently["seats", "dispatchBySeat"]) onquery_items(schema),GET /api/v1/info, and the.well-knownservice 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: trueseats in the same phase, a duplicate seat name, a seat named the reserved
unowned, or anaftercycle 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: thefeatures
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. twoenters: trueseats in one phase) is not rejected
retroactively — it is not re-validated just because the server upgraded. The server logs a
WARNand 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).
- Served as
-
Independence attestation gate (A2):
independent_ofis now enforced, not just served. A
top-levelindependence: {mode: off|warn|reject, require_verified: bool}config block (default
warn/false) turns a note-schema entry'sindependent_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 reportssame_actor/
missing_actor(fail-closed on an actor-less note) /unverified(withrequire_verified: true)
findings.warnmode reports without blocking;rejectmode blocks on any non-waived finding. A
note may waive its ownsame_actorfindings with a first body line of exactly
independence: temporal-only, when the note'screatedAtis strictly after every same-identity
conflicting note'screatedAt— the waiver never coversmissing_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-rootindependenceblock 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
violationsonget_context'sgateStatus,advance_item's success/failure
results andcascadeEvents,complete_tree's applied/gate-failed entries, RESTGET /items/{id}/gate'sGateStatusDto, RESTPOST /items/{id}/advance'sAdvanceResponseDto/
CascadeEventDto/422 gate_blockeddetails.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 noindependent_ofor mode is
off; on every surface OTHER thangateStatus, also omitted (not sent as[]) whenever the
check ran and simply found nothing —gateStatusalone reports an explicit[]for that case. - No tool description or
parameterSchemachange onadvance_item/get_context/complete_tree.
- Surfaced as
-
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 (isolatedin the
global file is treated aslayered, with a load warning), elselegacy. Absent everywhere is
legacy— no default flip, no behavior change for existing deployments beyond the tag-matching
fix below. Seeconfig-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 againstrootId's layered (per-root-over-global) config
in one response, plus resolved traits, the status graph, and the effectiveschemaResolution
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, defaultfalse). 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-timeVACUUM+ FTS5 shadow-table rebuild (with
integrity checks) + WAL checkpoint, gated onPRAGMA user_versionso it runs exactly once per
database file. This reclaims free-page copies of pre-upgradeactor_proofvalues 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 withDB_COMPACT_ON_UPGRADE=falseto
keep using the offline compaction runbook instead. (#365) -
Rule text per project (A3):
query_rulesMCP 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 existingmanage_plan_documentstool -- no new table, no
migration. A note-schema entry'sskillpointer doubles as a rule key: the newquery_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.rulesVersionon every response is the backing plan document's
contentHash.- New REST routes
GET /api/v1/roots/{rootId}/rules/{key}andGET /api/v1/roots/{rootId}/rules, converging on the sameRuleServiceas the MCP tool (no REST
item/skill-pointer mode). Seeapi-reference.md
andapi-rest.mdsection 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 samePlanDocumentService.stash
validation point bothmanage_plan_documentsand the REST plan PUT route already share (MCP
VALIDATION_ERROR/ REST413 payload_too_large). create_work_treenow refuses adocRef.slugstarting withrule/-- rule documents are
read-only viaquery_rulesand cannot be adopted into a work item.- This repository's own initial rule set ships git-tracked under
.taskorchestrator/rules/.
The plugin'sconfig-synchook pushes those files into the per-root store automatically at
session start (plugin 3.8.0, #375). See
config-format.md
→ "Rule text".
- New REST routes
Changed
-
features(onquery_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'sdocRefandmanage_plan_documents'body/bodyFromFileparameter
descriptions now state therule/limits: arule/docRef slug is rejected, and arule/<key>
body is capped at 16 KiB. -
featuresgained"independent_of"(A2):query_items(operation="schema"), RESTGET /items/{id}/schema,GET /api/v1/info, and the.well-knownservice descriptor now advertise
["seats", "dispatchBySeat", "independent_of"]—independent_ofwas parsed and served but not
enforced in A1, so it was deliberately absent fromfeaturesthen; 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 inServerComposition) instead of each
maintaining its own — REST previously built a separatePerRootConfigService/cache. This makes a
transient per-root DB read failure less likely to surface as503 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 anadvance_item/complete_treebatch) reports
config_unavailablewithout re-reading; the next call reads fresh. -
The global
.taskorchestrator/config.yamlfile is now parsed exactly ONCE per process, into a
single sharedGlobalConfigFile— 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'sYamlwith an explicit
SafeConstructor, matching the parser already used formanage_project_config/PUT /roots/{rootId}/configpushes. Defense in depth, not a vulnerability fix: SnakeYAML 2.x's
defaultTagInspectoralready 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 adefaultschema themselves.
Thedefaultfallback moved toLayeredConfig, which now applies it per the root's effective
schema_resolutionmode instead of unconditionally. Direct callers of this service (outside the
config-resolution path) that relied on the implicitdefaultfold must callgetSchemaForType/
getSchemaForTagson"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 whoseexp - iat(or remainingexp - 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-leveltypeandpropertiesparameters 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 adefaultschema — even when that tag had no
schema of its own — because the probe folded adefault-schema hit into "this tag matched". The
probe now requires an EXACT tag match before reporting a match; this fix applies in every
schema_resolutionmode, not justlegacy. 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 globaldefault, now resolves the first tag that DOES have an exact schema
(previously it resolveddefault). -
GET /api/v1/roots/{rootId}/config/effective's root lookup now returns500 db_errorfor a
repository failure other than not-found, instead of folding every lookup error into404 not_found. -
Fixed a malformed JWT claims set (e.g. a non-numeric
exp/iat) being misreported as
UNAVAILABLE/failureKind: networkunder DID trust, orREJECTED/failureKind: internalin
static-JWKS mode; both now reportREJECTED/failureKind: claims. (#366) -
Fixed
SQLITE_BUSY_SNAPSHOTon concurrent writers: every SQLite transaction now beginsIMMEDIATE
(writer lock acquired atBEGIN) instead of the driver defaultDEFERRED, so a transaction that reads
then writes (e.g. the plan-document upsert behindPUT /roots/{rootId}/plans/{slug}) queues on
DATABASE_BUSY_TIMEOUT_MSinstead 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 bundledktor-websocketsandktor-server-websockets3.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/v1and/.well-known. No behaviour change for clients.
Removed
- BREAKING (REST):
?include=proofandAPI_REDACT_ACTOR_PROOF, deprecated in 3.15.0.
?include=proofis now ignored like any unknown include value (no moreWarning: 299header);
API_REDACT_ACTOR_PROOFis no longer read (no startup WARN).actor.proofis removed from the
ActorClaimDtoschema (it was already never sent). Admins read proof evidence via
verification.proof. (#362)