Skip to content

Releases: boundsj/t3-fleet-gateway

v0.1.14: follow every job pull request

Choose a tag to compare

@boundsj boundsj released this 20 Oct 22:28
aac389b

Job pull request events no longer miss merges on jobs with more than 20 linked PRs. A job now follows up to 200 PRs and still shows or sends at most 20, open ones first. PR links must use GitHub's exact /pull/ path.

Fixed

  • A merged pull request could raise no pull_request_merged event, and later polls never reported it. 0.1.13 kept at most 20 PRs per job and treated a snapshot of 20 as a window. A PR that opened and merged between two polls could not outrank a window of open PRs and was ignored, and a followed PR that merged on a thread with 21 or more open PRs was cut from the snapshot before it was compared. A job now follows up to 200 PRs (beyond that, open ones first, then the newest) and compares the lists in full: a PR whose state became open or merged raises its event, and so does a PR the previous list did not hold. Only a list at the 200-PR bound is treated as a window, where a PR that merely re-enters because another left still raises nothing.
  • Everything shown or sent still carries at most 20 PRs, open ones first and then the newest, listed in that order: work_status, work_list, work_feed and the other job views, their text summaries, PR webhook payloads (which stay inside the 16 KiB bound for ordinary links, however many PRs the job follows) and the dashboard. When a job follows more, the text says 20 of 35 shown (20 of 200+ shown at the bound), and the PR an event is about may not be among those shown.
  • A PR URL must have the lowercase /pull/ segment and a number without leading zeros. 0.1.13 compared the whole path without regard to case and accepted https://github.com/owner/repo/PULL/23, which GitHub answers with 404. Owner, repository and host are still compared without regard to case, and the URL is stored and shown exactly as T3 gave it.

Compatibility

  • No migration, configuration or tool-shape change. jobs.pull_requests is the same column and now holds up to 200 PRs (about 26 KB for 200 typical links); the job views keep their fields and their 20-PR bound. Rolling back to 0.1.13 reads a longer stored list back as its 20-PR window.
  • Snapshots stored by 0.1.13 held at most 20 PRs. On the first read after the upgrade, a thread that links more than 20 PRs shows its extra PRs as new to the job, so that job may raise one pull_request_opened and one pull_request_merged once. Threads with 20 or fewer PRs raise nothing.
  • Known limit: a job whose list holds exactly 200 PRs is treated as having more, so a PR that opens and merges between two polls while older PRs are unlinked can go unreported there (design).

Typecheck and 597 tests pass on Node 24 and 26 (Linux) and 26 (macOS), and the change passed two independent review rounds. PR #24 · Changelog

v0.1.13: job pull requests for every repository

Choose a tag to compare

@boundsj boundsj released this 20 Oct 21:52
f9d3011

Job pull requests now show up for repositories with capital letters in their names and for long-running threads that link more than 20 PRs. Both were missing in 0.1.12 without any warning.

Fixed

  • Job pull requests were missing for repositories with capital letters and for threads that linked more than 20 PRs, with nothing logged. T3 lists repository in lower case while the PR URL keeps GitHub's casing, so the URL check rejected every PR of such a repository, and one rejected entry (or a 21st) discarded the whole list, leaving the job without a snapshot or frozen at its last one. The URL's path and host are now matched without regard to case, and the URL is stored and shown exactly as T3 gave it; every other check (https only, matching host, no port, credentials, query or fragment) is unchanged. T3's list is read entry by entry: an invalid entry is skipped, a repeat of a PR counts once, a list with entries but none valid is unknown (the last snapshot stays), and an empty list is none. The list_thread_pull_requests call itself no longer fails on a long list or an odd entry.
  • A snapshot holds at most 20 PRs: every open PR first, then the newest (highest number). work_status and work_list say so when they show 20 and older ones may be left out. A PR that leaves or re-enters a full snapshot raises no pull_request_opened or pull_request_merged event by itself.

Compatibility

  • No migration, configuration or tool-shape change, and stored snapshots read back unchanged. Jobs whose snapshot was never readable and is still unknown (existing and adopted jobs) get a silent baseline on their first good read; a job launched on 0.1.12 whose lists were always rejected still holds its initial empty snapshot and reports its PRs once then (at most one pull_request_opened and one pull_request_merged).

Also: a flaky dashboard privacy test is fixed, and there is one more regression test for workspace root reservations.

Typecheck and 588 tests pass on Node 24 and 26 (Linux) and 26 (macOS). PR #23 · Changelog

v0.1.12: follow your jobs' pull requests

Choose a tag to compare

@boundsj boundsj released this 20 Oct 20:58
64c2c3b

Your bot can now tell you which pull requests a job opened and whether they merged, without searching GitHub. This release also brings 0.1.10 and 0.1.11, which had no release of their own: work with no named project, and opt-in work in a project's own folder.

0.1.12: Follow a job's pull requests

  • work_status and work_list show each job's T3-linked pull requests with links, states and freshness, in the text that text-only clients read.
  • Dashboard jobs show a small PR #21 open badge (number and state only).
  • work_feed reports pull_request_opened and pull_request_merged. As webhooks they are opt-in; existing webhook connections keep their five default events.
  • A host whose T3 cannot list pull requests keeps working: lookup failures never count against host health, reads are bounded, and the last good data is kept. Only PRs registered with T3 (link_pull_request) are visible, and states refresh about once a minute.

0.1.11: Work in a project's own folder (opt-in)

  • hosts[].workspaceProbeCommand lets the gateway choose per job: a git repository root gets a fresh worktree from origin/<baseRef>, and any other folder runs in place. Agents can ask with work_start's workspace.
  • One gateway job at a time per folder; resuming an idle job while the folder is taken is refused with workspace_root_busy.
  • Hosts without a probe behave exactly as before.

0.1.10: Start work with no named project

  • A host's defaultProject names a scratch alias; work_start with target: { host } runs the job in T3's "No project" workspace, in a folder of its own, with no worktree or branch.
  • A stored launch plan the gateway cannot read now fails only its own job instead of being launched as a plain worktree job, and stored jobs are read with the caller's host scope applied in SQL.

Upgrade

  • Stop the gateway and back up its data directory and config before updating (Operations). Migrations 11 to 13 run on the first command of the new build; older builds refuse the upgraded ledger, so rolling back means restoring the backup.
  • Existing configurations work unchanged; every new behavior is opt-in.

Thanks

0.1.10 and 0.1.11 build on contributions by @prrcarvalho (#15, #16).

Typecheck and 576 tests pass on Node 24 and 26 (Linux) and 26 (macOS), and each change had an independent review. Changelog · README

v0.1.9 — Give your bot a doorbell

Choose a tag to compare

@boundsj boundsj released this 18 Oct 00:13
2c83375

Connect a webhook routine from the private dashboard: paste two values, choose a project, and click Connect & Test. No config edits or service restart for new connections.

Added

  • Connect Grok Bot in the private dashboard: paste a webhook URL and its full authentication header, choose a configured project, and select Connect & Test. Generic HTTPS receivers work too. A successful test means the receiver accepted the request; check the bot conversation to confirm its response. Connections send idle, needs_input, failed, cancelled and delegated_work_ended, with worker excerpts off.
  • A Webhooks view lists project, host, management source and safe last-test information. Dashboard connections support Send test, Replace key (including a replacement URL), and confirmed Disconnect, without editing files or restarting. Saved destinations and authentication values are never returned to the browser.
  • Migration 10 adds explicit config/dashboard ownership and safe last-test metadata. Existing subscriptions remain config-owned. Dashboard connections and their queues survive restarts independently of config reconciliation; the CLI can list attempts and ping either source.
  • dashboard:demo -- --webhook-receiver provides a local synthetic receiver for accepted, rejected and timed-out setup tests. Demo management refuses destinations outside literal loopback.
  • Opt-in outbound job webhooks with private per-subscription auth headers, project/host/event filters, metadata-only default payloads, bounded optional excerpts, durable atomic snapshots and deliveries, stable event IDs, restart recovery, bounded HTTPS attempts and retries, and safe CLI diagnostics/test ping. Existing feed cursor behavior and polling fallback remain unchanged. Migration 9 adds outbox/attempt tables and durable exact-run gateway attribution; unknown turn origins remain unknown.
  • Watcher same-state events (turn_finished, delegated_work_ended) commit atomically with their observed job changes. Whole turns between polls generate an idle webhook. Config revisions cancel queued payloads for replaced routes; read-only commands never apply stale config.

Documentation

  • Shorter, prompt-led README with a Grok routine walkthrough, private field-link instructions and an illustrated setup guide. Manual installation and connection commands moved to docs/setup.md; release history stays in the changelog and GitHub releases.

Security and lifecycle

  • The dashboard's private network boundary now permits webhook destination management. It remains separate from the MCP/OAuth listener; job, host and client controls are unchanged. Management requests require JSON, the custom API header, an exact request-host Origin and same-origin fetch metadata when present. Errors and audit records contain fixed codes only; no saved URL, header, response body or credential is exposed, and the browser stores no secrets.
  • Connection creation and replacement use the same bounded TLS-validating POST transport as delivery. A failed test preserves the existing route. Successful changes recheck the revision after the awaited test and cancel pending old-route payloads; disconnect cannot be undone by a slow test. Writes are refused before readiness and after shutdown begins; shutdown aborts and drains management pings before closing SQLite.
  • Automated tests and development smoke checks use synthetic receivers; verify a hosted routine by checking its conversation after Connect & Test.

Verification and upgrade

  • Typecheck and 473 tests passed; 29 HTTPS/retry/crash checks passed. Independent management smoke passed 53 distinct checks, and independent code review found no actionable findings.
  • Setup walkthrough · Verification evidence · PR #20.
  • Stop the gateway and back up its database, key and config before updating. This release adds migration 10; restore the matching backup if rolling back to older code. Keep the dashboard on a private network.

v0.1.6 — Restore thread watchers

Choose a tag to compare

@boundsj boundsj released this 15 Oct 13:36
7892926

Fixes compatibility with current T3 Code thread responses, which omit the former link field.

  • Launch, list and read responses without links now use the documented thread URL, preserving the exact opaque thread id.
  • Older supplied links and existing job links are retained.
  • Successful watcher observations clear stale response-shape errors.
  • No database migration or configuration change.

Validation: typecheck and all 323 tests passed; required CI and CodeQL checks passed. Before/after read-only checks reproduced affected threads failing in both views on v0.1.5 and confirmed all reads plus thread listing succeed with the fix. No real T3 work was launched or steered.

PR #10

v0.1.5 — Per-machine access

Choose a tag to compare

@boundsj boundsj released this 14 Oct 00:13
1d6e345

Agents can now receive Read or Operate access to selected T3 machines through the OAuth approval page or clients token --hosts host-a,host-b.

  • Every gateway tool enforces machine access; jobs on other hosts are hidden and refused before downstream calls. Lists, feeds and attention filter before limits.
  • Existing OAuth and bearer connections retain all-host access without reconnecting. Refresh preserves or narrows grants. No database migration.
  • Approval-code retries preserve the selected machines and access level.
  • README and operator/security documentation are updated, with actual approval screenshots and sanitized two-host evidence.

Implemented by Opus 5.5 High and independently reviewed by Sol 6.1 xhigh Fast. Typecheck and all 319 tests passed, as did all required CI and CodeQL checks. Separate synthetic verification passed 20 public OAuth/MCP assertions; real second-machine enrollment and multi-host qualification remain deferred.

PR #9 · Visual evidence

v0.1.4

Choose a tag to compare

@boundsj boundsj released this 13 Oct 00:39
0efd3e6

Real Codex questions can now be answered by text-only clients using the complete T3 pending request ID. Both work_respond and targeted work_status accept opaque IDs longer than 200 characters, while retaining nonempty validation and live job/request membership. Rejected caller-owned IDs are omitted from error messages.

A brief T3 waiting status with no pending requests now remains running, avoiding a false permission-approval alert. Actual questions and positive pending counts still require input.

The opt-in live lifecycle script now requires an explicit scratch-project alias, checks discovery/list/full messages, reports eight-of-nine tool coverage, and cleans up only its known jobs. README and operations/design documentation record the supervised QA results and their limits.

Validation: typecheck and 301 tests pass; all four new regressions fail before their fixes. Astra and Opus independently reviewed the implementation, and all required CI checks passed before merge.

The supervised run confirmed questions, the original operator approval, isolated provider failure, standing-job reuse, child delegation and parent resumption, SDK outage recovery and safe replay after a lost response. An actual Notion Custom Agent passed a fleet check through an MCP connection to the gateway. v0.1.4 approval detection was observed, but operator completion and actual Grok/Notion QA operations/recovery remain pending. No schema migration.

v0.1.2

Choose a tag to compare

@boundsj boundsj released this 12 Oct 16:55
01f2100

Fixed

  • work_status text now includes pending request and question ids, headers, question text, every option label/description/value, selection rules and answer constraints. Clients that consume only MCP text can answer with work_respond. allowCustomAnswer, required and multiSelect preserve true/false, with null for null or absent T3 flags. Unreadable questions remain explicitly temporarily unreadable.
  • Pending questions are bounded without partial actionable data: at most five requests read by default; pendingRequestId selects another. A request exceeding 50 questions, 100 options per question or 64,000 JSON characters returns questions: null and questionsUnavailable: "too_large", directing the operator to T3. read_limit and temporarily_unreadable explain other null results. work_cancel text points to work_status before answering a still-pending question.
  • Unspecified question flags retain honest nulls while text provides an answer path: follow explicit wording, and without instructions to select several try a single listed value as a conservative attempt. T3 validation remains authoritative; omitted/null flags alone do not require escalation.
  • Automatic renewal starts after all startup preflight checks settle, including an unexpected rejection, unless shutdown has begun. Rejections produce fixed diagnostics; logger errors still propagate and known executable failures still gate each host's renewal.
  • Probe cancellation explicitly force-kills the direct child, including probes that ignore SIGTERM on Node 26.0. Startup preflight never initiates or awaits health reads, so stalled MCP initialization cannot delay shutdown through preflight; doctor still compares fresh health.
  • doctor and serve startup preflight the mint executable in their own process environment, diagnose missing/non-executable commands and version-pinned paths, and distinguish known failure from unverified checks. Known executable failures stop automatic renewal before pairing registration while retaining the old credential.

Added

  • Optional hosts[].mintPairingCodeProbe: exact argv for an explicitly operator-configured non-mutating probe. No mint command or guessed --version is executed as preflight. Probes time out after two seconds, cap stdout/stderr at 4,096 bytes each, and are aborted on shutdown without being awaited. A bare version, exact native t3 v<semver> output or JSON {"version":"x.y.z"} enables comparison with the T3 server; disagreement is a warning, not an incompatibility verdict.
  • Synthetic public MCP and CLI regressions, question-limit/nullable-flag coverage, PATH/probe/startup/shutdown checks and logging/event leakage checks. Command diagnostics contain no raw stdout/stderr, argv or executable paths; enrollment no longer echoes command argv.

No database migration. This release covers the question text and renewal-command preflight changes only. Full live QA remains deferred. No new live QA is claimed.

v0.1.1

Choose a tag to compare

@boundsj boundsj released this 09 Oct 15:15
ba4aef6

Added

  • deploy/launchd/install.sh warns when T3_BIN (by default command -v t3) is inside one T3 version's directory (.../runtime/versions/<version>/). After T3 updates itself, a service PATH holding that directory keeps running the old t3 for credential renewal, and renewal fails with pairing_command_failed if the directory is removed.
  • docs/operations.md, "A t3 that follows T3's updates": why, and a wrapper that runs the version T3's service uses. The README's Setup section and the systemd unit point to it.

v0.1.0

Choose a tag to compare

@boundsj boundsj released this 09 Oct 01:19

Added

  • Agent-facing MCP server on /mcp (Streamable HTTP, stateless, JSON responses) that serves both 2025-era and 2026-07-28 clients and accepts requests without MCP-Protocol-Version.
  • OAuth 2.1 authorization server: metadata, dynamic registration for public clients, an approval page gated by one-time codes minted with pair (Read or Operate), PKCE S256, rotating refresh tokens with replay detection and a retry grace window, idle expiry, and per-client revocation.
  • Downstream T3 Code client per host, with enrollment through T3's pairing-code approval, verified credential storage, and automatic renewal before the 30-day expiry.
  • fleet_status tool.
  • CLI: serve, pair, clients list|revoke, hosts enroll|status, jobs adopt|list|release, doctor.
  • SQLite storage with versioned migrations, including the tables for the job layer.
  • launchd installer and systemd user unit.
  • Job layer: work_start, work_continue, work_respond, work_cancel, work_status, work_list and work_feed. Jobs run as T3 threads titled [job:<id>] in fresh worktrees on <branchPrefix><id>, with a durable ledger and append-only event feed, request-id idempotency, a per-host FIFO dispatcher that respects maxConcurrentJobs, a watcher that derives running, needs_input, idle and failed from T3 thread reads, host backoff that never changes job state (a slow launch makes only that job unknown; a T3 error about one job is recorded on that job and never stalls the others), reconciliation of launches whose outcome was lost or that T3 answered with an error that may leave a thread (by the title marker at the end of the title, in unsettled and settled threads, never relaunched; launch_not_confirmed after watcher.reconcileWindowMinutes), and cancels that keep interrupting each run that starts on the thread until it stops (one T3 clientRequestId per run). A found thread on which no run ever started fails the job with launch_not_started (keeping the thread's id and link) instead of reporting a finished turn; any failed lookup after a launch error leaves the job unknown; a search match T3 refuses to read is skipped.
  • T3 client wrappers for the thread and pending-request tools, and transport errors that say whether T3 may have received the call (T3TransportError.delivery; new error code t3_response_lost).
  • Every job view carries stateChangedAt (when the job entered its current state; the feed's attention window and order use it) and updatedAt (the last real change, including the host-unreachable mark, not the last poll).
  • Every job view carries link, which opens the job's thread in the T3 app, from the launch result on.
  • Error codes job_state_conflict and request_id_conflict.
  • Config: watcher.reconcileWindowMinutes; hosts[].defaultModelSelection, the model for projects on that host that set no modelSelection (T3 refuses launches without a model when the T3 project has no default). doctor fails a project that would have no model, serve logs project.model_missing at startup (in the background: shutdown does not wait for it, and a host that fails is not asked again for its other projects), and fleet_status shows each project's runtimeMode and modelConfigured.
  • config.example.json enrolls its host with access: "full-access": the host's access is only a ceiling, and each project's runtimeMode (default approval-required) is what limits a job.
  • scripts/e2e-live.ts: a live end-to-end check against a running gateway and a real T3 host (npm run e2e:live), outside the test suite. If the job it cancels has already finished, it starts one replacement and cancels that.
  • Standing jobs: t3-fleet-gateway jobs adopt <project> <threadId> [--title <text>] registers an existing T3 thread (for example a long-running coordinator thread) as a job agents drive with work_continue, work_respond, work_feed and work_status. The thread must belong to the project's T3 project; its state is read at adoption and its history is not replayed. Standing jobs hold no concurrency slot, a failed run leaves them idle, work_cancel interrupts their current turn only (new outcomes interrupt_requested and not_running; new event interrupt_requested), and jobs release <jobId> ends one in the new terminal state released without touching T3. Every job view has standing.
  • Config: projects[].allowWorkStart (default true). When false, work_start in the project fails with the new error code start_disabled, whose message names the project's standing jobs and says to use work_continue. fleet_status shows each project's allowWorkStart and its open standingJobs (job id, title, state, link), and the fleet_status, work_start and work_continue descriptions tell agents to drive a project's coordinator with work_continue and follow it with work_feed and work_status.
  • work_cancel on a standing job with nothing running returns pendingQuestions, the questions T3 still lists for its thread, and says they need an answer with work_respond: interrupting a turn that waited on a question may leave the question pending and the job needs_input. Its description also says the interrupted turn may be one the operator started in T3.
  • Coordinators that delegate: every job view has waitingOnDelegatedWork and delegatedTasks, from the thread's subagent items that T3 shows as still running (followed by item id until they finish). A turn that ends with delegated work running is reported with reason waiting_on_delegated_work, and a turn the job starts by itself (T3 resuming a coordinator when its child finishes, or a person typing in T3) with reason turn_started (or turn_finished when it began and ended between two checks). The idle state meaning, the attention reasons and the work_continue, work_feed and work_status descriptions now tell agents that a coordinator goes idle while its delegated work runs and resumes by itself: read its reply, and if it delegated or is waiting, wait for its next turn instead of sending instructions. Schema migration 3 adds jobs.delegated_work.
  • Delegated work that ends without the coordinator resuming (a child that failed or was cancelled, and T3 started no new parent run): the watcher appends the new event delegated_work_ended (reason no_turn_started), and the job enters work_feed's attention list with "Its delegated work has ended (finished, failed or cancelled) but it did not resume. Send work_continue asking it to check on that work." The idle state meaning, the attention reasons and the work_continue, work_feed and work_status descriptions now say that waitingOnDelegatedWork wins over the reply text when they disagree (live, T3 shows a running child's subagent item as running, then completed).
  • Delegated tasks followed per job raised from 20 to 200. Running tasks beyond those are counted in the new job view field delegatedTasksUntracked instead of being dropped; while it is above 0, waitingOnDelegatedWork stays true until the coordinator has run a new turn, and the attention reason says following 200 of N delegated tasks; ask the coordinator for status before sending more work. A job that finishes (cancelled, failed, released) no longer keeps its delegated work. Schema migration 5 adds jobs.delegated_untracked, jobs.delegated_untracked_run_id and jobs.delegated_ended_at.
  • The adoption scan for delegated work keeps at least 5 of its 20 reads for looking back from the thread's last message, so a coordinator with a long activity tail after it is still found waiting.
  • docs/operations.md gives the deploy order: stop the service, back up gateway.db and gateway.key, run doctor from the new checkout (it applies migrations and prints failures), then install.sh.
  • t3-fleet-gateway jobs list [--all]: open jobs (with --all, finished ones too) with project, state, standing flag, title and link.
  • Schema migration 2: jobs.standing and the terminal job state released, for standing jobs. It rebuilds the jobs table to change its state constraint and keeps every row, event and request id. Migrations are append-only from here on. Back up the data directory before updating a deployed gateway (see docs/operations.md).
  • doctor's public URL check requires this gateway's own metadata (resource_name t3-fleet-gateway), so another server's metadata for the same resource fails it.
  • clients token --name <name> [--access operate|read] [--ttl <duration>|never]: a bearer token for an agent that cannot sign in with OAuth (Notion's custom MCP connections offer only header authentication). Printed once and stored as a hash; Operate and one year by default; listed by clients list with its expiry and cut off by clients revoke. Durations accept y (365 days).
  • work_messages (read scope): a job's worker messages in full, read live from T3, up to 50,000 characters each, with textTruncated and length. Without before or after it returns the latest (limit, default 1, at most 10); earlier and later page back and forward. work_status has excerptTruncated (schema migration 6), and its summary and the attention reasons point to work_messages when the excerpt is cut.

Fixed

  • mine: true in work_feed and work_list left out standing jobs, so an agent filtering with it never saw the coordinator it drives. It now includes every standing job.
  • Delegated work (waitingOnDelegatedWork, delegatedTasks) was never detected against a real T3: the watcher looked for subagent items in thread reads that named no view, and T3's default messages view returns only user and assistant messages and plans. The watcher now reads T3's activity view for delegated work, with a read position of its own (schema migration 4 adds jobs.activity_position) and one character per item, while replies, the excerpt and the job's state still come from the messages view, so...
Read more