Files
commitea/docs/agent-tools.md
Croissant Le Doux 9920634e74 docs: settle open design items + dogfood backlog
Resolve the five design-session open items from PLAN.md:
- decisions.md: soft write-path, poll-only sync (NAT), lognormal
  cold-start priors, purity test binds the SQLite cache
- pm-state.md: sidecar layout + directive/capacity/calibration
  schemas + lifecycle inference table
- agent-tools.md: query_project read tool + three write tools

Also gitignore .env.* (protect the gitea PAT) and record the P0
actual: 10 labels, 5 milestones, 34 tracer-bullet issues + 51
dependencies filed on christian/commitea as the first managed project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:30:23 -04:00

5.0 KiB

Agent tools

Reginald gets few, fat tools so a small local model (gemma-4b class) can survive with one thing to reach for. One read tool, three write tools. All I/O is compact JSON; ticket data is fetched through tools, never copied into hot memory (PLAN.md memory layers).

Model routing (per PLAN.md): the read tool + prose/standup run on the small local model; capture_work decomposition and record_directive negotiation route to the big model.

query_project (read)

The single read tool. A view enum selects the shape; filters narrows it. The scheduler's deterministic output backs every forecast field — the model never computes, it reports.

query_project({
  view: "focus" | "issue" | "milestone" | "board" | "runway"
      | "calibration" | "directives" | "standup" | "search",
  filters?: {
    issueId?: number,
    milestoneId?: number,
    state?: "diagnosis" | "triage" | "steeping" | "in_review" | "done",
    assignee?: string,        // gitea username
    label?: string,
    query?: string,           // free text, for view:"search"
    limit?: number            // default 20, max 100
  }
})

View payloads (compact; forecasts always ranges, never point dates):

  • focus{ now, next[], later[] }, each { issue, title, rationale }.
  • issue — intent (title, description, comments, assignee, labels) + derived (lifecycle timeline, per-issue forecast range, dependency ids, provenance).
  • milestone{ due, hard, stats, cone: {p10,p50,p80 dates}, issues[] }.
  • board — issues grouped by the five lifecycle columns.
  • runway — per-milestone { due, band: {p50,p80}, slack } + capacity list.
  • calibration{ n, coldStart, globalMultiplier, byLabelBias[], byPersonBias[] }.
  • directives — pending consequence diff + recent ledger entries.
  • standup — drift report, per-person plan, stale blockers.
  • search — issues matching query.

capture_work (write — big model)

Braindump → interview → proposed issue set. Returns a proposal, never files directly; the Capture screen's review tray edits it before apply_changes files it. Decomposition + estimate negotiation is the one place the big model earns its keep.

capture_work({
  braindump: string,
  answers?: { question: string, answer: string }[]   // interview turns so far
})
// → { needsMoreInfo?: string[],   // follow-up questions; present as chips
//     proposal?: { issues: [{ title, body, estimate: EstimateLabel,
//                             priority?: PriorityLabel, deps?: number[],
//                             milestone?: number }],
//                  consequence: string } }           // one-line schedule impact

apply_changes (write — unified mutation)

Every mutation funnels here: filing captured issues, label/estimate/priority/ milestone/dependency edits. Additive ops apply directly; destructive ops require approved: true (the caller obtains approval via the consequence diff / Dialog first — see decisions.md D1). Batched so one call = one coherent change with one consequence.

apply_changes({
  ops: [
    { op: "create_issue", title, body, labels?, milestone?, deps? },
    { op: "set_estimate", issue, estimate: EstimateLabel },
    { op: "set_priority", issue, priority: PriorityLabel },
    { op: "set_milestone", issue, milestone: number | null },
    { op: "set_deadline_hard", milestone: number, hard: boolean },
    { op: "add_dep", issue, dependsOn: number },
    { op: "remove_dep", issue, dependsOn: number },   // destructive
    { op: "close_issue", issue },                     // destructive
    { op: "remove_label", issue, label }              // destructive
  ],
  approved?: boolean            // required iff any op is destructive
})
// → { applied: number, consequence: string, rejected?: {op, reason}[] }

Only touches labels in the CommiTea namespaces (est/*, p/*, deadline/hard) plus native issue fields — never invents labels, comments, or synthetic issues (zero-pollution goal).

record_directive (write — big model)

Appends to the directive log (pm-state.md), triggers a scheduler re-run, and returns the consequence diff for propose-approve. Does not mutate gitea itself — a directive is intent; its effects land through apply_changes after approval.

record_directive({
  kind: "reprioritize" | "reestimate" | "set-deadline" | "scope" | "capacity" | "note",
  quote: string,                // verbatim PM words, stored in the ledger
  target?: { issue?: number, milestone?: number, member?: string },
  params?: object,              // structured effect, e.g. { priority: 1 }
  rationale?: string
})
// → { directiveId, consequence: { before, after }[], summary: string }

Not tools

Reads that are pure UI state (theme, current view, back-stack) never go through tools. The scheduler, Monte Carlo, calibration fit, and lifecycle inference are code, invoked by the runtime around these tools — the model requests a view or proposes a change; deterministic code produces every number.