Remaining work after #66: loop rename, MCP tool discovery, shortcuts & more#67
Merged
Conversation
…tings (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.
…*->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).
…L/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).
…(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.
…riptions (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.
…proc 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.
…o-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.
…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).
PERIODIC_STOPPED_LABELS -> LOOP_STOPPED_LABELS formatPeriodicMaxDuration -> formatLoopMaxDuration Update lib.test.js imports, describe blocks, and all assertions to match. Refs: mitto-8ir.7
…Started 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
…ok 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
… .Session.IsLoop*, loop_* params) + config tests
…Icon + icon registry key, promptsPeriodic menu token (+coupled YAML icon:/menus:)
…iers (props, callbacks, state)
…identifiers, UI text, CSS, data-testid, localStorage keys)
…abStorage (legacy localStorage key ref, not the loop feature)
…y CEL rename (Session.IsPeriodic->Session.IsLoop)
…S.md, .augment/rules
- 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.
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.
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.
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).
…e 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.
Whitespace/formatting-only changes surfaced by 'make fmt'; no syntactic changes.
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.
…ailable 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.
…orkspaces 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.
… 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.
…vers (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).
…ive-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.
…PTools, 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).
…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).
…+ 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.
…r 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.Err() defensively at
the top of each loop iteration and returns promptly via
time.NewTimer+select during the backoff wait.
- ScheduleRetries(ctx, results, policy, probeFor, onReachable): thin
fire-and-forget convenience that launches one RetryUntilReachable
goroutine per unreachable result in results (skips reachable ones),
for the future session_ws.go wiring.
internal/mcpdiscovery/backoff_test.go (new):
- TestBackoffPolicy_NextDelay_Schedule: exact schedule assertions
(1s,2s,4s,8s,16s,30s,30s) for Base=1s/Factor=2/Max=30s.
- TestBackoffPolicy_NextDelay_Guards: Base<=0 -> 1s baseline;
Max<=0 -> uncapped exponential growth (32s at attempt 5, uncapped).
- TestRetryUntilReachable_DiscoveredQuickly: probe returns unreachable
for 3 calls then reachable; asserts discovered=true, onReachable
called once with the reachable Tools, exactly 4 probes, and a fast
wall-clock completion (tiny ms-scale Base/Max, no fake clock needed).
- TestRetryUntilReachable_NoNegativeOnTimeout: probe always returns
Reachable=false/Err=context.DeadlineExceeded, MaxAttempts=3; asserts
onReachable is NEVER called, discovered=false, exactly 3 probes, and
the returned result is still Reachable=false (proving a timeout is
surfaced as unreachable/keep-last-known-good, never a reachable-empty
negative).
- TestRetryUntilReachable_ContextCancellation: cancels ctx right after
the first unreachable probe with a 1-hour Base; asserts a prompt
return (well under the Base delay), discovered=false, onReachable not
called.
- TestScheduleRetries_OnlyProbesUnreachable: mixes one reachable and one
unreachable input result; asserts probeFor is never invoked for the
reachable server and onReachable fires exactly once for the server
that becomes reachable after a couple of probes, using a WaitGroup +
timeout guard against goroutine leaks/test hangs.
- All probe-call counters use sync/atomic or a mutex-guarded map (tests
run with -race).
Verified via grep that ServerToolsResult's field names/types
(Server/Tools/Reachable/Err) match discovery.go exactly; this change
does not modify discovery.go, session_ws.go, server.go, or any other
existing file.
Gate: gofmt -l internal/mcpdiscovery/ clean; go vet clean; golangci-lint
run ./internal/mcpdiscovery/... = 0 issues; go test -race -count=3
./internal/mcpdiscovery/... all green (no flakiness across 3 runs,
timing tests use ms-scale Base so the whole suite still completes in
~1.3s); go build ./... clean. make fmt run before and right before
commit, no reformatting needed.
…servers into triggerMCPToolsFetch, additive last-known-good merge (mitto-sys.5) Wire the bounded exponential-backoff scheduler primitive (built in the previous increment: internal/mcpdiscovery/backoff.go) into the live runtime so configured-but-unreachable MCP servers are re-probed until reachable, and newly-discovered tools are broadcast. Closes mitto-sys.5. internal/auxiliary/workspace_manager.go: - Added mcpBackoffActive map[string]bool + mcpBackoffMu sync.Mutex (dedups the per-workspace backoff goroutine, at most one active at a time) and mcpBackoffPolicy mcpdiscovery.BackoffPolicy (defaults to mcpdiscovery.DefaultBackoffPolicy() in the constructor, overridable in tests for speed). - Added mergeMCPToolsAdditive(workspaceUUID, fresh) ([]MCPToolInfo, bool): an additive-only merge that NEVER downgrades — tools absent from fresh are retained unconditionally (unlike applyMCPToolsCachePolicy's two-consecutive-negatives eviction, which would be wrong here since a backoff round only sees currently-reachable servers and would otherwise evict tools from servers still starting up or contributed by the LLM fallback). Refreshes descriptions of tools present in fresh, returns the merged name-sorted list and whether anything changed (no-op write when unchanged), and clears mcpToolsNegatives entries for reappearing tools. - Added EnsureMCPBackoffRetry(ctx, workspaceUUID, onUpdate): no-op when StdioToolsDiscoverer is nil or a loop is already active for the workspace; otherwise launches (at most once per workspace, tracked via mcpBackoffActive) a goroutine that calls mcpdiscovery.RetryUntilReachable with a probe closure invoking StdioToolsDiscoverer each round, additively merging newly-reachable servers' deduped tool names via mergeMCPToolsAdditive and invoking onUpdate with the merged list whenever it changes. The probe reports Reachable=true (stopping the backoff loop) only once every configured server has responded (none left Reachable=false); RetryUntilReachable itself guarantees a probe timeout/error is never treated as a negative. onReachable is passed as nil since the probe closure already broadcasts incrementally per round. Debug-logs start/stop (guarded by m.logger != nil). internal/web/session_ws.go: - Added the internal/auxiliary import. - In triggerMCPToolsFetch, after the existing eventsManager.Broadcast(WSMsgTypeMCPToolsAvailable, ...) block and before the go c.checkRequiredToolPatterns(...) call: if an auxiliary manager is configured, calls srv.auxiliaryManager.EnsureMCPBackoffRetry(context.Background(), ...) with an onUpdate callback that broadcasts WSMsgTypeMCPToolsAvailable with the updated tools list to all clients. Uses context.Background() (not the fetch's 30-minute ctx, which is cancelled by the function's deferred cancel() on return) since the backoff's own MaxAttempts bounds the loop; deduplication against concurrent/repeated triggers is handled inside the manager. Does NOT call checkRequiredToolPatterns from onUpdate (deliberate scope limit: its timer cascade would stack on every backoff round; frontend re-gating on late tools is a separate concern, mitto-sys.12). Tests (internal/auxiliary/workspace_manager_test.go): - TestMergeMCPToolsAdditive: empty-cache-plus-fresh; absent tool retained (no downgrade) with cache left unchanged; description refreshed; empty fresh is a no-op; a tool tracked only via a stale negatives-counter entry reappearing in fresh clears that entry. - TestEnsureMCPBackoffRetry_ReadyAfterN: discoverer unreachable for 3 calls then reachable with a tool; asserts onUpdate fires exactly once with the new tool and exactly 4 discoverer calls (acceptance #1: fast discovery, not a flat long wait). - TestEnsureMCPBackoffRetry_NoNegativeOnTimeout: discoverer always returns Reachable=false/Err=context.DeadlineExceeded with MaxAttempts=3; asserts onUpdate never fires and a pre-seeded cache entry survives untouched after the loop exhausts (acceptance #2: last-known-good intact). - TestEnsureMCPBackoffRetry_Dedup: two back-to-back EnsureMCPBackoffRetry calls for the same workspace; the second is a no-op against the already-active loop (mcpBackoffActive dedup), verified race-clean. - TestEnsureMCPBackoffRetry_NilDiscoverer_NoOp: nil StdioToolsDiscoverer -> no goroutine started, onUpdate never called, no panic. - All shared counters/collections in tests use sync/atomic or a mutex-guarded map/slice (run under -race). Verified via grep that mcpdiscovery.ServerToolsResult's field names (Server/Tools/Reachable/Err) match what's used here; did not modify internal/mcpdiscovery/backoff.go, discovery.go, or internal/web/server.go. Gate: gofmt -l internal/auxiliary/ internal/web/session_ws.go clean; go vet on both clean; golangci-lint run on both = 0 issues; go test -race -count=2 ./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.
…ol refresh (mitto-sys.4) Add a self-contained, persistent-session watcher primitive for MCP servers that support notifications/tools/list_changed, per docs/devel/mcp-tool-discovery.md. This is increment 1 of N for mitto-sys.4: the primitive only, NOT wired into session_ws.go or workspace_manager.go (a later increment). internal/mcpdiscovery/watch.go (new): - ToolListWatcher: holds the live *mcp.ClientSession and the transport's cleanup func (if any). Close() is idempotent/concurrency-safe via sync.Once, closing the session and running cleanup on the first call only, in that order. - WatchServer(ctx, srv, factory, onChange) (*ToolListWatcher, ServerToolsResult, error): builds the transport (factory defaults to DefaultTransportFactory when nil, matching DiscoverServers), connects mcp.NewClient with a *mcp.ClientOptions whose ToolListChangedHandler re-lists tools (via a DefaultTimeout-bounded context derived from ctx, so a hanging server can't block the SDK's read-loop goroutine indefinitely) and invokes onChange with the fresh ServerToolsResult (onChange may be nil; the notification is still handled without panicking). Unlike probeServer's one-shot connect-list-close, the session is deliberately kept open — its cleanup is deferred to Close(), not run inline. Does an initial ListTools and returns that result directly (not via onChange, which fires only for subsequent changes). On any transport-build/connect/initial-list failure: runs cleanup/closes any partially-opened session, and returns a nil watcher, an unreachable ServerToolsResult, and a wrapped error, matching probeServer's "mcpdiscovery: ..." error style. - listToolsResult: shared ListTools-to-ServerToolsResult conversion used by both the initial list and the change-notification re-list. internal/mcpdiscovery/watch_test.go (new): - newWatchableServer: in-memory mcp.Server + TransportFactory helper (mirrors discovery_test.go's newMockStdioServer pattern), reusing the existing noopToolHandler from discovery_test.go (same package). - TestWatchServer_InitialList: seeded tool appears in the initial result, Reachable=true, onChange not invoked for it. - TestWatchServer_ChangeEventFiresCallback: after connect, AddTool then RemoveTools on the live server; asserts onChange fires (via a buffered channel + 2s timeout) with the updated tool set both times (new tool present after AddTool, removed tool absent after RemoveTools). - TestWatchServer_CloseIsCleanAndIdempotent: first Close() returns nil, second Close() does not panic/double-close. - TestWatchServer_NilOnChangeDoesNotPanic: a change event with onChange=nil does not panic. Key implementation fix during development: the transport factory's cleanup func must NOT be deferred/run inside WatchServer itself (unlike probeServer, which is one-shot) — doing so was found to close the in-memory transport's underlying context immediately upon return, before any notification could ever be delivered. cleanup is now stored on ToolListWatcher and only invoked from Close(), alongside the session close. Gate: gofmt -l internal/mcpdiscovery clean; go vet clean; golangci-lint run internal/mcpdiscovery/... = 0 issues; go build ./... clean; go test -race -count=2 ./internal/mcpdiscovery/... all green, no flakiness. make fmt run before commit, no reformatting needed. Did not touch discovery.go, backoff.go, session_ws.go, or workspace_manager.go.
…ven tool refresh (mitto-sys.4) Build the manager-owned watcher pool + lifecycle on top of the mcpdiscovery.WatchServer primitive (increment 1/N, already committed). This is increment 2/N: pool + lifecycle + unit tests only. The web-layer wiring (MCPServerLister/MCPWatchTransportFactory injection, broadcast on onUpdate, StopMCPWatchers/CloseAllMCPWatchers calls on session/server teardown) is a separate increment 3/N. internal/auxiliary/workspace_manager.go: - Added internal/agents import (leaf package, no import cycle). - New WorkspaceAuxiliaryManager fields: MCPServerLister func(ctx, workspaceUUID) ([]agents.MCPServer, error) (exported seam, nil in tests/CLI -> EnsureMCPWatchers is a no-op); MCPWatchTransportFactory mcpdiscovery.TransportFactory (nil -> DefaultTransportFactory, overridable in tests); mcpWatchers map[string][]*mcpdiscovery.ToolListWatcher + mcpWatchersMu sync.Mutex (per-workspace pool registry; KEY PRESENCE, not slice length, marks a pool active/starting). Initialized mcpWatchers in the constructor. - EnsureMCPWatchers(ctx, workspaceUUID, onUpdate): no-op if MCPServerLister is nil; dedups via a _, exists check under mcpWatchersMu, immediately reserving the slot (map[ws] = nil) before launching a single goroutine, so concurrent calls made while servers are still being listed/connected correctly see the pool as already active. In the goroutine: lists servers, and for each builds an onChange closure (ServerToolsResult -> []MCPToolInfo, skipping unreachable results) that additively merges into the cache via the existing mergeMCPToolsAdditive and fires onUpdate on change; calls mcpdiscovery.WatchServer per server. On a per-server watch error, logs at debug and skips it (EnsureMCPBackoffRetry's polling path remains responsible for retrying that server). On success, applies onChange to WatchServer's initial result too (so tools already present at startup surface immediately, not just on the first subsequent change), then appends the watcher to the workspace's slice under mcpWatchersMu -- but only after re-checking the key still exists: if StopMCPWatchers/CloseAllMCPWatchers tore the pool down while this server was still connecting, the just-opened watcher is closed immediately instead of being registered, and no further servers in this pool are started. - StopMCPWatchers(workspaceUUID): grabs and deletes the workspace's slice under mcpWatchersMu, then closes each watcher outside any lock (ToolListWatcher.Close does not touch mcpWatchersMu, so this cannot deadlock). No-op when absent. - CloseAllMCPWatchers(): same teardown across every workspace at once (for server shutdown), replacing the whole map under the lock then closing everything outside it. Idempotent/safe when empty. - Deliberately reuses mergeMCPToolsAdditive as-is (never removes tools on a watcher update) for parity with EnsureMCPBackoffRetry; removal-on-list_changed is a documented follow-up, not attempted here. internal/auxiliary/mcp_watchers_test.go (new): - newWatchableMCPServer: in-memory mcp.Server + TransportFactory helper (mirrors mcpdiscovery/watch_test.go's newWatchableServer pattern, duplicated here since it isn't exported cross-package) with its own noopWatchToolHandler. - TestEnsureMCPWatchers_InitialAndChangeRebroadcast: satisfies the bead's "simulate a list_changed notification and assert re-broadcast" acceptance -- after EnsureMCPWatchers, the seeded tool lands in the cache and the initial onUpdate fires; then AddTool on the live server triggers a second onUpdate (within a 2s deadline via a buffered channel) with the new tool present, and the cache reflects it too. - TestEnsureMCPWatchers_Dedup: two EnsureMCPWatchers calls for the same workspace result in exactly one MCPServerLister invocation (atomic counter). - TestStopAndCloseAllMCPWatchers: StopMCPWatchers removes the workspace key and is idempotent on a second call; CloseAllMCPWatchers is likewise safe to call (including twice) when empty. - TestEnsureMCPWatchers_NilLister_NoOp: nil MCPServerLister -> no goroutine started, onUpdate never called, no watchers map entry created. - All shared test state uses sync/atomic or a mutex-guarded bool (run under -race). Gate: gofmt -l internal/auxiliary clean; go vet clean; golangci-lint run internal/auxiliary/... = 0 issues; go build ./... clean; go test -race -count=2 ./internal/auxiliary/... all green, no flakiness. make fmt run before commit, no reformatting needed. Did not touch internal/web/session_ws.go or internal/web/server.go.
…teardown (mitto-sys.4)
Wire the increment 1/2 primitives (mcpdiscovery.WatchServer,
WorkspaceAuxiliaryManager's watcher pool) into the live runtime end to
end: production server enumeration, WebSocket broadcast, and teardown
on both server shutdown and per-workspace removal. Also fixes a
reservation-leak bug found in increment 2's EnsureMCPWatchers. Closes
mitto-sys.4.
internal/web/server.go:
- Inside the existing agentsDir-resolved block (sharing agentMgr and
sessionMgr with the StdioToolsDiscoverer wiring), added a sibling
auxiliaryManager.MCPServerLister assignment: resolves the workspace's
ACP agent the same way StdioToolsDiscoverer does, then calls
agentMgr.ListMCPServers(ctx, agent.DirName, &agents.MCPListInput{Path:
ws.WorkingDir}) and returns out.Servers (nil out -> nil, nil). Left
MCPWatchTransportFactory unset (nil -> DefaultTransportFactory in
production; it exists only for test injection).
- In Shutdown(), added a auxiliaryManager.CloseAllMCPWatchers() call
right after sessionManager.CloseAll("server_shutdown"), tearing down
every persistent watcher session on server shutdown.
internal/web/session_ws.go (triggerMCPToolsFetch):
- Immediately after the existing EnsureMCPBackoffRetry block, added an
EnsureMCPWatchers(context.Background(), wsUUID, onUpdate) call (same
context.Background() rationale as the backoff call: the fetch's ctx is
cancelled on return, so the persistent watcher must use an independent
context whose lifetime is bounded by explicit teardown, not the fetch).
onUpdate broadcasts WSMsgTypeMCPToolsAvailable with the updated tools
(mirroring the backoff path), AND additionally broadcasts
WSMsgTypePromptsChanged (reason: "mcp_tools_event") so
enabledWhen-gated prompts re-evaluate immediately on a genuine
list_changed event, without waiting for the existing 30/60/120s
checkRequiredToolPatterns timer cascade (left untouched; that path
remains the fallback/eventual-consistency mechanism, its removal is a
separate concern per mitto-sys.12). Captures only srv/wsUUID (never
the per-client c), since this callback can fire long after the
triggering client disconnects.
internal/conversation/session_manager.go (RemoveWorkspace):
- After sm.wsRegistry.RemoveWorkspace(uuid), reads sm.auxiliaryManager
under sm.mu.RLock() (matching the existing read pattern used
elsewhere in this file, e.g. around line ~2765) and calls
auxMgr.StopMCPWatchers(uuid) when non-nil, so a removed workspace's
persistent MCP connections are torn down immediately rather than
leaking until server shutdown.
internal/auxiliary/workspace_manager.go (EnsureMCPWatchers bug fix):
- On MCPServerLister returning an error, the goroutine previously
returned without releasing the reserved-but-empty mcpWatchers[ws]=nil
slot set before it was launched, permanently blocking all future
EnsureMCPWatchers calls for that workspace (the dedup check would
always see the workspace key present). Fixed: before returning on a
lister error, re-locks mcpWatchersMu and deletes the entry only if it
is still the empty reservation (len(w)==0) — guarding against
clobbering a pool a concurrent call may have populated in the
meantime — so a subsequent EnsureMCPWatchers call can retry cleanly.
internal/auxiliary/mcp_watchers_test.go:
- Added TestEnsureMCPWatchers_ListerErrorReleasesReservation: an
EnsureMCPWatchers call with a lister returning an error is confirmed
to release the reservation (polled with a timeout), then a second
EnsureMCPWatchers call with a working lister is confirmed to actually
invoke it (atomic counter) and complete normally (onUpdate fires),
proving the earlier failure does not permanently block retries.
Verification (all commands run from repo root, actual output):
- gofmt -l internal/web internal/auxiliary internal/conversation -> (empty, clean)
- make fmt -> "go fmt ./..." (no files touched)
- go vet ./internal/web/... ./internal/auxiliary/... ./internal/conversation/... -> (empty, clean)
- golangci-lint run internal/web/... internal/auxiliary/... internal/conversation/... -> "0 issues."
- go build ./... -> (empty, clean)
- go test -race -count=2 ./internal/auxiliary/... ./internal/mcpdiscovery/... ->
ok github.com/inercia/mitto/internal/auxiliary 1.472s
ok github.com/inercia/mitto/internal/mcpdiscovery 2.101s
- go test ./internal/web/... ./internal/conversation/... ->
ok github.com/inercia/mitto/internal/web 9.491s
ok github.com/inercia/mitto/internal/web/handlers 1.510s
ok github.com/inercia/mitto/internal/web/middleware (cached)
ok github.com/inercia/mitto/internal/conversation 266.990s
Follow-ups noted (not fixed here, out of this increment's scope):
- mitto-sys.12: remove/shrink the 30/60/120s checkRequiredToolPatterns
timer cascade now that both the backoff and watcher paths broadcast
prompts_changed event-drivenly on tool changes.
- Increment 2's documented gap remains: mergeMCPToolsAdditive never
removes tools on a watcher-observed list_changed re-list (parity with
the backoff path's existing limitation) — a genuine tool removal is
not currently propagated to the cache/frontend.
- WatchServer's initial Connect+ListTools call (in EnsureMCPWatchers'
per-server loop) is not wrapped in its own bounded timeout the way the
backoff probe is; a wedged transport during the initial connect could
block that one server's watcher goroutine indefinitely. Worth a
follow-up if observed in practice.
…mitto-sys.12)
checkRequiredToolPatterns previously scheduled a 30s/60s/120s timed loop
re-broadcasting prompts_changed (reason mcp_tools_retry) as a band-aid
for slow/unreliable MCP tool discovery. That mechanism is now redundant:
late/changed tools surface via the event-driven watcher (mitto-sys.4:
EnsureMCPWatchers -> onUpdate -> broadcastPromptsChanged("mcp_tools_event"))
and the bounded-backoff re-probe path (mitto-sys.5), both of which
re-broadcast prompts_changed as soon as tools actually change instead of
guessing at fixed delays. Per docs/devel/mcp-tool-discovery.md (Q3.4/Q4).
internal/web/session_ws.go:
- Removed the retryDelays := []time.Duration{30s, 60s, 120s} loop
(including its time.NewTimer/select/c.ctx.Done() handling) from
checkRequiredToolPatterns entirely.
- Kept the pattern-collection early return, the debug log on the
no-patterns path, and the single immediate
c.broadcastPromptsChanged("mcp_tools_initial") call.
- Updated the method's doc comment and the debug log message (was
"scheduling re-broadcasts for late-loading tools", now describes the
event-driven watcher/backoff paths as the mechanism for late/changed
tools, and states this method only emits the initial filtered
broadcast).
- Did not remove the time import: it's still used elsewhere in the file
(e.g. broadcastPromptsChanged's time.Now()) -- confirmed by a clean
build/gofmt.
internal/web/session_ws_test.go:
- Added TestCheckRequiredToolPatterns_NoTimedRetry: writes a real
MITTO_DIR/prompts/ prompt file with an enabledWhen Tools.HasPattern(...)
expression (via a real config.PromptsCache, so
collectRequiredToolPatterns returns a non-empty pattern list -- the
precondition for the broadcast path to run at all), wires a real
GlobalEventsManager + a registered GlobalEventsClient with a capturable
send channel as server.eventsManager, then calls
checkRequiredToolPatterns in a goroutine and asserts: (1) it returns
within 1s (elapsed asserted < 500ms) -- proving no 30/60/120s timed
loop remains; (2) draining the capture channel yields EXACTLY ONE
prompts_changed broadcast, with reason "mcp_tools_initial"; (3) no
broadcast ever carries reason "mcp_tools_retry". This is a genuine
behavioral test exercising the real method end-to-end (real prompts
cache, real events manager/broadcast plumbing), not just a source-grep.
Verification (all commands run from repo root, actual output):
- gofmt -l internal/web/session_ws.go internal/web/session_ws_test.go -> (empty, clean)
- go vet ./internal/web/... -> (empty, clean)
- go build ./... -> (empty, clean)
- go test ./internal/web/... ->
ok github.com/inercia/mitto/internal/web 8.175s
ok github.com/inercia/mitto/internal/web/handlers (cached)
ok github.com/inercia/mitto/internal/web/middleware (cached)
- grep -n "mcp_tools_retry\|retryDelays" internal/web/session_ws.go -> no matches
- (additional) golangci-lint run internal/web/... -> "0 issues."
- make fmt run before commit; no reformatting needed.
…qwen-code (mitto-sys.14, mitto-sys.15)
… type-based local/remote shapes) (mitto-sys.13)
…/.junie/mcp[/mcp].json) (mitto-sys.10)
…p-list.sh scripts (mitto-sys.9 A+B)
…eader-protected e2e test (mitto-sys.9 C)
Persist the real-MCP-derived (deterministic tools/list) snapshot per workspace
so a restart reuses it within a TTL instead of re-probing every server. The
LLM fallback is never written to disk (ADR docs/devel/mcp-tool-discovery.md Q3.3).
- appdir: add MCPToolsCacheDir()/MCPToolsCacheDirName ($MITTO_DIR/mcp-tools-cache).
- WorkspaceAuxiliaryManager: exported MCPToolsPersistDir (wired by web layer;
empty in tests/CLI disables persistence) + mcpToolsTTL (default 15m).
- persistedMCPTools{Tools, UpdatedAt} on-disk snapshot; load/save/delete helpers
using fileutil.ReadJSON / WriteJSONAtomic.
- FetchMCPTools: on in-memory miss, reuse a fresh on-disk snapshot within TTL
(skips probe); after a probe, persist ONLY the deterministic list.
- ClearMCPToolsCache also removes the snapshot so manual refresh forces a probe.
- server.go: wire MCPToolsPersistDir to appdir.MCPToolsCacheDir().
- Tests: persist real-MCP, never persist LLM fallback, load-within-TTL skips
probe, expired snapshot re-probes + overwrites, clear removes snapshot.
…o-vky) EnsureBuiltinPrompts already pruned orphaned *.prompt.yaml files not in the embedded set, but left legacy old-format *.md builtin files behind (~60 observed cruft files from pre-migration Mitto versions). The loader only reads *.prompt.yaml, so these never load, but they accumulate in the global builtin dir. Extend the existing prune loop to also remove *.md files. The embedded set is *.prompt.yaml only, so every .md file in the builtin dir is legacy. Scope stays on the builtin targetDir only; no loader changes and no touching user/workspace .mitto/prompts overrides. Add config/builtin_prompts_test.go covering pruning of stale *.prompt.yaml and legacy *.md, preservation of embedded prompts and unrelated-extension files, plus an idempotency check.
Surface per-conversation usage/orchestration/activity statistics in the properties panel. Two sourcing strategies with different restart behavior: - Event-derived (survive restart): MCP call counts, UI/children-wait breakdown, turns, ACP tool calls, permissions allowed/denied, errors, and images uploaded are computed in a single pass over persisted events (computeEventStats), deduplicated by tool_call_id. children_spawned is derived from CountChildSessions. - In-memory (reset on restart): cumulative token usage (accumulated in promptDispatcher.accumulateTokenUsage) and child-wait count/total (accumulated via BackgroundSession.RecordChildWait from the parent's blocking mitto_children_tasks_wait calls). Stats are computed at WebSocket connect only (not per prompt_complete) to bound cost, and rendered in the ConversationPropertiesPanel Statistics section.
Loop config (prompt/arguments/trigger) survives archive in loop.json, but
MarkStopped(StoppedReasonArchived/ResumeFailures) left it disabled with no
broadcast, so clients never learned it was still there after unarchive.
restoreLoopOnUnarchive now:
- no-ops when the session has no loop configured,
- auto-re-enables loops stopped for an archive-related reason (manual
archive or ACP resume-failure auto-archive), bootstrapping
onCompletion loops,
- leaves other pause reasons (user-paused, max iterations) untouched,
- always re-broadcasts loop_updated so the editor/pill re-render.
…seudo-element The three tab labels combined daisyUI's .tab (tabs-lift) and .tooltip on the same element; both render into ::before/::after and collided. When a tab's hidden radio gained focus, the tooltip pseudo-element merged with tabs-lift's active-tab corner and painted a stray grey bar under the tabs. Replace the daisyUI tooltip/data-tip with a native title attribute (matching WorkspacesDialog's tabs) and add a Playwright regression guard.
Document the deterministic (internal/mcpdiscovery) vs LLM-fallback tool discovery, disk persistence with TTL, the strict-JSON parsing anti-pattern, and the per-agent mcp-list.sh audit table (known-broken scripts). Add the mcpdiscovery package to the overview and mcp-tool-discovery.md to the docs index.
The /api/issues, /api/issues/stats and /api/issues/{id} handlers returned a
bare HTTP 500 on bd query failures with no server-side error log, making them
invisible in mitto.log. Convert writeBeadsError to a *Handlers method that
logs the underlying error + stderr before writing the 500, and route the read
handlers through a new runBeadsRead helper that retries transient bd failures
(bounded; skips context-cancel and not-found) so transient beads-store
instability self-heals instead of surfacing as a 500.
Installing a filesystem watch for an absent .beads directory (a workspace without beads tracking) logged a WARN 'Failed to add watch for beads dir'. Return nil and log at DEBUG when the .beads dir and its parent (up to the filesystem root) do not exist, since absence is expected. Adds a regression test asserting no WARN is emitted.
…lint keyframe-declaration-no-important)
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.
Summary
This branch carries the 89 commits that landed locally after PR #66 ("Accumulated fixes and improvements") was squash-merged into
main. The original branch history mixed those already-merged commits with the new work; this branch was recreated by rebasing only the genuinely-unmerged commits ontomain(git rebase --onto origin/main <squash-point>), so it contains only the remaining work.326 files changed, +18,931 / −7,970.Major themes
periodic→looprename (mitto-8ir.*)End-to-end rename of the "periodic" feature to "loop" across the whole stack: core config/session types, conversation domain, REST/WS contracts + routes, MCP tool params, loop-runner engine, builtin prompts/processors (
.Session.IsLoop*,loop_*params), frontend (endpoints, hooks, components, CSS,data-testid, localStorage keys), docs, and tests. Includesperiodic.json→loop.jsonmigration andperiodic_*→loop_*settings keys.MCP tool discovery hardening (mitto-sys.*)
tools/listdiscovery components, wired intoagents.Manager.ListMCPServersandFetchMCPTools.ToolListWatcherpool (per-workspace) with runtime wiring + teardown; removes the blind 30/60/120sprompts_changedre-broadcast.MCPServer.Headersapplied to network transports; corrected/realmcp-list.shfor junie, opencode, github-copilot, qwen-code.Shortcut buttons & Toolbar
ShortcutsEditor(settings + workspaces dialogs) and extracted sharedToolbarcomponent; conversation actions promoted to buttons.Loop / auto-children / workspace
onTasksloop trigger with CEL condition gating.Beads
Prompts
beads-refine-implementationloop builtin; Support Slack triage workflow; JIRA CLI fallback when MCP tools are unavailable; gatemitto_conversation_*prompts on deterministicPermissions.*.Fixes
Testing
make build build-mock-acp— passmake test(Go unit + 1469 JS tests) — passmake test-integration— passmake fmt-check/make lint-go— clean