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 acrossBoardResolverTests(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 newcommand-bar.component.spec.tstests (debounce, chip render, forced-manual repo disables Confirm, Confirm-gates-dispatch for auto/middling/new-ticket bands, refusal toast, post-dispatch reset) + 2 newcommand.service.spec.tstests (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/.JiraProjectsare wired end to end (contract, aggregation inRepoRegistryReader.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. Seedocs/repo-registry-contract.mdfor the existing scanner contract this should extend. - Focused-card wiring:
CommandResolveRequest.FocusedCardIdand the backend affinity/ fallback signals that consume it are fully implemented and tested (IFocusedCardLookup), but no frontend UI yet sets it —CommandBarComponentnever passes afocusedCardIdtoday because nothing in the queue/session-tree lets an operator "focus" a card. Wiring that (e.g. a click handler onsession-tree-row.component.tsfeeding 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.knownBoardshardcodes['flt', 'wing'](mirrors the existingFALLBACK_TARGETSpattern) since there's noGET /api/command/boardsendpoint yet. A P2 could add one instead of hardcoding.