Skip to content

Accumulated fixes and improvements#66

Merged
inercia merged 458 commits into
mainfrom
fix/children-tasks-and-aux-model-switch
Jul 3, 2026
Merged

Accumulated fixes and improvements#66
inercia merged 458 commits into
mainfrom
fix/children-tasks-and-aux-model-switch

Conversation

@inercia

@inercia inercia commented Jul 1, 2026

Copy link
Copy Markdown
Owner

No description provided.

inercia added 30 commits June 26, 2026 00:42
Split the beads Cleanup operation into ListClosedIDs + DeleteIDs so the web handler can list closed issues synchronously, then delete them in batches on a detached goroutine. Progress is broadcast over the global-events WebSocket (beads_cleanup_progress), and the HTTP response returns immediately so the 30s middleware cap cannot cancel large cleanups. Adds concurrency guard per working dir and frontend progress display in BeadsView.
applyModelPreference now emits a Debug log with the decision taken (switching, skip_no_agent_models, skip_no_preference, skip_no_match, skip_already_satisfied) plus preferred/baseline/current/desired models, aiding diagnosis of multi-model session routing. Tests assert the decision string for each branch.
Flip writeErrorJSON in both package copies (internal/web and internal/web/handlers) from the flat {error,message} shape to the canonical nested envelope {error:{code,message,details?}} defined in rest-api-conventions.md sec 4.

Preserve the external-stable callback contract: POST /api/callback/{token} keeps its legacy flat shape via a dedicated writeCallbackError helper (17 call sites repointed), per the exception list in sec 6.

Update affected unit tests (http_helpers_test, queue_message_test) to decode the nested shape; callback_test stays flat. Document the external-stable flat-shape exemption in the conventions doc.

Part of mitto-ank.5 (increment 1: canonical error envelope). Deferred to later increments: http.Error plain-text to envelope migration, 405/methodNotAllowed JSON conversion, and the paired frontend error.message extraction.
inercia added 26 commits July 1, 2026 16:38
Adds new builtin prompt for guiding users through bug reproduction workflows.
Renames Git CEL/template functions for consistent naming:
- GitTracked → GitFileTracked
- GitDeleted → GitFileDeleted

All Git functions now follow the GitFile* prefix pattern (GitFileModified,
GitFileTracked, GitFileDeleted) for file-level operations, while GitDirModified
and GitRepo remain distinct for directory and repository-level checks.
… (mitto-qal.3)

During a prompt, a long tool call that streams no intermediate updates left the UI with an indefinite, frozen-looking spinner (SSE idle starvation UX gap). Emit a transient 'agent_working' WS heartbeat during genuine agent silence so the UI shows honest progress.

Backend: add optional AgentWorkingObserver sibling interface + AgentWorkingData; per-prompt heartbeat goroutine tied to promptCtx (stops on completion/cancel/process death); track in-flight tool titles to surface which tool is running.

The heartbeat reads a dedicated lastStreamActivityAt baseline advanced only by real streamed activity and never reset by the inactivity watchdog's tool-call pause, so reported idle grows monotonically through a long silent tool call instead of oscillating.

Frontend: handle 'agent_working' in useWebSocket (transient session.agentWorking, cleared on prompt_complete); MessageList renders a live 'Working - <tool> (mm:ss)' chip with 25s staleness auto-hide.
…cessing children

When mitto_children_tasks_wait times out but every pending child is still
actively responding (IsPrompting), the tool already returns a deterministic
still_processing signal per child, so the WARN log was redundant noise on
periodic re-delegation cadences. Log such timeouts at DEBUG; keep WARN for
genuinely stuck / non-responding pending children.

Adds a stillProcessingChildren helper and a still_processing_children log
field. The fallback path (no SessionManager) preserves prior WARN behaviour.

Refs: mitto-9uz
The blanket .mitto/ .gitignore rule also matched
tests/fixtures/workspaces/project-alpha/.mitto/prompts/, leaving the
Playwright prompt fixtures untracked. They only worked locally; a fresh
clone/CI would be missing them and the named-prompt-menu-send UI specs
would fail.

Add negation exceptions for tests/fixtures/**/.mitto/ so such fixtures
are tracked automatically, and add the 8 previously-untracked fixture
prompt files.
…selection (mitto-hke)

Introduce named Model profiles (Config.Models) as the preferred way to drive session-start and auxiliary model auto-selection, replacing legacy free-text matchMode/pattern constraints.

- ACPServer/ACPServerSettings gain ModelProfile; WorkspaceSettings gains AuxiliaryModelProfile
- Add Config.FindModelProfile (case-insensitive lookup)
- lookupACPServerConstraints merges the profile's Criteria into the model constraint (falls back to legacy Constraints when unset/unresolved)
- acpproc resolves AuxiliaryModelProfile via new ModelProfileResolver, wired in web server
Serialize and deserialize the ACP server model_profile field through the config REST API (get/save handlers and settings mapping), and update the config validation test struct to match.
…spaces dialogs

New ModelProfileSelect component picks a named Model profile. SettingsDialog replaces the raw matchMode/pattern model controls with the profile selector (preserving any legacy raw constraint unless explicitly cleared or overridden), and WorkspacesDialog exposes the auxiliary Model profile selection.
Carry PromptFile.Tags through to WebPrompt so prompt categorization tags survive the conversion, with a regression test covering tagged and untagged prompts.
…tto-d8e)

Hard cutover: prompt preferredModels no longer uses case-insensitive glob
patterns. It now references the global model profiles (Settings -> Models)
by profile name or by capability tag:

    preferredModels:
      - modelName: Claude Sonnet
      - modelTag: Coding

- config: add PromptPreferredModel{ModelName|ModelTag}; retype
  PromptFile/WebPrompt.PreferredModels; add pure ProfileByName/ProfilesByTag
  helpers (ModelProfileByName/ModelProfilesByTag delegate to them).
- conversation: rewrite SelectPreferredModel(prefs, profiles, models) to
  resolve modelName via ProfileByName+ResolveProfileModel and modelTag via
  ProfilesByTag (deterministic by profile order, first-yielding wins),
  preserving the "keep current model if it already satisfies" short-circuit;
  plumb the new type through the dispatcher, bgsession, background_session,
  session_manager and web resolver; update Go tests.
- frontend: mirror the resolver in prompts.js against config.models and the
  {modelName,modelTag} entries; wire config.models through
  app.js -> ChatInput -> PromptsMenu; update prompts.test.js.
- prompts: migrate 25 builtin prompts (19 -> modelTag: Coding; 6 ->
  modelTag: Cheap then Coding).
- docs: update 07-prompts.md, 08-config.md, config/models.md,
  devel/prompt-templates.md.
…Prompts tab

Adds a gear button at the bottom-right of the prompts drop-up footer that closes the drop-up and opens the Workspaces dialog with the current conversation's folder selected and the Prompts tab focused. The gear is only shown when config is editable and a working dir exists (guarded via onConfigurePrompts prop from app.js).

Closes mitto-y98
…o-gap.1)

Refactor the bug-fix driver to delegate each phase to a dedicated,
name-invoked prompt that declares its own preferredModels — Option A
per-phase model tiering. The driver no longer performs investigate /
reproduce / fix work inline; it reads the bead's live labels and
dispatches the matching phase prompt via mitto_conversation_send_prompt
(self-send), then ends its turn. On onCompletion the driver re-runs,
observes the newly-advanced label, and dispatches the next phase.

Each phase prompt runs under a transient model override applied by the
existing pipeline (resolvePreferredModelsByPromptName ->
SelectPreferredModel -> setActiveModelOnly), so the tier switch does
not touch the enclosing conversation's baseline model
(restoreBaselineIfOverride flips it back after each dispatch).

New phase prompts (menus: internal — hidden from every UI menu, still
resolvable by name for programmatic dispatch):

  - config/prompts/builtin/beads-issue-fix-phase-investigate.prompt.yaml
      preferredModels: [{ modelTag: Reasoning }]
      Adds bd comment 'Investigation: ...' and label 'researched'.

  - config/prompts/builtin/beads-issue-fix-phase-reproduce.prompt.yaml
      preferredModels: [{ modelTag: Coding }]
      Writes a failing test; adds bd comment 'Reproduction: ...' and
      label 'reproduced'.

  - config/prompts/builtin/beads-issue-fix-phase-fix.prompt.yaml
      preferredModels: [{ modelTag: Coding }]
      Implements the minimum change that makes the reproduction test
      pass; adds bd comment 'Fix: ...' and label 'fixed'; optionally
      commits when Commit=true is propagated from the driver.

Each phase prompt is self-contained: it resolves its target from
Session.BeadsIssue else Args.IssueID, re-reads live label state, and
carries its own Step 4 (Blocked -> Defer + Handoff) so external
orchestrators (not just this driver) can invoke it by name safely.

Driver changes (beads-issue-iterate-fixing-bug.prompt.yaml):

  - Step 3a/3b/3c: inline bd update --add-label bodies replaced with
    explicit mitto_conversation_send_prompt calls that pass IssueID
    (always) and Commit (Fix only) as arguments.
  - Step 3d (Done) stays inline — no phase work, just bd close and
    self-terminate via periodic_enabled: false.
  - Step 4 (Blocked handoff) stays inline as before.
  - Guidelines updated: 'One phase per run', explicit warning against
    doing phase work inline in the driver, silent/verbose branches
    reworded to talk about 'phase dispatched' milestones.
  - Frontmatter description updated to reflect the dispatch model.

Tests (internal/config/prompt_template_test.go):

  - TestIterateFixingBug_RendersForRepresentativeContexts updated to
    verify the driver renders all three phase-prompt names, uses
    mitto_conversation_send_prompt, propagates the resolved IssueID
    into the dispatch arguments (both linked-issue and arg-only
    branches), propagates Commit=true into the Fix dispatch, and does
    NOT leak inline 'bd update --add-label researched|reproduced|fixed'
    calls anywhere in the rendered body.
  - New TestBugFixPhasePrompts_ParseAndDeclarePreferredModels — parses
    each phase file, checks name, asserts menus == 'internal' (UI
    hidden), and asserts the expected preferredModels[0].ModelTag
    (Reasoning / Coding / Coding).
  - New TestBugFixPhasePrompts_RenderForRepresentativeContexts —
    renders each phase prompt in (a) linked-issue, (b) arg-only (with
    Commit=true for Fix), and (c) no-target contexts; asserts the
    phase prompt writes 'bd show <id> --json --include-comments' when
    a target is resolved, renders 'git commit -m' only when Commit=true
    in Fix, renders the missing-target guidance when no target, and
    never emits a broken empty 'bd show ' command.

All target tests pass (TestIterateFixingBug, TestBugFixPhasePrompts_*,
TestBuiltinPrompts_AllRenderWithoutError, TestBuiltinPrompts_NoDeprecatedMittoVars,
TestBuiltinPromptPeriodicModes); full internal/config package tests pass;
go build ./... succeeds.

Refs mitto-gap.1
… tests hermetic

- gofmt -w internal/conversation/prompt_dispatcher.go
  internal/conversation/prompt_dispatcher_test.go: pure whitespace/alignment
  fixes (struct field comment alignment), no logic changes. Fixes
  'make fmt-check' CI failure.

- internal/config/templatefuncs_test.go: newGitRepo() test helper now sets
  commit.gpgsign=false for the temp repo. Without this, 'git commit' inside
  the test fails on any developer machine with a global commit.gpgsign=true
  and no cached gpg-agent passphrase ('gpg failed to sign the data'). CI
  runners don't have this configured, but this makes the test hermetic
  regardless of the host's global git config. Fixes local
  TestParity_GitHelpers / TestBuildTemplateFuncMap_GitFuncsRenderSmoke
  failures uncovered while verifying 'make test' locally.
…tto-gap)

Persist epic-adjacent agent-guidance drafted during the model-tiering work: new .augment/rules/31-periodic-prompts.md (silent mode, spawn dedup, gate testing, state persistence) and a tidy of 07-prompts.md. This is rules guidance, NOT the formal gap.2 deliverable (docs/devel/prompt-templates.md still pending).
Add 4 internal phase prompts (plan=Reasoning, implement=Coding,
test=Coding, review=Reasoning) and refactor the Iterate implementing
feature driver to self-send each phase by name (Steps 3a-3d), mirroring
the shipped bug-flow pattern (mitto-gap.1). Step 3e Done + Step 4 Blocked
stay inline. Extends prompt_template_test.go with parse/tier + render
tests for the new phase prompts.
Removes stray double blank lines (gofmt normalizes consecutive blank
lines to one) introduced by recently-landed mitto-gap.6/mitto-gap.1
test additions. Pure whitespace change, no logic touched. Fixes
'make fmt-check' CI failure (Lint job, 3rd occurrence of a landing
feature commit reintroducing a gofmt miss).

Verified full local gate on an isolated worktree pinned to origin's
HEAD (220e924) to avoid interference from other in-flight work in
the shared main checkout:
  - make fmt-check: clean
  - make lint: 0 golangci-lint issues, frontend html/css/js clean
  - make test: 23 Go packages ok, JS 17 suites / 1445 tests passed
  - build-mock-acp + integration tests: ok, 0 failures (201.9s)
Adds a versioned .githooks/pre-commit hook that runs 'make fmt-check'
before every commit, plus a 'make install-hooks' target that wires it
up via 'git config core.hooksPath .githooks'.

Motivation: PR #66's Lint CI job has failed fast on gofmt at least 3
times in a row as new feature commits landed with unformatted Go
files, masking golangci-lint, frontend lint, Integration Tests, and
Smoke Tests every time. This catches it locally before it reaches CI.

Run 'make install-hooks' once per clone to enable it (not on by
default, since hooksPath is a local git config, not something git
applies automatically from a cloned repo).
TestTemplateRender_ModelTag was flaky in CI, rendering NOTSMART/NOTEXP
instead of SMART/NOTEXP. Root cause: for shared-process sessions, the
session/new RPC (which populates BackgroundSession.agentModels) is
deferred to the first prompt (see PrewarmACPSession doc comment) so
conversation creation never blocks on a busy agent process. But
PromptWithMeta renders Go templates (resolveAndSubstitute) synchronously
BEFORE spinning off the goroutine that completes this deferred handshake
(completeHandshakeOrAbort). So the very first templated prompt on a new
session could see a nil agentModels, causing the Model(tag) template func
to silently resolve no tags — a race that usually lost locally (fast
mock ACP round-trip) but could flip under CI's slower/loaded runners.

Fix: synchronously (and idempotently) complete the deferred handshake
right before rendering, only when the message actually needs a template
render and only for shared-process sessions. pdCompleteDeferredHandshake
already no-ops cheaply once the handshake is done, so this adds no
overhead on the common path (already-handshaked sessions, non-templated
prompts).

Verified: TestTemplateRender_ModelTag 10/10 local runs -> MT:SMART/NOTEXP.
Full integration suite: 0 failures (200s). make fmt-check, make lint,
go test ./...: all clean.
@inercia
inercia merged commit 579f1f4 into main Jul 3, 2026
4 checks passed
@inercia
inercia deleted the fix/children-tasks-and-aux-model-switch branch July 3, 2026 08:41
inercia added a commit that referenced this pull request Jul 5, 2026
…& more (#67)

* refactor(config,session): rename periodic -> loop core types/tags/settings (mitto-8ir.1)

Mechanical rename of the periodic/Periodic naming to loop/Loop in internal/config and internal/session (trigger enum values and frequency units unchanged). IsPeriodic/IsPeriodicForced/IsPeriodicConversation CEL vars are out of scope (owned by mitto-8ir.2). Builtin prompt YAML files (config/prompts/builtin) are out of scope (owned by the builtin-prompts child bead); TestBuiltinPromptLoopModes now skips affected cases until that migration lands.

* feat(migration): rename periodic.json->loop.json + settings periodic_*->loop_* keys (mitto-8ir.12)

Adds two idempotent, one-time data migrations for the mitto-8ir periodic->loop rename:

1. internal/session/migration_002_rename_periodic_to_loop.go: registers session
   migration 002 which renames each session dir's periodic.json to loop.json
   (content unchanged). Safe: no-op when periodic.json absent, never clobbers
   an existing loop.json, idempotent re-run.

2. internal/config/settings.go: migrateSettingsFileIfNeeded/migrateSettingsPeriodicKeys
   rewrite legacy periodic_* keys (nested under session.* and conversations.*)
   to loop_* in the raw settings.json before unmarshalling, called from both
   LoadSettings and LoadSettingsWithFallback. Old value moved only when the new
   key is absent; already-migrated or new-only files are left untouched.

Prompts / IsPeriodic templates / MCP wire params are explicitly NOT migrated
(out of scope, per epic).

* refactor(config,processors): rename periodic -> loop template vars/CEL/processor fields (mitto-8ir.2)

internal/config:
- prompt_template.go: template map keys periodic->loop, periodic_forced->
  loop_forced; @mitto:periodic -> @mitto:loop directives.
- cel_context.go/cel_evaluator.go: IsPeriodic/IsPeriodicForced/
  IsPeriodicConversation -> IsLoop/IsLoopForced/IsLoopConversation on
  SessionContext and IterationContext; CEL activation maps updated.
- prompt_template_test.go/prompts_test.go: updated synthetic literals to
  IsLoop; added usesLegacySessionIsPeriodic guard to skip (not fail)
  builtin prompt files that still reference the legacy
  .Session.IsPeriodic template field or periodic: frontmatter key,
  pending the separate builtin-prompts migration bead (mitto-8ir.9).

internal/processors:
- input.go: ProcessorInput.IsPeriodic/IsPeriodicForced (json is_periodic/
  is_periodic_forced) -> IsLoop/IsLoopForced (is_loop/is_loop_forced).
- hook.go: BuildCELContext maps IsLoop/IsLoopForced into
  Session/Iteration CEL context.
- executor.go: anon InputMessage struct field IsPeriodic -> IsLoop.
- variables.go: @mitto:periodic/@mitto:periodic_forced ->
  @mitto:loop/@mitto:loop_forced.
- processors_test.go/variables_test.go: updated tests accordingly.

No behavior changes. Trigger enum values, frequency units, and the
"periodic-runner" sender-ID sentinel (owned by mitto-8ir.3) are untouched.

Verify: go build/vet/test ./internal/config/... ./internal/processors/...
all pass. Full-repo "go build ./..." remains broken pending mitto-8ir.3/
.4/.5/.6 (pre-existing condition since mitto-8ir.1 landed; internal/
mcpserver already fails to build against session/config types renamed
there).

* refactor(conversation): rename periodic->loop in conversation domain (mitto-8ir.3)

File renames (git mv):
- periodic_data.go -> loop_data.go
- periodic_data_test.go -> loop_data_test.go

Symbol renames across internal/conversation:
- loop_data.go: BuildPeriodicUpdatedData -> BuildLoopUpdatedData; map keys
  periodic_configured/periodic_enabled/periodic_has_prompt/
  periodic_prompt_preview/periodic_stopped_reason -> loop_*; uses
  session.LoopPrompt (was session.PeriodicPrompt).
- ws_events.go: WSMsgTypePeriodicUpdated="periodic_updated" ->
  WSMsgTypeLoopUpdated="loop_updated".
- session_manager.go: BroadcastPeriodicUpdated -> BroadcastLoopUpdated;
  sm.store.Periodic(...) -> sm.store.Loop(...); nextPeriodic -> nextLoop;
  doc/prose periodic->loop.
- session_info.go: SessionInfo.NextPeriodicAt -> NextLoopAt.
- bgsession_title.go / title_coordinator.go (+ _test.go):
  TriggerTitleGenerationFromPeriodic -> TriggerTitleGenerationFromLoop;
  triggerFromPeriodic -> triggerFromLoop.
- bgsession_prompt.go / background_session.go (+ _test.go):
  PeriodicKind/PeriodicKindNone/Scheduled/Forced -> LoopKind/LoopKindNone/
  Scheduled/Forced; PromptMeta.IsPeriodicForced -> IsLoopForced;
  PromptMeta.PeriodicKind -> LoopKind; periodicContinuationMu/
  lastTurnScheduledPeriodic -> loopContinuationMu/lastTurnScheduledLoop;
  peekPeriodicContinuation/advancePeriodicContinuation/
  ResetPeriodicContinuation -> peek/advance/ResetLoopContinuation.
- prompt_dispatcher.go (+ _test.go): senderIDPeriodic="periodic-runner" ->
  senderIDLoop="loop-runner"; ProcessorInput.IsPeriodic/IsPeriodicForced ->
  IsLoop/IsLoopForced (matches mitto-8ir.2's processors.ProcessorInput
  rename).
- bgsession_acp_process.go: ResetPeriodicContinuation call site updated.
- follow_up_coordinator.go: sender-ID sentinel "periodic-runner" ->
  "loop-runner" in promptOriginFromSenderID.

Left unchanged (justified):
- interfaces.go / bgsession_prompt.go doc comments referencing
  PeriodicRunner (type still named PeriodicRunner in internal/web,
  owned by mitto-8ir.4) — informational reference only, not a symbol
  defined in this package.
- bgsession_queue.go: "periodic queue checking" — generic English prose
  about a recurring timer-driven check, unrelated to the loop feature.
- markdown_test.go / markdown_streaming_test.go: "Periodic persistence
  during long responses" — arbitrary fixture text for markdown rendering
  tests, unrelated to the loop feature.

Verify:
- go vet ./internal/conversation/... clean.
- go test ./internal/conversation/... passes (267s, all packages ok).
- go build ./internal/conversation/... could not be verified directly
  because internal/conversation has a real (non-test) import dependency
  on internal/mcpserver (mcpserver.Server, UIPrompt* types), and
  internal/mcpserver currently fails to compile due to pre-existing
  mitto-8ir.1 fallout (session.PeriodicPrompt/PeriodicTrigger,
  config.PromptPeriodic, store.Periodic(), GetMinPeriodicCompletionDelaySeconds
  — all owned by mitto-8ir.6). This blocker predates this bead (confirmed
  via git stash) and is NOT caused by this change.
  Verification workaround: applied a throwaway, uncommitted mechanical
  patch to internal/mcpserver (renaming those same pre-existing broken
  references) purely to unblock compilation, confirmed
  `go build ./internal/conversation/...` and `go vet` succeed and all
  conversation tests pass, then fully reverted the mcpserver patch via
  `git checkout` (verified zero diff afterward, nothing committed there).

No behavior changes. Trigger enum values, frequency units, and symbols
owned by other packages (internal/web PeriodicRunner, internal/mcpserver,
internal/acpproc SessionInfo.NextPeriodicAt consumer) are untouched.

* refactor(mcpserver): rename periodic->loop MCP tool params/types/descriptions (mitto-8ir.6)

Breaking MCP contract change (intended): external clients using the old
periodic_* argument names will break.

types.go:
- ConversationInfo.IsPeriodic / ConversationDetails.IsPeriodic -> IsLoop
  (json is_periodic -> is_loop).
- ConversationUpdateInput/Output: Periodic* fields -> Loop*, json tags
  periodic_* -> loop_* (prompt, frequency_value/unit/at, enabled,
  fresh_context, max_iterations, trigger, completion_delay_seconds,
  max_duration_seconds, condition, condition_preset); PeriodicIterationCount
  -> LoopIterationCount, PeriodicNextRun -> LoopNextRun.
- PromptInfo.Periodic / PromptDetail.Periodic (*config.PromptPeriodic) ->
  Loop (*config.PromptLoop), json periodic -> loop.

prompts.go:
- p.Periodic -> p.Loop (config.WebPrompt field) in handlePromptList /
  handlePromptGet.

server.go (~123 mechanical occurrences + tool description prose):
- periodicRunner field / PeriodicRunner interface / SetPeriodicRunner ->
  loopRunner / LoopRunner / SetLoopRunner.
- SessionManager.BroadcastPeriodicUpdated -> BroadcastLoopUpdated (matches
  mitto-8ir.3's conversation.SessionManager rename); param type
  session.PeriodicPrompt -> session.LoopPrompt.
- BackgroundSession.TriggerTitleGenerationFromPeriodic -> ...FromLoop
  (matches mitto-8ir.3).
- periodicDelayFloor -> loopDelayFloor; uses
  ConversationsConfig.GetMinLoopCompletionDelaySeconds() and
  config.DefaultMinLoopCompletionDelaySeconds (mitto-8ir.1 symbols).
- store.Periodic(id) -> store.Loop(id); session.PeriodicTrigger ->
  session.LoopTrigger; session.PeriodicPrompt -> session.LoopPrompt
  (mitto-8ir.1 symbols).
- mitto_conversation_run_periodic_now tool -> mitto_conversation_run_loop_now
  (breaking tool rename); handleRunPeriodicNow -> handleRunLoopNow;
  RunPeriodicNowInput/Output -> RunLoopNowInput/Output.
- ConversationStartInput/Output, ConversationUpdateInput/Output request
  fields renamed to match types.go's Loop* / loop_* map.
- All MCP tool Description strings for mitto_conversation_new and
  mitto_conversation_update updated: every 'periodic_*' param mention ->
  'loop_*', plus surrounding prose (fixed 3 sed-artifact words: "own
  loopity" -> "own loop", "send loopally" -> "send in the loop", "as loop"
  -> "as a loop").

server_test.go (114 occurrences): all mock method signatures
(BroadcastPeriodicUpdated -> BroadcastLoopUpdated, mockPeriodicRunner ->
mockLoopRunner, TriggerTitleGenerationFromPeriodic -> ...FromLoop), test
names (TestHandleRunPeriodicNow_* -> TestHandleRunLoopNow_*,
TestConversationUpdate_OnCompletionPeriodic ->
..._OnCompletionLoop, etc.), and payload/assertion keys updated to loop_*.
Fixed 2 "periodicity" sed artifacts in comments.

Left unchanged (justified, unrelated prose):
- server.go:4275 "startProgressHeartbeat emits periodic progress
  notifications" — generic SSE keepalive heartbeat, unrelated to the loop
  feature.
- server.go:4797 "Polling loop: check child agent status periodically" —
  generic recurring status poll, unrelated to the loop feature.

Verify:
- go build ./internal/mcpserver/... : OK.
- go build ./internal/conversation/... ./internal/mcpserver/... : OK
  (confirms mitto-8ir.3 + mitto-8ir.6 integrate).
- go vet ./internal/mcpserver/... : clean.
- go test -count=1 ./internal/mcpserver/... : ok (123s).
- gofmt clean on all touched files.
- grep -rIn -i periodic internal/mcpserver: only the 2 justified lines above.

* refactor(web,acpproc): rename periodic->loop loop-runner engine + acpproc GC/suspend (mitto-8ir.4)

Scope: the loop-runner engine in internal/web plus internal/acpproc's GC
suspend/resume logic. REST/WS JSON contract fields (internal/web/handlers,
session_api.go, session_ws.go, session_periodic_api.go, routes.go,
ws_messages.go, tasks_baseline.go doc refs) are intentionally left for
mitto-8ir.5 and remain in their pre-existing (already broken by earlier
beads) state.

File renames (git mv):
- internal/web/periodic_runner.go -> loop_runner.go
- internal/web/periodic_runner_tasks.go -> loop_runner_tasks.go
- internal/web/periodic_runner_test.go -> loop_runner_test.go (~249 occurrences)

loop_runner.go / loop_runner_tasks.go / loop_runner_test.go:
- PeriodicRunner -> LoopRunner (type, NewPeriodicRunner -> NewLoopRunner)
- All Set*/On* methods: SetOnPeriodicStarted/AutoStopped/Updated ->
  SetOnLoop*, SetMaxPeriodicIterations -> SetMaxLoopIterations,
  SetMinPeriodicCompletionDelaySeconds -> SetMinLoopCompletionDelaySeconds,
  MinPeriodicCompletionDelaySeconds -> MinLoopCompletionDelaySeconds,
  StopPeriodicForArchive -> StopLoopForArchive, SetMinLoopTasksCooldownSeconds
  (already loop-named).
- ErrPeriodicNotEnabled -> ErrLoopNotEnabled; MaxPeriodicResumeFailures ->
  MaxLoopResumeFailures; periodicScheduleBackoff(Base/Cap) -> loopScheduleBackoff*.
- Uses upstream-renamed symbols: session.Store.Loop()/LoopPrompt/LoopStore/
  ErrLoopNotFound (mitto-8ir.1), config.DefaultMaxLoopIterations/
  DefaultMinLoopCompletionDelaySeconds/GlobalMaxLoopIterations/
  EffectiveMaxLoopIterations (mitto-8ir.1), conversation.LoopKindScheduled/
  LoopKindForced/IsLoopForced/LoopKind (mitto-8ir.3). Sender ID literal
  "periodic-runner" -> "loop-runner" (matches conversation.senderIDLoop).
- Fixed 4 "loop loop" grammar artifacts from the blanket rename; preserved a
  doc-comment reference to handleSetPeriodic/handlePatchPeriodic (HTTP, still
  actually named that — owned by .5).

internal/acpproc (acp_process_gc.go, acp_process_gc_test.go,
acp_process_manager.go):
- GCConfig.PeriodicSuspendThreshold/PeriodicSuspendGracePeriod -> LoopSuspend*;
  UpdatePeriodicSuspendThreshold -> UpdateLoopSuspendThreshold; all doc
  comments and log fields (next_periodic_at -> next_loop_at, etc).
- Fixed real breakage from mitto-8ir.3's SessionInfo.NextPeriodicAt ->
  NextLoopAt rename (acp_process_gc.go referenced the old field name).
- Fixed 1 sed artifact ("runs RunGCOnce loopally" -> restored "periodically",
  a generic unrelated adverb).
- Build/vet/test verified standalone: go build, go vet, go test all pass
  (1.4-1.7s).

internal/web/server.go / config_handlers.go (runner wiring only, no JSON
contract changes):
- Server.periodicRunner field -> loopRunner (*LoopRunner); Server.PeriodicRunner()
  getter -> LoopRunner() (unused outside one now-already-broken integration
  test file pending a later bead).
- All call sites updated to the renamed LoopRunner API and config getters
  (GetMaxLoopIterations, GetMinLoopCompletionDelaySeconds, GetStartupLoopDelay,
  ParseLoopSuspendTimeout, GCConfig.LoopSuspendThreshold).
- mcpServer.SetPeriodicRunner -> SetLoopRunner (matches mitto-8ir.6).
- sessionManagerAdapter.BroadcastPeriodicUpdated -> BroadcastLoopUpdated to
  satisfy mcpserver.SessionManager interface (mitto-8ir.6) — was a real
  compile error (method name mismatch), not a JSON contract change.
- config_handlers.go: fixed ParsePeriodicSuspendTimeout/UpdatePeriodicSuspendThreshold
  call site to use the already-renamed upstream/acpproc names.
- Left untouched (owned by .5, all already broken pre-existing due to earlier
  beads' upstream renames): Server.BroadcastPeriodicUpdated/BroadcastPeriodicStarted
  (WS broadcast functions + WSMsgTypePeriodicUpdated/Started + handlers.Deps
  field names TriggerPeriodicNow/StopPeriodicForArchive/ErrPeriodicNotEnabled/
  PeriodicDelayFloor/BroadcastPeriodicUpdated), Server.periodicDelayFloor()
  (session_periodic_api.go).

Verification:
- go build/vet/test ./internal/acpproc/...: all pass standalone.
- go build ./internal/web/...: fails ONLY inside internal/web/handlers (11
  errors, all undefined session.Periodic{Prompt,Store,Trigger}/ErrPeriodicNotFound
  / store.Periodic — squarely .5's REST contract scope).
- Verified runner+server integration via a THROWAWAY, fully-reverted scratch
  patch: mechanically renamed the remaining periodic->loop symbols in
  handlers/*.go, session_api.go, session_periodic_api.go, session_ws.go, and
  server.go's BroadcastPeriodicUpdated to their known upstream-renamed names;
  `go build ./internal/web/...` then succeeded cleanly, and `go vet` failed
  only on server_test.go's TestBuildPeriodicUpdatedData_* tests (which test
  the .5-owned WS payload builder directly). Confirms my loop_runner.go +
  server.go changes integrate correctly once .5 lands. Scratch patch fully
  reverted (git checkout -- handlers/ session_api.go session_periodic_api.go
  session_ws.go; server.go restored from a pre-scratch backup); working tree
  verified diff-clean against intended changes before committing.
- gofmt clean on all touched files.

* refactor(web): rename periodic->loop REST/WS contracts + routes (mitto-8ir.5)

BREAKING wire change (JSON keys, URL paths, WS message types). Frontend bead
(.7) depends on this. This is the LAST piece needed for internal/web to build
fully — .4 (loop-runner engine + acpproc) already landed.

File renames (git mv):
- internal/web/handlers/session_periodic.go -> session_loop.go
- internal/web/handlers/session_periodic_write.go -> session_loop_write.go
- internal/web/handlers/session_periodic_run.go -> session_loop_run.go
- internal/web/handlers/session_periodic_test.go -> session_loop_test.go
- internal/web/session_periodic_api.go -> session_loop_api.go

REST paths (routes.go):
- /api/sessions/{id}/periodic -> /api/sessions/{id}/loop
- /api/sessions/{id}/periodic/{subPath} -> /api/sessions/{id}/loop/{subPath}
- handleSessionPeriodic -> handleSessionLoop (session_api.go + routes.go)
- handlers.HandleSessionPeriodic -> HandleSessionLoop; handleGetPeriodic/
  handleSetPeriodic/handlePatchPeriodic/handleDeletePeriodic/
  handleRunPeriodicNow -> handleGetLoop/handleSetLoop/handlePatchLoop/
  handleDeleteLoop/handleRunLoopNow.

JSON field renames (all keys, request + response bodies):
- PeriodicPromptRequest/PeriodicPromptPatchRequest -> LoopPromptRequest/
  LoopPromptPatchRequest (json tags unchanged: trigger/delay_seconds/etc.
  were already generic, not periodic_*).
- RunPeriodicNowRequest -> RunLoopNowRequest.
- handlers/session_list.go SessionListResponse: PeriodicConfigured|Enabled|
  Frequency|StoppedReason|Trigger|IterationCount|MaxIterations|DelaySeconds|
  MaxDurationSeconds|HasPrompt|PromptPreview -> Loop* fields with loop_* json
  tags (loop_configured, loop_enabled, loop_frequency, loop_stopped_reason,
  loop_trigger, loop_iteration_count, loop_max_iterations, loop_delay_seconds,
  loop_max_duration_seconds, loop_has_prompt, loop_prompt_preview).
- session_api.go: IsPeriodicConversation -> IsLoopConversation (matches
  config.SessionContext.IsLoopConversation, mitto-8ir.2).
- handlers.go Deps struct: TriggerPeriodicNow/StopPeriodicForArchive/
  ErrPeriodicNotEnabled/PeriodicDelayFloor/BroadcastPeriodicUpdated ->
  TriggerLoopNow/StopLoopForArchive/ErrLoopNotEnabled/LoopDelayFloor/
  BroadcastLoopUpdated (wired from server.go's already-.4-renamed loopRunner).
- callback.go: public webhook error codes "periodic_disabled" -> "loop_disabled"
  (intentional breaking change per task scope; endpoint doc-comment already
  flags external callers as depending on this legacy shape).
- ui_preferences.go: persisted filter_tab_grouping key "periodic" -> "loop".
- session_create.go, session_update.go, queue.go: doc-comment/wiring updates
  only (StopLoopForArchive call site, no JSON/behavior change).

WebSocket:
- ws_messages.go: WSMsgTypePeriodicStarted="periodic_started" ->
  WSMsgTypeLoopStarted="loop_started" (WSMsgTypeLoopUpdated already existed
  from mitto-8ir.3).
- session_ws.go: data keys periodic_configured/periodic_enabled ->
  loop_configured/loop_enabled.
- server.go: Server.BroadcastPeriodicUpdated/BroadcastPeriodicStarted ->
  BroadcastLoopUpdated/BroadcastLoopStarted, now correctly calling
  conversation.BuildLoopUpdatedData / conversation.WSMsgTypeLoopUpdated /
  WSMsgTypeLoopStarted (mitto-8ir.3's already-renamed symbols — this resolves
  the build breakage flagged as deferred-to-.5 in mitto-8ir.4's report).
- sessionManagerAdapter.BroadcastLoopUpdated already fixed by .4; unaffected.
- server_test.go: TestBuildPeriodicUpdatedData_* -> TestBuildLoopUpdatedData_*
  (tests conversation.BuildLoopUpdatedData directly).

loop_runner.go (mitto-8ir.4 file): updated its one remaining doc-comment
reference (handleSetPeriodic/handlePatchPeriodic -> handleSetLoop/
handlePatchLoop) now that those handlers are renamed by this bead.

Grammar fixes: 3 "loop loop" sed artifacts (handlers.go, session_list.go,
session_update.go doc comments), 1 "loopally"->"periodically" (unrelated
adverb in server.go's GC comment, restored), 1 ALL-CAPS "PERIODIC"->"LOOP"
sidebar-category comment in session_list.go (sed is case-sensitive and
missed it), and reworded "can be loop"->"can be loops" for grammar (+ updated
the matching test assertion string in session_loop_test.go).

Out of scope (left alone, per task): tests/integration/** (owned by final
sweep mitto-8ir.11), web/static/** frontend (owned by .7/.8), docs/devel/**.

Verification:
- go build ./internal/web/...: PASSES fully (previously blocked on
  internal/web/handlers; now clean).
- go build ./internal/web/... ./internal/acpproc/... ./internal/conversation/...
  ./internal/mcpserver/... (combined): PASSES.
- go vet ./internal/web/...: CLEAN.
- go test -count=1 ./internal/web/...: ok for internal/web (9.3s),
  internal/web/handlers (1.5s), internal/web/middleware (2.0s).
- gofmt clean on all touched files.
- grep -rIn -i periodic internal/web: only 5 justified unrelated-prose hits
  remain (generic "periodically"/"cleanupLoop periodically" adverbs in
  middleware/auth.go, middleware/security_ratelimit.go,
  middleware/auth_ratelimit.go, negative_session_cache.go, server.go's GC
  comment) — none reference the loop feature.

* mitto-8ir.7: loop rename: frontend API/data layer (endpoints, hooks, utils)

Renames periodic -> loop across the frontend API/data layer to match the
already-renamed backend JSON contract (beads .1-.6) and REST/WS surface
(bead .5).

utils/:
- endpoints.js (+ test): sessions.periodic -> loop, periodicRunNow -> loopRunNow.
- prompts.js (+ test): promptPeriodicMode/IsToggleable/DefaultOn ->
  promptLoop*, promptResolveAsPeriodic -> promptResolveAsLoop,
  prompt.periodic field access -> prompt.loop. promptsPeriodic menu
  identifier intentionally preserved (frontend-only menu id, out of scope).
- storage.js (+ test): FILTER_TAB.PERIODIC -> LOOP, DEFAULT_CATEGORY_FILTER /
  getCategoryFilter / setCategoryFilter "periodic" field -> "loop"
  (sessionStorage shape change, session-scoped so no migration needed).
  Historical migrateLegacyTabStorage() literal old-key strings preserved.
- sessionGrouping.js (+ test): periodic_enabled/periodic_configured ->
  loop_enabled/loop_configured; FILTER_TAB.PERIODIC -> LOOP; filter.periodic
  -> filter.loop.

hooks/:
- useConversationSeeding.js (+ test): decidePeriodicAction -> decideLoopAction,
  makePeriodicNow -> makeLoopNow, configurePeriodicSchedule ->
  configureLoopSchedule; action strings new-periodic/make-periodic ->
  new-loop/make-loop; endpoints.sessions.periodic(RunNow) -> loop(RunNow).
- useWorkspacePrompts.js: periodicPrompts -> loopPrompts.
- useWebSocket.js: periodic_* session/WS fields -> loop_* (loop_configured,
  loop_enabled, loop_stopped_reason, loop_trigger, loop_delay_seconds,
  loop_max_duration_seconds), WS event types periodic_started/periodic_updated
  -> loop_started/loop_updated.
- useBeadsIntegration.js: promptResolveAsPeriodic import -> promptResolveAsLoop;
  startConversationWithPrompt({ periodic }) -> ({ loop }).
- index.js: re-export names updated.

constants.js: PERIODIC_PROGRESS_* -> LOOP_PROGRESS_*.

Necessary downstream fixes (function/field renames are breaking, no aliases
per epic decision) in files owned by later beads (.8 frontend components):
app.js, ChatInput.js, ContextMenu.js, PromptsMenu.js, BeadsView.js(+test),
ConversationPropertiesPanel.js, PeriodicFrequencyPanel.js, SessionItem.js,
SessionList.js, SessionPanel.js, SettingsDialog.js, useConversationMenu.js —
only the plumbing (imports, function calls, JSON field names, endpoint calls)
was fixed to keep the app functional; UI text/component/prop naming
(onPeriodicPrompt, PeriodicFrequencyPanel, "Make periodic" labels, etc.) is
intentionally deferred to bead .8.

Verification: npm test (17 suites, 1445 tests) all pass; node --check on all
changed files; eslint reports 0 errors (pre-existing warnings only).

* mitto-8ir.7: rename lib.js periodic exports to loop

PERIODIC_STOPPED_LABELS -> LOOP_STOPPED_LABELS
formatPeriodicMaxDuration -> formatLoopMaxDuration

Update lib.test.js imports, describe blocks, and all assertions to match.

Refs: mitto-8ir.7

* mitto-8ir.7: rename useWebSocket periodicStarted state export to loopStarted

Rename hook state var, setter, clearer and the returned field:
  periodicStarted     -> loopStarted
  setPeriodicStarted  -> setLoopStarted
  clearPeriodicStarted -> clearLoopStarted

Also update the loop_started WS handler's console.log/comment to say
'Loop' instead of 'Periodic'. Handler branch name (loop_started) was
already renamed in an earlier commit.

Refs: mitto-8ir.7

* mitto-8ir.7: app.js — fix broken /periodic REST paths + follow lib/hook renames

Two fixes:

1. Fix broken hardcoded REST paths. The backend was renamed to /loop in
   an earlier commit but two call sites in app.js still used the raw
   apiUrl(`/api/sessions/${id}/periodic`) — 404 against the new backend.
   Replace both with endpoints.sessions.loop(sessionId), matching every
   other loop caller in the codebase.

2. Update consumers to match renamed lib.js and useWebSocket.js exports:
     PERIODIC_STOPPED_LABELS   -> LOOP_STOPPED_LABELS
     formatPeriodicMaxDuration -> formatLoopMaxDuration
     periodicStarted           -> loopStarted
     clearPeriodicStarted      -> clearLoopStarted
   Toast/handler/UI-text strings ("Make periodic", "Periodic
   Conversation", handleMakePeriodic/NonPeriodic, etc.) are deliberately
   left in place — those belong to mitto-8ir.8 (UI text + components).

Verified: node --check on all modified files; npm test (17 suites,
1445 tests) all pass.

Refs: mitto-8ir.7

* mitto-8ir.9: rename builtin prompt periodic->loop (frontmatter loop:, .Session.IsLoop*, loop_* params) + config tests

* mitto-8ir.8 inc1: rename Periodic* component files/symbols, Periodic*Icon + icon registry key, promptsPeriodic menu token (+coupled YAML icon:/menus:)

* mitto-8ir.8 inc2: rename shared periodic->loop hook/component identifiers (props, callbacks, state)

* mitto-8ir.8 inc3: rename all remaining periodic->loop in web/static (identifiers, UI text, CSS, data-testid, localStorage keys)

* mitto-8ir.8: preserve historical 'periodic' literal in migrateLegacyTabStorage (legacy localStorage key ref, not the loop feature)

* fix(mitto-8ir): repair 7 builtin processor enabledWhen gates broken by CEL rename (Session.IsPeriodic->Session.IsLoop)

* mitto-8ir.10: rename periodic->loop in docs, README, CLAUDE.md, AGENTS.md, .augment/rules

* mitto-8ir.11: rename periodic->loop in test client + backend stragglers

- internal/client/client.go: PeriodicFrequency/SetPeriodicRequest/PeriodicConfig
  -> LoopFrequency/SetLoopRequest/LoopConfig; SetPeriodic/GetPeriodic/RunPeriodicNow
  -> SetLoop/GetLoop/RunLoopNow; /api/sessions/{id}/periodic[/run-now] -> /loop[/run-now]
  (matches backend session_loop.go contract; test client used by integration tests).
- processors/types.go: Origin doc 'periodic-runner' -> 'loop-runner' (runtime emits loop-runner).
- processors/processors_test.go: origin-filter test 'periodic-runner' -> 'loop-runner'.
- conversation/interfaces.go: PromptResolver doc PeriodicRunner -> LoopRunner.
- session/loop.go: StoppedReasonResumeFailures doc MaxPeriodicResumeFailures -> MaxLoopResumeFailures.

* mitto-8ir.11: reword 'periodically' prose in builtin prompts/processors

Adverb rewording (NOT substitution) to eliminate the last 'periodic' prose in
config/ while keeping natural English:
- Loop-scheduling prose -> 'as a loop' / 'Regularly' (jira/github sync, babysit,
  slack-review, architectural-analysis; all reference mitto_conversation_set_loop).
- Processor re-fire comments -> 're-runs repeatedly' (check-mcp-tools, beads-*).
grep -ri periodic config/ = zero; internal/config tests green.

* mitto-8ir.11: rename periodic->loop in Playwright E2E specs

* mitto-8ir.11: rename periodic->loop in Go integration tests

* feat(conversation): per-child initial model profile for auto-children

Add an optional ModelProfile to auto-child workspace config so each
auto-created child can start on a specific global Model profile instead
of the ACP server's default model selection.

Plumb a per-session ModelConstraintOverride through SessionManager
(new ResumeSessionWithModelConstraint / resumeSessionWithConstraint) and
BackgroundSession via applyModelConstraintOverride, and expose a model
profile dropdown in the auto-children settings UI.

Refs mitto-9x8.

* fix(web): preserve workspace selection on Workspaces dialog reopen

loadData() reset the selected workspace to valid[0] on every reload/
reopen. valid[0] reflects the backend's non-deterministic map-iteration
order, not the sorted tree, so reopening the dialog could land on a
different workspace than the one just edited. With many workspaces
sharing the same legacy auxiliary-model setting, this made a just-saved
Auxiliary Model change look reverted.

Preserve the previously-selected workspace across reload/reopen when it
still exists, and otherwise fall back to a deterministic first entry
(sorted by display name, then ACP server).

* fix(conversation): deliver queued human free-text verbatim on template render error (mitto-nvb)

Queued human free-text messages were run through the Go prompt-template renderer and, on render failure, dropped instead of delivered. The 'queue' senderID was overloaded, covering both human-typed messages that got queued (agent busy) and cross-session MCP dispatches, so human input was misclassified as automated and fail-closed.

Fix distinguishes the enqueuer's origin: add QueuedMessage.Origin (QueueOriginUser default / QueueOriginAgent) via new AddWithOrigin (Add delegates, no double-lock). The 4 cross-session MCP enqueue sites mark messages agent-origin. Origin flows through PromptMeta.QueueOrigin to resolveAndSubstitute, where fail-closed now requires named-prompt OR loop-runner OR (queue AND agent-origin); user/empty-origin queue free-text fails open (delivered verbatim). Removed now-unused isAutomatedDispatch. mitto-e7u fail-closed guarantee preserved for agent/loop/named dispatches.

* chore(config): apply gofmt formatting

Whitespace/formatting-only changes surfaced by 'make fmt'; no syntactic changes.

* chore(prompts): rename builtin iterate prompts to loop

Rename the 5 beads-issue-iterate-* and 3 iterate-* builtin prompts to their loop-* equivalents, continuing the periodic-to-loop terminology unification. Update docs/config/prompts.md references.

* feat(prompts): fall back to the jira CLI when JIRA MCP tools are unavailable

Widen the enabledWhen gate on the six JIRA builtin prompts to also accept CommandExists(jira), and add per-prompt command tables mapping each jira_* operation to its ankitpokhrel/jira-cli equivalent, so the prompts work when only the CLI is present.

* fix(web): flush pending workspace edits when switching selection in Workspaces dialog

Track the previously selected workspace and commit its transient edit fields into the workspaces array before repopulating for the new selection, so edits to multiple workspaces in a single dialog session are no longer lost. Reset the tracker on data reload.

* feat(web): rich workspace and model-profile dropdowns for auto-create children

Add a generic RichSelect controlled dropdown (details-based, supports icon and badge rows) and adopt it in the Auto-Create Children settings for the target workspace (color badge plus name) and model profile selectors, replacing the plain select elements.

* style(web): reformat styles.css with prettier

Whitespace-only prettier reformatting (multiline font-family, transition and box-shadow declarations, brace expansion, blank-line normalization). No rule changes.

* fix(web): soften global focus indicator with an accent glow

Replace the harsh detached daisyUI outline on form controls and focus-visible targets with a low-opacity mitto-accent halo via box-shadow, keeping a transparent outline for forced-colors mode. Exclude the chat composition textarea, which owns its own focus treatment.

* feat(web): extract shared Toolbar and promote conversation actions to buttons

Introduce a reusable Toolbar component and use it across the conversation header, BeadsView, and SessionList. Fixed conversation actions (Copy, Flush, loop toggle, Archive, Delete) are promoted from the overflow menu into top-level buttons, while hierarchical prompt groups remain behind a single dropdown. Add a linked-issue status badge to the header toolbar and improve ContextMenu flyout placement on narrow viewports.

* test(ui): update Playwright specs for toolbar refactor

Adapt the UI specs and shared selectors to the new Toolbar structure and promoted conversation action buttons.

* refactor(prompts): rename beads discuss to assess and add new discuss prompt

Repurpose the former beads-issue-discuss prompt into beads-issue-assess (Assess issue readiness) and add a new beads-issue-discuss (Discuss a topic) prompt. Update the guard test, developer/config docs, and the prompts agent rule to reference the renamed exemplar.

* docs(rules): refresh component-extraction and testing agent rules

Add a post-extraction cleanup note to the component-extraction rule and condense/refresh the testing agent rule (integration setup, JS tests, smoke tests, timeout anti-pattern).

* feat(session): runtime-configurable agent inactivity watchdog timeout (mitto-54y)

Make the prompt inactivity watchdog cancellation timeout configurable at
runtime instead of a compile-time constant, so a wedged shared ACP process no
longer pins a session as stuck forever.

- Add SessionConfig.AgentInactivityTimeout ("" default 10m, "disabled",
  "5m"/"10m"/"15m"/"30m") with ParseAgentInactivityTimeout() and validation.
- Back promptInactivityWatchdogTimeout with an atomic.Int64 plus
  SetPromptInactivityTimeout()/promptInactivityWatchdogTimeout() so the value
  can be updated concurrently with running watchdog goroutines.
- Wire the timeout from settings at server startup and apply live settings
  changes at runtime via the config handler (no restart needed).
- Update tests to use SetPromptInactivityTimeout; add
  TestParseAgentInactivityTimeout; refresh acp.md docs.

* fix(beads): align detail-panel toolbar and fix mobile layout

Refactor the issue detail panel to use the portable Toolbar component and
fix mobile/layout regressions reported after the toolbar rollout.

- Detail-panel actions now render via the reusable Toolbar (block variant)
  on their own row below the title, left-aligned and vertically aligned
  with the list and sidebar toolbars. The gap uses mt-6 so the toolbar
  baseline matches the list toolbar (~64px from the header top).
- Lift the 85vw drawer cap on mobile so the panel fills the viewport
  (dock-maxw:100% when isMobile), matching fullscreen behavior.
- Wrap the description formatting toolbar (flex-wrap) to prevent overflow
  on narrow viewports.
- Update Playwright selectors for the new toolbar (beads-issue-toolbar).
- Sync docs (CLAUDE.md, 25-web-frontend-components, 30-testing) with the
  reusable Toolbar usage (detail-panel header uses variant="block").

* feat(prompts): add onTasks loop trigger with CEL condition gating

Introduce a third prompt-loop trigger, onTasks, that fires when beads/tasks
in the workspace change, optionally gated by a CEL condition.

- prompts.go: add loop.condition field and validate it at parse time via
  ValidateCondition (fail-fast, mirrors the runtime ConditionValidator seam).
  Extend the trigger doc/comments to cover onTasks.
- Frontend seeding (useConversationSeeding.js): thread the condition from the
  prompt's loop frontmatter default and include it in the seed payload only
  when trigger === "onTasks".
- ChatInput.js: pre-populate the local condition state from the prompt's
  onTasks frontmatter default so the loop panel reflects it immediately.
- Docs (prompts.md): document the onTasks trigger and condition field.
- Tests: prompts_test.go and useConversationSeeding.test.js.

* feat(workspace): select auxiliary model by capability tag

Add an AuxiliaryModelTag axis so a workspace can pick its auxiliary-session
model by capability tag (e.g. "Fast") instead of a specific profile name.

- workspaces.go: add auxiliary_model_tag field. Precedence is
  auxiliary_model_profile > auxiliary_model_tag > auxiliary_model_selection.
- acp_process_manager.go: add ModelProfilesByTagResolver and resolve the tag
  to the first Model profile carrying it whose Criteria matches an available
  model; fall back to AuxiliaryModelSelection when nothing resolves.
- server.go: wire ModelProfilesByTagResolver to Config.ModelProfilesByTag.
- ModelTagSelect.js: new dropdown for choosing a capability tag (dedup/sorted
  union of tags across profiles), used alongside ModelProfileSelect.
- WorkspacesDialog.js: integrate ModelTagSelect as a mutually-exclusive way
  to pick the auxiliary model.
- Docs (workspace.md): document auxiliary_model_tag and precedence.

* feat(ui): per-folder prompt shortcut buttons in conversation and issue toolbars

Surface per-folder configurable prompt shortcuts as toolbar buttons, mirrored
across the conversation toolbar and the beads issue detail toolbar.

- app.js: load the folder's `conversations` shortcut section, resolve each
  entry's linked prompt, and render shortcut buttons in the conversation
  toolbar. A missing/renamed prompt renders disabled rather than erroring.
  Refreshes live on the mitto:folder_shortcuts_updated event.
- BeadsView.js: same pattern for the `beadsIssue` section, rendering shortcut
  buttons in the issue detail header toolbar keyed to the open issue.

* feat(prompts): add beads-refine-implementation loop builtin (mitto-mx4)

Add a tracked builtin prompt that refines open/unrefined beads into
implementation-ready specs on a smart model, then labels or defers them.

- New config/prompts/builtin/beads-refine-implementation.prompt.yaml with a
  frontmatter loop block (mode: always, trigger: onTasks, CEL condition gating
  on open beads lacking the implementation-refined label; maxIterations: 20,
  maxDuration: 4h). Enabled by mitto-pei landing loop.condition support.
- Uses the canonical silent-vs-interactive template vars
  (.Iteration.IsUninterrupted / .Session.IsLoop[Forced]).
- Add regression test TestRefineImplementation_LoopAndModes covering
  parse-time CEL validation of the condition, onTasks/always loop assertions,
  and the silent/interactive render branches.

Closes mitto-mx4.

* feat(beads): configure prompt arguments for Pull/Push/Sync upstream actions (mitto-22s)

Add a sliders/args button next to each Pull/Push/Sync prompt dropdown in
Workspaces -> Beads -> Prompt Actions. The button opens the shared
PromptParameterDialog pre-seeded with saved values, persists collected
arguments per action, and forwards them when the upstream action dispatches
its prompt. Parametrized prompts are now selectable (previously filtered out).

Backend: persist pull/push/sync prompt arg maps (camelCase in folders.json,
snake_case *_prompt_args at the API); GET returns saved args; relaxed the
validation that rejected prompts declaring parameters.

Frontend: SlidersIcon button per row (disabled unless the selected prompt has
params), dialog wiring threaded through app.js, and saved args forwarded via
handleBeadsLaunchPrompt(action, name, args).

* docs(rules): expand debugging log-pattern guidance

Add logfmt field-order caveat and broaden processor log-pattern grep
examples in the debugging rules.

* fix(web): relax Split-IP CSRF fingerprint for mobile network drift (mitto-tz4)

The Split-IP login WARN fingerprinted the exact client IP, so mobile
carrier NAT / Wi-Fi<->cellular handoff tripped it for the user's own
device. The WARN is log-only (never blocks); CSRF double-submit and
session cookies are not IP-bound, so the correlated 401 burst was
benign pre-auth GETs, not caused by this check.

- Coarsen the fingerprint to a network prefix (/24 IPv4, /64 IPv6) so
  benign drift within one network no longer trips it.
- Fold the User-Agent into the fingerprint so a genuinely suspicious
  mismatch (different UA) still fires the WARN from the same network.
- Dedup the WARN per (network-prefix, UA) key (<=1 per 10m, pruned on
  the existing cleanup tick) so real anomalies stand out.

Double-submit cookie logic unchanged; graceful degradation for
old-format tokens preserved.

* fix(prompts): gate mitto_conversation_* prompts on deterministic Permissions.* (mitto-1ae.1)

Replace the flaky Tools.HasPattern("mitto_conversation_*") existence check
with the deterministic Permissions.* flag matching each prompt's actual
tool usage, verified against each prompt body and the MCP server's
permission checks (internal/mcpserver/server.go):

- Permissions.CanStartConversation: creates children via mitto_conversation_new
  (beads-issue-loop-fixing-bugs, beads-issue-loop-implementing-features,
  beads-issue-loop-until-complete, beads-issue-work, beads-work,
  child-continue-new, child-create-minions, github-iterate-babysit-new-prs,
  github-post-merge-cleanup, jira-work)
- Permissions.CanSendPrompt: only sends via mitto_conversation_send_prompt /
  mitto_children_tasks_wait, never creates (beads-issue-loop-fixing-bug,
  beads-issue-loop-implementing-feature — self-dispatch to next phase;
  child-continue; report-to-parent)
- Term dropped entirely (no Permissions.* substitute): beads-triage-bugs,
  beads-publish-post, loop-until, child-cleanup — their bodies only call
  mitto_conversation_update/get/delete, which require no session permission
  flag at all, so no capability-based gate applies.

* feat(config): add global shortcut buttons with backend API and defaults

Add per-section global shortcut buttons stored in settings.json and served via GET/PUT /api/global/shortcuts, mirroring the folder shortcuts endpoint. Seed safe defaults for new installs (conversations/beadsIssue/tasksList) in the embedded default config, and expose the Commit changes prompt in the conversation menu. buildNewSettings preserves shortcuts on unrelated saves. Includes round-trip and embedded-default unit tests.

* feat(ui): add ShortcutsEditor for global and folder shortcut buttons

Introduce a shared ShortcutsEditor panel reused for both global (Settings dialog) and folder (Workspaces dialog) shortcut configuration. Toolbars in app.js and BeadsView merge global + folder shortcuts at render time (global first, folder duplicates dropped/greyed via redundantPromptNames) and refresh on mitto:global_shortcuts_updated / mitto:folder_shortcuts_updated. IconPicker reworked with grouped icons for reuse; endpoints registry gains global.shortcuts; useWorkspacePrompts filters by any requested menu.

* test(ui): add Playwright spec for adding workspace shortcuts

Cover the folder shortcut add flow end-to-end in the Workspaces dialog.

* docs: document global shortcuts feature and refresh agent rules

Describe the global + folder shortcuts architecture, backend API, render-time merge/dedupe, and the shared ShortcutsEditor component in CLAUDE.md and the config/frontend-components agent rules.

* fix(ui): align ShortcutsEditor select rows in settings dialog

Restore the daisyUI select-sm height (2rem) for selects grouped in a join within the settings dialog, so ShortcutsEditor rows align with their btn-sm join buttons instead of stretching to the 38px settings-dialog select height.

* docs(mcp): ADR for deterministic MCP tool discovery (mitto-1ae.2)

* feat(prompts): add parallel child fan-out to loop & beads iteration prompts

Loop and beads iteration drivers now decompose an increment into genuinely independent, disjoint-file subtasks and fan them out across child conversations (guarded by Can start conversation + Can Send Prompt), synthesizing reports before proceeding. Falls back to inline work when interdependencies exist or flags are off.

* docs(mcp): expand tool-discovery ADR for dynamic/late-starting servers (Q4)

Adds Q4 covering MCP servers that come online after the agent starts or change tools mid-session: per-server availability state, event-driven refresh via notifications/tools/list_changed, and bounded backoff so late tools surface without reintroducing flicker.

* fix(ui): snap tooltips to whole pixels to fix blurry text

PortalTooltip and daisyUI tooltip centering could land on a half-pixel, causing WebKit sub-pixel anti-aliasing (blurry text). Round positions/translate to whole pixels; CSS guarded by @supports round() with graceful fallback.

* fix(ui): use dedicated loop/unloop icons for loop toggle buttons

Add LoopOffIcon (circular arrow with slash) and refine LoopIcon; wire the header Loop button to LoopIcon and the Unloop button to LoopOffIcon (previously ClockIcon/MittoIcon) for clearer affordance.

* refactor(ui): show prompt text in shortcut button tooltips

Shortcut button tooltips now display the raw prompt text instead of the 'Run "..."' wrapper, matching the concise tooltip style; aria-label retains the descriptive wording.

* feat(beads): add label add/remove and workspace label listing

Add a bd label client (add/remove) and ListAllLabels, expose them via
POST /api/issues/{id}/labels and GET /api/issues/labels, wire endpoints,
and add label add/remove UI with workspace-wide suggestions in BeadsView.
Update stub/fake beads clients to satisfy the extended interface.

* feat(ui): pulse in-progress issue status badge in beads list

Add beadsInProgressPulse keyframes so the in_progress status badge flashes
brighter with an expanding glow and slight scale, making active work stand
out in the beads list. Disabled under prefers-reduced-motion.

* feat(prompts): add Support Slack triage workflow prompts

Add a self-contained Support prompt suite (watch-channel, gather-info,
check-status, continue-conversation, reply-to-user) that triages Slack
customer questions into beads issues and keeps them in sync.

* feat(prompts): list "Loop until ..." in the prompts menu

Add the prompts menu to loop-until so it is discoverable from the prompts list in addition to the conversation menu.

* feat(config): per-server MCP availability state model (mitto-sys.1)

Replace the single global ToolsContext.Available bool latch with a
per-server availability state model, so late-starting/unreachable MCP
servers stay fail-open while reachable ones are authoritative
(fail-closed). See docs/devel/mcp-tool-discovery.md, Q3.2/Q4.1.

- Add ServerToolState enum (Unknown/Reachable/Unreachable) and
  ServerToolInfo{State, Names} in internal/config/cel_context.go.
  ToolsContext now carries Servers map[string]ServerToolInfo; Names/
  Available are kept as flattened/derived legacy convenience fields.
- Add NewReachableToolsContext (groups a flat tool-name list by owning
  server, e.g. jira_* -> "jira", marking each Reachable) and
  NewProcessorToolsContext (catch-all AllServersToolKey entry so
  processors keep their pre-existing fail-closed, no-warm-up-grace
  behavior without real per-server identity).
- internal/config/templatefuncs.go: hasPattern/hasAllPatterns/
  hasAnyPattern now resolve a pattern's owning server (prefix before
  first underscore) against the per-server map; fail open unless that
  server is Reachable, in which case matching is name-based against
  that server's own tool names. Cold-start (empty Servers map) and an
  unknown/never-discovered server both fail open.
- internal/config/cel_evaluator.go: mirrors the same resolution logic
  for the CEL tools.hasPattern/hasAllPatterns/hasAnyPattern path via
  new Tools.ServerStates/Tools.ServerNames activation variables,
  reconstructing map[string]ServerToolInfo and delegating to the same
  Go functions (single source of truth).
- internal/web/session_api.go: build per-server state from the cached
  MCP tool names (grouped by prefix, marked Reachable) instead of a
  flat Available=true latch, in both buildSessionPromptEnabledContext
  and applyWorkspaceNamespace.
- internal/processors/hook.go: use NewProcessorToolsContext instead of
  force-setting Available=true, preserving processor semantics
  (always fail-closed, no warm-up grace).
- Updated internal/config/cel_evaluator_test.go,
  internal/config/templatefuncs_test.go, and
  internal/web/session_api_test.go for the new shape, added
  TestHasPattern_PerServerStates covering all three states, prefix->
  server mapping, and cold-start fail-open (Go/CEL parity).

Gate: go build ./..., go vet ./..., golangci-lint (0 issues) on
internal/config, internal/processors, internal/web; go test ./...
all green (internal/conversation, internal/mcpserver, etc.
unaffected). make build-mock-acp && make test-integration: the
inprocess integration package fails to build on this branch's HEAD
already (undefined: TestServer in sleep_wake_helpers.go/
event_injector.go), confirmed pre-existing via git stash — unrelated
to this change.

* feat(mcpdiscovery): direct stdio MCP tools/list discovery component (mitto-sys.2)

Add a standalone, well-tested primitive for deterministic MCP tool
discovery over stdio, per docs/devel/mcp-tool-discovery.md (Q1). Not
wired into the live session context / auxiliary cache yet — that is a
separate follow-up increment (intertwines with the last-known-good
cache policy in mitto-sys.6).

New package internal/mcpdiscovery (no internal/config or internal/web
imports):
- ServerToolsResult{Server, Tools, Reachable, Err}: a failed
  connect/list/timeout (Err != nil, Reachable=false) is kept
  distinguishable from a genuine reachable-but-empty tool list
  (Reachable=true, Tools=nil/empty) so callers never cache an
  unreachable probe as a negative.
- TransportFactory seam + CommandTransportFactory (production stdio
  transport via exec.CommandContext + mcp.CommandTransport, env =
  os.Environ() + srv.Env, never logged).
- DiscoverStdioServer(ctx, srv, timeout, factory): bounded per-server
  timeout (DefaultTimeout=8s when timeout<=0), Connect + ListTools +
  Close, never panics.
- DiscoverStdioServers(ctx, servers, timeout, factory): iterates only
  stdio servers (Command != \\, skips URL-only/http-sse ones),
  sequential with per-server isolation (one failure/timeout never
  aborts the others).

Tests (discovery_test.go) use mcp.NewInMemoryTransports() + a mock
mcp.Server (no real subprocess, deterministic): reachable-with-tools,
reachable-but-empty, transport-build-error, real-CommandTransportFactory
connect-failure (nonexistent binary), and a mixed
DiscoverStdioServers case proving per-server isolation and that
URL-only servers are skipped.

Gate: go vet/build clean; golangci-lint 0 issues on internal/mcpdiscovery;
go test (incl. -race, -count=5) all green; no go.mod/go.sum changes
needed (SDK already vendored). make fmt run before and right before
commit, no reformatting needed for these files.

* feat(beads): toggleable + button reveals inline add-label input

Replace the always-visible add-label input in the issue detail panel with
a compact + button. Clicking it reveals an inline input (with datalist
autocomplete) that auto-focuses; Enter/confirm adds, Escape or empty blur
collapses back to the +. Also integrate the remove (X) into each label
chip's badge with hover feedback and regenerate tailwind.css.

* feat(mcpdiscovery): wire stdio discovery to agents.Manager.ListMCPServers (mitto-sys.2)

Add DiscoverWorkspaceStdioTools: resolves a workspace's configured MCP
servers via a ServerLister (agents.Manager.ListMCPServers with
{Path: workspacePath}), then probes the stdio ones with
DiscoverStdioServers. http/sse servers are skipped (mitto-sys.3);
per-server probe failures are reported in-band, list failures as a
top-level error. Compile-time assertion binds *agents.Manager to
ServerLister. Adds 3 tests (list-then-probe, list error, nil lister).

* feat(auxiliary): last-known-good MCP tools cache policy (two-consecutive-negatives) (mitto-sys.6)

Replace the old "only cache non-empty results" behavior in
FetchMCPTools with a last-known-good merge policy, per
docs/devel/mcp-tool-discovery.md (Q3.1): a previously-observed tool is
never downgraded to absent on a single negative fetch; removal
requires two consecutive independent negative fetches. A tool that
reappears between negatives resets its counter.

- WorkspaceAuxiliaryManager: add mcpToolsNegatives map[string]map[string]int
  (workspaceUUID -> toolName -> consecutive-negative count), guarded by
  the existing mcpToolsCacheMu (no new mutex). Initialized in
  NewWorkspaceAuxiliaryManager.
- New unit-testable method applyMCPToolsCachePolicy(workspaceUUID,
  fresh) []MCPToolInfo: merges a successful fetch's fresh tool list
  into the cache — tools present in fresh have their negative counter
  reset and description refreshed; tools absent from fresh have their
  counter incremented and are dropped only once it reaches 2; new
  fresh tools are added. Result is sorted alphabetically and stored.
  When there is no prior cache entry and fresh is empty, no entry is
  created (empty first fetch establishes nothing).
- FetchMCPTools now calls applyMCPToolsCachePolicy after a successful
  parse and returns the merged result instead of the raw fresh fetch;
  the early cache-hit short-circuit is unchanged.

Tests (workspace_manager_test.go): first non-empty fetch stores; first
empty fetch establishes nothing; one negative retains; two consecutive
negatives remove; reappearance resets the counter (and a subsequent
single negative does not re-remove); new tool added; description
refreshed when present in fresh; per-workspace isolation.

Gate: gofmt -l internal/auxiliary/ clean; go vet ./internal/auxiliary/...
clean; golangci-lint run ./internal/auxiliary/... = 0 issues; go test
-race -count=3 ./internal/auxiliary/... all green; go build ./...
clean. make fmt run before and right before commit.

* feat(auxiliary): consume deterministic stdio MCP discovery in FetchMCPTools, LLM fallback only for unreached servers (mitto-sys.6, mitto-sys.2)

Wire mcpdiscovery.DiscoverWorkspaceStdioTools into the live
FetchMCPTools call site, so deterministic tools/list results are used
whenever every configured stdio server is reachable, and the LLM
introspection fallback only runs to supplement tools from servers that
couldn't be reached (or when discovery itself errors/is unset).

internal/auxiliary/workspace_manager.go:
- Add exported StdioToolsDiscoverer func field to WorkspaceAuxiliaryManager
  (nil by default; injected by the web layer, matching the existing
  ACPProcessManager.WorkspaceConfigProvider injection style).
- Restructure FetchMCPTools (after the unchanged cache-hit short-circuit):
  call StdioToolsDiscoverer when set; union each Reachable server's tool
  names into a deterministic set (deduped); need the LLM path only when
  discovery errored, is unset, or any server was Reachable=false, or zero
  servers were probed. Run the LLM path (extracted verbatim into the new
  fetchMCPToolsViaLLM helper) only when needed; on LLM failure, propagate
  the error only if there were zero deterministic tools, otherwise log and
  proceed with deterministic-only. Union LLM tools into the merged result,
  deduped by name with deterministic entries winning. All results still
  flow through applyMCPToolsCachePolicy (last-known-good preserved). Net
  effect: nil seam / no discoverer set ⇒ behavior identical to before.

internal/web/server.go:
- After sessionMgr.SetAuxiliaryManager, resolve appdir.AgentsDir() and
  wire auxiliaryManager.StdioToolsDiscoverer to a closure that maps a
  workspace UUID -> ACP agent (mirroring the resolution already used in
  internal/web/handlers/workspace_mcp.go) and calls
  mcpdiscovery.DiscoverWorkspaceStdioTools. Logs a warning and leaves the
  discoverer unset if the agents dir can't be resolved.

Tests (internal/auxiliary/workspace_manager_test.go): nil discoverer
(pure LLM, existing behavior); all-reachable skips the LLM entirely
(provider call would t.Fatal); one unreachable server unions with LLM
tools; discoverer error falls back to LLM; all-reachable with zero
tools makes no cache entry and never calls the LLM; a tool name present
in both deterministic and LLM output appears exactly once, with the
deterministic entry winning.

Gate: gofmt -l internal/auxiliary/ internal/web/ clean; go vet on both
clean; golangci-lint run on both = 0 issues; go test -race -count=3
./internal/auxiliary/... all green; go build ./... clean; go test
./internal/web/... (incl. handlers/middleware) all green. make fmt run
before and right before commit, no reformatting needed; no go.mod/go.sum
changes (mcpdiscovery/agents already vendored/present).

* feat(mcpdiscovery): http/sse tools/list discovery via Streamable/SSE transports, live in FetchMCPTools (mitto-sys.3)

Extend deterministic MCP tools/list discovery from stdio-only to all
transports (stdio + http/sse), reusing the existing probe/connect/list
plumbing, and wire it into the live FetchMCPTools call path.

internal/mcpdiscovery/discovery.go:
- Extract the body of DiscoverStdioServer into an unexported probeServer
  (ctx, srv, timeout, factory) helper; DiscoverStdioServer is now a thin
  wrapper defaulting factory to CommandTransportFactory. Behavior
  unchanged.
- Add NetworkTransportFactory: the production TransportFactory for
  http/sse servers. Builds *mcp.SSEClientTransport when srv.URL's path
  ends in /sse (case-insensitive, parsed via net/url with a raw
  strings.HasSuffix fallback), else *mcp.StreamableClientTransport.
  Errors on empty URL. srv.Env/auth headers are not applied (no Headers
  field yet, mitto-sys.9); auth-required endpoints surface as
  Reachable=false via Connect/ListTools failure.
- Add DefaultTransportFactory: dispatches Command!="" to
  CommandTransportFactory, URL!="" to NetworkTransportFactory, errors
  when neither is set.
- Add DiscoverNetworkServer (thin wrapper over probeServer, factory
  defaults to NetworkTransportFactory).
- Add DiscoverNetworkServers: probes URL!=""&&Command=="" servers only,
  sequential, per-server isolated, input order, default factory
  NetworkTransportFactory.
- Add DiscoverServers: probes every server except those with neither
  Command nor URL, sequential, per-server isolated, input order, default
  factory DefaultTransportFactory (mixed-transport aware).
- Add DiscoverWorkspaceTools: identical to DiscoverWorkspaceStdioTools
  except it calls DiscoverServers instead of DiscoverStdioServers, so a
  workspace's http/sse servers are probed too.
- DiscoverStdioServers and DiscoverWorkspaceStdioTools are unchanged
  (still stdio-only, still skip http/sse servers) — existing callers/tests
  unaffected.

internal/web/server.go:
- Swap the live StdioToolsDiscoverer closure's call from
  mcpdiscovery.DiscoverWorkspaceStdioTools to
  mcpdiscovery.DiscoverWorkspaceTools (same args, nil factory ->
  DefaultTransportFactory), making http/sse discovery live in
  FetchMCPTools. Updated the adjacent comment (stdio -> stdio + http/sse).

internal/mcpdiscovery/discovery_test.go:
- NetworkTransportFactory: SSE selection by /sse suffix (asserts
  *mcp.SSEClientTransport + Endpoint), Streamable default (asserts
  *mcp.StreamableClientTransport + Endpoint), empty URL error.
- DefaultTransportFactory dispatch: stdio/network/neither subtests.
- DiscoverNetworkServer reachable-with-tools via an injected in-memory
  factory.
- Real Streamable HTTP integration test against
  httptest.NewServer(mcp.NewStreamableHTTPHandler(...)).
- Real SSE integration test against
  httptest.NewServer(mux with mcp.NewSSEHandler mounted at /sse), probing
  ts.URL+"/sse" — a genuine SSE round-trip, not just a factory-selection
  unit test (see the test's doc comment for why mounting at /sse works
  with SSEHandler's per-request endpoint computation).
- Auth-required graceful failure: httptest server returning 401 for all
  requests -> Reachable=false, Err set, no panic, empty Tools.
- DiscoverServers mixed-transport test: one stdio, one network, one
  neither (skipped), via a branching injected factory -> 2 reachable
  results.
- DiscoverWorkspaceTools test: stubLister returns a stdio + a network
  server, both are probed (contrast with the existing
  DiscoverWorkspaceStdioTools test, which still skips the network one).

Gate: gofmt -l internal/mcpdiscovery/ internal/web/server.go clean; go
vet on both clean; golangci-lint run on both = 0 issues; go test -race
-count=3 ./internal/mcpdiscovery/... all green (incl. real Streamable +
SSE integration tests); go build ./... clean; go test ./internal/web/...
(incl. handlers/middleware) all green. make fmt run before commit, no
reformatting needed; no go.mod/go.sum changes (go-sdk's SSE/Streamable
client+server types were already vendored).

* feat(auxiliary): harden LLM MCP tool fallback - strict object schema + single retry + plausibility (mitto-sys.7)

Harden fetchMCPToolsViaLLM (the LLM-introspection fallback used when
deterministic stdio/http/sse discovery is unavailable, errors, or reports
an unreachable server), per docs/devel/mcp-tool-discovery.md Q2.

internal/auxiliary/utils.go:
- parseMCPToolsList is now strict: trims, strips markdown fences, trims
  again, then parses the WHOLE remaining string as a single JSON object.
  Removed the biggest-substring {...} extraction, the bare-array
  fallback, and the array-substring fallback. Validates via
  map[string]json.RawMessage that at least one of "tools"/"error" is
  present before unmarshaling into mcpToolsResponse. {"tools":[]} is a
  valid, legitimate zero-tools answer (not an error); {} (neither key) is
  a parse error. Updated the doc comment to describe the strict
  object-only contract.

internal/auxiliary/prompts.go:
- Added mcpToolsRetryReminder: a plain (non-embedded) Go string constant
  appended to FetchMCPToolsPromptTemplate on retry attempts, restating
  the required strict JSON response shape.

internal/auxiliary/workspace_manager.go:
- fetchMCPToolsViaLLM signature gained a configuredServerCount int
  parameter and now retries up to maxMCPToolsAttempts=2 times: on a
  provider error (aborting immediately without retry if ctx.Err() != nil
  and returning that error instead), on a strict-parse failure, or on an
  implausible empty result (zero tools while configuredServerCount > 0,
  only up to attempt < maxMCPToolsAttempts). An explicit agent-reported
  error is always definitive and never retried (zero tools -> error;
  some tools -> returned immediately). Retries append
  mcpToolsRetryReminder to the prompt. Existing debug/warn logging
  preserved, now tagged with the attempt number.
- FetchMCPTools: declares configuredServerCount := -1 before the
  discoverer check, sets it to len(results) on successful discovery (so
  the discoverer's own view of "how many servers were configured" feeds
  the plausibility check), and passes it through to
  fetchMCPToolsViaLLM. -1 (nil/errored discoverer) disables the
  plausibility check entirely, so a genuine zero-tools LLM answer is
  still accepted outright in that path (CLI/tests default). No change to
  the deterministic-tools branch, applyMCPToolsCachePolicy, or the
  cache-hit short-circuit.

Tests:
- utils_test.go: flipped "bare JSON array fallback" and "fenced bare
  JSON array" to wantErr:true (relied on removed leniency); added valid
  {"tools":[]} (0 tools, no error), bare {} (error), and a
  prose-with-embedded-object case (error, proving substring extraction
  is gone). Kept the still-valid object/fenced-object/agent-error/
  invalid-JSON cases (the trailing-commentary-braces case still passes
  unchanged since stripMarkdownFences already isolates the fenced
  content before parseMCPToolsList ever sees it).
- workspace_manager_test.go: added
  TestFetchMCPToolsViaLLM_RetryAndPlausibility with 5 subtests exercising
  fetchMCPToolsViaLLM directly via a call-counting promptFunc closure:
  parse-fail-then-success (2 calls), empty-plausible-then-success with
  configuredServerCount=2 (2 calls), empty accepted with count<=0 (1
  call, no retry), explicit agent error with count=2 (1 call, no
  retry), and both attempts failing (2 calls, error returned).

Verified via grep that parseMCPToolsList and fetchMCPToolsViaLLM have no
other production callers outside these files.

Gate: gofmt -l internal/auxiliary/ clean; go vet clean; golangci-lint
run ./internal/auxiliary/... = 0 issues; go test -race -count=1
./internal/auxiliary/... all green; go build ./... clean. make fmt run
before and right before commit, no reformatting needed.

* feat(mcpdiscovery): bounded exponential-backoff re-probe scheduler for unreachable MCP servers (mitto-sys.5)

Add a self-contained, unit-testable bounded exponential-backoff primitive
for re-probing configured-but-unreachable MCP servers, per
docs/devel/mcp-tool-discovery.md (Q4, point 3). This delivers the
primitive only; live wiring into session_ws.go is a separate follow-up
increment.

internal/mcpdiscovery/backoff.go (new):
- BackoffPolicy{Base, Factor, Max, MaxAttempts} + DefaultBackoffPolicy()
  (Base=1s, Factor=2.0, Max=30s, MaxAttempts=10).
- nextDelay(attempt int) time.Duration: pure, deterministic (no jitter)
  schedule function. attempt is 0-based (attempt 0 = delay before the
  2nd probe = Base; attempt n = Base*Factor^n, capped at Max when
  Max>0). Guards: Base<=0 -> 1s; Factor<1 -> 1.0; Max<=0 -> uncapped
  growth; overflow/non-finite results clamp to Max (or math.MaxInt64
  when uncapped).
- ProbeFunc = func(ctx) ServerToolsResult (reuses the existing
  discovery.go type unmodified).
- RetryUntilReachable(ctx, policy, probe, onReachable) (ServerToolsResult,
  bool): loops probe/backoff-sleep until Reachable=true, ctx is
  cancelled, or MaxAttempts is exhausted. A timeout/connect/list failure
  is NEVER treated as a negative — it only schedules another retry.
  onReachable fires exactly once, only on a genuine reachable result;
  never on exhaustion or cancellation. Checks ctx.…
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.

2 participants