Skip to content

XERK-123: per-ticket model pin on the board#309

Merged
xerhab merged 1 commit into
mainfrom
XERK-123
Jul 25, 2026
Merged

XERK-123: per-ticket model pin on the board#309
xerhab merged 1 commit into
mainfrom
XERK-123

Conversation

@xerhab

@xerhab xerhab commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

What

Adds a Model row to the board's ticket detail panel, right below the Agent row (XERK-38): the operator can pin which model a ticket's session runs, defaulting to the login's default model. The agent must advertise its probed models for the picker to list actually-available choices — exactly what the ticket asks for.

Mirrors the agent-pin pattern end to end (hub + agent + web + Android).

How it works

  • Hub-owned durable state, like the agent pin — not an agent-ledger fan-out. The model is carried on the spawnTicket command the hub already routes (ticketModelPin → the command's model), so the hub is the one party that has to remember the choice.
    • POST /api/jira/<siteKey>/<issueKey>/model ({model} to pin, {auto:true}/{model:"default"} to release) → authoritative 200, stored in /data/ticket-models.json, riding /api/agents as top-level ticketModels + a ticketModels SSE event.
    • The alias must be one the org actually offers (orgModelAliases: union of the org's hosts' probed models.available, non-bracketed, + the static family aliases). The agent still re-validates host-side (resolve_model) before launch.
  • Agent: spawn_ticket takes the model and passes it to spawn (validated like any composer spawn). Unpinned → no model on the command → default, unchanged.
  • Web (board.js/board.html): the Model row + picker (Default + the org's curated aliases via modelChoices/prettyModel), saveModel POST, the SSE listener, and pin-change repaint.
  • Android parity (same PR): ticketModels/ModelsInfo/TicketModelPin wire types, FleetRepository plumbing, Board.kt merge + helpers, the Model row + picker in BoardScreen, BoardViewModel.setTicketModel, the HubApi endpoint, and BoardTest.kt cases.

The pin also feeds the auto-start sweep (an auto-started To Do ticket carries its pinned model).

Verification

  • Unit tests: 610 turma JS + 731 agent Python pass. New cases in server.test.js (/model endpoint, spawnTicket-carries-model, persistence), board.test.js (modelPinOf/modelPickerHtml/modelChoices/prettyModel/mergeSites), TestSpawnTicket (model lands on session + command line, dispatch), and BoardTest.kt (parity).
  • Real browser drive (headless Chrome over CDP against a booted hub, 11/11 checks): the ticket card and Model row render, the row states the default (Default (Sonnet 5) — the agent's default model), the picker offers Default + opus/sonnet/haiku (the org's probed aliases), selecting Opus POSTs /model → 200, the hub payload gains the pin, the row reads "Opus — set by you", and releasing to Default drops the pin.
  • Android JVM tests weren't run locally (no JDK/Gradle in this env) — they run in android-ci.yml.

Base cut from origin/main.

Add a Model row to the board's ticket detail panel, beside the Agent row:
pin which model a ticket's session runs, defaulting to the login's default.
The agent must advertise its probed models for the picker to offer real
choices.

Hub-owned durable state like the agent pin (XERK-38), not an agent-ledger
fan-out: the model rides the spawnTicket command the hub already routes, so the
hub is the one party that remembers the choice.

- server.js: ticket-models store (/data/ticket-models.json), POST
  /api/jira/<siteKey>/<issueKey>/model (200-authoritative), ticketModels on the
  /api/agents payload + SSE event, orgModelAliases allowlist (union of the org's
  hosts' probed models.available + static family aliases), and the pinned model
  carried on spawnTicket from both the Start button and the auto-start sweep.
- hub-agent.py: spawn_ticket takes the model and passes it to spawn, which
  validates it via resolve_model like any composer spawn (bad model -> error
  card; unpinned -> default, unchanged).
- board.js/board.html: the Model row, picker (Default + the org's curated
  aliases), modelPinOf/modelChoices/prettyModel, saveModel POST, the
  ticketModels SSE listener, and refreshOpenTicket pin-change detection.
- Android parity: ticketModels/ModelsInfo/TicketModelPin wire types,
  FleetRepository plumbing, Board.kt merge + helpers, the Model row + picker in
  BoardScreen, BoardViewModel.setTicketModel, the HubApi endpoint.

Verified: 610 turma JS + 731 agent Python tests pass; new cases across
server.test.js, board.test.js, TestSpawnTicket, and BoardTest.kt. Drove the
board in a real browser (headless Chrome over CDP): the Model row renders the
default, the picker offers Default + the probed aliases, selecting pins the
model (POST 200, hub payload updated, row reads "Opus — set by you"), and
releasing drops the pin.
@xerhab
xerhab merged commit 2833690 into main Jul 25, 2026
7 checks passed
@xerhab
xerhab deleted the XERK-123 branch July 25, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant