REPL-Driven Ticket Triage: A Data-First Agent Tool Backend

Table of Contents

1. Thesis

The best interface between an agent and a complex system is a REPL. A ticket backlog is that problem worn as a queue: a store of work items, each with a status you mutate and a shape you query, that an agent pulls from and pushes back to. You do not learn the backlog's contract from docs — you evaluate (next-ticket) and watch the map come back, then (set-status! …) and watch one line change on disk.

This is the companion to REPL-Driven Feed Crawling and Flight Tracking. Same thesis, different domain. There the divergence between replicas of one system was the signal; here the interesting object is the tool backend itself — and the claim is that when you build it REPL-first, "the agent's tools", "the configuration you edit", and "the dataset you analyze" stop being three layers and become one.

2. Source landscape: mock-Python utils vs a live store

Anthropic's cookbooks ship a reference tool backend, tool_use/utils, used by the compaction and programmatic-tool-calling demos. It is a good baseline precisely because every axis is the opposite of the REPL-driven one:

tool_use/utils (Python) this backend (Clojure/bb/REPL)
customer_service_api generates mock tickets wal-sh.site.beads reads the real issue store
tools are @beta_tool decorated functions tools are EDN data (tool-defs) + a dispatch fn
visualize.py renders responses the REPL is the visualizer (group-by, frequencies)
edit code → re-run the whole script (next-ticket) / (agent/run …) incrementally in one session
domain is a fixture domain is beads — a git-native issue tracker

The Python version is the right shape for a tutorial — hermetic, reproducible, no external state. The REPL version is the right shape for doing the work: the backend is the thing you actually triage against, and you reshape it while it runs.

3. The backend is data, not code

The whole tool surface an agent sees is a literal value you can print, diff, and edit at the REPL. No decorator, no registration step:

(require '[wal-sh.site.beads :as b])

b/tool-defs
;;=> [{:name "get_next_ticket"
;;     :description "Get the next ready (open, unblocked) ticket..."
;;     :input_schema {:type "object" :properties {} :required []}}
;;    {:name "update_ticket_status"
;;     :input_schema {:properties {:id {...} :status {:enum ["open" "in_progress" "closed" "blocked"]}}
;;                    :required ["id" "status"]}}]

Because it is data, adding a tool is conj, and constraining one is assoc-in — evaluated live, no restart. The dispatch side is one pure-ish function, (handle-tool name input), returning the JSON string the Messages API wants. That is the entire "SDK": a vector and a case.

4. Boundaries verified at the REPL

The point of a REPL-first backend is that you probe its edges by evaluating them, not by reading a spec. Every contract below was checked against the live backlog; the shapes are the tooling's, not the backlog's.

The schema an agent's tools resolve against (structure, no content):

(sort (keys (first (b/read-issues))))
;;=> (:acceptance_criteria :comment_count :created_at :created_by :dependencies
;;    :dependency_count :dependent_count :description :id :issue_type
;;    :owner :priority :status :title :updated_at)

The failure modes — each returns a value, never throws, so the agent loop can keep going:

;; empty queue: get_next_ticket yields an error object with a progress count
(let [h (b/queue-backend [])] (h "get_next_ticket" {}))
;;=> {"error":"No ready tickets in the queue","processed":0,"total":0}

(b/handle-tool "nope" {})
;;=> {"error":"unknown tool: nope"}

;; updating an id that doesn't exist mutates nothing and returns nil
(b/set-status! "does-not-exist-xyz" "closed")
;;=> nil

Two boundaries matter for correctness and were confirmed live:

  • Readiness is dependency-aware. ready excludes open tickets whose dependencies aren't closed — 14 of 147 open tickets (~9%) were dependency-blocked, so get_next_ticket never hands the agent work it can't start.
  • set-status! is line-preserving. Setting one ticket's status rewrites only that line of the JSONL; every other line is byte-identical, so the git diff is a single line and the store stays reviewable. (Verified: 303 lines before = 303 after, exactly one changed.)

5. Configuration and analysis are the same surface

The claim that makes this more than "Python but parens": at the REPL the ticket store is simultaneously the config you edit and the dataset you analyze, with no API or visualization layer between.

Analyze — ordinary Clojure over the maps, no visualize.py:

(->> (b/read-issues) (map :priority) frequencies (into (sorted-map)))
;; a histogram across priority buckets

(->> (b/read-issues) (filter #(seq (:dependencies %))) count)
;; how many items carry a dependency edge (the blocked-work risk)

(->> (b/ready-queue) (map :issue_type) frequencies)
;; the shape of what's actually workable right now

Configure — the same values, mutated in place:

(b/set-status! "SYNTHETIC-001" "in_progress")   ; advance one item
(b/claim-next!)                                  ; pop the next ready + claim it

In the Python fixture these are different files — an API module to read, a tools module to act, a visualize module to see. Here group-by is the dashboard and assoc is the migration. The dataset, the configuration, and the agent's tools are one store seen three ways.

6. The agentic harness

The same backend drives an autonomous loop. wal-sh.site.agent is a Messages-API tool-use loop (a port of Anthropic's compaction cookbook) that takes any tool-defs + handle-fn; pointing it at beads is one line:

(require '[wal-sh.site.agent :as a])
(a/run-tickets {:compact-threshold 6000 :max-turns 20})   ; dry-run backend

Run under the dry-run backend (queue-backend), the agent processes the real ready queue while mutating nothing — the cursor advances in memory. That run is written up empirically in Compaction Against a Beads Backend (same work, 37% fewer tokens with client-side compaction). The REPL is where you watch it: each turn's context size, each tool call, printed as it goes — the loop is not a black box you submit, it's a form you step.

7. The workflow, concretely

A triage session is a sequence of evaluations, each informing the next — the literate-transcript form this site is built on:

  1. (b/stats (b/read-issues)) — how big is the problem.
  2. (->> … frequencies) — where is it concentrated (priority, type, blocked).
  3. (b/next-ticket) — pull one; read its shape; decide.
  4. (b/set-status! id "in_progress") — claim it; one line moves on disk.
  5. (a/run-tickets …) — or hand a batch to the agent and audit the transcript with chat.clj.

No step is a build. Every step is a value. The gap between "inspect" and "act" is the gap between two forms in the same buffer.

8. Open conjectures

  • Data-first tools generalize. The tool-defs + handle-tool shape is domain-agnostic; the same two-function backend should wrap logs, the search index, or the verdict ledger with no new machinery. Refutation: a domain whose tool surface can't be expressed as EDN + a case.
  • Line-preserving writes keep an agent's mutations reviewable. If every agent-driven set-status! is a one-line diff, a day of autonomous triage is a readable patch series, not an opaque state jump. Refutation: a mutation that necessarily rewrites unrelated lines.
  • The REPL beats a visualize module. For a store small enough to hold in a process, group-by at the prompt is faster to author and adapt than any rendering layer. Refutation: an analysis whose ergonomics genuinely need a dedicated view (large-scale, streaming, or shared dashboards).

9. Claims to test   refutation

The tool backend is fully data (no code regen to change tools)
a required tool capability that can't be added by editing tool-defs + handle-tool.
Every tool call returns a value, never throws
an input that escapes the case / error-object contract and propagates an exception into the loop.
Configuration and analysis are one surface
a triage task that forces a separate read-model and write-model rather than one store.

10. Resources

Store + tools
src/wal_sh/site/beads.clj (read-issues, ready, ready-queue, next-ticket, set-status!, queue-backend, tool-defs, handle-tool).
Harness
src/wal_sh/site/agent.clj · run: gmake ticket-agent / gmake tickets.
Empirical companion
Compaction Against a Beads Backend.
The series
Feed Crawling · Flight Tracking · Chat Mutation.
Upstream baseline
claude-cookbooks/tool_use/utils.
Tracker
beads (git-native issues).