Accumulated fixes and improvements#66
Merged
Merged
Conversation
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.
…uting (mitto-ank.6)
…utes (mitto-ank.6)
…ethod+pattern routes (mitto-ank.6)
…move legacy dispatcher (mitto-ank.6)
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.
…ing (mitto-ank.5)
… envelope + FE parsing (mitto-ank.5)
… fix FE create retry (mitto-ank.5)
…velope (mitto-ank.5)
… consumers (mitto-ank.5)
…nsumers (mitto-ank.5)
…pair FE consumer (mitto-ank.5)
… consumers (mitto-ank.5)
…r FE consumers (mitto-ank.5)
… consumers (mitto-ank.5)
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
…prompt (mitto-gap.1)
…e-machine prompt (mitto-gap.5)
…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.
…mpts (mitto-gap.2)
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
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.…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.