Skip to content

Releases: SeasonTemple/agent-arranger

v1.8.1 — v1.4a multipart staging→worktree pipe fix

Choose a tag to compare

@SeasonTemple SeasonTemple released this 21 May 06:19

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/new upload pipe wired end-to-end.
    Handler now stages uploaded files at .arranger/staging/<plan> /<task>/ (additive bootstrap — no schema migration), persists the
    path on tasks.staging_path at 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 eager os.RemoveAll(stagingDir)
    defer is replaced with an error-only defer; happy-path cleanup is
    now owned by arranger 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-worktree excludes.local (via
    extensions.worktreeConfig + core.excludesFile) so subsequent
    git add -A inside the worktree does not commit user-input bytes
    into per-task commits.

Changed

  • arranger gc covers staging dirs. Both --dry-run and --apply
    now enumerate .arranger/staging/<plan>/<task>/ per terminal plan;
    --apply removes them and nulls tasks.staging_path. An orphan
    sweep after the per-plan loop reaps .arranger/staging/<plan>/ dirs
    whose <plan> is not in the plans table (submission-failed-after-
    mkdir, manual leftovers, pre-fix residue).
  • arranger gc --json shape. Per-plan entries gain
    staging_dirs (paths enumerated) and removed.staging_dirs (count
    removed under --apply). Summary gains staging_dirs_total,
    orphan_staging_dirs, orphan_staging_removed, orphan_errors.
  • workspace.Manager.RemovePlan signature. Now takes
    (planID, workspacePaths, stagingDirs, removePlanBranch) —
    caller-orchestrated staging removal. Only caller is arranger gc.

v1.8.0-ui1 — HITL UI chip — w3b functional-layer closeout

Choose a tag to compare

@SeasonTemple SeasonTemple released this 21 May 06:19

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 from approval: required) render a state-badge state-hitl sibling span in the status <td> plus inline
    button.hitl-approve and button.hitl-reject controls. 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>/signal with explicit X-Requested-With: arranger header; chip flip happens via SSE task.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 distinct state-badge state-suspended sibling span. The badge's title attribute carries
    the literal arranger 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 + RuntimeRID fields on the
    snapshot read path; populated by QuerySnapshot via a correlated
    subquery on the runtimes table that picks the latest-attempt RID
    per task. CLI's arranger show JSON gains runtime_state and
    runtime_rid naturally; 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.resume join the existing
    evType switch alongside task.stalled / task.abandoned /
    terminals. data-task-state is the JS-side single source of truth
    for the wave-3 runtime state visual; data-task-rid refreshes 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:sseError followed by htmx:sseOpen, debounces 250 ms, then
    fetches GET /api/plans/{id} and reconciles each chip's
    data-task-state against the snapshot's RuntimeState. Closes the
    SSE-backfill-window-miss footgun without raising the 200-event
    backfill limit.

Changed

  • event_item.tmpl partial stamps data-task-id on each
    rendered <li>, parsed from the _rt envelope. Closes a latent
    post-wave-1 regression where SSE-driven status flips silently
    no-op'd because the lookup used data-aggregate-id (the RID since
    w1) against tr[data-task-id] (the stable task id).
  • Terminal SSE handler now also clears any HITL / SUSPENDED state
    badge + buttons on task.cancelled / task.completed /
    task.failed / task.abandoned / task.budget_exceeded.
    task.cancelled is the new wave-0 terminal the chip handles
    natively (the dispatcher's terminal for sig.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 legacyFoldQuerySnapshot body in
    internal/orchestrator/replay.go byte-identical vs main at every
    unit boundary (verified via awk-extracted diff).
  • state ≠ status discipline: 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

Choose a tag to compare

@SeasonTemple SeasonTemple released this 21 May 06:19

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_path column (additive, nullable) stamped by Step A
    on task.ready via the existing COALESCE-on-update mechanism.
    RuntimeRow.WorkspacePath exposed 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. Branch arranger/<plan>/<task> is shared across retries; R14
    reuse via git 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 reads error, not reason
    — 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) and EnumeratePlanResources(planID, workspacePaths []string, includePlanBranch bool): signatures take the
    caller-supplied workspace paths instead of walking the filesystem.
    cli/gc.go discovers paths via the runtimes table.
  • internal/orchestrator/append_task.go::injectUpstreamArtifacts and
    internal/ui/artifact.go::handleArtifact resolve the upstream
    worktree via Store.ResolveLatestRID(planID, taskID) →
    WorktreePath(rid). Empty RID returns a 404 / nil artifacts list
    rather than synthesizing a path.
  • arranger gc enumerates runtimes by plan_id from the DB rather
    than walking .arranger/worktrees/<plan>/. On --apply, NULL out
    runtimes.workspace_path after the removal so subsequent
    arranger runtime ls reflects the absent workspace and recover's
    workspace_missing gate fires cleanly on next boot.

Removed (wave-3c)

  • workspace.Manager.CreateTaskWorktree(planID, taskID, base) —
    superseded by CreateRuntimeWorktree.
  • workspace.Manager.WorktreePath(planID, taskID) — old task-keyed
    accessor (the new WorktreePath(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 legacyFoldQuerySnapshot body in
    internal/orchestrator/replay.go byte-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

Choose a tag to compare

@SeasonTemple SeasonTemple released this 21 May 06:18

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/adapter is now
    the complete, provider-agnostic, transport-invariant host↔driver contract.
    New optional RuntimeRecoverer capability (RecoverFromHandle) — continues
    a durable provider session under a fresh observed process after a host
    crash. Provider-internal-free internal/adapter/stub driver + 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-invisible task.session_bound event into the runtimes
    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.
  • runtimes columns session_handle, pgid, owner_nonce (additive on the
    wave-1 nullable schema).

Changed

  • Session.OpencodeSessionID() → provider-agnostic Session.SessionHandle().
  • opencode self-verifies inside opencode.NewAdapter; the host no longer reads
    opencode internals or gates boot on them (Orchestrator.OpencodeDB removed).
  • ReapOrphans lifted out of orchestrator.New() to run after Recover()
    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)

Choose a tag to compare

@SeasonTemple SeasonTemple released this 21 May 06:21

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 event aggregate_id. The task id
    and per-task attempt ride in the event payload under a single shared
    _rt envelope (store.RuntimeRefKey).
  • runtimes projection table — (id, task_id, attempt, plan_id, state, created_ns, ended_ns), task_id/attempt/plan_id nullable so a wave-2
    bare Runtime is purely additive. Upserted by Step A of the PostInsertHook
    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 frozen legacyFoldQuerySnapshot, boot-replay, and an explicit
    runtimes-projection + recover-no-stuck assertion.

Changed

  • Event aggregate_id for task-bearing events is the RID, not the task id.
    correlation_id remains the plan id and is now the canonical plan binding;
    the aggregate_id = plan_id consumer disjunct meaningfully matches plan.*
    events only (origin R7 correction).
  • Task-addressed reads (QueryTaskOutput, QueryTaskOutputDroppedMarker,
    lastTaskLifecycleStatus, arranger logs --task) resolve (plan,task) →
    most-recent RID (MAX attempt) via the runtimes registry.
  • rebuildRetryQueue recovery joins the runtimes (task_id, attempt) index
    instead of the broken per-RID aggregate_id self-join; later-run detection
    is by attempt, never wall-clock.
  • handlePlanPurge also deletes the plan's runtimes rows.
  • cmd/arranger/main.go Version 1.4.0-b → 1.5.0-rt1.

Removed

  • The dead sessions table (provably 0 reads/writes repo-wide) — superseded
    by runtimes (ADR 0001 addendum). The v3 session aggregate_type /
    session.* namespace is retired; "session" is reserved for the provider
    concept (opencode's opencode_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_cursors replay 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).