Skip to content

BUILD-REPORT-FLT135: intelligent command bar P1 (resolve chips + confirm-dispatch)

Worktree: wt/WT-6022/FLT-135 (fmc-cmdbar), built off origin/master @ 55529d3. Not merged.

What shipped

Backend - POST /api/command/resolve — classifies free text into board/ticket/repo chips with confidence + source. Never dispatches, never creates a ticket, never writes anything (one Jira read call — SearchIssuesAsync — is the only I/O besides reading the repo registry). - POST /api/command/dispatch — the only write. Takes a fully-confirmed payload, optionally mints a Jira ticket (JiraBoardService.CreateIssueAsync), resolves repoId→AbsPath via the existing RepoRegistryReader.Resolve, then dispatches via the existing AgentRunDispatcher/AgentRunValidator path — no parallel dispatch logic. - New pure/static resolvers (mirroring TierLadder/AgentRunValidator's style — no I/O, fully unit-testable): - BoardResolver (FLT-138): explicit wing:/flt: prefix → focused-card's repo→project → ticket-key-prefix-in-text → Command:DefaultBoard config → weak keyword classifier. AutoResolveEnabled (the spec's "auto-flt"/"auto-wing") gates ONLY the chip's Auto flag — resolve never dispatches regardless, so this can never become auto-run. - TicketRanker (FLT-136): scores candidates from a board-scoped Jira JQL search — summary 0.45 / description 0.25 / recent-comments 0.15 + 0.1 open-or-in-progress + 0.2 focused-card affinity + up to 0.05 recency, clamped [0,1]. Bands: ≥0.85 auto-select, 0.55-0.84 propose + RequiresConfirm=true, <0.55 falls to the board's configured GeneralTicketKey (a real Jira key from config — never a placeholder) or "+ New" when none is configured. An explicit ticket key typed in the text (e.g. FLT-123 fix...) always pins at 1.0, skipping ranking. - RepoResolver (FLT-137): explicit repo:<name> token / a single verbatim registry-name mention → resolved-ticket's Jira component→repoId map (new optional RepoInventoryEntry.Components/.JiraProjects fields, aggregated onto RepoOption) → keyword overlap vs registry names → focused card's repo → the board's configured DefaultRepoId. Two candidate signals within 0.15 of each other force ForcedManual=true and leave the chip unresolved — never guesses. The scanner that would populate Components/JiraProjects doesn't exist yet (FLT-126 follow-up); the signal is skipped, not blocking, when absent. - CommandBarConfig loads from appsettings.json's Command section: Command:DefaultBoard, Command:Boards:{prefix}:JiraProjectKey/GeneralTicketKey/ AutoResolve/IssueType/DefaultRepoId/Keywords. Shipped with flt→FLT and wing→WW boards (WorkWingman's Jira project, same *.atlassian.net site JiraBoardService already reads). Both boards ship with GeneralTicketKey: "" (unset) by design — no placeholder key; an operator must configure a real one before the General-fallback path activates. - CommandResolverService — the orchestrator: resolves board first (scopes the Jira search), then ticket (its selected candidate's components feed the repo signal), then repo. Read-only except for the Jira search. IFocusedCardLookup/FocusedCardLookup split out so the service is constructible in tests without standing up FleetPollingService's hub/governor chain.

Frontend - CommandBarComponent (fmc-command-bar) — single text input pinned atop the cockpit (above .cockpit-container in status-wall.component.ts). Debounced (350ms) POST to /api/command/resolve renders live chips: board · ticket · repo · agent · PC, each showing confidence% and an auto/manual badge. Board/ticket/repo chips are click-to-reveal dropdowns (typeahead deferred — candidates list is small enough for a plain <select> in P1); agent/PC stay plain manual selects per scope. A Confirm dispatch button shows a one-line spend/target summary and is the only caller of dispatchCommand() — resolving (typing, editing a chip) never dispatches. - CommandService.resolveCommand/.dispatchCommand — session-gated like every other write, added alongside the existing launch/cancel/planDesign/launchDesign. - NSwag client regenerated (npm run generate:client equivalent via npx nswag run nswag.json) from the rebuilt OpenAPI doc — openapi/FleetMissionControl.Api.json and frontend/src/app/api/fleet-api.generated.ts both committed.

API contract

POST /api/command/resolve   (RequireFleetAuth)
  body: { text, focusedCardId?, overrides?: { board?, ticketKey?, newTicket?, repoId? } }
  200: { board: BoardChip, ticket: TicketChip, repo: RepoChip, resolvedAt }
    BoardChip:  { board?, projectKey?, confidence, source, auto }
    TicketChip: { key?, isNew, isGeneral, confidence, source, auto, requiresConfirm,
                  candidates: [{ key, summary, status, score }] }
    RepoChip:   { repoId?, name?, confidence, source, auto, forcedManual }

POST /api/command/dispatch  (RequireFleetAuth)
  body: { board, ticketKey? | createNewTicket, newTicketSummary?, repoId, worktreePath?,
          target, agent, text, branch?, timeoutSec? }
  202: { jobId, target, ticketKey, createdTicket, acceptedAt }
  400: unconfigured board / missing repoId-target-text / not-exactly-one ticket disposition /
       repo not indexed on target / AgentRunValidator rejection
  502: Jira ticket creation failed (nothing dispatched)

Test counts

  • Backend: 475/475 passing (dotnet test), up from 429 baseline — 46 new tests across BoardResolverTests (10), TicketRankerTests (16), RepoResolverTests (13), CommandBarConfigTests (5), CommandResolverServiceTests (4, HTTP-stubbed Jira + signed-inventory RepoRegistryReader — verifies board resolves before the Jira call fires, the component-map wiring, and that resolve never writes anything).
  • Frontend: 218/218 passing (npx vitest run), up from 208 — 10 new command-bar.component.spec.ts tests (debounce, chip render, forced-manual repo disables Confirm, Confirm-gates-dispatch for auto/middling/new-ticket bands, refusal toast, post-dispatch reset) + 2 new command.service.spec.ts tests (session-gating).
  • dotnet build -warnaserror: 0 warnings, 0 errors.
  • ng build: succeeds (365 kB initial bundle).
  • ng lint: clean.

P2 / TODO (explicitly out of scope for this pass)

  • FLT-139 (branch wildcard/auto) and FLT-140 (transcripts) — not built, per instructions.
  • Component→repoId scanner: RepoInventoryEntry.Components/.JiraProjects are wired end to end (contract, aggregation in RepoRegistryReader.GetSnapshot, RepoResolver's component-map signal, CommandResolverService's wiring from the selected ticket) but nothing populates them yet — same posture as the FLT-120 repo registry's scanner-side TODO. The signal is inert (skipped, not blocking) until FLT-126 ships a scanner that writes these fields. See docs/repo-registry-contract.md for the existing scanner contract this should extend.
  • Focused-card wiring: CommandResolveRequest.FocusedCardId and the backend affinity/ fallback signals that consume it are fully implemented and tested (IFocusedCardLookup), but no frontend UI yet sets it — CommandBarComponent never passes a focusedCardId today because nothing in the queue/session-tree lets an operator "focus" a card. Wiring that (e.g. a click handler on session-tree-row.component.ts feeding a signal into the command bar) is a small, clearly-scoped P2 follow-up.
  • Ticket typeahead: chips use a plain <select> over the top-5 ranked candidates rather than a free-typing typeahead — sufficient for P1 given the small candidate list, but the spec mentioned "dropdown/typeahead" and a richer combobox is a reasonable P2 polish item.
  • Explicit-key existence check: an explicit ticket key typed in the text (e.g. FLT-999) pins at confidence 1.0 without a live Jira existence check — Jira itself will 404 at dispatch time if it's bogus (surfaced as a toast), but a resolve-time verification call would catch it earlier. Deferred to keep resolve to a single Jira round-trip.
  • Known-boards list on the frontend: CommandBarComponent.knownBoards hardcodes ['flt', 'wing'] (mirrors the existing FALLBACK_TARGETS pattern) since there's no GET /api/command/boards endpoint yet. A P2 could add one instead of hardcoding.