Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.24.1 - 2026-09-17
Added
The dashboard and
GET /api/v1/statenow disclose, by reason, how many sessions the token and cost totals leave out:running_unreportedfor a running session that has not reported usage yet,running_non_reportingfor a running session whose agent reports no usage at all,unmeasured_sessionsfor an already-ended session whose usage was never recorded, andcost_unpriced_runningfor a running session left out ofEst. Costbecause no rate is configured for its agent. The dashboard’s footer note for each reason now reads correctly for a single session instead of always using the plural. The already-ended count survives a restart and, after the upgrade, also includes earlier sessions that ended without recorded usage, back to v1.19.0. (#1066)Agents that Sortie starts on a remote host through
worker.ssh_hostscan now receive environment variables from Sortie’s own environment: list their names under the newworker.ssh_pass_envsetting. A variable named under the newworker.ssh_disallow_pass_envsetting is one Sortie never sends, so the value or login the remote host holds stays in effect; anssh_configof your own that lists the variable underSendEnvstill forwards it, which Sortie cannot suppress. Both settings take variable names literally: an entry written as a$VARreference is ignored, and the warning that reports it gives the entry’s position, never its value. (#1048)
Fixed
A coding-agent subprocess that spawns a descendant inheriting its standard output handle no longer wedges the turn, leaks the process, or loses buffered standard error, across the
claude-code,copilot-cli,kiro, andopencodeagent kinds. Thecodexapp-server adapter no longer discards the runtime’s last messages when it reaps the subprocess, and now also ends a turn or a session within a bound when the runtime dies while a descendant still holds the output handle. Theagent-client-protocoladapter takes the same subprocess pipe ownership and releases those pipes as the last step of its teardown, and now also ends a turn or a session within a bound when its runtime exits while a descendant still holds the output handle. (#982, #1084, #1081)The
codexagent kind now reports what the agent runtime wrote to its standard error, so a session that fails during the handshake, or a turn that ends because the runtime stopped producing output, carries the runtime’s own error message rather than an exit status alone. Wherever an agent kind reports a local runtime’s standard error, output captured before collection has to be cut short now survives alongside the notice that it is incomplete, instead of being replaced by it. (#1083)A
codexoragent-client-protocolsession no longer stalls, fails, or leaves the agent without a reply when the agent sends a burst of output or stops reading its input. A burst no longer holds acodexturn untilagent.stall_timeout_msoragent.turn_timeout_msends it, failscodexauthentication, or times out the start of anagent-client-protocolturn or resumed session. An agent that stops reading no longer holds a turn pastagent.turn_timeout_msor a stop past its graceful period, and acodexsession start gives up on an unanswered request afteragent.read_timeout_ms. Acodexsession also logs an MCP server that fails to start while the session is starting and answers every request the agent sends, and stopping anagent-client-protocolsession answers the agent’s open permission requests before closing its input, so the agent can exit within the graceful period instead of being force-terminated. (#1087)The
kiro,copilot-cli, andopencodeagent kinds, workspace hooks, the reaction triage command, and the self-review phase no longer wait indefinitely, or misreport a successful exit as a failure, when a process the command started outlives it and holds its output open. The handoff-evidence check no longer waits indefinitely for such a process either; one that escapes the command’s process group entirely still leaves the check without the command’s full output, and the check reports the inspection as failed. On Windows, a process theclaude-code,copilot-cli,kiro,opencode,codex, oragent-client-protocolruntime starts no longer survives the session or turn that started it. A Windows launch Sortie cannot place under process-group containment still runs, the failure is logged, and only the command’s own process is terminated when it ends. (#1080)Sessions that end while Sortie is under load no longer undercount the tokens spent during self-review. A session that starts on an issue right after the previous one ended no longer inherits that session’s token usage or self-review progress, which counted the same tokens twice against
agent.max_tokensand could stop work on the issue before its budget was spent. (#1077)The
cost_budgetagent tool now counts the tokens the running session has already spent, soused_tokensandremaining_tokensreflect that spend againstagent.max_tokenswhile the session is still working, andused_tokens_completestaysfalseuntil that spend has been recorded. Previously the running session was left out, so the first session on an issue read zero spend throughout. (#1074)If an agent runtime reports token usage for a session shown as reporting no token usage, Sortie now ignores it instead of adding it to the dashboard’s token and cost totals, the JSON API’s totals, and
sortie stats, and counting it againstagent.max_tokens. Sortie logs one warning per run naming the agent kind when it ignores such a report. (#1066)An
opencodeturn that is cancelled, stopped byagent.read_timeout_ms, or fails while reading the agent’s output now records the token usage the agent reports, so that spend counts in the token and cost totals and againstagent.max_tokens. Anopencodesession whose model reports zero tokens is now recorded as having spent nothing, instead of being counted among sessions whose usage was never recorded. (#1078)The dashboard’s Turns column and
turn_countinGET /api/v1/statenow advance with every turn of acodex,opencode, orkirosession, self-review turns included. They previously stayed at1forcodexandopencode, and at0forkiro, however many turns the session ran. (#1065)An integer setting in
WORKFLOW.md, or itsSORTIE_*override, set to a number too large or too small for Sortie to hold, such asagent.turn_timeout_ms: 99999999999999999999, is now rejected with a message naming the setting and the range an integer setting accepts. Previously the message named an unrelated fault, such as the value having to be greater than 0, or the setting silently took a different value, so a very largeagent.max_turnscould limit every session to one turn. A workflow that loaded with such a value, including one inhooks.timeout_msoragent.max_concurrent_agents_by_state, now fails to load until the value is within range. (#1038)The
KIRO_API_KEYof a remotekiroagent and theCODEX_API_KEYof a remotecodexagent no longer appear in the process list of the machine running Sortie or of the remote host. A remotecodexagent no longer findsCODEX_API_KEYin its environment; Sortie still signs the agent in with that key, as it does on a local launch. (#1048)A workflow that sets
worker.ssh_hostsno longer warns at startup thatworker.max_concurrent_agents_per_host,worker.ssh_pass_env, orworker.ssh_disallow_pass_envhas no effect. Those settings took effect from the first poll onward; only the startup message was wrong. (#1048)An agent Sortie starts on a remote host now runs only after it has entered the workspace directory and delivered the environment variables the launch carries. An
agent.commandcontaining a shell operator such as||or;could previously run part of itself even when neither of those steps succeeded, starting the agent in the wrong directory or without the variables it was to receive. Anagent.commandthat ends in;or&, and one written as acommand: |block, now also receive the arguments Sortie passes the agent; the remote shell previously ran the first of those arguments as a command of its own, and the launch failed without ever starting the agent properly. Sortie waits for the agent it starts and talks to it, so a command ending in&detaches the agent and the session cannot work. (#1048)A retry that came due while Sortie was stopped now starts on one of the hosts
worker.ssh_hostsnames and carries the variablesworker.ssh_pass_envnames, even when it is the first work Sortie dispatches after the restart. Such a retry could previously start on the machine running Sortie, or start remotely without those variables, because it could be dispatched before Sortie had read theworkersettings for the first time. (#1048)Cancelling a local agent process now terminates every process still in its process group when graceful shutdown expires, instead of leaving descendants running against the workspace. (#1035)
webhooknotifications now carry adispatch_id, identifying the agent run that sent them, andsession_idnow carries the agent’s own session ID once the agent reports one, instead of staying empty for the whole run. (#1104)Sortie no longer writes through a symbolic link placed in a workspace’s
.sortiedirectory, and a run whose.sortiedirectory is itself a symbolic link now fails before the agent starts. (#1104)
Changed
On Linux and macOS, a process that a workspace hook, the reaction triage command, or a self-review verification command leaves running is now terminated when the command exits, matching what Windows hooks already did; a verification command’s leftover processes on Windows are now terminated too. A service meant to outlive the command now has to start through a supervisor, which the workflow reference documents per platform, and hooks and the reaction triage command now receive
XDG_RUNTIME_DIRandDBUS_SESSION_BUS_ADDRESSso they can reach the user’s service manager. Two log messages are new:leftover processes terminated after the command exited, logged when that termination reached a process the command left behind, andsubprocess group termination failed after the launch returned, logged whenever Sortie cannot confirm that a command’s or an agent session’s processes are gone. On Windows, the warningshook process tree did not settle,hook job object creation failed; child tree may survive timeout, andhook process resume failedare renamedsubprocess tree did not settle,process group assignment failed, andprocess resume failedand now cover launches other than hooks, whilehook job termination after wait failed; drain may not settle,hook job accounting query failed; drain skipped, andhook job processes still active after drain deadlineare no longer logged; an alert built on any of the old text stops matching. (#1080, #1099)A remote
claude-codeorcopilot-cliagent now receives the credential variables its agent kind reads when Sortie’s environment sets them:ANTHROPIC_AUTH_TOKEN,ANTHROPIC_API_KEY, andCLAUDE_CODE_OAUTH_TOKENforclaude-code, andCOPILOT_GITHUB_TOKEN,GH_TOKEN, andGITHUB_TOKENforcopilot-cli. Such a variable takes precedence over a login stored on the remote host, so if Sortie’s environment sets one for another purpose,GITHUB_TOKENfor the tracker for example, while your hosts sign in on their own, name it underworker.ssh_disallow_pass_env. A remote host now needs the standardddutility for any launch that sends a variable, which includes every remoteopencodelaunch; a launch on a host without it fails and logs a message saying so. (#1048)
Migrations
- Add
dispatch_id TEXT NOT NULL DEFAULT ''tosession_metadata, naming the dispatch whose running session last recorded the row; it is empty once that session has ended. A pre-migration row reads back empty, socost_budgetnever counts it as a running session’s spend. (#1074)
1.24.0 - 2026-09-11
Added
The new
agent-client-protocolagent kind runs Agent Client Protocol-compatible runtimes over stdio and resumes previous sessions when supported by the runtime. Locally launched runtimes can use workflow-configured MCP servers, while requests requiring human input are refused or end the attempt rather than waiting indefinitely. Token-based budgets do not apply because the protocol does not report token usage; when the runtime itself ends a turn at its own token limit the claim is released rather than retried into the same limit, while a turn that exhausts the runtime’s request or turn budget is retried on a fresh session. Stopping a session closes it through the protocol when the runtime advertised that capability at handshake. A runtime running over SSH gets no other clean close, because the termination signal reaches the local relay rather than the runtime itself. Gemini CLI is the first runtime published on this route, with a ready-to-copy sample workflow atexamples/WORKFLOW.agent-client-protocol.md; the sample sets the runtime’s own workspace-trust and tool-approval switches, which an unattended run needs and which the sample states the cost of. (#976, #1010, #1012, #1023)A new
agent.stop_grace_msfield, default5000, sets the period an adapter waits for an agent to exit on its own before it force-terminates the process group, and now reaches every adapter kind that has such a period instead of leaving it fixed at a five-second built-in constant. The orchestrator’s per-session stop deadline no longer followsagent.read_timeout_msat all: it derives fromagent.stop_grace_msalone. The shutdown-drain ceiling now derives from the same field, which lengthens the worker drain at the default configuration from 30 s to 50 s. A stop against an agent that ignores its termination signal now runs the full teardown before force-termination, so at the default configuration it can hold a worker or a shutdown for up to 20 s where the previous release gave it 5. (#1006, #1014)The install scripts for macOS, Linux, and Windows now warn when the
sortiecommand still resolves to a different copy than the one just installed, naming that path. An older binary earlier inPATH, left by an install run as root or by the Homebrew cask, previously kept winning while the installer reported success. The macOS and Linux script also gains-f,--force, which reinstalls a release already present in the target directory instead of skipping it. (PR #1046)Kiro CLI is now published on the
agent-client-protocolroute, with a ready-to-copy sample workflow atexamples/WORKFLOW.agent-client-protocol.kiro.md. This route delivers Sortie’s own tool servers and session continuation, neither of which the nativekirokind delivers; both kinds stay available and neither retires the other. The runtime spells workspace trust and tool-approval posture with a single switch,-a, which the sample sets and states the cost of. One credential caveat decides whether the route is worth taking at all: withKIRO_API_KEYthe runtime’s backend refuses it a governance profile and the runtime then disables tool servers for the session, silently and with no error Sortie can see, so a stored device login on the machine running Sortie is what makes Sortie’s tools reach the agent. (#989)The dashboard’s running-session panel now states whether a session’s token figures arrive during each turn, only when a turn ends, or never, and whether they attribute to a model or to the session as a whole. A new
Usage reportingrow says it once, and the Model, API Requests, Tokens, and Est. Cost rows below no longer show a zero for a request count nobody measured or a token total that silently omits the turn still running.kiroandagent-client-protocolsessions report no token usage at all, andcopilot-clireports none when it runs over SSH.sortie validategains two warnings for that case,agent.kind.no_usage_reportingandagent.kind.no_cost_estimate, raised whenagent.max_tokensor atoken_ratesentry is configured against a kind whose sessions report nothing. Each running entry in the JSON API carries the same facts in four new fields:usage_arrival,usage_attribution,tokens_pending, andapi_requests_measured. The JSON API and the persisted session metadata row now carry the same distinction for the request count, the JSON API and thesortie_statustool carry it for the token figures, and the dashboard also catches a session whose runtime declared per-request figures and then stopped delivering them, which the declaration alone could not see. (#1059, #1061)A running session’s token spend is now checked against
agent.max_tokensas usage arrives, not only at the next dispatch decision, so a run that crosses the ceiling mid-turn is stopped in flight instead of running to completion before the ceiling ever gets a chance to hold it. A run stopped this way recordsbudget_stoppedin the run history and increments a newsortie_runs_stopped_by_budget_total{reason}counter; the existing budget-hold tracker comment now also states when a hold followed a session stopped this way. (#1062)
Fixed
A string-typed adapter configuration key whose value carries another YAML type, such as
tracker.endpoint: 123,agent.kind: 123, or a mistypedclaude-code.model, is now rejected with a diagnostic naming the key and the type found, instead of being silently coerced to the empty string and then treated as absent or defaulted to the adapter’s own default. A workflow that previously started with such a value now fails at config load, at adapter construction, or offline throughsortie validate, whichever reads the key first; the fix is to quote the value or remove the key. (#911)A turn whose output handle is held open by a surviving descendant process no longer leaks its process group and no longer withholds its outcome: the wait for that output is now bounded, and the turn’s reap, process-group cleanup, and result publication all still run once that bound expires. On
kiro, such a turn is reported as a failure, because its success evidence lives on that output and could not be read. (#918)copilot-cliandcodexruns now reportmodel_nameand a per-model request count, carried on thetoken_usageevents each adapter already emits, instead of leaving both fields blank for every run on those two adapter kinds.kiro’s inability to report an effective model is now documented rather than left as an unexplained blank. (#972)Cancelling a
codexrun now shuts down the whole process tree the agent started, instead of force-killing the agent alone and leaving its child processes running against the workspace. The agent first receives a catchable termination signal and a bounded grace period to exit on its own, so a runtime that flushes state on a clean exit reaches that path. Stopping a run already behaved this way; cancelling one did not. (#1013)A self-review verification command now stops the process it started when the command exceeds
self_review.verification_timeout_msor the run is cancelled. Previously only the shell wrapping the command was killed, so a build or test suite kept running against the workspace after the run had moved on, and a run cancelled rather than timed out left it running with nothing to stop it. (PR #1020)Stopping a
codexsession now honors the caller’s deadline, as every other agent kind already did.StopSessionignored the deadline it was given, so a stop asked to finish sooner than the configured stop grace waited out the whole grace anyway and then reported success. It now ends the graceful phase when the deadline expires, force-terminates the process group, and reports the deadline back to its caller. (#1014)A
brewcommand that loads the sortie tap no longer prints Homebrew’s deprecation warning for theverifiedparameter in the cask’surlstanza. Homebrew does not honor that parameter and verifies cask download URLs through its own default behavior instead. (#1036)copilot-cliandclaude-codeno longer reportturn_failedfor a turn that produced assistant output or completed tool activity but carried no per-message output-token count or terminal result event; work evidence now comes from a shared observer reading model-authored content and tool-call activity on each adapter’s own turn stream, rather than a runtime-specific token count. Onkiro, a non-blank stdout line is now, alongside the credits trailer, a positive success signal, so a zero exit with an authentication marker on stderr and a non-blank stdout line now reportsturn_completedinstead ofturn_failed. (#1060)A locally launched
copilot-clisession now reports the model behind its token figures, because each turn’s recovered usage figure arrives as one usage report naming it.model_namenow appears in the JSON API and the persisted session record for such a run,usage_attributionnow readsper_model, and the dashboard Model row now names the model instead of saying the figures are not attributed to one. A run over SSH reads no session journal, so it reports no token figure and no model, as before. (#1073)
Changed
A second interrupt (Ctrl-C) during shutdown now ends every remaining shutdown wait at once, instead of being silently discarded until shutdown finishes on its own. Each abandoned wait logs a warning naming what was given up. (#1014)
The macOS and Linux install script now checks for the commands it needs to download and verify a release,
uname,tar,curlorwget, andsha256sumorshasum, before it fetches anything, and names every missing one in a single message. A missingsha256sumorshasumpreviously surfaced only after the release archive had already been downloaded. (PR #1046)A protocol session that fails to start now reports the runtime’s own standard error at
Warn, where it previously reachedDebugonly. A runtime that exits before answering the handshake, which is what a missing or rejected credential looks like on this route, reported justagent connection ended before respondingand discarded the runtime’s own explanation of why. (#989)On
/api/v1/stateand/api/v1/{identifier}, a running row’sapi_request_countand the four members of itstokensobject are nownullwhen no measurement produced them, where each was previously an integer reading0;requests_by_modelis absent on the same condition. Thesortie_statustool nulls its own four token figures on that condition too and gains atokens_measuredfield beside them, so an agent prompt that parses that response may need revising. A session with a turn still in flight keeps reporting the figures it has. (#1061)
Migrations
- Add
api_requests_measured INTEGER NOT NULL DEFAULT 0tosession_metadata; a pre-migration row reads back as unmeasured, because its request count was recorded before anything qualified it, and the row self-heals the next time that issue runs. The default is the opposite ofrun_history.tokens_measured’s, which reads back as measured, becauserun_historyis an append-only record no later run can correct. (#1061)
1.23.0 - 2026-08-31
Added
An agent can now write
no-change-neededto.sortie/statusto declare that the requested outcome already held and it changed nothing. Such a run is recorded as a success rather than a failure, schedules no retry, and does not count towardagent.max_consecutive_absences, so an issue that repeatedly needs no change is no longer parked for it. A newtracker.no_change_state, also settable asSORTIE_TRACKER_NO_CHANGE_STATE, names the state the issue moves to; unset, it istracker.handoff_state. It is the one state field allowed to name a member oftracker.terminal_states, for a board where a handoff carrying no pull request and no diff would put nothing in front of a reviewer. Where self-review runs, the declaration stands only where that phase confirms it: a failing verification command, or a run that needed a fix turn, withdraws the declaration and the run continues on the ordinary path. A run that produces nothing and declares nothing is still an absence of work and keeps every outcome it had. (#889)sortie validatenow reports a new error,dispatch.agent.missing_block, for a kind named bydispatch.default.agentordispatch.rules[*].agentthat differs from the top-levelagent.kindand carries no top-level settings block of its own;agent.kinditself is never affected. An empty block, or a bare key with nothing following, satisfies the requirement. A deployment whoseWORKFLOW.mdalready routes a dispatch rule ordispatch.defaultto such a kind will refuse to start until the block is added. (#929)reactions.ci_failure,reactions.review_comments,reactions.bot_review, andreactions.merge_conflictsnow accept an optionaltriageblock, naming ascriptto run in the issue workspace when the reaction fires and before any agent starts, plus an optionaltimeout_ms(default60000, maximum600000). The script learns which reaction fired fromSORTIE_REACTION_KIND, reads the details from the JSON file named bySORTIE_REACTION_INPUT, and writeshandled,dispatch-agent, orescalateto the file named bySORTIE_REACTION_RESULT.handledcloses the reaction without starting an agent, so deterministic work such as a clean rebase or a re-run of a known-flaky job costs no agent session, no continuation attempt, and no tokens;escalateapplies the kind’s configuredescalationright away;dispatch-agentstarts the agent exactly as before. A timeout, a non-zero exit, a malformed answer, or a missing workspace logs one warning and falls back todispatch-agent, so a broken script cannot strand the work. The block is a configuration error under any other reaction key, and a workflow that sets notriageblock is unaffected. (#959)
Fixed
reactions.ci_failure.watch_window_msnow rejects a value above9223372036854(about 292 years) instead of converting it to a window of a fraction of a millisecond or to no bound at all.0still means no time limit. A deployment currently carrying a larger value is refused at startup and bysortie validateuntil the value is lowered. (#956)On Windows, a hook’s background child no longer escapes the timeout’s process-tree kill by starting before the hook process joined the Job Object that the kill targets. The hook subprocess is now created suspended and only allowed to run once it is a member, closing the window in which such a child survived termination and kept the workspace directory locked. A hook whose Job Object cannot be created still runs, as it did before, and the failure is logged. (#883)
A
codexturn that Sortie already cancelled is now recorded as cancelled, not as a success, when the runtime’s completion notification for that turn arrives afterward, whatever status the runtime reports. The turn no longer counts toward the run’s completed turns. (#916)A
copilot-cliworkflow that set onlydenied_tools,available_tools, orexcluded_toolsno longer loses the blanket approval grant. Previously any one of those three keys, likeallowed_tools, dropped--allow-allfrom the launch, so every permissioned call was denied without a prompt while the process still exited 0 and the turn was recorded as a success that changed nothing. The three keys now compose with the grant instead, so a workflow setting one of them again runs with file-path verification and URL approval disabled, the posture an unscopedcopilot-cliworkflow already has.allowed_toolsstill replaces the grant, because it is an approval allow-list the grant would otherwise subsume and defeat. The validation check for this is renamed fromcopilot-cli.tool_scoping.interactivetocopilot-cli.allowed_tools.auto_denyand now fires only whenallowed_toolsis set. (#934)A
copilot-cliturn that the CLI ends without reporting the task complete, which is what reachingcopilot-cli.max_autopilot_continuesproduces, is now recorded as a failed turn with the newturn_incompleteerror kind instead of as a success. The attempt ends there and the scheduled retry resumes the same session, so the work continues rather than being lost. The turn no longer counts toward the run’s completed turns. (#935)copilot-cliruns record token counts and API-request counts again. A newer CLI release moved the per-message output-token count to a different place in its output, so the count was silently going unread and every such run reported zero spend and, in some cases, a turn that succeeded as failed for producing no measurable output.
Changed
reactions.review_comments,reactions.bot_review,reactions.merge_conflicts, andreactions.auto_mergenow bound a pending entry’s age with a per-reactionwatch_window_mskey instead of a hardcoded thirty-minute constant. The default stays1800000(thirty minutes), so a deployment that sets nothing behaves exactly as before; setting0removes the bound entirely. A workflow with noauto_merge, where a person reviews and merges, will normally want a larger value than the default. The four expiry log records changed their message text and renamed theirttl_msattribute towindow_ms. (#953)
1.22.0 - 2026-08-25
Added
A
sortie_candidate_holds_totalcounter reports how many issues the scheduler held back and why, separating an unfinished blocker from a blocker list that could not be read, one the tracker reported as incomplete, and one left unread because the poll had already spent its budget of dependency lookups.sortie --dry-runnames the same reason for each issue it would not start, so an issue held by a dependency is no longer indistinguishable from one held by a full slot. (#920)sortie validatenow reports a warning when an agent block setsmcp_configfor an agent kind that never receives the generated MCP configuration file.claude-code,codex,copilot-cliandopencodereceive it;kirodoes not, so anmcp_configvalue in akiroblock had no effect and nothing said so. The reference documentation now states which kinds consume the file. It is a warning and not an error: such a configuration stays valid, the run proceeds, and the exit code is unchanged. (#928)An issue that Sortie has stopped dispatching because it reached its
agent.max_sessionsoragent.max_tokensceiling now says so. Previously such an issue stayed in an active tracker state and was simply never picked up again, with no log line at any level, no marker on the tracker, andGET /api/v1/{identifier}reporting it as unknown; the only way to explain the stall was to count the issue’s rows in the run history against the ceiling configured inWORKFLOW.md. The orchestrator now logs a warning naming the issue, which of the two ceilings stopped it, and its usage against that ceiling, once when the hold begins rather than on every poll, and the tick summary reports how many issues are held.GET /api/v1/statelists them with the same numbers, the per-issue endpoint answers for a held issue instead of reporting it unknown, and the dashboard shows a card and a table. Two metrics,sortie_budget_exhaustions_totalandsortie_budget_exhausted_issues, report how often a hold begins and how many issues are held right now. What the ceilings count, and when they stop an issue, is unchanged. (#936)The tracker issue itself now carries the reason an issue stopped. When a per-issue session or token ceiling stops dispatch, Sortie posts one comment on the issue naming the ceiling that stopped it and the setting that raises it, so the stall is explained where an operator is already looking rather than only in the log, the API and the dashboard. (#944)
Fixed
A malformed end-of-turn notification from the
codex app-serverno longer leaves the turn outcome reported as the bare wordturnfollowed by a trailing space. A payload that fails to parse carries no status word, so the turn now reports the shared failure message instead: the status API’slast_messagefield and the recorded run history both readturn failed. (#842)A malformed
tracker.endpointis now reported bysortie validateand rejected at startup by every adapter that accepts one, instead of passing validation and failing later as a network error. The GitHub, Gitea and Linear adapters handed the configured value to the HTTP client without parsing it, and the Gitea CI status provider and SCM adapter accepted an unusable one with no error at all. An IPv6 address written without brackets,http://fd00::1:3000instead ofhttp://[fd00::1]:3000, is now named as a fault in the endpoint field before any request is made. A username or password embedded in the endpoint is masked in that diagnostic; previously the failure arrived as a transport error quoting the whole endpoint, credential included. (#908)A run no longer stalls when an agent stops to ask for something only a person could give, such as permission to run a command, to change a file, or an answer to a direct question. Where the runtime can be answered, Sortie declines the request and the agent carries on by another route; where it cannot be answered, or where the agent is putting a question to a person, the attempt ends at once and releases its claim on the issue. Previously the turn stayed open until a timeout expired, the attempt was reported as a timeout rather than as a run that needed a person, and the retry that followed could only re-enter the same wait. A run that ended this way is now recorded under its own
needs_personstatus instead of being counted among ordinary failures in run reports. (#837)A runtime configuration that would let an agent stop and ask for approval mid-turn (
codex.approval_policy,claude-code.permission_mode) is now refused bysortie validate, at startup and on reload, instead of being accepted and surfacing later as a stalled run. Not every such request is governed by an approval setting, so this check reduces how often the situation arises rather than removing it. (#837)A failed OpenCode turn now reports the failure detail OpenCode put on the run stream, instead of its generic
Unexpected server error. Check server logs for details.placeholder. One failure can produce both reports in either order, and the adapter kept whichever arrived last. Only the unknown-model case was recovered afterwards, by a secondopencodecall; every other cause reached the operator as the placeholder. (#839)On GitHub and Gitea, an issue whose blockers are still open is no longer started. Both trackers reported every candidate issue as having no blockers at all, so a dependency recorded in the tracker had no effect on what ran: work began on issues whose prerequisites were unfinished, holding a slot until the agent discovered for itself that it could not proceed. Jira and Linear were unaffected. An issue is now held until every blocker reaches a terminal state, and where its blocker list cannot be read the issue is held and retried on the next poll rather than started on an unread list. A forge that does not serve issue dependencies at all is now reported as an error instead of read as an empty list, and no issue on it is started while that lasts. Reading dependencies costs GitHub and Gitea up to four extra tracker requests per poll; an issue whose own tracker data already proves it has no dependencies costs none. Workflow templates on those two trackers now receive the real blocker list, which was previously always empty. (#920)
An issue routed by a dispatch rule to an agent kind other than the workflow default now runs with the MCP servers configured in its own agent settings block. It was given the default kind’s
mcp_configinstead: servers and credentials meant for another agent were loaded, its own were silently dropped, and a stale or malformed path in the default block failed the session at startup and sent it into retry backoff, naming a file the operator had never associated with that agent. A session running on the workflow default was unaffected. (#924)A
codexoropencodesession no longer keeps the first-turn “Available Sortie tools” section for tools it has no way to call. Both kinds now translate the worker-generated MCP configuration into their own runtime’s configuration form and deliver it on a local launch, so a tool the prompt advertises to them is reachable; an SSH session on either kind gets neither the channel nor the advertisement.kirostops receiving the advertisement entirely, since its runtime disables MCP under API-key authentication and can reach a tool by no other means.sortie validatealso warns once per reachable kind with no tool execution channel. (#841)A workflow that sets
claude-code.session_persistence: falseis now rejected before the run starts, bysortie validateand at startup. The setting prevents Claude Code from resuming a session, so such a run previously failed partway through. (#879)An issue that reached a terminal state during a run no longer receives a session-failure comment, a failed run record, or a retry that is then discarded. Sortie now re-checks the issue’s tracker state immediately before recording that kind of failure, and stays silent when the issue is already finished. (#887)
An
after_runhook that inspects.sortie/statusafter a run whose self-review phase ended onblockednow finds the file absent, the same as it already found for a phase-endingneeds-human-review. A run that never enters the self-review phase is unchanged. (#894)
Changed
- The consecutive-absence ceiling no longer follows
agent.max_sessions. It now reads a new field,agent.max_consecutive_absences, which defaults to three and rejects0as a configuration error; a deployment that wants no absence checking at all setstracker.handoff_evidence: offinstead.agent.max_sessionsitself is unchanged: it remains the total per-issue session budget. A deployment that setagent.max_sessionswell above three, paired with a workflow whose runs legitimately leave the workspace untouched, now parks such an issue considerably sooner than before; the park is announced and reversible. An operator who chose a lowagent.max_sessionsto bound a loop should revisit that value, because a workflow advancing one issue through several phases needs a session budget sized to those phases rather than to the runaway guard it was previously also serving. (#942)
1.21.0 - 2026-08-20
Fixed
A check run cancelled by a newer commit no longer spends a retry from the
reactions.ci_failurebudget. The CI verdict counts onlyfailureandtimed_outas failing; a cancelled check now withholds green instead of asserting failure, holding the verdict at pending, so it dispatches no fix continuation and appends no failure to run history. A required workflow configured to cancel its own in-flight runs when a newer commit lands was spending the whole bounded budget on ordinary pushes, so a commit that genuinely failed later went unremediated. The merge gate answers by the same rule and reports such a head as pending rather than failing, which on GitLab also covers acanceledpipeline status. The escalation raised on budget exhaustion now names exactly the checks the verdict counted as failing. (#831)Merge-completion polling no longer retries forever when a forge reports a pull request merged but never supplies its merge commit identifier. The first such response starts a persisted thirty-minute grace period with exponential polling backoff; if the identifier is still absent, Sortie stops without transitioning the issue and sends the configured escalation. Time spent waiting for review does not count toward the grace period, restarts do not reset it, and failed escalation delivery can be retried by a later fresh pending entry without reopening the stopped polling loop. If such a later entry observes a real identifier, it follows the normal exactly-once transition path. A retry can occasionally redeliver the escalation instead of delivering it for the first time, when the tracker write succeeded but the internal marker that records delivery failed to write: a label escalation repeats harmlessly, because reapplying a present label is a no-op, but a comment escalation posts a second comment. (#777)
The
ci_failurereaction now evaluates the pull request’s current head on every pass and keeps watching after a passing result, so a commit pushed later that fails CI is observed and receives a fix continuation. Previously the watch retired on the first passing result and the commit it polled never advanced past the one the agent handed off, so a branch could sit on a failing commit with its linked issue stuck in the review state and no escalation raised. A new head restores the attempt budget only when Sortie can establish that the commit is not its own work, so an agent cannot extend its own budget by pushing. The watch ends when the pull request merges or closes, and otherwise afterreactions.ci_failure.watch_window_ms(default86400000, twenty-four hours) with no new commit;0removes that bound, and applying the configured fix label re-arms a pull request by hand. (#871)agent.turn_timeout_msis now enforced. A turn that exceeds the configured bound ends, the attempt is recorded as failed with theturn_timeoutreason, and a retry is scheduled with the usual exponential backoff. The bound covers self-review turns too: a self-review turn that exceeds it fails the attempt rather than completing it, so such a run is retried instead of handed off. Previously the value was parsed and reported bysortie resolvebut applied nowhere, so nothing bounded a turn whose agent kept producing output; stall detection could not cover the gap, because it measures silence rather than duration. (#834)An agent that writes
blockedto its status file during a self-review turn now ends the run as a blocked soft stop however the run entered that phase. Previously the signal was honoured only when the agent had signalled completion; a run that entered self-review by exhaustingagent.max_turnshad it discarded and finished as an ordinary completed run, so the issue moved totracker.handoff_statewhere one is configured, or was dispatched again on a continuation retry, over work the agent had just reported it could not carry further. Such a run now takes no handoff transition and schedules no retry: the claim is released and the issue is parked, held out of dispatch until a human changes its state or removes the parking label. Releasing a parked issue now also clears its consecutive handoff-absence count whatever reason parked it, so a release no longer leaves a count behind that would park the issue again sooner than expected. (#856)A cancelled Codex turn no longer consumes CPU while it waits for the agent to wind down. It previously spun a full core from the moment of cancellation until the turn reached its terminal state, so a shutdown that cancelled several concurrent Codex turns spun one core each. Cancellation reaches this path on shutdown, on stall detection, and when
agent.turn_timeout_msexpires. (#845)
Changed
reactions.ci_failurenow resolves the pull request’s current head through the same SCM provider every other active SCM-backed reaction uses, so a deployment namingreactions.ci_failurewith one provider and another active SCM-backed reaction with a different provider now fails at startup instead of running with a currency-blind CI watch.sortie validatenow reports the same conflict offline, under thereactions.scm_provider_conflictcheck. The previously accepted shape, two providers across the active SCM-backed reactions includingci_failure, is no longer valid; name one forge across every active SCM-backed reaction, includingci_failure, to start again. (#871, #890)A non-positive
agent.turn_timeout_msis now rejected at startup, bysortie validate, and on reload.0or a negative number is no longer accepted;0did not disable the bound before either, it silently meant one hour. Unlikeagent.stall_timeout_ms, this bound cannot be disabled. (#834)The
codex.skip_git_repo_checkpass-through key is removed. It never had an effect: the value was parsed and read by no launch path, and thecodex app-servertransport the adapter drives exposes no equivalent protocol field and rejects the equivalent flag, which exists only oncodex exec. The key also promised something the adapter never needed. A workspace that is not a Git repository is the default and already works, because the refusal the key named lives in thecodex execwrapper, above the layer the adapter talks to. Nothing validates unknown keys inside thecodexblock, so a WORKFLOW.md that still sets the key is ignored rather than rejected. (#840)
1.20.0 - 2026-08-18
Added
sortie validatenow checks the numeric settings of thereactions.review_commentsandreactions.merge_conflictsblocks: apoll_interval_msbelow30000on either block, and a negativedebounce_msor amax_continuation_turnsof zero or less onreview_comments. All four previously passed validation and then stopped the run at startup, after the state database had already been created. (#803)Sortie now withholds the handoff transition from a run that produced no work it could observe. The workspace is compared against a baseline taken immediately before the agent starts, and the issue advances to
tracker.handoff_stateonly when the run moved the committed position, changed the working tree, or left a pushed branch or pull request behind, so a session that finished with nothing to show no longer arrives for human review as if it had. Such a run is recorded as failed, names the verdict as its reason, counts onsortie_handoff_transitions_total{result="withheld"}, and leaves the issue in its active state for a backoff retry. The newtracker.handoff_evidencefield selects the policy:observed, the default, withholds only where the workspace could be inspected and showed nothing;strictalso withholds where it could not be inspected at all, such as a workspace that is not a Git tree;offcomputes no verdict and restores the previous behavior. A deployment whose agents leave their result outside the workspace (a tracker comment, say) sees those issues withheld and re-dispatched on every run, and sets the field tooff. (#768)An issue whose runs keep producing nothing is now parked instead of being dispatched again indefinitely. Sortie counts the consecutive withheld handoffs on each issue and, on reaching the ceiling, attaches the escalation label configured under
reactions.review_comments.escalation_label(needs-humanwhen that block or value is absent), stops the retry sequence, and dispatches the issue no further. The ceiling isagent.max_sessionswhere the deployment sets one and3otherwise, which is the first attempt plus two more. Parking is announced with the issue, how many consecutive empty runs were seen, the ceiling, and the label applied, so a parked issue can be told apart from an abandoned one. A run that produces work clears the count at once; a run that ends without an evidence verdict, such as an agent reporting itself blocked, leaves the count where it stood. The count is neither kept nor consulted undertracker.handoff_evidence: off, and a review-comment or CI continuation retry is never stopped by this ceiling. (#769)
Fixed
Adapter endpoint validation errors no longer print credentials embedded in the configured
endpoint. A Jira or GitLab endpoint written asscheme://user:secret@hostthat fails validation is now reported with its user and password masked, so the secret cannot reach the operator log. (#791)GitHub: inline review comments now reach the agent with the lines they were written against. Both human and bot review feedback arrived with no location at all, so the agent had to find the referenced code itself and a prompt template guarded on the start line (including the example published in the reference documentation) never rendered its branch. Comments left on an outdated diff report their original lines; pull-request-level review bodies remain unlocated. (#776)
Gitea: review comments anchored to the old side of the diff now reach the agent, carrying the line they were left on, instead of being silently discarded as outdated. A review whose comments were all on the old side dispatched no agent turn and never escalated either, so sortie appeared to ignore the review outright until the pending check expired. Gitea review comments are no longer filtered as outdated at all, because the platform reports no signal for an anchor that a later push has superseded. (#778)
A review comment whose author is listed in
reactions.bot_review.bot_usernamesno longer triggers the humanreview_commentsreaction. The allowlist previously suppressed an author only from the bot-review loop, so an allowlisted reviewer’sCHANGES_REQUESTEDreview also drove the human loop on any provider with a bot-account marker, consuming two independent continuation budgets for the same feedback. On Gitea, which exposes no bot-account marker at all, the allowlist is the only classification signal that exists, so a bot review there drove the human loop unconditionally. The exclusion requires an activereactions.bot_reviewblock, because that is wherebot_usernameslives. (#665)Self-review now runs when the agent reports its work complete, instead of only when the agent exhausts its turn budget. An operator who set
self_review.enabled: truegot the verification commands and the review turn on the one path a finished run never takes, so work reached the handoff state with none of the configured checks having run. A run that ends this way now takes longer, counts its review and fix turns alongside its coding turns, records a review outcome where it previously recorded none, and passes that outcome to theafter_runhook in place ofdisabled. (#813)An agent that writes
blockedto.sortie/statusnow holds its issue until a person acts, instead of having it dispatched again on the next poll and on every poll after that. Sortie parks the issue: it attaches the escalation label configured underreactions.review_comments.escalation_label(needs-humanwhen that block or value is absent), holds the issue out of dispatch and out of the retry lane, keeps it parked across a restart, and counts it onsortie_issue_parks_total{reason}. The issue keeps the tracker state it was dispatched in, so the label is what marks it as waiting on a person. Nothing previously outlasted the run, leavingagent.max_sessionsas the only bound on the repeat and no bound at all where it is unset. A park is released when Sortie observes someone act on the issue: moving it to another tracker state, or removing the parking label. The same release now applies to an issue parked for producing no observable work. Wheretracker.query_filterexcludes the parking label, Sortie never confirms the label is present and removing it releases nothing, so release those issues by moving them instead. (#811)GitLab: a merge request whose pipeline is waiting on a manual job is no longer held out of auto-merge indefinitely. Such a pipeline was reported as still running on every poll, so the auto-merge entry expired on its timeout and the merge fell to a person. The verdict now follows the pipeline’s own jobs: one waiting only on manual jobs counts as passing, one that also holds a failed job reports failing instead of looking identical to a healthy one, and one with work still queued stays pending. Reading a pipeline in this state costs one extra API call per poll; every other pipeline state is unchanged. (#827)
GitLab: auto-merge no longer acts on a CI verdict belonging to an earlier commit. GitLab reports a merge request’s pipeline as stored, not as re-checked against the current commit, so a push that produced no pipeline of its own (removing the CI configuration, or a change the pipeline rules exclude) left the previous commit’s result in place. A merge request could merge on a passing result that never covered the commit being merged, and the same staleness held the gate shut the other way. The verdict is now withheld as pending whenever the pipeline on offer describes a commit other than the merge request head. A merge request whose branch never produced a pipeline still reports no verdict and merges where the deployment allows it. Projects using merged results pipelines or merge trains keep the previous behavior, because those pipelines run on a commit that exists in neither branch and never match the head by design. (#828)
GitLab: mergeability reads no longer warn about values GitLab documents and the adapter already handles. A draft merge request, one that is no longer open, and one whose pipeline is still running each logged
unrecognized gitlab detailed_merge_status valueat WARN on every poll for as long as the condition held, burying the diagnostic that exists to surface a value a newer GitLab release introduced. The warning is now raised only for a value outside the set GitLab’s API documents, and every mergeability verdict is unchanged. Licensed instances stop warning on five further blocking values, among them failing status checks and security policy violations, which the adapter had been matching against the wrong spelling. A merge request held back by a merge check is now reported at DEBUG, naming the value and the merge request it came from. (#829)
Migrations
Add the
handoff_absence_resetstable, recording per issue where its consecutive-absence count was last cleared by an observed piece of work. An issue with no row there has its recorded empty runs counted in full, so an upgrade carries any absences already in the database into the new ceiling.Add the
parked_issuestable, holding one row per issue currently held out of dispatch. It records current state rather than history: the row is deleted when the park is released. An upgrade starts with no parked issues, so an issue whose agent reported itself blocked before the upgrade is parked the next time it reports it.
1.19.0 - 2026-08-11
Added
Install script (macOS and Linux): command-line flags next to the existing environment variables (
--version,--install-dir,--no-verify,--binary, and--help) passed through a pipe withsh -s --. A flag overrides the matching variable, and an unrecognized flag now aborts the install instead of being ignored.--binaryinstalls a binary already on disk instead of downloading one. Re-running the script no longer re-downloads a release that is already installed in the target directory. On a GitHub Actions runner the install directory is appended to$GITHUB_PATH, so later steps findsortiewithout extra wiring. Resolving the latest release no longer consumes the unauthenticated GitHub API quota, which is what made unpinned installs fail on shared CI runners.sortie validateJira adapter config validation: emits offline diagnostics fortracker.kind: jiracovering endpoint presence, endpoint URL shape (a scheme and a host), an endpoint that already contains/rest/api/, andapi_keyshape (a colon in the first or last position, and a colon-free key against an Atlassian Cloud host). All four checks are errors that block dispatch.sortie validatenow reports a malformed Giteatracker.query_filterusing the same grammar the adapter enforces at startup, and an untrimmed element intracker.active_statesortracker.terminal_stateson GitHub and Gitea, matching the existing GitLab and Linear diagnostics. The empty-element and untrimmed-element diagnostic wording is now identical across every tracker adapter that reports it.sortie validatenow checks the Jira API version, catching threetracker.kind: jiramisconfigurations that used to pass validation and then abort the run at startup: atracker.api_versionother than"2"or"3";"2"against an Atlassian Cloud endpoint, which only serves version 3; and a colon-freetracker.api_keyagainst a self-hosted endpoint whose effective version is"3"(the default whentracker.api_versionis unset) where a personal access token needs either anemail:tokenkey ortracker.api_version: "2". All three are errors that block dispatch. (#785)GitLab SCM provider: Sortie’s pull-request automation now runs against GitLab.com or a self-managed GitLab instance, at parity with the GitHub and Gitea SCM providers. Set
provider: gitlabon areactions.auto_merge,reactions.review_comments,reactions.bot_review,reactions.merge_conflicts,reactions.ci_failure, orreactions.label_commandsblock to drive it through GitLab: Sortie routes human and bot review feedback on a merge request back into the agent session, reacts to merge conflicts, to a failing pipeline on an agent’s merge request (surfacing an excerpt of the failing job’s log), and to thesortie:review/sortie:fixlabel commands, and, withreactions.auto_merge, merges an approved merge request once its approval state, pipeline status, and mergeability satisfy the configured preconditions (sending the expected head SHA so a moved head aborts the merge rather than merging stale work) and deletes the merged branch. Auto-merge on GitLab requires an access token carrying GitLab’sapiscope; a token without it is reported at startup. (#720, #721, #722)
Fixed
Follow-up work already queued for an issue is no longer discarded when a second kind of follow-up becomes due for the same issue. Sortie keeps one queued continuation per issue and the last writer won silently, so a queued CI fix, review fix, bot-review fix, post-merge-conflict rebase, or
sortie:review/sortie:fixlabel command could be dropped and never run, and a dropped continuation could reappear after a restart. The losing side now waits and runs on a later poll once the queued work has been dispatched; a label command keeps its label on the pull request until it actually starts, so an unremoved label means the command is accepted but not yet running; and a worker finishing normally no longer cancels work queued while it was running. Two cases that could otherwise hold the queue indefinitely are now bounded and reported: a reaction for an issue parked outside every configured state is dropped after 30 minutes with a warning naming the reaction kind and the issue state, instead of retrying at the backoff ceiling for the life of the process and blocking that issue’s other reactions, and a retry whose timer event was lost under load is re-armed on a later poll instead of stalling. (#743)Token usage recorded for a run was undercounted on every adapter that reports it (
claude-code,codex,copilot-cli, andopencode) by between one and three orders of magnitude, and was zero oncodexturns and onclaude-codesessions whose work ran inside sub-agents. A multi-turn session recorded only its largest single turn rather than the whole session. Recorded figures now match what each runtime reports for the same session, andtotal_tokensmeans input plus output on every adapter, counting prompt-cache reads once within the input total instead of adding them again. Everything derived from these figures moves with them (agent.max_tokensenforcement, thecost_budgetagent tool,sortie stats, dashboard cost estimates, and the Prometheus token counters), so anagent.max_tokensceiling tuned against the previous behavior will bind far sooner and is worth revisiting before upgrading. Rows already written torun_historykeep their original figures, so asortie statswindow spanning the upgrade mixes both. Oncopilot-cliinput tokens are recovered from the runtime’s session journal after the agent process exits, so they remain unreported when the agent runs over SSH. (#756)A run whose coding agent reported no token usage was stored, summed, priced, and displayed as a run that spent nothing, so an unmeasured run looked identical to a genuinely free one. Each run now records whether its token figures are a measurement, and the surfaces that report spend keep the two apart.
sortie statscounts tokens and cost over measured runs only, labels them that way, and footnotes how many runs it skipped;--format jsongainstokens_unmeasured_runsoverall and per group. The dashboard shows a running session that has reported no usage yet asnot reported, leaves it out of the active token and cost totals, and says how many it left out; the state API gainstokens_measuredper running entry. Thecost_budgetagent tool gainsunmeasured_sessionsandused_tokens_completeso an agent can tell a lower bound from an exact figure. An unmeasured run still contributes nothing to theagent.max_tokensceiling, but the orchestrator now logs a warning that the ceiling could not be fully evaluated instead of treating the incomplete total as authoritative; the dispatch proceeds either way. (#757)An
opencodeturn that exits cleanly having produced no model output at all (no text, no reasoning, no tool call) is no longer reported as completed. The turn now fails and is retried, instead of counting as work done and letting the run advance the issue on nothing.A failed or cancelled turn on
opencodeandcodexnow records why it ended. Both agents reported the outcome with no accompanying error, so the run’serrorcolumn and the dashboard showed a failure with no reason attached; the runtime’s own diagnostic now reaches both. Oncodex, a turn that fails before it starts and one whose subprocess output ends early also reach the event stream, so the dashboard’s last event no longer stops at the last step that worked.The GitHub auto-merge CI gate read only the first page of a commit’s combined statuses and check runs, so a commit carrying more than 30 of either could report the wrong merge verdict. Both routes are now paginated to exhaustion. (#784)
The Gitea auto-merge CI gate treated a commit status it did not recognize as passing, letting auto-merge proceed on a signal it could not interpret. It now treats an unrecognized or empty status as pending, matching the Gitea CI reaction’s own reading of the same value.
An auto-merge on GitHub that lost the race to a merge performed by someone else no longer retries until it escalates. Sortie recognized that case only when GitHub’s rejection wording said the pull request was already merged, which it does not say, so the reaction re-polled a merge that had already landed and eventually asked for a human. Sortie now re-reads the pull request after a rejected merge and treats a confirmed merge as success, closing out the reaction and counting it as merged, which is what Gitea already did. (#786)
On GitHub,
reactions.merge_completionnever moved an issue to its terminal state after the pull request merged. The GitHub API version Sortie pins stopped reporting the merge commit the reaction uses to recognize a merge, so the issue stayed in its pre-merge state, its workspace was never cleaned up, and a warning repeated at every poll for the life of the process. Sortie now reads the merge commit from GitHub’s GraphQL API and the transition lands on the first poll after the merge. A GitHub token used withmerge_completionmust therefore be able to read the GraphQL API; a token that cannot now fails the read with a logged error and backoff instead of looping silently. (#775)On Gitea, a pull request label event whose timestamp the forge returned in an unreadable form silently skipped the
sortie:reviewandsortie:fixlabel commands. The unreadable value was substituted with the epoch, which sorts ahead of every position the detector had already recorded, so the command was passed over and its label left on the pull request. The read now fails with a payload error and backs off, which is what GitHub already did. A review comment’s timestamp is still tolerated, because it feeds only the review debounce window. And because Gitea folds its review decision from each review’s submission time, a review that can change the verdict and carries an unreadable timestamp now fails the precondition read rather than letting a superseded approval outrank the changes-requested review that supersedes it, soreactions.auto_mergedefers instead of merging on a misread verdict. (#798)
Changed
opencodetransport failures (a stdout read error, a session id mismatch, or a timeout waiting for the first response) now reportexit_reason=turn_failedinstead ofturn_ended_with_error, which no built-in coding agent reports any more. An alert or dashboard filter on the old value must match on the error kind instead, which already drew the same distinction.Turn failure text is now the same across every coding agent: a turn that exits successfully having produced nothing reports
agent exited without producing output, and a non-zero exit reportsexit code N. An alert matching the previouskirooropencodewording needs updating.run_history.turns_completedno longer counts a turn that ended in failure or cancellation onopencodeandcodex, so the column means the same thing on every coding agent. Turn counts and mean turns per run insortie statsand on the dashboard drop for those two agents at this release, with no change in behavior behind the numbers.The multi-label state WARN, logged when an issue carries more than one configured active, terminal, or handoff label, now identifies the issue with
issue_identifieron every forge, replacingissue_indexon Gitea andiidon GitLab. GitHub now logs this WARN as well, matching Gitea and GitLab. An operator’s saved log filter on the old attribute name needs updating.A source-control failure on GitHub or Gitea that is not a merge no longer reports the
scm_conflict_errorcategory. A 405 or 409 from a review read, a CI read, a label removal, or a branch delete now reportsscm_api_error; only a rejected merge reports a conflict. An operator’s alert onscm_conflict_errornow fires on merges only.Release tags now carry a
vprefix (v1.19.0). Every earlier version was additionally tagged under its prefixed name against the same commit, so the Go module proxy now publishes the full version list andgo install github.com/sortie-ai/sortie/cmd/sortie@v1.18.0resolves; it previously published no versions at all, leaving the module installable only at a pseudo-version. The install scripts for macOS, Linux, and Windows take a pinned version with or without the prefix, so an existingSORTIE_VERSION=1.18.0or--version 1.18.0still selects that release.
Migrations
- Add
tokens_measured INTEGER NOT NULL DEFAULT 1torun_history; pre-migration rows read back as measured, so a run recorded before the upgrade that reported no token usage still counts as a genuine zero.
1.18.0 - 2026-08-09
Added
sortie statssubcommand: summarizes how past runs went and what they cost, opening the database read-only so it never blocks a running orchestrator.--format text|jsonselects the output;--sinceand--untilbound the report by when a run finished, accepting an exact timestamp, aYYYY-MM-DDdate, or an age such as24h. The report breaks runs down by outcome, by coding agent, by dispatch rule, and by prompt template, with run counts, success rate, p50/p95/mean duration, mean turns, and token totals for each. When the workflow configurestoken_rates, USD cost is derived through the same formula and renderers the dashboard uses, reported as total spend and as spend per succeeded run; withouttoken_ratesthe report shows token counts and no cost figures rather than zeros. Against a database written by an older binary the command still works: it reports the figures that database can supply and warns which ones are missing, rather than failing outright. (#274)workspace.retention_days: an opt-in age bound that removes a swept workspace whose latest recorded activity is older than the configured window, independently of tracker state. Off by default (0); the smallest permitted non-zero value is 30 days, matching the window pending-reaction recovery honors after a restart, so a workspace the bound removes is always one recovery would already treat as stale. The periodic sweep now emits one summary record per pass, whether or not it removed anything, reporting how many workspaces were excluded as in-flight, removed as terminal, removed by age, retained inside the window, retained for want of an activity record, or not yet evaluated. (#706)reactions.merge_completion: an opt-in, default-off reaction that observes the merge of a Sortie-managed pull request, whether performed by the orchestrator, by a human, or by a forge automation rule, and transitions the linked issue to a single configured terminal state exactly once. Requirestracker.handoff_stateand a writtentracker.terminal_stateslist;sortie validatereports a misconfigured target state, an unset prerequisite, or a poll interval below the floor before a run begins. (#707)
Fixed
reactions.ci_failureandreactions.review_commentsescalation now clears only its own kind’s pending entry, attempt counter, and fingerprint instead of every reaction on the issue, so an unrelated escalation no longer silently endsreactions.merge_completionobservation (or any other sibling reaction) for that issue. (#707)- An issue reaching a terminal tracker state now stops all of its reaction polling immediately, including for a
sortie:revieworsortie:fixlabel-command entry, which previously kept polling the pull request’s label journal for the life of the orchestrator process even after the issue closed. This applies whether or not a worker is still running for the issue, and it releases the issue for a fresh dispatch as soon as it is reopened into an active state, rather than after a pending retry happens to fire. (#741) - An issue moved to a state in
tracker.terminal_stateswhile its worker is finishing its last turn is no longer overwritten withtracker.handoff_state. Cancelling a running task by relabelling the issue took effect before only when a reconcile tick observed the new state ahead of the worker exit; otherwise the exit applied the handoff state from the state read at dispatch, leaving a closed, cancelled issue marked as awaiting review. Sortie now decides from the freshest state it has observed and re-reads the issue immediately before the transition, and a terminal state suppresses the handoff transition, the continuation retry, and every pending reaction for that run, soreactions.auto_mergecan no longer merge the pull request of an issue the operator cancelled. The suppression is logged and counted onsortie_handoff_transitions_total{result="skipped"}; a failed pre-transition read proceeds with the handoff as before. The same applies withouttracker.handoff_stateconfigured, where a terminal state now ends the run instead of scheduling a continuation retry. (#749)
Changed
- A terminal issue whose pull request still carries a pending
sortie:revieworsortie:fixlabel-command entry is now cleaned up by the periodic workspace sweep like any other terminal issue, instead of being retained forever. This reaches every deployment on upgrade without an opt-in: previously, a pending label-command entry excluded its workspace from cleanup even after the linked issue reached a terminal tracker state; the label-command detection loop is unaffected by the change, since it reads nothing from the workspace directory. (#706)
1.17.0 - 2026-08-06
Added
- GitLab tracker adapter: set
tracker.kind: gitlabto run Sortie against GitLab.com or a self-managed instance, withtracker.projectthe target project as agroup/projectpath or numeric project ID,tracker.api_keya GitLab access token, andtracker.endpointthe instance base URL (optional; defaults tohttps://gitlab.com, and/api/v4is appended when absent). It runs the same autonomous workflow as the Jira, GitHub, Linear, and Gitea adapters: candidate polling, handoff transitions, lifecycle comments, and CI-failure escalation labels. State is label-driven throughactive_states,terminal_states, andhandoff_state; a transition to a terminal state closes the issue and a transition back to an active state reopens it, escalation labels are attached without disturbing the issue’s other labels, and a state label the project does not yet hold is created on first use. At startup the adapter checks the project and reads its label catalog, so a bad token, a wrong project, or an unreachable instance fails before the first poll. Because GitLab issues carry no priority field and Community Edition has no blocking relationship,issue.priorityandissue.blocked_byare always empty in prompt templates.tracker.query_filtertakes a GitLab issue-list query fragment (for exampleassignee_username=review-bot&labels=ready,not[...]negation included) to scope candidate polling to matching issues; the adapter-owned keysstate,issue_type,order_by,sort,page,per_page,pagination, andwith_labels_detailsare rejected, as is any key GitLab’s issue list does not support, so a typo fails at startup instead of being silently ignored. (#676, #677, #679) sortie validateGitLab adapter config validation: emits offline diagnostics fortracker.kind: gitlabcoveringtracker.endpointURL shape (flagging a cleartexthttpscheme and a base URL that already ends in/api/v4),tracker.projectas agroup/projectpath or a numeric project ID, a$SORTIE_GITLAB_TOKENenvironment-variable hint,tracker.query_filtersyntax and adapter-owned keys, and empty, untrimmed, or overlapping active/terminal state labels. Errors block dispatch; warnings are advisory. (#678)
1.16.1 - 2026-08-03
Fixed
sortie validateno longer reportsunknown template variablefor the reaction continuation variables (ci_failure,review_comments,bot_review_comments,merge_conflict,label_review,label_fix). A mistyped sub-field of one of them is now reported as an unknown field with the valid field list, and a reference to one of them from inside a{{ range }}or{{ with }}body is now reported as dot-context misuse. (#696)sortie validateand startup now reject atracker.handoff_stateortracker.in_progress_statethat collides with the tracker adapter’s own fallback state list when the matching workflow list is empty. Leavingtracker.active_statesortracker.terminal_statesout no longer hides the collision, so a workflow that passed validation before this release can now fail: either write the list out or pick a state outside the adapter’s fallback. (#695)
1.16.0 - 2026-07-19
Added
- Gitea SCM provider: Sortie’s pull-request automation now runs against a self-hosted Gitea instance (Forgejo and Codeberg included), at parity with the GitHub SCM provider. Set
provider: giteaon areactions.auto_merge,reactions.review_comments,reactions.bot_review,reactions.merge_conflicts,reactions.ci_failure, orreactions.label_commandsblock to drive it through Gitea: Sortie routes human and bot review comments back into the agent session, reacts to merge conflicts, to a failing CI run on an agent’s pull request (surfacing an excerpt of the first failing check when available), and to thesortie:review/sortie:fixlabel commands, and, withreactions.auto_merge, merges an approved pull request once its review decision, CI status, and mergeability satisfy the configured preconditions (sending the expected head SHA so a moved head aborts the merge rather than merging stale work) and deletes the merged branch. Auto-merge on Gitea requires the configured token’s user to hold repository write access; a token that cannot push is reported at startup. (#656, #657, #658) sortie validatereaction and CI feedback checks: before dispatch,sortie validatenow reports a reaction orci_feedbackblock that names an SCM or CI provider Sortie does not recognize, active reactions that disagree on the SCM provider, abot_reviewbot_usernamesallowlist that is not a list of names, and anauto_mergestrategythat is notmerge,squash, orrebase. These faults block dispatch, and apply to every SCM provider including the new Gitea one. (#659)
1.15.0 - 2026-07-16
Added
- Gitea tracker adapter: set
tracker.kind: giteato run Sortie against a self-hosted Gitea instance (Forgejo and Codeberg included), withtracker.endpointthe instance URL (required; there is no default host),tracker.api_keya Gitea access token, andtracker.projectthe targetowner/repo. It runs the same autonomous workflow as the Jira, GitHub, and Linear adapters: candidate polling, handoff transitions, lifecycle comments, and CI-failure escalation labels. State is label-driven throughactive_states,terminal_states, andhandoff_state; a terminal transition closes the issue and an active transition reopens it, and any missing state or escalation label is created in the repository on demand.tracker.query_filtertakes a Gitea issue-list query fragment (for exampleassigned_by=review-bot&labels=ready) to scope candidate polling to matching issues; the adapter-owned keysstate,type,page, andlimitare rejected. (#629, #630, #632) sortie validateGitea adapter config validation: emits offline diagnostics fortracker.kind: giteacoveringtracker.endpointpresence and URL shape (required for a self-hosted instance, which has no default host to fall back on),tracker.projectasowner/repo, a$SORTIE_GITEA_TOKENenvironment-variable hint, empty state labels, and active/terminal state overlap. Errors block dispatch; warnings are advisory. (#631)
1.14.1 - 2026-07-13
Fixed
- Failing lifecycle hooks (
after_create,before_run,after_run,before_remove) now log their captured stdout and stderr in ahook_outputattribute on the failure WARN record, so the output is visible at the default log level without enabling debug; previously the output was discarded and a failed hook, such as agit cloneinafter_create, could not be diagnosed from the logs even at--log-level debug. A hook that succeeds while printing output logs it at debug level on ahook completedrecord.hook_outputkeeps the last 8 KiB of output and starts with a truncation marker when longer. (#643)
1.14.0 - 2026-07-11
Added
Bot-review reaction kind: review-bot comments (linters, static analyzers, security scanners, and AI reviewers) on a Sortie-created pull request are now detected and routed back into the agent session as continuation turns, separately from human review comments. Configure it with a
reactions.bot_reviewblock in WORKFLOW.md, whereprovideractivates the kind,bot_usernamesallowlists bot logins, andmax_continuation_turns,poll_interval_ms, andescalationtune the retry budget, poll cadence, and handoff. A comment is classified as bot-authored by its platform author type or the allowlist, never by its content; bot comments dispatch immediately with no debounce window and own an independent retry budget, fingerprint, and escalation, so the bot-review and human-review kinds never interfere on the same pull request. (#415)Merge-conflict reaction kind: the orchestrator now polls mergeability of each open Sortie-managed pull request per reconcile cycle and, on a no-conflict-to-conflict transition, dispatches a single continuation turn that rebases the PR branch onto its base and resolves the conflicts on the existing workspace. Configure it with a
reactions.merge_conflictsblock in WORKFLOW.md, whereprovideractivates the kind andmax_retries,poll_interval_ms, andescalationtune the retry budget, poll cadence, and handoff; the retry budget defaults lower than other reaction kinds because conflict resolution rarely succeeds on retry. Tracking is episodic: resolving a conflict resets the budget, so a later independent conflict gets a fresh attempt rather than immediate escalation. (#416)PR label commands: apply a label to a Sortie-managed pull request to trigger an agent action on it. Two commands share a
reactions.label_commandsblock in WORKFLOW.md. Applyingsortie:review(thereview_label) runs a read-only session that posts review comments and changes no code; applyingsortie:fix(thefix_label) runs a session that checks out the PR branch, addresses the outstanding review comments, pushes the fixes, and posts a summary comment.provideractivates the feature (for exampleprovider: github); both labels default to theirsortie:names and are active onceprovideris set, so disable either command by setting its label to"".poll_interval_ms(default 60000, minimum 30000) sets how often the labels are checked. Sortie removes the label once it accepts the command, so re-applying it after the run finishes starts a new one. The operator creates the labels (Sortie never creates them) and adds the matching{{ if .label_review }}or{{ if .label_fix }}branch to the prompt template. (#584, #585)
Changed
- Homebrew installs now use
brew install --cask sortie-ai/tap/sortie. The tap distributes a Homebrew cask covering both macOS and Linux, replacing the previous formula. (#613)
1.13.0 - 2026-06-15
Added
- Linear tracker adapter: configure with
tracker.kind: linearandtracker.projectset to a Linear team key (the prefix in identifiers such asABC-123). The adapter speaks Linear’s GraphQL API over a single endpoint and authenticates with a personal API key, validating the key against the workspace at construction time. It implements the fullTrackerAdapterinterface: cursor-paginated candidate fetch, issue and comment retrieval, and state reconciliation on the read path;TransitionIssue,CommentIssue, andAddLabelon the write path, so a Linear-backed deployment performs handoff transitions, posts lifecycle comments, and attaches escalation labels on par with the Jira and GitHub adapters. Workflow states are mapped by display name, matched case-insensitively and verified against the team at startup, rather than by Linear’s immutable statetype.tracker.query_filteraccepts a LinearIssueFilterJSON fragment merged with the adapter-owned team and state constraints; a top-levelteamorstatekey is reserved and rejected. Linear returns application errors inside HTTP 200 bodies, so the adapter classifies the response body before any HTTP-status check; the request rate limit is read from response headers rather than hardcoded. Ships with anexamples/WORKFLOW.linear.mdsample workflow. (#237, #589, #599, #593) sortie validateLinear adapter config validation: emits offline diagnostics fortracker.kind: linearcoveringtracker.projectas a Linear team key, a$SORTIE_LINEAR_API_KEYenvironment-variable hint, empty state labels, and active/terminal state overlap, matching the checks already provided for the Jira and GitHub adapters. Errors block dispatch; warnings are advisory. (#590)
1.12.0 - 2026-06-12
Added
cost_budgetagent tool with per-issue token budget enforcement: a new Tier 1 MCP tool reports cumulative token spend and remaining budget for the current issue so an agent can adjust strategy (skip expensive work, return a partial result, or hand off) before hitting a ceiling. It reads cumulative totals fromrun_historyin read-only mode and returns the standard{"success": true, "data": ...}envelope. A companion hard ceiling, the new optionalagent.max_tokensfield (sibling toagent.max_sessions, default0for unlimited), blocks dispatch at preflight once an issue’s cumulative token spend is exhausted; when the session and token budgets are exceeded on the same evaluation, the token reason takes precedence in the recorded budget-exhaustion state.cost_budgetcalls are counted onsortie_tool_calls_total. (#240)notify_operatoragent tool: a new Tier 2 MCP tool lets an agent send real-time notifications to operator-configured channels during a session (to escalate a decision, report progress on a long-running task, or flag a blocker) without terminating the session. Version 1 ships Slack and generic-webhook backends, configured under a new optional top-levelnotificationsblock inWORKFLOW.md, with per-session volume bounded bymax_per_session. Backend secrets must use the$SORTIE_-prefixed environment indirection or they stay invisible to the sidecar process. Envelope fields (issue, session, attempt, agent kind) are injected by the orchestrator and cannot be forged by the agent, and error paths never echo the endpoint URL or payload. Registration is all-or-nothing: an invalid backend config fails sidecar startup rather than partially registering, so the prompt advertisement andtools/listalways agree. When no backend is configured the tool is not registered. (#242)- Jira adapter: Jira Server and Data Center support via a new optional
tracker.api_versionfield. The default"3"targets Jira Cloud (REST v3) and leaves existing configurations unchanged;"2"targets Server / Data Center (REST v2), switching to/rest/api/2endpoints, offset-based search pagination, and raw issue and comment bodies in Jira wiki markup (the v3 ADF-to-text flattening does not run on v2, so descriptions reach prompts as markup rather than plain text). On v2 theapi_keyshape selects authentication: a colon-free value is sent as a Personal Access Token (Authorization: Bearer), while auser:passwordvalue uses HTTP Basic; Cloud v3 continues to use Basicemail:token. Comment creation posts a raw{"body": ...}payload on v2 instead of an ADF document. A construction-time guard rejects inconsistent configuration at startup: a Cloud (*.atlassian.net) endpoint combined withapi_version: 2, or an endpoint that is not a URL with a scheme and host; it also warns when a self-hosted endpoint is left on the default v3. A bare YAML integer (api_version: 2) is coerced rather than treated as absent, thoughsortie validatestill advises quoting it. (#549)
Changed
- Agent tools: the built-in tools now share one uniform result envelope:
{"success": true, "data": <payload>}on success and{"success": false, "error": {"kind": "...", "message": "..."}}on a domain failure. For operators upgrading, this changes the result shape of the two pre-existing Tier 1 tools:sortie_statusandworkspace_historypreviously returned a bare success object and a flat{"error": "message"}failure, and now nest their payload underdataand report failures with a closederror.kind(state_unavailable/state_malformedforsortie_status,query_failedforworkspace_history).tracker_api’s output is unchanged, and the newcost_budgetandnotify_operatortools adopt the envelope natively. Agent prompts or downstream consumers that parsed the previous bare or flat shapes must now read the payload underdataand read failures aserror.kindanderror.message. (#567)
Fixed
- OpenCode adapter: restore the actionable “model not found” diagnostic on invalid-model turns. OpenCode 1.16.0 replaced its per-run unknown-model error with a generic masked server error, so a turn configured with a model absent from the catalog failed with no actionable detail. The adapter now detects the masked placeholder, queries
opencode models, and emits a “model not found” turn failure when the configured model is missing (including over SSH, reusing the existing remote-command path). (#562) - Agent tool advertisement: the first-turn prompt now lists the same tools the MCP server serves for the session. Previously the prompt advertised only
tracker_apiwhile the sidecar also served the Tier 1sortie_statusandworkspace_historytools, so an agent that relied on the prompt for tool discovery was never told those tools existed. The orchestrator worker and the MCP sidecar now build the session tool set through a single shared path, keeping the advertised set and the MCPtools/listresponse identical. (#565) - Windows: workspace hook cleanup no longer fails with a sharing violation when a hook spawns child processes.
TerminateJobObjectandKILL_ON_JOB_CLOSEcan return before dying descendants release their handles, so a child still holding the hook working directory open made the caller’s cleanup fail.RunHooknow terminates any survivors and polls the Job Object until its active-process count reaches zero (2-second cap) before returning. (PR #575)
Migrations
- Add token-accounting columns (
input_tokens,output_tokens,total_tokens,cache_read_tokens) torun_historyasNOT NULL DEFAULT 0; pre-migration rows read back as zero.
1.11.0 - 2026-05-29
Added
- Kiro CLI agent adapter: configure with
agent.kind: kirofor autonomous issue-to-code workflows using the Kiro CLI viakiro-cli chat --no-interactive, following the subprocess-per-turn model of theclaude-codeandcopilot-cliadapters. Kiro headless mode emits no structured event stream and no token counts, so turn outcome is classified from process exit status and stderr: the▸ Credits:cost trailer marks success, anAuthentication failed.line on a bare exit 0 marks failure, and signal exits map to cancellation.StartSessionrequiresKIRO_API_KEYand validates it up front to avoid the silent device-login hang an invalid key would otherwise trigger. The model is pinned per turn with--model, continuation turns resume the workspace conversation with--resume, and akiropassthrough config block exposes the model selector, the tool-trust mode (--trust-tools/--trust-all-tools), and an optional--agentselector. Because the headless path reports no tokens, only time-based budget enforcement applies and notoken_usageevents are emitted; MCP tool injection is unavailable on theKIRO_API_KEYpath.sortie validateacceptsagent.kind: kiroand flags unknownkirosubkeys. Ships with a companionexamples/docker/kiro.Dockerfile(a glibc Debian base, sincekiro-cliis dynamically linked) and anexamples/WORKFLOW.kiro.mdsample workflow. (#515, #517) install.ps1PowerShell installer for Windows: install with the one-linerirm 'https://get.sortie-ai.com/install.ps1' | iex, mirroring the POSIXinstall.sh. Detects architecture, resolves the release tag (honoringSORTIE_VERSIONwhen set), downloads the matchingsortie_<version>_windows_<arch>.zip, verifies its SHA-256 againstchecksums.txt(skippable withSORTIE_NO_VERIFY=1), installs to%LOCALAPPDATA%\Programs\sortieby default (override withSORTIE_INSTALL_DIR), and appends the install directory to the User-scopePATH. Compatible with Windows PowerShell 5.1 and PowerShell 7+, depends only on built-in cmdlets, and forces TLS 1.2. (#541)- Authenticode-signed Windows binaries: the release pipeline now signs Windows
.exeartifacts via SignPath before they are archived, so downloaded binaries are no longer blocked by Microsoft SmartScreen or Smart App Control. Signing runs as a GoReleaser post-build hook and is a no-op for local, snapshot, and pull-request builds. (PR #548)
1.10.0 - 2026-05-27
Added
- Auto-merge reaction for Sortie-created PRs: a new opt-in
reactions.auto_mergeblock inWORKFLOW.mdinstructs the orchestrator to merge an agent-created pull request directly through the SCM adapter once review decision, CI conclusion, draft state, and mergeability all satisfy the configured preconditions. The reconcile loop polls everypoll_interval_ms(default 60 s, minimum 30 s), callsMergePRwith the expected head SHA to close the TOCTOU window, and treats an “already merged” 409 response as success. Merge strategy (squashdefault, alsomergeorrebase),require_ci(defaulttrue),delete_branch(defaulttrue), and the standardmax_retries/escalation/escalation_labelfields are configurable. At startup the orchestrator runs a one-shot scope preflight against the SCM provider; an auth-class failure sets a stickyauto_merge_preflight_failedflag that disables merge attempts for the process lifetime, while a transport-class failure schedules a single retry after 5 minutes.reactions.review_commentsandreactions.auto_mergemust declare the same SCM provider; a mismatch or an unknown provider fails startup. Workflows without anauto_mergeblock are unaffected. TheSCMAdapterinterface gains five write methods (GetReviewDecision,GetCIStatus,GetMergeability,MergePR,DeleteBranch) and a newErrSCMConflicterror kind; see ADR-0012. (#417) - Extension
$VARresolution: every string leaf inside top-level front matter keys outside the core schema (for examplegithub.api_key,worker.ssh_hosts[0],server.host) now resolves$VARand${VAR}environment indirection in a single pass duringNewServiceConfig, afterSORTIE_*overrides are applied. Nested maps and lists are traversed recursively; non-string leaves (integers, booleans, floats, timestamps, nil) are returned unchanged.sortie validatenow emits a new advisoryunresolved_extension_varwarning naming the field path and the unset variable name when a referenced variable is absent from the process environment; the variable’s resolved value never appears in any warning, log, or error. Exit code remains 0 andvalidremainstruewhen only this advisory warning is present. Operators of cross-platform setups (for example a Jira tracker paired with a GitHub SCM adapter) may now reference secrets such as$SORTIE_GITHUB_TOKENdirectly inside the adapter extension block without an externalenvsubststep. (#512) - Dispatch rule routing: a new optional
dispatch:block inWORKFLOW.mdfront matter routes each issue to a specific agent kind and prompt template based on issue metadata. Rules match first-wins onlabels,issue_type,priority,identifier, andassignee(AND across keys, OR within a key), with optionaldispatch.defaultand a final fallback to the workflow-wideagent.kindand body template. Per-rule templates live as Markdown files under the workflow tree and must not carry their own front matter; absolute paths,~expansion, and symlink targets that escape the tree are rejected at load time.sortie validatereports unknown agent kinds, unreachable catch-all rules, duplicate names, malformed globs and priority predicates, and missing or out-of-tree templates before dispatch. The resolved(agent_kind, template_id, rule_name)is frozen at first dispatch and reused across every retry and reaction continuation. Routing outcomes are exposed via thesortie_dispatch_rule_match_total{layer,rule}Prometheus counter and newdispatched_by_rule/dispatched_by_default/dispatched_by_fallbackfields on thetick completedlog line. Workflows without adispatch:section are unaffected. (#435)
Fixed
- Orchestrator: CI-failure and review-comment retries now continue from the configured
tracker.handoff_stateinstead of being dropped when the issue is no longer in an active state. Fresh dispatch remains limited to active states. (#513)
Migrations
- Add
rule_name,template_id, andagent_kindtoretry_entriesandrule_name,template_idtorun_history. Existing rows read back as empty strings and are treated as legacy fallback dispatches.
1.9.1 - 2026-05-14
Fixed
- Orchestrator: CI and PR review pending reactions are now enqueued when
tracker.handoff_stateis configured. Previously, a successful handoff released the claim beforeHandleWorkerExitchecked reaction eligibility, so the reconcile loop had no entry to poll. Post-run CI failures and review comments on agent-created PRs went unobserved and no continuation turn was dispatched. Eligibility now derives from the exit-time claim state and the handoff path; blocked soft stops remain ineligible and existing review-reaction idempotency is preserved. (#506) - Orchestrator: handoff-stage pending review and CI reactions are now reconstructed on startup.
state.PendingReactionsis a runtime-only map, so a restart after a successful handoff previously left the issue with no pending review entry: the tracker issue was no longer active, the dispatch loop did not rediscover it, and human review comments on agent-created PRs went unobserved until an operator manually re-engaged the issue. Startup now rebuilds eligible review and CI pending entries fromrun_history, tracker state, and.sortie/scm.json, bounded to the most recent 200 unique issues completed within the last 30 days and gated by a single batchedFetchIssueStatesByIDscall. Stale candidates age out via a new optionalpushed_atfield in.sortie/scm.json(falling back torun_history.completed_atwhen absent), and existingreaction_fingerprintscontinue to suppress duplicate review-fix dispatches. (#507)
1.9.0 - 2026-04-26
Added
- OpenCode CLI agent adapter: configure with
agent.kind: opencodefor autonomous issue-to-code workflows using the OpenCode CLI viaopencode run --format json. Supports fork-per-turn execution, JSON event normalization, SSH remote dispatch, permission policy synthesis, token accounting, and companion deployment examples viaexamples/docker/opencode.Dockerfileandexamples/WORKFLOW.opencode.md. (#476, #478, #479)
Fixed
- SSH worker command assembly: multi-token remote commands are now appended verbatim instead of shell-quoted as a single word, so remote agent launches such as
codex app-serverand other pre-formed shell fragments no longer fail withcommand not found. (#493) - GitHub and Jira tracker adapters no longer return partially populated issues, slices, or state maps when tracked fetch operations fail; the nil-on-error contract is preserved for issue detail and state lookups. (PR #496)
1.8.0 - 2026-04-17
Added
- Codex CLI agent adapter: configure with
agent.kind: codexfor autonomous issue-to-code workflows using OpenAI Codex CLI via thecodex app-serverJSON-RPC 2.0 protocol. Supports the same structured lifecycle as Claude Code and Copilot CLI adapters: event normalization, token tracking, timeout enforcement, graceful SIGTERM→SIGKILL shutdown, and session resume viaResumeSessionID. Tool calls are serialized through a channel to prevent concurrent stdin corruption. Handshake reads are cancellable via context, preventing stalled subprocesses from hanging the worker indefinitely. (#238)
1.7.1 - 2026-04-15
Changed
- CLI:
--versionnow outputs a single diagnostic line including commit SHA, build date, Go version, and OS/architecture, e.g.sortie 1.7.0 (commit: a1b2c3d, built: 2026-04-15, go1.26.1, linux/amd64). The previous GNU-style copyright/warranty block is removed. Build tooling (Makefile,Dockerfile,.goreleaser.yaml, and the release workflow) now injectsCommitandDatevia-ldflagsat all build sites.
Fixed
- Orchestrator:
sortie_ci_escalations_totalover-counted during CI escalation.escalateCIFailureincremented the metric unconditionally before calling the tracker API, then incremented again on error, producing two increments for one failed operation. It also incremented whenTrackerAdapterwas nil, recording a phantom escalation that was never performed. Both defects are fixed; the metric now increments exactly once per operation outcome, matching the pattern inescalateReviewFailure. (#449)
1.7.0 - 2026-04-13
Added
- Cross-retry session resume: continuation retries now propagate the session ID from the exiting worker through the retry entry so the next worker can resume the agent conversation instead of starting a fresh session. The session ID is persisted to SQLite and restored on startup recovery. (#207, #441)
- Token usage cost estimation on the dashboard and JSON API: operators configure per-adapter token rates in WORKFLOW.md front matter (
token_ratesblock); the dashboard surfaces per-session and aggregate USD cost estimates computed from running sessions. The JSON API includesactive_estimated_cost_usdwhen token rates are configured. (#436, #446) - Dashboard accordion tables: Running Sessions, Retry Queue, and Run History tables use an expand/collapse accordion pattern. Primary status columns are visible at a glance; secondary detail expands on click. Expansion state survives the 5-second auto-refresh via sessionStorage. (#432, #443, #444, #445)
StderrCollectorbuffer hardening: agent stderr collection now uses a 10 MiB scanner buffer cap and a head/tail ring-buffer retention strategy with a configurable byte budget, preventing silent line truncation and unbounded memory growth during long agent turns. (#387, #440)
Changed
- Retry timer tracker validation:
HandleRetryTimernow callsFetchIssueByIDinstead of scanning all candidate issues viaFetchCandidateIssues, reducing each retry timer fire from O(pages) tracker API calls to exactly one. (#206, #442)
1.6.1 - 2026-04-11
Added
- Runtime terminal workspace sweep: workspace directories for issues that reach a terminal tracker state after their worker has exited are now cleaned up periodically by the event loop. This closes the gap between the startup sweep and in-flight reconciliation, preventing unbounded disk accumulation on long-running instances. (#428, #430)
Fixed
- Orchestrator:
needs-human-reviewsoft-stop now correctly triggers the handoff transition. Previously, theSoftStopbranch inHandleWorkerExitmatched all soft-stop reasons before the handoff case was reached, leaving the issue active and causing an infinite re-dispatch loop. (#426, #427)
1.6.0 - 2026-04-10
Added
- Self-review loop before PR creation: the orchestrator generates a workspace diff, executes configurable verification commands (tests, linters), assembles a structured review prompt, and iterates with the agent up to a configurable cap before proceeding. Opt-in via the
self_review:block in WORKFLOW.md front matter (max_iterations,verify_commands,diff_max_bytes). (#312, #413) - PR review comment routing: when a reviewer requests changes on an agent-created PR, the orchestrator detects
CHANGES_REQUESTEDreviews, extracts the review comments, and dispatches a continuation turn so the agent can address feedback automatically. Configurable viareactions.review_commentsin WORKFLOW.md (max_retries,debounce_ms,escalation,escalation_label). IncludesSCMAdapterdomain interface for PR and review operations with a GitHub Checks/Reviews API implementation. (#305, #425) - Unified
reactionsconfig block in WORKFLOW.md for event-driven continuation triggers.reactions.ci_failurereplaces the top-levelci_feedbackkey (which remains supported for backward compatibility). Each reaction type sharesprovider,max_retries,escalation, andescalation_labelfields. (#418, #422) - Windows process lifecycle support for agent adapters and workspace hooks. Agent subprocesses are now placed in Windows Job Objects with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, enabling full process tree cleanup on timeout or cancellation. Graceful shutdown sendsCTRL_BREAK_EVENTto the process group; force-terminate usesTerminateJobObject. Workspace hooks execute viacmd.exe /Con Windows with their own Job Object for timeout enforcement. Theprocutilpackage exposes cross-platform functions (SignalGraceful,AssignProcess,CleanupProcess,SetProcessGroup,KillProcessGroup), andWasSignaledis now platform-aware. Adapters no longer referencesyscall.SIGTERMdirectly. (#390, #391, #407, #409)
Changed
- CI pending backoff base now derives from the operator-configured
poll_intervalinstead of a hardcoded 10 s default. A 30 s poll interval produces a(60 s, 120 s, 240 s, 300 s…)backoff schedule. Falls back to 10 s whenpoll_intervalis zero or negative. (#385, #411)
Deprecated
ci_feedbacktop-level config key in WORKFLOW.md. Usereactions.ci_failureinstead. The legacy key continues to work; when both are present,reactions.ci_failuretakes precedence. (#418, #422)
Fixed
- Workflow Manager continued using the pre-reconfiguration logger after
logging.levelwas applied from WORKFLOW.md extensions, causing reload diagnostics to use the wrong log level. The Manager now updates its logger after every reconfiguration. (#394, #410) - Reaction dispatch fingerprinting:
MarkReactionDispatchedwas called at schedule time rather than actual dispatch time. If the process restarted between scheduling and dispatch, the fingerprint was permanently marked dispatched while the retry entry was lost, silencing future CI-fix reactions for that SHA. (#420, #421) - Config: non-string YAML values in
reactionsfields (provider,escalation,escalation_label) now produce aConfigErrorinstead of silently coercing to an empty string. (#423, #424) install.sh: detect Rosetta 2 on macOS and prefer the nativearm64binary overamd64. (#391, #409)
1.5.1 - 2026-04-08
Added
- GNU-style CLI help output with grouped sections, column-aligned descriptions, usage examples, and a “Learn more” link. Short aliases
-h(help) and-V(version) are now recognized. Help text prints to stdout instead of stderr. Covers all three commands:sortie,sortie validate,sortie mcp-server. (#398, #403)
Fixed
- Copilot CLI adapter: prefix
--additional-mcp-configfile paths with@to match the documented Copilot CLI syntax. Bare file paths worked in Copilot CLI ≤1.0.18 via an undocumented fallback that was removed in v1.0.21, causingInvalid JSON in --additional-mcp-configon every turn. Operator-providedcopilot-cli.mcp_configvalues are now auto-detected: inline JSON is passed unchanged,@-prefixed paths are preserved, and bare file paths receive the@prefix automatically. (#404, #405) - Agent adapters: reclassify exit-code-0 turns with zero output tokens and no
resultevent asturn_failedinstead ofturn_completed. Previously, when an agent subprocess crashed immediately (e.g., MCP config parse error) but exited 0, all turns were counted as successful, causing the orchestrator to exhaustmax_turnsand trigger a false-positive handoff transition. Failed turns now retry with exponential backoff. Applies to both Claude Code and Copilot CLI adapters. (#404, #406)
1.5.0 - 2026-04-07
Added
- Always-on HTTP server with default port 7678 (mnemonic: SORT on T9). The server now starts unconditionally; the
--portflag overrides the default but no longer acts as an activation trigger. Pass--port=0to disable. Prometheus metrics, health probes, and dashboard are available out of the box without flags. --hostCLI flag andserver.hostworkflow config field for configurable bind address. Default127.0.0.1; container deployments override with--host 0.0.0.0. Resolution order: CLI flag > config > default.--log-formatCLI flag with valuestext(default) andjson. JSON format emits one JSON object per log line withtime,level,msg, and structured fields (issue_id,session_id, etc.) for integration with Loki, Datadog, CloudWatch, and ELK.- Dockerfile: multi-stage build producing a distroless container image with only
/usr/bin/sortie. Users consume viaCOPY --from=ghcr.io/sortie-ai/sortie:latest /usr/bin/sortie /usr/bin/sortiein their own Dockerfile. Build flags match.goreleaser.yaml:CGO_ENABLED=0,-trimpath,-s -w, tagsosusergo,netgo. .dockerignoreexcluding build artifacts,.git, test fixtures, and documentation.- Agent-specific example Dockerfiles:
docker/claude-code.Dockerfileanddocker/copilot.Dockerfilewith non-root user, health checks, and volume mounts. - Kubernetes deployment examples:
k8s/deployment.yaml(Recreate strategy, liveness/readiness probes on/livezand/readyz),k8s/configmap.yaml,k8s/service.yaml,k8s/pvc.yaml. - Grafana dashboard template at
grafana-dashboard.jsoncovering all 22 Prometheus metrics. Panels fordispatch_transitions_total,tracker_comments_total,ci_status_checks_total, andci_escalations_totaladded in dedicated CI Feedback and Integration rows. Uses__inputs/DS_PROMETHEUSpattern for portable data source selection.
Fixed
- Agent stderr is now surfaced at WARN level when a turn fails instead of DEBUG. Both Claude Code and Copilot CLI adapters buffer stderr during a turn and log at WARN on non-zero exit, making startup rejections (e.g.,
--dangerously-skip-permissionsunder root) visible at default log level. Successful turns continue to log stderr at DEBUG.
1.4.0 - 2026-04-04
Added
- CI feedback loop: when a CI pipeline fails on an agent-created branch, the orchestrator detects the failure, injects the CI failure logs into the next agent turn, and dispatches a continuation session so the agent can diagnose and fix the issue automatically. Controlled by a new
ci_feedbackconfig section inWORKFLOW.md(kind,max_retries,max_log_lines,escalation). Feature activation follows kind-based convention: presentci_feedback.kindenables, absent disables. CIStatusProviderdomain interface: adapter contract for fetching CI check status from an SCM platform. Returns structuredCIResultwith overall status (pending/passing/failing), individual check runs, and an optional log excerpt from the first failing check.- GitHub
CIStatusProviderimplementation via the Checks API: fetches check runs for a git ref, computes aggregate status, and retrieves truncated log output from the first failing GitHub Actions job. Log fetching is controlled bymax_log_lines(0 disables). ANSI escape sequences are stripped from log output. - CI failure escalation: when
ci_feedback.max_retriesis exceeded, the orchestrator applies a configurable escalation action: add a label (defaultneeds-human) or post a comment on the issue. - Exponential backoff for CI pending re-enqueue:
reconcileCIStatusnow appliesbase * 2^attemptsbackoff (capped at 5 minutes) when CI checks remain pending or on transient API errors, reducing GitHub Checks API request volume from ~120 to ~15 per 20-minute CI run per issue. Stale pending entries expire after a 30-minute TTL. TrackerOpsWgshutdown drain: fire-and-forget tracker API goroutines (comment posting, label adding) are now tracked by a dedicatedsync.WaitGroupand drained during graceful shutdown with a 35-second timeout, preventing orphaned goroutines on process exit.- Automatic credential merging: when
ci_feedback.kindmatchestracker.kind, the orchestrator merges tracker credentials (api_key,project,endpoint) into the CI provider adapter config at startup, eliminating the need to duplicate credentials in a pass-through block.
1.3.0 - 2026-04-03
Added
- MCP tool execution channel: agents can now call registered tools at runtime via the Model Context Protocol. The worker generates
.sortie/mcp.jsonper session and passes it to the agent runtime via--mcp-config(Claude Code) or--additional-mcp-config(Copilot CLI). The agent runtime spawnssortie mcp-serveras a stdio sidecar; the orchestrator does not manage the sidecar lifecycle. sortie mcp-serversubcommand: MCP stdio JSON-RPC server that exposes registeredAgentToolimplementations viatools/listandtools/call. Constructs its ownTrackerAdapterandToolRegistryby re-readingWORKFLOW.mdfrom an absolute path passed via--workflow.sortie_statusMCP tool (Tier 1): returns live session runtime metadata: current turn number, remaining turns, attempt number, session duration, and cumulative token usage. Reads from.sortie/state.json, a worker-written file updated at session start, each turn start, and on token usage events.workspace_historyMCP tool (Tier 1): returns up to 10 most recent completed run attempts for the current issue from therun_historySQLite table. Opens the database in read-only mode (?mode=ro). Non-fatal on database open failure: the MCP server continues with other tools available.- Agent-to-orchestrator file protocol: agents can write
blockedorneeds-human-reviewto.sortie/statusto suppress continuation retries. The orchestrator reads the file after each turn and before the tracker state refresh. Absent, unrecognized, or unreadable files degrade to normal behavior. Symlinks on either path component are rejected viaLstat. RuntimeStatusSuffixauto-injection: the orchestrator appends A2O protocol instructions to the first-turn prompt so agents know how to signal blocked status without workflow author intervention. Continuation turns omit the suffix.- Soft-stop exit path in
HandleWorkerExit: when the worker exits with a recognized status file signal, the orchestrator releases the claim and suppresses continuation retry. The issue re-dispatches only on tracker state change. - Operator MCP config merging: if
mcp_configis set in WORKFLOW.md, the worker merges the operator’s config with thesortie-toolsentry. Name collision onsortie-toolsis a validation error.
Documentation
- Agent-to-orchestrator file protocol specification (
docs/agent-to-orchestrator-protocol.md): 9-section normative document covering file format, recognized values, read timing, cleanup lifecycle, symlink rejection, and conformance checklist. - ADR-0009: MCP stdio sidecar for tool execution. Documents the chosen transport mechanism, process model, credential handling, adapter integration, and alternatives analysis.
- Architecture Section 10.4 rewrite:
AgentToolinterface contract,ToolRegistryinvariants, tier classification framework, andtracker_apitool specification aligned with implementation.
1.2.1 - 2026-04-01
Fixed
- Prevent re-dispatch loop when effort budget (
agent.max_sessions) is exhausted for an issue; the orchestrator now records a durable budget-exhaustion guard so the dispatch loop does not restart the issue on the next tick - Persist and display
turns_completedper run in the dashboard and run history table - Show fully qualified
owner/repo#Ndisplay identifiers for GitHub issues in the dashboard and API instead of bare issue numbers - Pass
handoff_statetofindCurrentStateLabel,extractState,normalizeIssue, andnormalizeBlockersin the GitHub adapter soTransitionIssueaccepts the handoff state and stale labels are removed during transitions - Rename dashboard footer cache label from “Cache:” to “Cache Read:” and add explanatory tooltips in the footer and Running Sessions table
- Defer
token_usageevent emission in the Claude adapter when an assistant message carries zero output tokens (tool_use-only messages in Claude Code 2.xstream-jsonformat); the adapter now accumulates input tokens and falls back to the result event which carries correct totals
Migrations
- Add index
idx_run_history_issue_idonrun_history(issue_id) - Add
turns_completed INTEGER NOT NULL DEFAULT 0torun_history - Add nullable
display_identifiercolumn torun_history
1.2.0 - 2026-03-31
Added
- GitHub Copilot CLI adapter: configure with
agent.kind: copilotfor fully automated issue-to-code workflows using GitHub’s headless Copilot CLI. Supports local execution and SSH remote dispatch viaworker.ssh_hosts. Tool scope is controlled byallowed_tools,denied_tools,available_tools, andexcluded_tools;--allow-allis the default when none are set. Session continuity across turns via--resume. Authentication uses token env vars when present, falling back togh auth status. worker.ssh_strict_host_key_checking: new optional worker config field controlling OpenSSHStrictHostKeyCheckingfor remote SSH agent sessions. Acceptsaccept-new(default, Trust On First Use),yes(strict verification, requires a pre-populatedknown_hosts), orno(disable host-key checking). Applies to both the Claude Code and Copilot CLI adapters.
1.1.0 - 2026-03-30
Added
- GitHub Issues tracker adapter: configure with
tracker.kind: githubandtracker.project: OWNER/REPO. State management is label-based;TransitionIssueapplies and removes GitHub labels with convergent retry on partial failure. (#311) - GitHub adapter: in-memory ETag cache for reconciliation polls.
If-None-Matchconditional requests return304 Not Modifiedon unchanged issues, reducing GitHub API rate limit consumption during active runs. (#316) sortie validateGitHub adapter config validation: emits diagnostics fortracker.projectformat (OWNER/REPO),GITHUB_TOKENenvironment variable hint, empty state labels, and active/terminal state label overlap. Errors block dispatch; warnings are advisory. (#317)
Fixed
install.sh: checksum verification used substring matching (grep | awk) that could accept a checksum entry for the wrong archive entry; now uses exact field matching (awk '$2 == f').
1.0.0 - 2026-03-29
Added
- SPDX JSON Software Bill of Materials (SBOM) included with every release archive, generated via
syftin the GoReleaser pipeline for supply-chain auditing.
0.0.10 - 2026-03-28
Added
sortie validatetemplate static analysis: three advisory warning classes,WarnDotContext(top-level key referenced inside{{ range }}or{{ with }}where dot is redefined),WarnUnknownVar(variable not in the{issue, attempt, run}contract), andWarnUnknownField(valid top-level key with an unknown sub-field, including depth-4+ field chains on known level-3 scalars). Warnings appear in both text and JSON output without blocking dispatch or changing the exit code.sortie validatefront matter schema validation: detects unknown top-level keys, unknown sub-keys within known sections, type mismatches, and semantic issues (non-positivehooks.timeout_ms, non-numericmax_concurrent_agents_by_stateentries). Field paths are included in every diagnostic so operators can locate the offending key. Warnings are advisory only.SORTIE_*environment variable config overrides: any workflow front matter key can be overridden via aSORTIE_-prefixed environment variable (e.g.,SORTIE_POLLING_INTERVAL_MS=5000). Non-empty real env vars take precedence over.envfile values. Raw line content is removed from.envparse errors and override values are excluded from debug logs to prevent secret leakage.- Orchestrator: tracker comments posted at session lifecycle points (session start, successful completion, and failure) with run duration and attempt metadata. Comments fire from a detached goroutine to avoid blocking the event loop.
- Orchestrator: issues are transitioned to the configured
in_progress_stateon dispatch, with a no-op skip when the issue is already in the target state.sortie_dispatch_transitions_totalPrometheus counter trackssuccess,error, andskippedoutcomes.
0.0.9 - 2026-03-27
Added
sortie validatesubcommand for one-shot workflow file validation without starting the orchestrator, opening the database, or spawning a filesystem watcher. Supports--format text(stderr diagnostics) and--format json(structured stdout output) for CI pipelines and pre-commit hooks.--dry-runflag for a single read-only poll cycle that validates the full startup sequence (workflow load, preflight, database, adapter wiring) without dispatching work or persisting state.--log-levelflag andlogging.levelworkflow extension key to set the minimum log severity at startup (debug,info,warn,error).- Dashboard: Workflow column in Active Sessions table and a new Run History table showing completed session outcomes, timing, and workflow file. SQL migration 003 adds a nullable
workflow_filecolumn torun_history. - Workspace root write-permission check in dispatch preflight. Surfaces a clear diagnostic instead of failing mid-dispatch.
- Homebrew tap distribution via GoReleaser-managed tap repository (
brew install sortie-ai/tap/sortie). - Jira adapter:
User-Agentheader (sortie/<version>) sent on every HTTP request. - Claude Code adapter: per-request
APIDurationMSontoken_usageevents for API-call-level latency visibility, clamped to a minimum of 1 ms. - Claude Code adapter: tool error text now included in
EventToolResult.Message.
Fixed
- CLI: adapters implementing
io.Closerare now closed during graceful shutdown, preventing resource leaks. - CLI:
sortie validateroutes flag-parse errors through the diagnostics emitter and no longer prefixes the error kind redundantly. - Orchestrator:
TurnCountincrements onsession_startedinstead of session finalization, correctly reflecting in-progress turns. - Claude Code adapter: tool error messages stripped of XML markup and tail-truncated to prevent oversized events.
0.0.8 - 2026-03-26
Added
- JSON API server with
GET /api/v1/state,GET /api/v1/<identifier>, andPOST /api/v1/refreshendpoints for programmatic access to orchestrator state. Enabled via--portflag orserver.portconfig. - HTML dashboard at
/with auto-refreshing view of running sessions, retry queue, token totals, and runtime statistics when the HTTP server is enabled. /livezand/readyzhealth endpoints following Kubernetes z-pages conventions./readyzchecks database accessibility, preflight validation, and workflow loading.- Prometheus
/metricsendpoint exposing session gauges, dispatch/worker/retry counters, token counters, tracker request counters, tool call counters, poll and worker duration histograms, andsortie_build_info. Uses a dedicatedprometheus.Registry, compatible with standard Prometheus scrape configs. tracker_apiclient-side tool: agents can query the tracker during sessions to fetch issues and comments, scoped to the configured project.- SSH worker extension via
worker.ssh_hostsconfig: dispatch agent runs to remote hosts over SSH with round-robin host selection and per-host concurrency limits (worker.max_concurrent_agents_per_host). - Per-session token breakdown in JSON API and dashboard:
input_tokens,output_tokens,cache_creation_tokens,cache_read_tokens. - Per-session timing breakdown in JSON API and dashboard:
elapsed,agent_time,idle_time,agent_pct. - Claude Code adapter:
tool_resultevents now emitted, making agent tool invocations visible in the dashboard and API. - Worker failure logging in
HandleWorkerExit: WARN withnext_attemptanddelay_msfor retryable errors, ERROR for non-retryable errors. - Structured logging:
issue_id,issue_identifier, andsession_idcontext fields now present on all orchestrator lifecycle log lines. Agent tool calls logged at INFO level. - POSIX-compatible install script (
install.sh) for automated binary installation.
Changed
POST /api/v1/refreshreturns409 Conflictduring graceful shutdown instead of accepting requests that cannot be fulfilled.
Fixed
- Claude Code adapter: duplicate
token_usageevents no longer emitted when assistant-level usage is already reported in the result message. - HTTP server:
405 Method Not Allowedresponses now include theAllowheader per RFC 9110. - Jira adapter:
sortie_tracker_requests_totalcounter no longer increments on no-op calls with empty ID lists.
0.0.7 - 2026-03-24
Added
- Graceful shutdown: on
SIGTERM/SIGINTthe orchestrator now drains running workers (up to 30 s), persists final state to SQLite, flushes pending agent events, and cancels retry timers before exiting. - Issue handoff via
tracker.handoff_stateconfig field. When an agent session completes normally and the issue is still in an active state, the orchestrator transitions it to the configured handoff state (e.g., “In Review”) and skips the continuation retry. TransitionIssueoperation on theTrackerAdapterinterface. Jira adapter uses the workflow transitions API; file adapter uses an in-memory override map.- Per-issue effort budget via
agent.max_sessions. Limits total agent sessions dispatched per issue before releasing the claim. Default 0 (unlimited). - Documentation site at https://docs.sortie-ai.com/ with initial configuration reference.
Fixed
- Orchestrator: continuation retry attempt counter now increments correctly across sessions instead of resetting to 1 on every normal exit.
- CLI: orchestrator-only fields (
max_turns,max_concurrent_agents,max_retry_backoff_ms,max_concurrent_agents_by_state) removed from the adapter config map, fixing silent shadowing of adapter extension keys such asclaude-code.max_turns. - Jira adapter:
extractStringSlicenow handles[]stringfrom the config layer. Previously only[]anywas handled, silently reverting to default states and causing configuredactive_states/terminal_statesto be ignored. - Jira adapter:
FetchIssueStatesByIDsnow queries by numericidinstead ofkey, and results are keyed by issue ID, fixing reconciliation failures where state changes on running issues were never detected. - Jira adapter: non-numeric IDs are now rejected instead of silently mangled, and empty ID lists no longer produce invalid
id IN ()JQL. - File and Jira adapters now return
ErrTrackerNotFoundfor missing issues inFetchIssueByIDandFetchIssueComments. - Orchestrator: INFO-level tick summary log after each dispatch cycle with candidate, dispatched, running, and retrying counters to distinguish normal operation from a stall.
0.0.6 - 2026-03-23
Added
- Orchestrator engine with state management, concurrency-limited dispatch, worker lifecycle, exponential-backoff retry scheduling, active-run reconciliation, and event-driven poll loop with graceful shutdown.
- Full startup sequence: workflow load, preflight validation, database open, state reconciliation, and poll loop, in that order.
- Dispatch preflight checks that validate adapter availability, required API keys, and agent configuration before dispatching work.
- Adapter metadata via
AdapterMetaandRegisterWithMetaso adapters can declare requirements (e.g.,RequiresAPIKey) checked during preflight. - Retry classification on
TrackerErrorKindandAgentErrorKind. Errors are now classified as retryable or permanent for dispatch decisions. ErrTrackerNotFounderror kind for HTTP 404 responses from tracker adapters.- Configurable
db_pathfield in workflow configuration with~and$VARexpansion. - Workflow validation callback (
ValidateFunc) that guards config promotion during hot-reload.
Fixed
- Workspace
CleanupByPathnow rejects non-canonical paths and uses the actual workspace path for pending cleanup instead of reconstructing it from config. - Startup: preflight checks now run before opening the database, preventing
.sortie.dbcreation when configuration is invalid. - Startup:
.sortie.dbis now created adjacent toWORKFLOW.mdinstead of in the working directory.
0.0.5 - 2026-03-21
Added
- Workspace manager: safe path computation from issue identifiers with containment validation and symlink rejection.
- Workspace manager: atomic directory creation and reuse with
CreatedNowflag for hook gating. - Workspace hook execution with configurable timeout, truncated output capture, and restricted subprocess environment (only
PATH,HOME,SHELL, andSORTIE_*variables are inherited). - Workspace lifecycle orchestration:
Prepare,Finish, andCleanupfunctions that sequenceafter_create,before_run,after_run, andbefore_removehooks with appropriate failure semantics (fatal vs best-effort) andcontext.WithoutCancelfor teardown hooks. - Batch workspace cleanup (
CleanupTerminal) for removing terminal-state issue workspaces with per-identifier error collection and best-effortbefore_removehook execution. ListWorkspaceKeysfor enumerating workspace directory names under a root, skipping non-directories and symlinks.
0.0.4 - 2026-03-20
Added
AgentAdapterinterface and normalized event model: 13 event types,TokenUsage,AgentConfig,Session,TurnResult, andAgentErrorwith 9 error kinds.- Agent adapter registry (
registry.Agents) for registration and lookup by kind. - Claude Code agent adapter (kind
"claude-code") that launches the CLI as a subprocess, reads JSONL events from stdout, and normalizes them to domain event types. Supports graceful SIGTERM→SIGKILL shutdown on context cancellation and session resumption viaResumeSessionID.
Fixed
- Claude Code adapter: double-wait race between
RunTurnandStopSession.gracefulKillis now fire-and-forget with timer-based SIGKILL escalation. - Claude Code adapter: error on missing binary now includes the actual command name instead of a hardcoded string.
0.0.3 - 2026-03-20
Added
- Normalized
Issuemodel andTrackerAdapterinterface for multi-tracker support. - Typed adapter registry with thread-safe registration and lookup.
- File-based tracker adapter for local JSON task definitions.
- Jira Cloud REST API v3 adapter with cursor-based paginated search, issue detail retrieval, state tracking, and comment fetching.
- BFS flattener for Atlassian Document Format (ADF) descriptions to plain text.
- JQL builder with string escaping and optional
query_filterclause support. query_filterfield in tracker configuration for custom JQL expressions.- GoReleaser configuration for reproducible cross-platform binary releases (linux/darwin/windows, amd64/arm64).
Fixed
- Jira search endpoint migrated from retired
/rest/api/3/searchto/rest/api/3/search/jql(Atlassian returns 410 Gone on the old endpoint). - Infinite loop guard in Jira comment pagination when the API returns inconsistent offsets.
0.0.2 - 2026-03-19
Added
- SQLite persistence layer with WAL mode and single-writer enforcement.
- Schema migration runner with versioned SQL files.
- CRUD operations for retry entries, run history, session metadata, and aggregate metrics.
- Startup recovery loader that resumes incomplete retry entries on restart.
Fixed
- Deterministic ordering for session metadata queries via
session_idtie-breaker.
0.0.1 - 2026-03-18
Added
WORKFLOW.mdfile loader with YAML front matter and prompt body parsing.- Typed configuration layer with
$VARenvironment variable resolution and~home directory expansion. - Prompt template engine using Go
text/templatein strict mode (unknown variables and filters cause hard errors). - Turn-based prompt builder for multi-turn agent conversations.
- Filesystem watcher for live
WORKFLOW.mdreload viafsnotify. - CLI entry point (
sortie) with graceful shutdown and signal handling.
Fixed
- Environment variable expansion now preserves inline
$VARreferences inside URIs instead of silently dropping them. - Fractional float values no longer silently coerced to integers during config parsing.
0.0.0 - 2026-03-18
Added
- Go module scaffold and project directory structure.
- Structured logging built on
log/slogwith issue-aware and session-aware contextual fields. - CI pipeline with
golangci-lint,gofmtenforcement, and test execution via GitHub Actions. - Architecture Decision Records (ADR-0001 through ADR-0005).
Was this page helpful?