Repository navigation
Releases: SeasonTemple/agent-arranger
Release list
v1.8.1 — v1.4a multipart staging→worktree pipe fix
Fixes the v1.4a regression where multipart POST /plans/new uploads
landed at the adapter as bare basenames after the handler had already
deleted the staging dir on return — opencode then failed to find the
files. v1.4b JSON-body submission stayed functional throughout; only
the file-upload path was affected. Branch: fix/v1.4a-multipart-staging -pipe. Decision chain: docs/brainstorms/2026-05-21-002-fix-v14a -multipart-staging-pipe-requirements.md, docs/plans/2026-05-21-002 -fix-v14a-multipart-staging-pipe-plan.md. Replay oracle
(legacyFoldQuerySnapshot) body byte-identical vs v1.8.0-ui1.
Fixed
- Multipart
POST /plans/newupload pipe wired end-to-end.
Handler now stages uploaded files at.arranger/staging/<plan> /<task>/(additive bootstrap — no schema migration), persists the
path ontasks.staging_pathat submit time, and the executor copies
each staging file into the per-runtime worktree at dispatch time
(KD4 copy-not-move so retries land in a fresh worktree without
staging mutation). The handler's eageros.RemoveAll(stagingDir)
defer is replaced with an error-only defer; happy-path cleanup is
now owned byarranger gc. On copy failure the executor publishes
task.failed{reason: upload_move_failed}against the RID aggregate,
surfaces the failed file in the error message, and never spawns the
adapter. - Per-worktree commit exclusion for upload bytes. Copied upload
files are listed in the per-worktreeexcludes.local(via
extensions.worktreeConfig+core.excludesFile) so subsequent
git add -Ainside the worktree does not commit user-input bytes
into per-task commits.
Changed
arranger gccovers staging dirs. Both--dry-runand--apply
now enumerate.arranger/staging/<plan>/<task>/per terminal plan;
--applyremoves them and nullstasks.staging_path. An orphan
sweep after the per-plan loop reaps.arranger/staging/<plan>/dirs
whose<plan>is not in theplanstable (submission-failed-after-
mkdir, manual leftovers, pre-fix residue).arranger gc --jsonshape. Per-plan entries gain
staging_dirs(paths enumerated) andremoved.staging_dirs(count
removed under--apply). Summary gainsstaging_dirs_total,
orphan_staging_dirs,orphan_staging_removed,orphan_errors.workspace.Manager.RemovePlansignature. Now takes
(planID, workspacePaths, stagingDirs, removePlanBranch)—
caller-orchestrated staging removal. Only caller isarranger gc.
v1.8.0-ui1 — HITL UI chip — w3b functional-layer closeout
Closes the operator-facing UI gap left by v1.7.0-rt3 (wave-3b's HITL
backend shipped with no UI surface). Decision chain: docs/brainstorms/ 2026-05-21-001-feat-hitl-ui-chip-requirements.md, docs/plans/ 2026-05-21-001-feat-hitl-ui-chip-plan.md, ADR 0001 wave-3 UI addendum
(sixth addendum). Replay oracle (legacyFoldQuerySnapshot) body
byte-identical vs v1.7.0-rt3.
Added
- HITL chip on
/ui/plans/{id}. Tasks in the HITL state (the
approval gate fromapproval: required) render astate-badge state-hitlsibling span in the status<td>plus inline
button.hitl-approveandbutton.hitl-rejectcontrols. Approve is
single-click; reject is a two-step inline button (4-second timeout
on first click, second click commits). Clicks POST to the existing
/api/runtimes/<rid>/signalwith explicitX-Requested-With: arrangerheader; chip flip happens via SSEtask.resume/
task.cancelled, not via HTTP ACK (KD9 race-converged-by-SSE
posture). - SUSPENDED chip visual. Tasks the operator paused via CLI
(arranger runtime suspend <rid>) render a distinctstate-badge state-suspendedsibling span. The badge'stitleattribute carries
the literalarranger runtime resume <rid>invocation so hover
surfaces both the recommended command and the literal RID. No
buttons — suspend/resume stay CLI-only (KD2). TaskSnapshot.RuntimeState+RuntimeRIDfields on the
snapshot read path; populated byQuerySnapshotvia a correlated
subquery on theruntimestable that picks the latest-attempt RID
per task. CLI'sarranger showJSON gainsruntime_stateand
runtime_ridnaturally; UI server-render and SSE reconnect resync
use the same data shape.- SSE handler extension (
internal/ui/static/ui.js): cases
task.hitl/task.suspend/task.resumejoin the existing
evTypeswitch alongsidetask.stalled/task.abandoned/
terminals.data-task-stateis the JS-side single source of truth
for the wave-3 runtime state visual;data-task-ridrefreshes on
every event that marks a new RID's life so retry-chain transitions
are observable on the chip. - Reconnect snapshot resync (R14): JS detects SSE reconnect via
htmx:sseErrorfollowed byhtmx:sseOpen, debounces 250 ms, then
fetchesGET /api/plans/{id}and reconciles each chip's
data-task-stateagainst the snapshot'sRuntimeState. Closes the
SSE-backfill-window-miss footgun without raising the 200-event
backfill limit.
Changed
event_item.tmplpartial stampsdata-task-idon each
rendered<li>, parsed from the_rtenvelope. Closes a latent
post-wave-1 regression where SSE-driven status flips silently
no-op'd because the lookup useddata-aggregate-id(the RID since
w1) againsttr[data-task-id](the stable task id).- Terminal SSE handler now also clears any HITL / SUSPENDED state
badge + buttons ontask.cancelled/task.completed/
task.failed/task.abandoned/task.budget_exceeded.
task.cancelledis the new wave-0 terminal the chip handles
natively (the dispatcher's terminal forsig.reject).
Scope cap (KD7)
The brainstorm's KD7 explicit scope cap is reflected in what is NOT
in this entry: no global "pending approvals" panel, no modal /
confirm() reject infrastructure, no UI for sig.suspend /
sig.resume, no new endpoint, no new SSE channel. Expansion waits
for real operator usage data — explicitly out of scope until then.
Hardness
go test -race -p 1 ./...green across all 17 packages on every
unit commit.- Frozen
legacyFoldQuerySnapshotbody in
internal/orchestrator/replay.gobyte-identical vs main at every
unit boundary (verified viaawk-extracted diff). state ≠ statusdiscipline:data-task-status(wave-0) is
unchanged on HITL / SUSPENDED chips;data-task-state(wave-3) is
the new attribute carrying the runtime state. Frozen oracle reads
Status only; RuntimeState / RuntimeRID are wave-3 additive on the
materialized projection.
v1.7.0-rt3 — wave-3 lifecycle FSM + HITL signal track + workspace re-root
Wave 3 of the runtime-host relayering (ADR 0001). Breaking, no compat
shims (pre-stable incubation). The replay oracle (frozen
legacyFoldQuerySnapshot) remains the merge gate and its body is
byte-unchanged across every sub-wave. Decision chain: docs/brainstorms/ 2026-05-20-*-requirements.md, docs/plans/2026-05-20-*-plan.md,
docs/adr/0001-* (wave-3 / wave-3b / wave-3c addenda), CONTEXT.md.
Wave 3a — Lifecycle FSM (additive guard)
A canonical Runtime lifecycle FSM (internal/orchestrator/fsm.go) runs
inside the existing PostInsertHook transaction. Illegal new-space
transitions roll back the entire publish (event row never commits, FSM
reject rolls back atomically). Conservative-extension contract: every
pre-w3a fixture passes byte-for-byte; only the SPAWNING / WAIT_TOOL /
HITL / SUSPENDED / CHECKPOINT new-space states gate.
Wave 3b — HITL signal track (opt-in approval)
Side-channel sig.* events flow through a dedicated dispatcher (NOT the
FSM) with aggregate_type="signal". Core 4 verbs: sig.approve,
sig.reject, sig.suspend, sig.resume. Plan-spec opt-in approval: required adds a pre-dispatch HITL gate; executor stages pendingApproval
and OnSignalResume clears it. HTTP POST /api/runtimes/<rid>/signal
and four CLI verbs (arranger runtime approve|reject|suspend|resume)
are the operator surfaces.
Wave 3c — Workspace re-root by RID
Live worktrees move from .arranger/worktrees/<plan>/<task>/ to
.arranger/runtimes/<rid>/. Workspace is now a recreatable attachment
to a Runtime, not a (plan, task) identity. Breaking shape — no
migration: existing .arranger/worktrees/ trees are orphaned and
hand-removed.
Added (wave-3c)
runtimes.workspace_pathcolumn (additive, nullable) stamped by Step A
ontask.readyvia the existing COALESCE-on-update mechanism.
RuntimeRow.WorkspacePathexposed through the read path
(ListRuntimes/GetRuntime/arranger runtime ls|inspect).workspace.Manager.CreateRuntimeWorktree(ctx, rid, branch, base)+
WorktreePath(rid)+RemoveRuntime(rid)— new RID-keyed worktree
API. Brancharranger/<plan>/<task>is shared across retries; R14
reuse viagit worktree add --force(the prior-RID worktree's branch
ref is invalidated; files remain for audit).- New recover-fail reason
workspace_missing(Plan R13). Operator triage
distinguishes a crashed orchestrator (workspace intact —
orchestrator_restart) from a workspace whose directory or column is
gone (workspace_missing). Frozen oracle readserror, notreason
— byte-equal-safe. store.ListPlanRuntimes(planID)+store.NullifyWorkspacePaths(rids)
for gc's DB-driven enumeration + KD9 cleanup.
Changed (wave-3c)
workspace.Manager.RemovePlan(planID, workspacePaths []string, removePlanBranch bool)andEnumeratePlanResources(planID, workspacePaths []string, includePlanBranch bool): signatures take the
caller-supplied workspace paths instead of walking the filesystem.
cli/gc.godiscovers paths via the runtimes table.internal/orchestrator/append_task.go::injectUpstreamArtifactsand
internal/ui/artifact.go::handleArtifactresolve the upstream
worktree viaStore.ResolveLatestRID(planID, taskID)→
WorktreePath(rid). Empty RID returns a 404 / nil artifacts list
rather than synthesizing a path.arranger gcenumerates runtimes byplan_idfrom the DB rather
than walking.arranger/worktrees/<plan>/. On--apply, NULL out
runtimes.workspace_pathafter the removal so subsequent
arranger runtime lsreflects the absent workspace and recover's
workspace_missing gate fires cleanly on next boot.
Removed (wave-3c)
workspace.Manager.CreateTaskWorktree(planID, taskID, base)—
superseded byCreateRuntimeWorktree.workspace.Manager.WorktreePath(planID, taskID)— old task-keyed
accessor (the newWorktreePath(rid)claims the canonical name).workspace.Manager.Remove(planID, taskID)— was unused.- Legacy
.arranger/worktrees/<plan>/<task>/filesystem layout. No
migration — operators rerun the affected plans after upgrade.
Hardness
go test -race ./...green across all 17 packages on every sub-wave.- Frozen
legacyFoldQuerySnapshotbody in
internal/orchestrator/replay.gobyte-equal vs the wave-2 release
(verified via awk-extracted diff at every sub-wave boundary). - Single-arranger assumption retained. Global git mutex retained.
PostInsertHook two-step contract retained. FSM conservative-extension
contract retained.
v1.6.0-rt2 — wave-2 SBI hardening + recoverable-session
Wave 2 of the runtime-host relayering (ADR 0001). Breaking, no compat shims
(pre-stable incubation). Builds on the wave-1 RID keystone; the replay oracle
(frozen legacyFoldQuerySnapshot) remained the merge gate and its body is
byte-unchanged. Decision chain: docs/brainstorms/2026-05-19-002-*,
docs/plans/2026-05-19-002-*, docs/adr/0001-* (wave-2 addendum), CONTEXT.md.
Added
- SBI — the Stable Driver Boundary Interface.
internal/adapteris now
the complete, provider-agnostic, transport-invariant host↔driver contract.
New optionalRuntimeRecoverercapability (RecoverFromHandle) — continues
a durable provider session under a fresh observed process after a host
crash. Provider-internal-freeinternal/adapter/stubdriver + a durable
host-purity guard test prove a second driver needs zero host change. - In-process recoverable-session. A hard crash no longer abandons in-flight
agents. The durable session handle (+ pgid + owner nonce) is captured early
via a snapshot-invisibletask.session_boundevent into theruntimes
projection; boot recover probes liveness and recover-reattach (alive:
kill the unobservable orphan, then continue) / recover-resume (dead:
continue) / recover-fail (no handle or driver lacks the capability,
byte-compatible with the old zombie-fail). A plan with ≥1 recovered Runtime
is not plan-zombie-failed. runtimescolumnssession_handle,pgid,owner_nonce(additive on the
wave-1 nullable schema).
Changed
Session.OpencodeSessionID()→ provider-agnosticSession.SessionHandle().- opencode self-verifies inside
opencode.NewAdapter; the host no longer reads
opencode internals or gates boot on them (Orchestrator.OpencodeDBremoved). ReapOrphanslifted out oforchestrator.New()to run afterRecover()
with an intentional keep-pgid ownership guard (R9), replacing the accidental
instance-id-mismatch skip.
v1.5.0-rt1 — wave-1 runtime-host relayering (ADR 0001)
Wave 1 of the runtime-host relayering (ADR 0001). Breaking, no migration —
this is a pre-stable incubation cutover (memory feedback-no-conservatism-incubation):
point arranger at a brand-new DB file; pre-cutover events are discarded, not
migrated. Decision chain: docs/ideation/2026-05-19-*, docs/brainstorms/2026-05-19-001-*,
docs/plans/2026-05-19-001-*, docs/adr/0001-*, CONTEXT.md.
Added
- Runtime as a first-class event aggregate. Every run is minted an opaque
rt-<id>Runtime Identity (RID); it is the eventaggregate_id. The task id
and per-taskattemptride in the event payload under a single shared
_rtenvelope (store.RuntimeRefKey). runtimesprojection table —(id, task_id, attempt, plan_id, state, created_ns, ended_ns),task_id/attempt/plan_idnullable so a wave-2
bare Runtime is purely additive. Upserted by Step A of thePostInsertHook
in the same transaction as the events INSERT, keyed on RID-presence (not
aggregate_type).arranger runtime ls/runtime inspect <rid>+GET /api/runtimes,
GET /api/runtimes/{rid}(read-only; under the/api/*CSRF wrapper).- 3-leg replay oracle (merge gate): fold-equivalence on the re-keyed stream
vs the frozenlegacyFoldQuerySnapshot, boot-replay, and an explicit
runtimes-projection + recover-no-stuck assertion.
Changed
- Event
aggregate_idfor task-bearing events is the RID, not the task id.
correlation_idremains the plan id and is now the canonical plan binding;
theaggregate_id = plan_idconsumer disjunct meaningfully matchesplan.*
events only (origin R7 correction). - Task-addressed reads (
QueryTaskOutput,QueryTaskOutputDroppedMarker,
lastTaskLifecycleStatus,arranger logs --task) resolve(plan,task)→
most-recent RID (MAXattempt) via the runtimes registry. rebuildRetryQueuerecovery joins the runtimes(task_id, attempt)index
instead of the broken per-RIDaggregate_idself-join; later-run detection
is byattempt, never wall-clock.handlePlanPurgealso deletes the plan'sruntimesrows.cmd/arranger/main.goVersion1.4.0-b→1.5.0-rt1.
Removed
- The dead
sessionstable (provably 0 reads/writes repo-wide) — superseded
byruntimes(ADR 0001 addendum). The v3sessionaggregate_type /
session.*namespace is retired; "session" is reserved for the provider
concept (opencode'sopencode_session_id).
Notes
- Cutover is a brand-new DB file at a distinct path — NOT an in-place
DELETE FROM events/VACUUM. A truncate would leave a stale
subscriber_cursorsreplay watermark ahead of an empty log; a fresh file
makes the watermark genuinely absent so boot-replay is a clean no-op. A
store test asserts this failure mode is unreachable under the new-file
mechanism. - Deferred to wave 2/3: out-of-process supervisor, heartbeat/pid/workspace/
driver columns, lifecycle FSM, typed signals, multi-provider driver,
standalone-Runtime GC. Wave 1 is keystone-only; the nullable schema + the
hook two-step shape make those purely additive (no further keystone re-key).