diff --git a/.augment/rules/01-go-conventions.md b/.augment/rules/01-go-conventions.md index 9fb096822..647be22f9 100644 --- a/.augment/rules/01-go-conventions.md +++ b/.augment/rules/01-go-conventions.md @@ -81,6 +81,12 @@ for attempt := 1; attempt <= maxAttempts; attempt++ { **Key principle**: Only *tighten* deadlines, never extend. `shouldFailFastCreateAttempt` bails when remaining budget < per-attempt timeout. +### Cross-Package Mirrored Constants Drift + +When a package can't import another (e.g. `internal/conversation` can't import `internal/acpproc`), it may hardcode a **local mirror** of a schedule/budget constant, flagged with a comment like `// Mirror of shared_acp_process.go set_model constants` (see `internal/conversation/constraints_test.go`'s `scheduleSum`). Changing the source constant does **not** auto-update the mirror. + +**Rule**: When changing a retry schedule/timeout constant, `grep` for comments referencing it (e.g. the old sum/values) across the whole module — not just the owning package — and update every mirror + stale doc comment describing the old math in the same change. + ## Explicit Lock Management in Retry Loops `defer mu.Unlock()` does **not** compose safely with manual unlock + retry. If the locked variable is reassigned during retry, defer fires on the wrong object → double-unlock panic. diff --git a/.augment/rules/02-session.md b/.augment/rules/02-session.md index cf3c8d072..f480254b5 100644 --- a/.augment/rules/02-session.md +++ b/.augment/rules/02-session.md @@ -130,6 +130,8 @@ Stored in `loop.json`. Only top-level sessions may have loop prompts (child → **Critical**: Changing `LoopStore.Update()` signature requires updating BOTH `session_loop_api.go` (PATCH handler) AND `mcpserver/server.go` (MCP tool) — both call `Update()`. +**Un-loop/re-loop persistence symmetry**: `Detach()` saves settings to a slot and clears the active config (un-loop); `GetSaved()`/restore reads it back. A fresh loop `Set()` (not a restore) must be followed by `ClearSaved()` so a stale saved slot doesn't leak in later — required in both `session_loop_write.go` (`handleSetLoop`) and `mcpserver/server.go` (MCP create-loop path). + ## Auxiliary Package The `internal/auxiliary` package provides a hidden ACP session for utility tasks. Lazy init, auto-approve permissions, file writes denied, thread-safe. diff --git a/.augment/rules/03-cli-acp.md b/.augment/rules/03-cli-acp.md index ae0f21dd5..0e33f8e3a 100644 --- a/.augment/rules/03-cli-acp.md +++ b/.augment/rules/03-cli-acp.md @@ -95,3 +95,4 @@ Located in `config/agents/builtin//` (shipped) or `MITTO_DIR/agents/custo **MCP scopes**: `user` (global), `project` (per-repo), `local` (uncommitted). **Agent defaults** (seeded at discovery): Pre-fill ACP server settings. Request-wins: user values take precedence. **Commands**: `mcp-list.sh`, `mcp-install.sh`, `mcp-remove.sh` (scope must match metadata). +**stderrPatterns** (mitto-k6h): Optional per-agent regex patterns applied by the ACP stderr monitor. `crash` (union with hardcoded baseline → `onCrashDetected` bypasses SDK 60s timeout), `ignore` (suppress from debug log; buffer capture unaffected), `degraded` (plumbed only, behaviour deferred). Compiled once at process start; invalid regex is skipped with a warn (never fatal). CI guard: `make check-stderr-patterns`. Compile happens in `internal/web` (only layer with both `agents.Manager` and ACP-server mapping); `*conversation.CompiledStderrPatterns` is injected into `SharedACPProcessConfig`/`BackgroundSessionConfig` via a per-server-name resolver. See [docs/devel/acp.md § Stderr Pattern Detection](../../docs/devel/acp.md). diff --git a/.augment/rules/07-prompts.md b/.augment/rules/07-prompts.md index 8b1618bc8..a94d5d973 100644 --- a/.augment/rules/07-prompts.md +++ b/.augment/rules/07-prompts.md @@ -163,7 +163,9 @@ Updates replicate the 5-layer REST API merge. Name slugification via `config.Slu ## enabledWhen Filtering & Preferred Models -Server-side via `filterPromptsByEnabled()` / `buildPromptEnabledContext()`. Use `enabledWhen` (CEL) exclusively. Full CEL context: see `05-msghooks.md`. Useful functions: `FileExists(".git/config")`, `CommandExists("gh")`, `Tools.HasPattern("github_*")`. +Server-side via `filterPromptsByEnabled()` / `buildPromptEnabledContext()`. Use `enabledWhen` (CEL) exclusively. Full CEL context: see `05-msghooks.md`. Useful functions: `FileExists(".git/config")`, `CommandExists("gh")`, `Tools.HasPattern("github_*")`, `BeadsCount("label", "open,in_progress")` / `HasBeads("label", "open,in_progress")`. + +**Beads gating (`BeadsCount` / `HasBeads`)**: query the workspace's `bd` (beads) DB from CEL AND Go templates. Both accept two comma-separated string args — `labels` (ALL match) and `statuses` (ANY match) — and run `bd list -l --status --all --json` in `Workspace.Folder` (5s timeout, 5s in-memory cache). **Fail-open**: missing `bd`, non-zero exit, unparseable JSON, or timeout returns a positive sentinel (count=1 / true) so gated prompts are never wrongly hidden; a legitimate `[]` returns 0/false. Always short-circuit with cheap gates first so `bd` isn't exec'd when there's no DB: `CommandExists("bd") && DirExists(".beads") && HasBeads("support-question", "open,in_progress")`. Shared pure-Go helper (`beadsCount` in `internal/config/templatefuncs.go`) is the single source of truth for both surfaces — the CEL macros (`beadsCountMacro`, `hasBeadsMacro`) auto-inject `Workspace.Folder`. **Per-conversation user data (`UserData`)**: exposed as a `map[string]string` in both the template context (`{{ UserData "NAME" }}` / `{{ index .UserData "NAME" }}`) and CEL (`UserData["NAME"]` / `"NAME" in UserData`), built from the same conversation attributes that back `Session.UserDataJSON`. Wired exactly like `Args` (struct field + `cel.Variable` + `buildActivation` normalization + template func), but populated at **both** menu time (`buildPromptEnabledContext`) and send time (`buildProcessorInput`) — the parity invariant — so menu gating and body rendering agree. Use it for set-if-unset, else-do-Y flows; the opaque `UserDataJSON` blob cannot drive a per-field conditional. @@ -175,7 +177,7 @@ Prompts may declare preferred model(s) for auto-selection at prompt-dispatch tim ```yaml preferredModels: - - modelName: Claude Sonnet # matches a profile by its `name` (case-insensitive) + - modelName: Claude Sonnet 4 # matches a profile by its `name` (case-insensitive) - modelTag: Coding # selects any profile carrying this tag (case-insensitive) ``` diff --git a/.augment/rules/08-config.md b/.augment/rules/08-config.md index 4dd2610b9..2e1867d01 100644 --- a/.augment/rules/08-config.md +++ b/.augment/rules/08-config.md @@ -79,7 +79,7 @@ Per-workspace config via RC files (`{workspace}/.mittorc` or `.mitto/mittorc[.ya ## Workspace Persistence -Workspaces persisted in `workspaces.json` (except CLI `--dir`). `folders.json` (crash-safe) holds folder-level settings; metadata stays in `.mittorc` (version-controllable). +Workspaces persisted in `workspaces.json` (except CLI `--dir`). `folders.json` (crash-safe) holds folder-level settings; metadata stays in `.mittorc` (version-controllable). Folder-level settings include a `pinned` bool (defaults `false`) that, when `true`, keeps the folder visible in the sidebar even without conversations; toggled via `GET/PUT /api/folders/pin` (`internal/web/handlers/folder_pin.go`) and projected onto workspace records by `ApplyFolderDefaults`. ## Global Settings REST API @@ -101,7 +101,7 @@ Note: `/mitto/api/settings` manages global `settings.json`. For per-session feat models: - name: Claude Opus # UI label (read-only) criteria: { matchMode: contains, pattern: Opus } # Case-insensitive pattern matching - tags: [Smartest, Reasoning, Expensive] # Interface-only semantic tags + tags: [Smartest, Reasoning, Expensive] # Capability tags, consumed at runtime ``` **Tag union matching** (additive): If a model name matches multiple profiles (e.g., `Claude Opus 4.5`): @@ -109,7 +109,9 @@ models: - Then: Matches `Opus` profile → Adds `[Smartest, Reasoning, Expensive]` - Result: `[Anthropic, Smartest, Reasoning, Expensive]` (union) -Use `matchMode: contains` for robust cross-version matching. Tags are interface-only; runtime consumption is tracked separately (see `mitto-2cc`). Shipped defaults include: Claude, Opus, Sonnet, Haiku, GPT-5, GPT-4, Gemini. Test: `TestParse_EmbeddedDefaultModelProfiles()` in `internal/config/config_test.go`. +Use `matchMode: contains` for robust cross-version matching. Shipped defaults include: Claude, Opus, Sonnet, Haiku, GPT-5, GPT-4, Gemini. Test: `TestParse_EmbeddedDefaultModelProfiles()` in `internal/config/config_test.go`. + +**Canonical Go defaults (always available, not just first-run seed)**: `config.DefaultModelProfiles()` hardcodes the same 7 profiles in Go — the single source of truth, kept in sync with `config.default.yaml`'s `models:` block by `make check-model-tags`. `(*Config).EffectiveModelProfiles()` returns `settings.json`'s `Models` unioned with these defaults (user profile wins on name collision, defaults fill gaps; nil-safe). All tag/name resolution — `ModelProfileByName`, `ModelProfilesByTag`, `ResolveModelTags` — routes through `EffectiveModelProfiles()`, so a prompt's `preferredModels: {modelTag: Coding}` resolves even when `settings.json` predates or omits `models:` (previously it silently no-opped to the baseline model — this was the root cause of `preferredModels` "not working" for users with pre-existing settings). `config.CanonicalModelTags()` returns the sorted, de-duped tag set; `make check-model-tags` rejects any builtin prompt's `modelTag` that isn't in this set. ## ACP Server Constraints diff --git a/.augment/rules/09-macos-app.md b/.augment/rules/09-macos-app.md index df2c5404a..6627e5fba 100644 --- a/.augment/rules/09-macos-app.md +++ b/.augment/rules/09-macos-app.md @@ -28,8 +28,11 @@ touch Mitto.app && killall Finder && killall Dock ## Environment Detection +Prefer the shared `isNativeApp()` from `web/static/utils/native.js` (re-exported by `utils/index.js`) over redefining the predicate inline. Implementation: `typeof window.mittoPickFolder === "function"` — `mittoPickFolder` is the sentinel bound only by the WKWebView host. Use it to gate any UI whose action requires a bound `window.mitto*` function (e.g. the folder-header "Open ▸" submenu in `SessionList.js`). + ```javascript -const isMacApp = typeof window.mittoPickFolder === "function"; +import { isNativeApp } from "../utils/index.js"; +if (!isNativeApp()) return []; // hide native-only UI in browser context ``` ### Bound Native Functions diff --git a/.augment/rules/15-web-backend-session-lifecycle.md b/.augment/rules/15-web-backend-session-lifecycle.md index c18d759bd..d999fea7f 100644 --- a/.augment/rules/15-web-backend-session-lifecycle.md +++ b/.augment/rules/15-web-backend-session-lifecycle.md @@ -71,12 +71,23 @@ The GC suspends idle loop sessions whose next prompt is far away, saving ACP res | `inactivity` | `ArchiveReasonInactivity` | Auto-archive after configured inactive period| | `acp_start_failures`| `ArchiveReasonACPFailures` | `ACPStartFailureCount` ≥ threshold (3) | -Broadcast in `session_archived` WebSocket message as `archive_reason` field. +Broadcast in `session_archived` WebSocket message as `archive_reason` field. `acp_start_failures` on a loop conversation is the only reason eligible for Auto-Unarchive Recovery (below). ## Auto-Archive Config: `session.auto_archive_inactive_after: "1w"` (in `checkAutoArchive()`). Excluded: already-archived, child sessions, sessions with loop prompts (enabled or paused). +## Auto-Unarchive Recovery + +Loop conversations auto-archived due to broken ACP (`ArchiveReasonACPFailures`) are retried automatically by `LoopRunner.checkAutoUnarchiveRecovery()`, called from `RunOnce()` right after `checkAutoArchive()`. + +- **Eligibility**: `meta.Archived && meta.ArchiveReason == ArchiveReasonACPFailures` + loop config exists. Excludes `Manual`/`Inactivity` and non-loop ACP-failure archives. +- **Retry cadence**: 1h per conversation (`DefaultAutoUnarchiveRetryInterval`), anchored on `Metadata.AutoUnarchiveLastAttemptAt` else `ArchivedAt`. Retries indefinitely — the resume-failure path re-archives on continued outage, restarting cadence from the new `ArchivedAt`. +- **Anti-storm stagger**: 10m global minimum gap (`DefaultAutoUnarchiveStaggerInterval`), tracked in-memory (`LoopRunner.lastAutoUnarchiveAttempt`, `r.mu`); each poll attempts only the most-overdue eligible session. **Never `time.Sleep`** in this path — it stalls loop delivery. +- **On-by-default**: `autoUnarchiveEnabled=true`; `SetAutoUnarchiveRecovery(enabled, retryInterval, stagger)` — duration `<= 0` keeps current value (lets tests override one). +- **Callback** (`onAutoUnarchive`, wired in `server.go`): mirrors manual unarchive — clear archive fields → `ResumeSession()` → broadcast → `handlers.RestoreLoopOnUnarchive()`. +- **Cadence persistence**: `attemptAutoUnarchive()` persists `AutoUnarchiveLastAttemptAt` (outside `r.mu`) *before* the callback so it survives a crash; cleared only on `nil` return. Also cleared on any successful manual unarchive (`session_update.go`). + ## ACP Process Crash Recovery `classifyACPError()` → **Permanent** (command not found, syntax error) = stop + guidance. **Transient** (network, crash) = retry with backoff. @@ -89,6 +100,12 @@ Death detection (three layers): OS polling (~2s), `conn.Done()` EOF (~seconds), When ACP handshake times out transiently, `BackgroundSession.InitializeWithACP()` defers retry up to 3 attempts with exponential backoff. The error event is persisted in the session event log (viewable in UI). Retries happen deferred in a separate goroutine to avoid blocking session creation or WebSocket initialization. After 3 attempts, the session enters error state with guidance. +## Shared Handshake Budget: Stale vs. Cold-Timeout (anti-regression) + +`internal/conversation/shared_session_handshaker.go` bounds concurrent `session/load` + `session/new` handshakes with **one shared deadline** to prevent stacking (`mitto-1ut`). The budget cap MUST be **released** for the `session/new` fallback when the `session/load` probe **timed out** (process genuinely cold) — otherwise `session/new` inherits only `budget − probeTimeout` (e.g. `240s − 45s = 195s`), less than a single `MCPInitTimeout` attempt (240s), guaranteeing starvation. Only cap the fallback when the probe **fast-failed** (JSON-RPC `-32602` stale). Track this via `probeTimedOut`. Test seam: `loadBlocksUntilCtxDone` in `fakeSharedProcess` (`TestHandshaker_ResumeSharedACPSession_ColdProbeTimeout_NoNewDeadline`). Pre-attempt cancellations in `acpproc/shared_acp_process.go` emit a self-diagnosing error (elapsed vs. per-attempt budget) instead of a raw deadline. + +**Clear persisted `acp_session_id` on load failure (`mitto-y1g`)**: In `resumeSharedACPSession`, both load-failure branches (`-32602 Session not found` **and** probe timeout) must call `hsClearPersistedACPSessionID()` before falling back to `session/new`. Otherwise the stale ID stays on disk and every subsequent cold-start / process recycle re-triggers the same doomed `session/load` — catastrophic when an always-active loop session is one of the offenders (each recycle burns the probe cap before the `session/new` fallback). Mirror `hsPersistACPSessionID`; assert both branches clear in the regression test. + ## MCP Server Lifecycle | Event | MCP Server Action | diff --git a/.augment/rules/20-web-frontend-core.md b/.augment/rules/20-web-frontend-core.md index 77101f4f4..2a68455f1 100644 --- a/.augment/rules/20-web-frontend-core.md +++ b/.augment/rules/20-web-frontend-core.md @@ -45,6 +45,10 @@ App └── Dialogs (Settings, Workspace, Rename, Delete, etc.) ``` +### Sidebar Tree Sources + +`SessionList` synthesizes its folder groups from **two sources**: (a) the working directories of active + stored conversations, and (b) any configured workspace whose `folders.json` entry has `pinned: true`. This lets users keep folders visible in the sidebar even when they hold no conversations — pinned via the sidebar toolbar's `AddFolderDialog`, unpinned via the folder context menu's "Remove from sidebar" entry (both call `PUT /api/folders/pin`). + ## File Structure | File | Purpose | @@ -58,6 +62,7 @@ App | `components/Icons.js` | SVG icon components | | `components/SessionPanel.js` | Unified side panel (Properties + User Data tabs) | | `components/SettingsDialog.js` | Settings modal | +| `components/AddFolderDialog.js` | "Add folder to sidebar" dialog: pin existing hidden workspace or delegate to WorkspacesDialog for a new one | | `hooks/useWebSocket.js` | WebSocket connection management | | `hooks/useResizeHandle.js` | Drag-to-resize with mouse and touch | | `hooks/useSwipeNavigation.js` | Mobile swipe gestures | diff --git a/.augment/rules/25-web-frontend-components.md b/.augment/rules/25-web-frontend-components.md index 88c898215..3cfed906e 100644 --- a/.augment/rules/25-web-frontend-components.md +++ b/.augment/rules/25-web-frontend-components.md @@ -119,7 +119,7 @@ const { showToast, dismissToast, toasts } = useToast(); showToast({ message: "Saved", style: "success" }); // auto-dismiss 5s ``` -Durations: info/success=5s, warning/error=10s. Max 5 simultaneous. Render via ``. Use `error` (red) for actual errors only. +Durations: info/success=5s, warning=10s. `error` toasts NEVER auto-dismiss — they persist until the user closes them (dismiss button), so critical errors can't be missed. Max 5 simultaneous. Render via ``. Use `error` (red) for actual errors only. ## useResizeHandle / useSwipeNavigation diff --git a/.augment/rules/32-testing-playwright.md b/.augment/rules/32-testing-playwright.md index 725b1e2cc..7811c704c 100644 --- a/.augment/rules/32-testing-playwright.md +++ b/.augment/rules/32-testing-playwright.md @@ -123,6 +123,23 @@ In Docker, ACP connections take ~1.1s. After clicking new-session, **wait for `m await page.waitForFunction(() => !!localStorage.getItem("mitto_last_session_id")); ``` +## Testing Native-App-Gated UI + +For features gated on `isNativeApp()` (`web/static/utils/native.js`), stub the sentinel `window.mittoPickFolder` **before navigation** with `page.addInitScript` — Playwright runs in a plain browser, so `isNativeApp()` returns `false` by default. + +```typescript +async function stubNativeApp(page) { + await page.addInitScript(() => { + Object.defineProperty(window, "mittoPickFolder", + { configurable: true, get: () => () => {}, set: () => {} }); + }); +} +``` + +**Apply per-test, NOT in `beforeEach`** — move `helpers.navigateAndWait(page)` into each test so a negative test can navigate un-stubbed to assert the gated UI is absent. Cover both positive (stubbed → visible) and negative (un-stubbed → hidden) paths in the same spec. Reference: `tests/ui/specs/open-in-context-menu.spec.ts`. + +Same rule applies when patching backend config the frontend reads once via `fetchConfig()` on mount (e.g. `ui.mac.open_in` targets): PATCH it in `beforeEach` **before** the first `navigateAndWait`, otherwise the mount reads stale defaults. + ## Browser-Specific Issues | Issue | Browser | Cause | diff --git a/.augment/rules/42-mcpserver-development.md b/.augment/rules/42-mcpserver-development.md index 0035d37be..25284269d 100644 --- a/.augment/rules/42-mcpserver-development.md +++ b/.augment/rules/42-mcpserver-development.md @@ -26,6 +26,14 @@ Single global MCP server at `http://127.0.0.1:5757/mcp`. Two tool classes: - **Global tools** (no session): `mitto_conversation_list`, `mitto_get_config`, `mitto_get_runtime_info` - **Session-scoped tools** (require `self_id`): UI prompts, conversation control, history, prompt management (`mitto_prompt_list/get/update`), loop control (`mitto_conversation_set_loop`, `mitto_conversation_run_loop_now`) +## Cold-Start MCP Wedge (mitto-54k / mitto-6hr) — Mitto-side SSE stall, FIXED + +Earlier diagnosis ("agent-side, unfixable in Mitto" — auggie hard-gating on MCP `initialize`) was **wrong**, corrected 2026-07-08 after the same wedge reproduced in **both** auggie and Claude Code, isolated to workspaces with **concurrent MCP sessions** (e.g. multiple workspace UUIDs + an active loop sharing one `working_dir`). + +**Root cause**: `startSSE()` (`server.go:347`) builds the Streamable HTTP handler with `nil` options → stateful mode, where a POST's response can ride the client's per-session **GET SSE stream** instead of the POST body. Under concurrency that stream stalls (observed: ~97s gap between GET completions, an SSE GET held open, never completes) — `initialize` times out even though Mitto's `/mcp` handler already returned 200 in 0ms (`duration_ms=0` in the access log is a red herring — check GET-stream completions, not POST latency). + +**Fix (`mitto-6hr`, P1, epic `mitto-54k`, APPLIED)**: `startSSE()` now passes `&mcp.StreamableHTTPOptions{JSONResponse: true}` to `NewStreamableHTTPHandler` so POST responses resolve inline, independent of the SSE GET. **Not** `Stateless: true` — rejects server→client *requests*, breaking `UIPrompter` (mitto_ui_options/form). Still-valid secondary mitigations (reduce concurrency, don't fix the stall): `mitto-clc` (disable proactive keep-warm), `mitto-cgc` (stagger aux-session creation). + ## Adding New Tools Handler signature (3-arg form — SDK unmarshals input automatically): @@ -96,22 +104,11 @@ if callerMeta.WorkingDir != targetWS.WorkingDir { ## Optional Late-Bound Dependencies -Some dependencies (e.g. `LoopRunner`) are initialized after the MCP server and wired in via setter methods rather than through `Dependencies`: - -```go -// In internal/web/server.go — after s.loopRunner.Start(): -if s.mcpServer != nil { - s.mcpServer.SetLoopRunner(s.loopRunner) -} -``` - -The `LoopRunner` interface (defined in `mcpserver/server.go`) is satisfied by `*web.LoopRunner`. Use setter methods (not `Dependencies`) when a dependency must exist before `NewServer()` completes but the dependency itself starts later. +Some dependencies (e.g. `LoopRunner`) are wired in via setter methods (`s.mcpServer.SetLoopRunner(s.loopRunner)` in `internal/web/server.go`, after `s.loopRunner.Start()`) rather than through `Dependencies`, since they must exist before `NewServer()` completes but start later. The `LoopRunner` interface (in `mcpserver/server.go`) is satisfied by `*web.LoopRunner`. ## Processor Auxiliary Session MCP Access -Processor auxiliary sessions (purpose prefix `"processor:"`) get a stdio MCP proxy so the agent can call Mitto tools. Configured in `internal/web/acp_process_manager.go` via `ACPProcessManager.MCPServerURL`. Non-processor auxiliary sessions (title-gen, follow-up, etc.) do NOT get MCP access. - -See `docs/devel/mcp.md` for detailed documentation. +Processor auxiliary sessions (purpose prefix `"processor:"`) get a stdio MCP proxy so the agent can call Mitto tools. Configured in `internal/web/acp_process_manager.go` via `ACPProcessManager.MCPServerURL`. Non-processor auxiliary sessions (title-gen, follow-up, etc.) do NOT get MCP access. See `docs/devel/mcp.md` for detailed documentation. ## Input Validation in Tools @@ -147,3 +144,5 @@ API endpoint: `GET /api/workspace-mcp-tools?acp_server=NAME&dir=PATH` (handler i | github-copilot | BROKEN (mitto-sys.14) | wrong path: real is `~/.copilot/mcp-config.json` | | qwen-code | BROKEN (mitto-sys.15) | wrong path: real is `~/.qwen` | | junie | stub (mitto-sys.10) | always returns `{"servers": []}` | + +**Auggie git-root divergence** (not a script bug): `auggie mcp list` resolves `` to the **git toplevel**, not the target `workingDir` — so when `workingDir` is a git subdirectory, `mcp-list.sh` (which reads `/.augment/settings.local.json` literally) can report servers (e.g. `slack`) that auggie itself never loads (it reads `/.augment/settings.local.json` instead). Verify workspace vs. git-root config before trusting the MCP tab for auggie workspaces nested in a larger repo. diff --git a/.gitignore b/.gitignore index 9e272dc94..3960a5e6e 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,10 @@ playwright-report/ .beads/config.yaml .augment/settings.local.json tests/ui/test-results-auth/ + +# Frontend bundler spike artifacts (mitto-qcm — scripts kept, output ignored) +scripts/spike-bundler/dist/ +scripts/spike-bundler/vite-chatinput/node_modules/ +scripts/spike-bundler/vite-chatinput/dist/ +scripts/spike-bundler/vite-chatinput/package-lock.json .tokensave diff --git a/CLAUDE.md b/CLAUDE.md index 2bb1b8b17..5e732c4c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,8 @@ go test -v -tags integration ./tests/integration/inprocess/ - **Log authoritative source**: Check `events.jsonl` (session dir) when debugging; server logs rotate and have gaps. - **daisyUI drawer GPU bug**: `.drawer-side` + fixed-position overlay compete for pointer events → blank artifacts. Fix: See `web/static/styles.css` for verified pattern. Do NOT use `translateZ(0)`. - **Zombie WebSocket recovery**: When phone sleeps or app backgrounded, WS may enter "zombie" state (appearing open but dead). On visibility change or app activate, force-close and reconnect. This is expected behavior — not a bug. See `.augment/rules/23-web-frontend-mobile.md` for resilience patterns. -- **Verify prior edits actually persisted**: Don't trust that a previous turn's file edits are still on disk (session gaps, restarts, or reverted stashes can silently drop them). Before continuing/relying on earlier work, re-check with `git status`/`git diff` or re-view the file rather than assuming. +- **Verify prior edits actually persisted**: Don't trust that a previous turn's file edits are still on disk — session gaps, restarts, or a **concurrent loop conversation** (e.g. a PR-babysitting/cleanup loop sharing the same repo) stashing/resetting the working directory mid-task can silently drop them. Re-check with `git status`/`git diff`/re-view before relying on earlier work; if files vanish unexpectedly, check `git stash list` first — work is often auto-stashed, not lost. +- **Cold-start MCP wedge (mitto-54k) — corrected 2026-07-08: it's a Mitto-side SSE stall, not agent-side**: symptom `⏳ mitto (timed out)`/hung first prompt for minutes. The earlier "agent-side, unfixable in Mitto" diagnosis was **wrong** — it was overturned after the same wedge reproduced in **both** auggie and Claude Code, isolated to workspaces with **concurrent MCP sessions** (e.g. multiple workspace UUIDs + an active loop sharing one `working_dir`). Root cause: `internal/mcpserver/server.go:347` builds the Streamable HTTP handler with `nil` options → stateful mode, where a POST's response can ride the client's per-session **GET SSE stream** instead of the POST body; under concurrency that stream stalls (~97s gap observed, SSE GET held open, never completes) so `initialize` times out even though Mitto's handler already returned 200 in 0ms (`duration_ms=0` in logs is a red herring). Fixed by `mitto-6hr` (P1, APPLIED): `startSSE()` passes `&mcp.StreamableHTTPOptions{JSONResponse: true}` so POST responses resolve inline, independent of the SSE GET — **not** `Stateless: true` (breaks `UIPrompter`/`mitto_ui_options`). See `.augment/rules/42-mcpserver-development.md`. Secondary mitigations (reduce concurrency, don't fix the stall): `mitto-clc` (disable proactive keep-warm pin), `mitto-cgc` (stagger aux-session creation). ## New Agent Capability Checklist @@ -101,15 +102,11 @@ Used in `BeadsView.js` (list actions + issue-detail header). ## Model Selection & Preferred Models -Prompts can declare `preferredModels:` to route to specific ACP models. `selectPreferredModel()` in `constraints.go` picks the best match using configurable match modes (`"contains"`, `"exact"`, `"startsWith"`, `"regex"`, `"lookAlike"`). **Key insight**: If the active model already satisfies the preference, it's kept; otherwise the preference is applied. This avoids unnecessary model switches in multi-model sessions. +Prompts can declare `preferredModels:` to route to specific ACP models. `selectPreferredModel()` in `constraints.go` picks the best match using configurable match modes (`"contains"`, `"exact"`, `"startsWith"`, `"regex"`, `"lookAlike"`). If the active model already satisfies the preference, it's kept; otherwise applied — avoids unnecessary switches in multi-model sessions. -**Per-prompt transient overrides**: When a prompt declares `preferredModels`, `setActiveModelOnly()` temporarily switches models for that prompt's execution **without** recording a `session_change` event. This is **intentional**: -- Baseline model (conversation-level setting) remains unchanged -- No "Model changed to X" message in timeline (silent override) -- After prompt completes, `restoreBaselineIfOverride()` flips model back to baseline -- Result: Heavy-lift work runs on cheaper models (e.g., Sonnet) while conversation stays on your chosen baseline (e.g., Opus) +**Per-prompt transient overrides**: `setActiveModelOnly()` switches models for a prompt's execution **without** recording a `session_change` event (silent; conversation-level baseline is untouched). `restoreBaselineIfOverride()` flips back after the prompt completes. **Contrast**: manual UI selection → `applyConfigOption()` → `cmRecordSessionChange()` → persistent event, updates baseline. -**Contrast**: Manual model selection (via UI dropdown) → `applyConfigOption()` → `cmRecordSessionChange()` → records persistent `session_change` event and updates baseline. +**Config-level tag resolution**: `(*Config).EffectiveModelProfiles()` unions `settings.json`'s `Models` with hardcoded `config.DefaultModelProfiles()` (7 canonical profiles), user wins by name — so `modelTag:` always resolves even when `settings.json` predates/omits `models:`. `make check-model-tags` keeps `config.default.yaml` and the Go defaults in sync and rejects unknown tags in builtin prompts. See `.augment/rules/08-config.md`. ## CEL Tool Evaluation (Fail-Open Behavior) @@ -130,6 +127,8 @@ Two-tier discovery for `enabledWhen`/CEL `tools.*` gating (see `docs/devel/mcp-t Per-agent `mcp-list.sh` config paths/keys are **not** interchangeable across agents — verify against real docs before writing/trusting one (audit + known-broken scripts: `.augment/rules/42-mcpserver-development.md`). +**Auggie git-root divergence**: `auggie mcp list` resolves `` to the **git toplevel**, not the Mitto workspace's `working_dir` — so a workspace whose `working_dir` is a git subdirectory sees servers registered in `/.augment/settings.local.json` (not its own `.augment/settings.local.json`). Mitto's `mcp-list.sh` reads `working_dir` literally, so the MCP tab can show servers (e.g. `slack`) the running agent never actually loads. Fix: move servers to the git-root config, register at user scope (`auggie mcp add`, no `--local`), or point `working_dir` at the git root. + ## Loop Conversations **onCompletion trigger** (distinct from schedule-based loop): @@ -140,6 +139,8 @@ Per-agent `mcp-list.sh` config paths/keys are **not** interchangeable across age - `app.js` line ~1928: `headerLoopState()` returns `{ state, label, badgeClass }` pill object - Issue `mitto-36nm` tracks UI clarity improvement (prompt visibility + pill disambiguation) +**Persistence symmetry (LoopStore, `internal/session/loop.go`)**: un-loop calls `Detach()` (saves settings to a slot, clears active config); re-loop/restore reads it back via `GetSaved()`. A **fresh** loop create must call `ClearSaved()` right after `Set()` so a stale saved slot doesn't leak into a later un-loop — done identically in REST (`session_loop_write.go` `handleSetLoop`) and MCP (`mcpserver/server.go` create-loop path) to keep both interfaces symmetric. + ## Tokensave Rule (Mandatory) **NEVER use Explore agents for code research when tokensave is available.** Use `tokensave_context`, `tokensave_search`, `tokensave_callees`, `tokensave_callers`, `tokensave_impact`, `tokensave_node`, `tokensave_files`, or `tokensave_affected` first. See CLAUDE.md in project root for full details. diff --git a/Makefile b/Makefile index a74e0c96b..991e3c24d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build install test test-go test-js test-integration test-integration-go test-integration-cli test-integration-api test-integration-client test-ui test-ui-headed test-ui-debug test-ui-report test-all test-ci test-setup test-clean clean run fmt fmt-check fmt-docs fmt-docs-check lint lint-go lint-frontend deps-go deps-js deps tailwind vendor-codemirror build-mac-app clean-mac-app test-webviewlog build-mock-acp ci install-hooks homebrew-generate homebrew-test homebrew-test-style homebrew-test-install homebrew-test-cask homebrew-tap-setup homebrew-clean smoke-build smoke-test-cli smoke-test smoke-clean +.PHONY: build install test test-go test-js check-model-tags check-stderr-patterns test-integration test-integration-go test-integration-cli test-integration-api test-integration-client test-ui test-ui-headed test-ui-debug test-ui-report test-all test-ci test-setup test-clean clean run fmt fmt-check fmt-docs fmt-docs-check lint lint-go lint-frontend deps-go deps-js deps tailwind vendor-codemirror build-mac-app clean-mac-app test-webviewlog build-mock-acp ci install-hooks homebrew-generate homebrew-test homebrew-test-style homebrew-test-install homebrew-test-cask homebrew-tap-setup homebrew-clean smoke-build smoke-test-cli smoke-test smoke-clean # Binary name BINARY_NAME=mitto @@ -44,6 +44,22 @@ test-js: deps-js @echo "Running JavaScript tests..." $(NPM) test +# Validate builtin model-tag references against the canonical Go tag set. +# Fails if any builtin prompt references a modelTag not in config.CanonicalModelTags(), +# any builtin processor's enabledWhen calls Session.HasModelTag("") with an unknown tag, +# or if config/config.default.yaml `models:` drifts from config.DefaultModelProfiles(). +check-model-tags: + @echo "Validating builtin model-tag references (prompts + processors)..." + $(GOTEST) -run 'TestBuiltinPrompts_ModelTagsAreCanonical|TestDefaultModelProfiles_MatchesEmbeddedYAML|TestCanonicalModelTags|TestEffectiveModelProfiles_MergeAndPrecedence' ./internal/config/ + $(GOTEST) -run 'TestBuiltinProcessors_HasModelTagArgsAreCanonical|TestHasModelTagArgsChecker_CatchesTypo' ./internal/processors/ + +# Validate builtin agent stderr patterns compile as valid Go regexes (mitto-k6h). +# Fails if any pattern in config/agents/builtin/*/metadata.yaml stderrPatterns +# (crash / ignore / degraded) fails regexp.Compile. +check-stderr-patterns: + @echo "Validating builtin agent stderr patterns..." + $(GOTEST) -run 'TestBuiltinAgents_StderrPatternsCompile' ./internal/agents/ + # ============================================================================= # Integration & UI Tests # ============================================================================= diff --git a/cmd/mitto-app/main.go b/cmd/mitto-app/main.go index 4a5740cf8..c5b4e2825 100644 --- a/cmd/mitto-app/main.go +++ b/cmd/mitto-app/main.go @@ -1234,6 +1234,7 @@ func run() error { HasRCFileServers: hasRCFileServers, PromptsCache: promptsCache, AccessLog: accessLogConfig, + BeadsCache: true, // mitto-is2.5: read-cache on by default in the macOS app } // Set legacy fields as fallback (for auxiliary sessions, etc.) diff --git a/config/agents/builtin/augment/metadata.yaml b/config/agents/builtin/augment/metadata.yaml index c69d258c1..2fc53cdaa 100644 --- a/config/agents/builtin/augment/metadata.yaml +++ b/config/agents/builtin/augment/metadata.yaml @@ -12,3 +12,28 @@ install: args: ["--workspace-root=$MITTO_WORKING_DIR", "--acp"] mcp: scopes: ["user", "project", "local"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess. Default (~4-6 GB) + # is exhausted by large agent turns on big workspaces, aborting the process + # with FATAL ERROR: Ineffective mark-compacts near heap limit and tearing + # down every session sharing it (mitto-54k.10). Users can override in + # settings.json acp_servers[].env if 12 GB is inappropriate for their host. + env: + NODE_OPTIONS: "--max-old-space-size=12288" +# Per-agent stderr patterns (mitto-k6h). Unioned with the hardcoded baseline in +# internal/conversation. Regexes are compiled once at process start; a bad regex +# is skipped with a warn log (never fatal). +stderrPatterns: + # Node/V8 fatal-heap messages already covered by baseline; listed here as a + # documented example that per-agent extension is allowed. + crash: + - "FATAL ERROR: .* Allocation failed" + ignore: + # Suppress node's MaxListenersExceededWarning noise (plugin-config-changed + # listeners emitted by auggie's plugin subsystem during hot init). + - "(?i)MaxListenersExceededWarning" + - "(?i)Use `node --trace-warnings" + - "(?i)method not found" + degraded: + # Plumbed for future use (mitto-k6h defers behavioural wiring). + - "(?i)rate limit" diff --git a/config/agents/builtin/claude-code/cmds/mcp-list.sh b/config/agents/builtin/claude-code/cmds/mcp-list.sh index ac409b52e..e218f1fec 100755 --- a/config/agents/builtin/claude-code/cmds/mcp-list.sh +++ b/config/agents/builtin/claude-code/cmds/mcp-list.sh @@ -3,35 +3,84 @@ # Input: {"path": "/optional/workspace/path"} (optional, via stdin) # Output: {"servers": [{"name": "...", "command": "...", "args": [...], "url": "...", "env": {...}}]} +# Claude Code stores MCP servers under "mcpServers" in: +# user: ~/.claude.json (top-level mcpServers) +# local: ~/.claude.json (per-project entry: projects..mcpServers) +# project: /.mcp.json (top-level mcpServers, shared/checked-in) +# NOTE: ~/.claude/settings.json is NOT read for mcpServers (silently ignored by Claude Code). +# Later scopes override earlier ones by server name. + INPUT=$(cat 2>/dev/null || echo '{}') -CONFIG_FILE="${HOME}/.claude/settings.json" -if [ ! -f "$CONFIG_FILE" ]; then - echo '{"servers": []}' - exit 0 +# Extract optional workspace path from input +WORKSPACE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('path',''))" 2>/dev/null) + +USER_CONFIG="$HOME/.claude.json" +PROJECT_CONFIG="" +if [ -n "$WORKSPACE_PATH" ]; then + PROJECT_CONFIG="$WORKSPACE_PATH/.mcp.json" fi +# Merge mcpServers from all scopes (paths passed via env to avoid quoting issues). +MITTO_USER_CONFIG="$USER_CONFIG" \ +MITTO_PROJECT_CONFIG="$PROJECT_CONFIG" \ +MITTO_WORKSPACE_PATH="$WORKSPACE_PATH" \ python3 -c " -import json, sys -try: - with open('$CONFIG_FILE') as f: - config = json.load(f) - servers = config.get('mcpServers', {}) - result = [] - for name, cfg in servers.items(): - entry = {'name': name} - if 'command' in cfg: - entry['command'] = cfg['command'] - if 'args' in cfg: - entry['args'] = cfg['args'] - if 'url' in cfg: - entry['url'] = cfg['url'] - if 'env' in cfg: - entry['env'] = cfg['env'] - if 'headers' in cfg: - entry['headers'] = cfg['headers'] - result.append(entry) - print(json.dumps({'servers': result})) -except Exception: - print(json.dumps({'servers': []})) +import json, os + +def load_json(path): + if not path or not os.path.isfile(path): + return None + try: + with open(path) as f: + return json.load(f) + except Exception: + return None + +def servers_of(data): + if not isinstance(data, dict): + return {} + servers = data.get('mcpServers', {}) + return servers if isinstance(servers, dict) else {} + +merged = {} + +# 1) user scope: top-level mcpServers in ~/.claude.json +user_data = load_json(os.environ.get('MITTO_USER_CONFIG', '')) +for name, cfg in servers_of(user_data).items(): + if isinstance(cfg, dict): + merged[name] = cfg + +# 2) local scope: per-project mcpServers keyed by workspace path in ~/.claude.json +ws = os.environ.get('MITTO_WORKSPACE_PATH', '') +if ws and isinstance(user_data, dict): + projects = user_data.get('projects', {}) + if isinstance(projects, dict): + proj = projects.get(ws, {}) + for name, cfg in servers_of(proj).items(): + if isinstance(cfg, dict): + merged[name] = cfg + +# 3) project scope: /.mcp.json +proj_data = load_json(os.environ.get('MITTO_PROJECT_CONFIG', '')) +for name, cfg in servers_of(proj_data).items(): + if isinstance(cfg, dict): + merged[name] = cfg + +result = [] +for name, cfg in merged.items(): + entry = {'name': name} + if cfg.get('command'): + entry['command'] = cfg['command'] + if cfg.get('args'): + entry['args'] = cfg['args'] + if cfg.get('url'): + entry['url'] = cfg['url'] + if cfg.get('env'): + entry['env'] = cfg['env'] + if cfg.get('headers'): + entry['headers'] = cfg['headers'] + result.append(entry) + +print(json.dumps({'servers': result})) " diff --git a/config/agents/builtin/claude-code/metadata.yaml b/config/agents/builtin/claude-code/metadata.yaml index 3f3546960..21168f930 100644 --- a/config/agents/builtin/claude-code/metadata.yaml +++ b/config/agents/builtin/claude-code/metadata.yaml @@ -5,6 +5,12 @@ acpId: "claude-code" description: "ACP wrapper for Anthropic's Claude" repository: "https://github.com/agentclientprotocol/claude-agent-acp" license: "proprietary" +# claude-agent-acp forks a fresh `claude` OS process per ACP session/new, +# unlike auggie which multiplexes over one node process. The aux-prewarm +# scheduler uses this to widen its stagger and rely on rush-on-demand so it +# does not fork N cold `claude` processes back-to-back at cold start +# (mitto-7yj). +sessionSpawnsProcess: true install: method: "npx" package: "@agentclientprotocol/claude-agent-acp" @@ -12,3 +18,21 @@ mcp: scopes: ["user", "project", "local"] defaults: contextFlushCommand: "/clear" + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" +# Per-agent stderr patterns (mitto-k6h). Unioned with the hardcoded baseline. +stderrPatterns: + crash: + # Rust-layer panics from claude-code-agent-sdk that don't already match + # baseline substrings. + - "^thread '.*' panicked at" + ignore: + - "(?i)method not found" + # OpenTelemetry startup warnings are informational. + - "(?i)OTEL_" + degraded: + # Anthropic quota / rate-limit chatter (plumbed only, mitto-k6h). + - "(?i)rate limit" + - "(?i)429 Too Many Requests" diff --git a/config/agents/builtin/cline/cmds/mcp-list.sh b/config/agents/builtin/cline/cmds/mcp-list.sh index b8cfa2c74..d5d20df55 100755 --- a/config/agents/builtin/cline/cmds/mcp-list.sh +++ b/config/agents/builtin/cline/cmds/mcp-list.sh @@ -3,35 +3,82 @@ # Input: {"path": "/optional/workspace/path"} (optional, via stdin) # Output: {"servers": [{"name": "...", "command": "...", "args": [...], "url": "...", "env": {...}}]} -INPUT=$(cat 2>/dev/null || echo '{}') -CONFIG_FILE="${HOME}/.cline/mcp_settings.json" +# Cline (VSCode extension "saoudrizwan.claude-dev") stores MCP servers under +# "mcpServers" in its globalStorage settings file. Location is OS-specific: +# macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json +# Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json +# Windows: %APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json +# CLI/SDK variant: ~/.cline/data/settings/cline_mcp_settings.json +# Overrides honored: CLINE_MCP_SETTINGS_PATH (full file path), CLINE_DIR (base dir). +# The first existing candidate wins. -if [ ! -f "$CONFIG_FILE" ]; then - echo '{"servers": []}' - exit 0 -fi +INPUT=$(cat 2>/dev/null || echo '{}') python3 -c " -import json, sys -try: - with open('$CONFIG_FILE') as f: - config = json.load(f) - servers = config.get('mcpServers', {}) - result = [] - for name, cfg in servers.items(): - entry = {'name': name} - if 'command' in cfg: - entry['command'] = cfg['command'] - if 'args' in cfg: - entry['args'] = cfg['args'] - if 'url' in cfg: - entry['url'] = cfg['url'] - if 'env' in cfg: - entry['env'] = cfg['env'] - if 'headers' in cfg: - entry['headers'] = cfg['headers'] - result.append(entry) - print(json.dumps({'servers': result})) -except Exception: - print(json.dumps({'servers': []})) +import json, os, sys + +home = os.path.expanduser('~') +rel = os.path.join('saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json') + +candidates = [] + +# Explicit overrides first. +override = os.environ.get('CLINE_MCP_SETTINGS_PATH', '') +if override: + candidates.append(override) +cline_dir = os.environ.get('CLINE_DIR', '') +if cline_dir: + candidates.append(os.path.join(cline_dir, 'data', 'settings', 'cline_mcp_settings.json')) + +# OS-specific VSCode globalStorage location. +if sys.platform == 'darwin': + candidates.append(os.path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', rel)) +elif sys.platform.startswith('win'): + appdata = os.environ.get('APPDATA', os.path.join(home, 'AppData', 'Roaming')) + candidates.append(os.path.join(appdata, 'Code', 'User', 'globalStorage', rel)) +else: + candidates.append(os.path.join(home, '.config', 'Code', 'User', 'globalStorage', rel)) + +# CLI/SDK variant. +candidates.append(os.path.join(home, '.cline', 'data', 'settings', 'cline_mcp_settings.json')) + +def load(path): + if not path or not os.path.isfile(path): + return None + try: + with open(path) as f: + return json.load(f) + except Exception: + return None + +config = None +for path in candidates: + config = load(path) + if config is not None: + break + +servers = {} +if isinstance(config, dict): + s = config.get('mcpServers', {}) + if isinstance(s, dict): + servers = s + +result = [] +for name, cfg in servers.items(): + if not isinstance(cfg, dict): + continue + entry = {'name': name} + if cfg.get('command'): + entry['command'] = cfg['command'] + if cfg.get('args'): + entry['args'] = cfg['args'] + if cfg.get('url'): + entry['url'] = cfg['url'] + if cfg.get('env'): + entry['env'] = cfg['env'] + if cfg.get('headers'): + entry['headers'] = cfg['headers'] + result.append(entry) + +print(json.dumps({'servers': result})) " diff --git a/config/agents/builtin/cline/metadata.yaml b/config/agents/builtin/cline/metadata.yaml index 0130d2621..d141597e8 100644 --- a/config/agents/builtin/cline/metadata.yaml +++ b/config/agents/builtin/cline/metadata.yaml @@ -12,3 +12,8 @@ install: args: ["--acp"] mcp: scopes: ["user", "project"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" diff --git a/config/agents/builtin/codex/cmds/mcp-list.sh b/config/agents/builtin/codex/cmds/mcp-list.sh index 045265098..ceee3858d 100755 --- a/config/agents/builtin/codex/cmds/mcp-list.sh +++ b/config/agents/builtin/codex/cmds/mcp-list.sh @@ -3,35 +3,182 @@ # Input: {"path": "/optional/workspace/path"} (optional, via stdin) # Output: {"servers": [{"name": "...", "command": "...", "args": [...], "url": "...", "env": {...}}]} +# Codex stores MCP servers in TOML under [mcp_servers.] tables in: +# user: ~/.codex/config.toml +# project: /.codex/config.toml (trusted projects) +# Each table has: command, args = [...], url, and an [mcp_servers..env] subtable. +# Later scopes override earlier ones by server name. +# TOML is parsed via tomllib/tomli when available, else a minimal embedded parser +# (the machine's python3 may lack tomllib, so we must not depend on py3.11+). + INPUT=$(cat 2>/dev/null || echo '{}') -CONFIG_FILE="${HOME}/.codex/config.json" -if [ ! -f "$CONFIG_FILE" ]; then - echo '{"servers": []}' - exit 0 +# Extract optional workspace path from input +WORKSPACE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('path',''))" 2>/dev/null) + +USER_CONFIG="$HOME/.codex/config.toml" +PROJECT_CONFIG="" +if [ -n "$WORKSPACE_PATH" ]; then + PROJECT_CONFIG="$WORKSPACE_PATH/.codex/config.toml" fi +MITTO_USER_CONFIG="$USER_CONFIG" \ +MITTO_PROJECT_CONFIG="$PROJECT_CONFIG" \ python3 -c " -import json, sys -try: - with open('$CONFIG_FILE') as f: - config = json.load(f) - servers = config.get('mcpServers', {}) - result = [] - for name, cfg in servers.items(): - entry = {'name': name} - if 'command' in cfg: - entry['command'] = cfg['command'] - if 'args' in cfg: - entry['args'] = cfg['args'] - if 'url' in cfg: - entry['url'] = cfg['url'] - if 'env' in cfg: - entry['env'] = cfg['env'] - if 'headers' in cfg: - entry['headers'] = cfg['headers'] - result.append(entry) - print(json.dumps({'servers': result})) -except Exception: - print(json.dumps({'servers': []})) +import json, os, re + +def parse_toml(text): + # Prefer a real TOML parser when present. + try: + import tomllib # py3.11+ + return tomllib.loads(text) + except Exception: + pass + try: + import tomli # backport + return tomli.loads(text) + except Exception: + pass + return _mini_toml(text) + +def _strip_comment(s): + # Remove an unquoted trailing '#' comment. + out = [] + in_str = False + quote = '' + i = 0 + while i < len(s): + c = s[i] + if in_str: + out.append(c) + if c == quote: + in_str = False + else: + if c in ('\"', \"'\"): + in_str = True + quote = c + out.append(c) + elif c == '#': + break + else: + out.append(c) + i += 1 + return ''.join(out) + +def _parse_value(v): + v = v.strip() + if not v: + return '' + if v[0] == '[' and v[-1] == ']': + inner = v[1:-1].strip() + if not inner: + return [] + # Split top-level commas (values here are simple strings/numbers). + items, buf, in_str, quote = [], [], False, '' + for c in inner: + if in_str: + buf.append(c) + if c == quote: + in_str = False + elif c in ('\"', \"'\"): + in_str = True + quote = c + buf.append(c) + elif c == ',': + items.append(''.join(buf).strip()) + buf = [] + else: + buf.append(c) + if buf: + items.append(''.join(buf).strip()) + return [_parse_scalar(x) for x in items if x != ''] + return _parse_scalar(v) + +def _parse_scalar(v): + v = v.strip() + if len(v) >= 2 and v[0] == v[-1] and v[0] in ('\"', \"'\"): + return v[1:-1] + if v == 'true': + return True + if v == 'false': + return False + try: + if re.fullmatch(r'-?[0-9]+', v): + return int(v) + return float(v) + except Exception: + return v + +def _mini_toml(text): + root = {} + cur = root + for raw in text.splitlines(): + line = _strip_comment(raw).strip() + if not line: + continue + if line.startswith('[') and line.endswith(']'): + path = line[1:-1].strip() + # Split on unquoted dots. + parts, buf, in_str, quote = [], [], False, '' + for c in path: + if in_str: + buf.append(c) + if c == quote: + in_str = False + elif c in ('\"', \"'\"): + in_str = True + quote = c + elif c == '.': + parts.append(''.join(buf).strip()) + buf = [] + else: + buf.append(c) + if buf: + parts.append(''.join(buf).strip()) + cur = root + for p in parts: + cur = cur.setdefault(p, {}) + continue + if '=' in line: + k, _, v = line.partition('=') + cur[k.strip()] = _parse_value(v) + return root + +def servers_of(data): + if not isinstance(data, dict): + return {} + s = data.get('mcp_servers', {}) + return s if isinstance(s, dict) else {} + +def load(path): + if not path or not os.path.isfile(path): + return {} + try: + with open(path) as f: + return parse_toml(f.read()) + except Exception: + return {} + +merged = {} +for var in ('MITTO_USER_CONFIG', 'MITTO_PROJECT_CONFIG'): + for name, cfg in servers_of(load(os.environ.get(var, ''))).items(): + if isinstance(cfg, dict): + merged[name] = cfg + +result = [] +for name, cfg in merged.items(): + entry = {'name': name} + if cfg.get('command'): + entry['command'] = cfg['command'] + if cfg.get('args'): + entry['args'] = cfg['args'] + if cfg.get('url'): + entry['url'] = cfg['url'] + if cfg.get('env'): + entry['env'] = cfg['env'] + if cfg.get('headers'): + entry['headers'] = cfg['headers'] + result.append(entry) + +print(json.dumps({'servers': result})) " diff --git a/config/agents/builtin/codex/metadata.yaml b/config/agents/builtin/codex/metadata.yaml index a77807cda..a3108b5fa 100644 --- a/config/agents/builtin/codex/metadata.yaml +++ b/config/agents/builtin/codex/metadata.yaml @@ -10,3 +10,8 @@ install: package: "@zed-industries/codex-acp" mcp: scopes: ["user"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" diff --git a/config/agents/builtin/gemini/metadata.yaml b/config/agents/builtin/gemini/metadata.yaml index fcaca680b..69363f3e4 100644 --- a/config/agents/builtin/gemini/metadata.yaml +++ b/config/agents/builtin/gemini/metadata.yaml @@ -12,3 +12,18 @@ install: args: ["--acp"] mcp: scopes: ["user", "project"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" +# Per-agent stderr patterns (mitto-k6h). Unioned with the hardcoded baseline. +stderrPatterns: + ignore: + - "(?i)method not found" + # Node deprecation notices from @google/gemini-cli. + - "(?i)DeprecationWarning" + degraded: + # Google API quota chatter (plumbed only, mitto-k6h). + - "(?i)RESOURCE_EXHAUSTED" + - "(?i)quota exceeded" diff --git a/config/agents/builtin/github-copilot/metadata.yaml b/config/agents/builtin/github-copilot/metadata.yaml index a1fb5dae0..0a3e80e04 100644 --- a/config/agents/builtin/github-copilot/metadata.yaml +++ b/config/agents/builtin/github-copilot/metadata.yaml @@ -12,3 +12,8 @@ install: args: ["--acp"] mcp: scopes: ["user", "project"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" diff --git a/config/agents/builtin/kilo/metadata.yaml b/config/agents/builtin/kilo/metadata.yaml index 4163096a8..d6fbe8c7b 100644 --- a/config/agents/builtin/kilo/metadata.yaml +++ b/config/agents/builtin/kilo/metadata.yaml @@ -12,3 +12,8 @@ install: args: ["acp"] mcp: scopes: ["user", "project"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" diff --git a/config/agents/builtin/qwen-code/metadata.yaml b/config/agents/builtin/qwen-code/metadata.yaml index 58972f385..5913b4b2e 100644 --- a/config/agents/builtin/qwen-code/metadata.yaml +++ b/config/agents/builtin/qwen-code/metadata.yaml @@ -12,3 +12,8 @@ install: args: ["--acp", "--experimental-skills"] mcp: scopes: ["user", "project"] +defaults: + # Raise Node/V8 old-space heap cap for the ACP subprocess (mitto-54k.10). + # See augment/metadata.yaml for rationale. + env: + NODE_OPTIONS: "--max-old-space-size=12288" diff --git a/config/beads_loop_prompts_defects_test.go b/config/beads_loop_prompts_defects_test.go new file mode 100644 index 000000000..b567758be --- /dev/null +++ b/config/beads_loop_prompts_defects_test.go @@ -0,0 +1,99 @@ +package config + +import ( + "io/fs" + "regexp" + "strings" + "testing" +) + +// TestBeadsLoopPrompts_Defects_mitto6am is the failing reproduction test for +// mitto-6am: the "Loop implementing features" / "Loop implementing feature" +// prompt family shares the same three structural defects as its bug-fix +// siblings (mitto-dj9, mitto-i5k, mitto-fko), plus a fourth Step 3f fan-out +// vector unique to the features driver. +// +// This test asserts the ABSENCE of the four defective patterns in the +// embedded builtin prompt YAMLs. While the defects are still present it +// fails; when the fix phase rewrites the prompts (prompt_name-based spawns, +// unconditional Done branch, recently-closed-parents filter, named worker +// prompt for Step 3f) it will flip to green. +func TestBeadsLoopPrompts_Defects_mitto6am(t *testing.T) { + const ( + bugsOrch = "beads-issue-loop-fixing-bugs.prompt.yaml" + bugDriver = "beads-issue-loop-fixing-bug.prompt.yaml" + featsOrch = "beads-issue-loop-implementing-features.prompt.yaml" + featDriver = "beads-issue-loop-implementing-feature.prompt.yaml" + ) + + load := func(name string) string { + b, err := fs.ReadFile(BuiltinPromptsFS, BuiltinPromptsDir+"/"+name) + if err != nil { + t.Fatalf("read embedded prompt %s: %v", name, err) + } + return string(b) + } + + // Defect 1 — placeholder short-circuit vector (mitto-dj9). + // Orchestrator Step 4 spawns children via `initial_prompt: ` + + // `loop_prompt: ` instead of `prompt_name:` + `arguments:`. + // Fix: replace with `prompt_name: "Loop fixing bug" | "Loop implementing feature"`. + placeholderInitial := regexp.MustCompile(`initial_prompt:\s*`) + placeholderLoop := regexp.MustCompile(`loop_prompt:\s*`) + for _, name := range []string{bugsOrch, featsOrch} { + body := load(name) + if placeholderInitial.MatchString(body) { + t.Errorf("[defect-1 placeholder-vector, mitto-dj9] %s still contains `initial_prompt: ` at Step 4; expected `prompt_name:` + `arguments:` so the server expands the driver body from the named template", name) + } + if placeholderLoop.MatchString(body) { + t.Errorf("[defect-1 placeholder-vector, mitto-dj9] %s still contains `loop_prompt: ` at Step 4; expected `prompt_name:` + `arguments:` so the server expands the driver body from the named template", name) + } + } + + // Defect 2 — soft-gated Done branch (mitto-i5k). + // Per-item driver Done branch marks `bd close` as "optional but + // recommended", inviting the LLM to skip past `loop_enabled: false`. + // Fix: unconditional `bd close` + unconditional `loop_enabled: false`, + // evaluated as the very first branch against a mandatory fresh + // `bd show --json`. + softClose := regexp.MustCompile(`bd close[^\n]*# optional but recommended`) + for _, name := range []string{bugDriver, featDriver} { + body := load(name) + if softClose.MatchString(body) { + t.Errorf("[defect-2 soft-gated-done, mitto-i5k] %s Done branch still marks `bd close` as `# optional but recommended`; expected unconditional close + unconditional loop_enabled:false as the first branch of Step 3", name) + } + } + + // Defect 3 — subtask spawn after parent closes (mitto-fko). + // Orchestrator Step 2 enumerates via `bd ready` (falling back to + // `bd list --status open`) with no filter for entries whose + // `parent-child` dependency's parent was closed within the current + // outer run. The prompt should mention this filter explicitly so the + // LLM applies it. + // + // Signal the fix has been applied by requiring EITHER a mention of + // `parent-child` OR of `recently-closed` in the orchestrator body. + // This is a lint-style check on prompt text, not on behaviour: the + // fix must instruct the LLM to exclude recently-closed parents' + // children. + parentChild := regexp.MustCompile(`parent-child`) + recentlyClosed := regexp.MustCompile(`recently[- ]closed`) + for _, name := range []string{bugsOrch, featsOrch} { + body := load(name) + if !parentChild.MatchString(body) && !recentlyClosed.MatchString(body) { + t.Errorf("[defect-3 subtask-spawn, mitto-fko] %s Step 2 has no filter language for `parent-child` deps or `recently-closed` parents; expected instructions to exclude beads whose parent bead was closed within the current outer run", name) + } + } + + // Defect 4 — Step 3f grand-child fan-out with inline free-text worker + // prompts (features driver only, worse than the bug family because it + // spawns from *inside* an already-scheduled loop driver). + // The current text tells the LLM to synthesize a "fully self-contained + // worker prompt" inline; the fix registers that worker body as a named + // workspace prompt and spawns it via `prompt_name:` + `arguments:`. + body := load(featDriver) + if strings.Contains(body, "self-contained worker prompt") { + t.Errorf("[defect-4 step3f-inline-worker, mitto-6am-unique] %s Step 3f still tells the driver LLM to seed a `self-contained worker prompt` inline; expected `mitto_conversation_new(..., prompt_name: \"\", arguments: {...})` so the grand-child body is expanded server-side and cannot short-circuit to a placeholder", featDriver) + _ = body + } +} diff --git a/config/config.default.yaml b/config/config.default.yaml index b80b8e457..2fca3e808 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -33,8 +33,17 @@ acp: [] # first run); existing settings.json files are left untouched. Tags overlap by design: # a model name is matched against every profile and the union of matching tags applies # (e.g. "Claude Opus 4.x" resolves to Anthropic + Smartest + Reasoning + Expensive). -# Tags are interface-only today (parsed and exposed via the Go API, not yet consumed -# at runtime). Edit or extend these to match the models you use. +# +# Tags ARE consumed at runtime: a prompt's `preferredModels` (modelTag/modelName) +# resolves against these profiles to pick the model a prompt runs on. This YAML is only +# the first-run seed — the canonical, always-available set lives in Go +# (config.DefaultModelProfiles); the two are kept in sync by `make check-model-tags`. +# Edit or extend these to match the models you use. +# +# Priority: list order = priority. For tag-based resolution the FIRST profile carrying +# a tag wins, so put newer/preferred variants first. Example: if you list a "Claude +# Sonnet 5" profile before "Claude Sonnet 4" (both tagged Coding), prompts asking for +# `modelTag: Coding` resolve to Sonnet 5; swap the two entries and Sonnet 4 wins. models: - name: Claude criteria: @@ -45,11 +54,16 @@ models: criteria: matchMode: contains pattern: Opus - tags: [Smartest, Reasoning, Expensive] - - name: Claude Sonnet + tags: [Smartest, Reasoning, Thinking, Deep, Slow, Expensive] + - name: Claude Sonnet 5 + criteria: + matchMode: contains + pattern: Sonnet 5 + tags: [Smart, Coding] + - name: Claude Sonnet 4 criteria: matchMode: contains - pattern: Sonnet + pattern: Sonnet 4 tags: [Smart, Coding] - name: Claude Haiku criteria: @@ -60,7 +74,7 @@ models: criteria: matchMode: contains pattern: GPT-5 - tags: [Smart, Reasoning, Coding] + tags: [Smart, Reasoning, Thinking, Deep, Coding] - name: GPT-4 criteria: matchMode: contains @@ -294,6 +308,26 @@ conversations: # external_images: # enabled: false # Allow external HTTPS images (default: false) +# Adaptive ACP/MCP pre-warming thresholds (mitto-mw0) +# Pre-warming warms a workspace, probes its health (session/new latency + MCP +# readiness), and pins a warm keepalive session only for slow/broken workspaces. +# Healthy workspaces are left alone (GC reaps them). +# prewarm: +# session_new_fast: "10s" # T_fast: session/new latency at/under which a workspace is "fast" +# mcp_ready: "10s" # T_mcp: max time for all configured MCP servers to be reachable +# healthy_probes_to_unpin: 3 # Hysteresis: consecutive healthy probes required before unpinning +# max_pin_duration: "30m" # Cap on how long a pinned session is held (use "disabled" for no cap) +# max_pinned_workspaces: 5 # Blast-radius cap on simultaneously-pinned workspaces +# # Per-purpose staggered delays for the cold-start auxiliary session prewarm +# # (mitto-cgc). A single worker serializes creation, so only one session/new +# # is in flight at a time; these delays offset each purpose from the moment +# # prewarm starts. Empty/invalid values fall back to the defaults shown. +# aux_schedule: +# mcp_check: "0s" # tier 0 — gates prompt/tool visibility +# mcp_tools: "0s" # tier 0 — tool discovery for gating +# title_gen: "5s" # tier 1 — not needed until first prompt titled +# follow_up: "8s" # tier 2 — only after first agent response + # Permission handling configuration # Controls how permission requests from agents are handled. # Permission requests occur when an agent wants to perform sensitive operations diff --git a/config/processors/builtin/auggie-manage-rules.yaml b/config/processors/builtin/auggie-manage-rules.yaml index 8ff5b95fe..7518d3bea 100644 --- a/config/processors/builtin/auggie-manage-rules.yaml +++ b/config/processors/builtin/auggie-manage-rules.yaml @@ -80,6 +80,21 @@ prompt: | - Make rules actionable for coding decisions - Include common pitfalls and anti-pattern examples - Reference existing repo docs rather than duplicating + - **Allowed content**: facts about *this codebase* (architecture, patterns, + conventions, APIs, gotchas, lessons learned from code changes) and generic + developer procedures that apply to any contributor working on this project + (how to build, run tests, lint, project-wide tooling commands). + - **Forbidden content**: never write anything personal or user-specific. + Do NOT include: + - Absolute paths containing usernames or home directories + (e.g. `/Users//...`, `/home//...`, `C:\Users\\...`). + - Locations of the current user's credentials, SSH keys, GitHub tokens, + API keys, or any other secrets. + - Machine-specific configuration (hostnames, local port numbers currently + in use, personal editor/IDE settings, shell aliases). + - Anything that could identify or leak information about the current user + or their local environment. + If in doubt, omit it — rules are shared across every contributor. ## Notification diff --git a/config/processors/builtin/auggie-update-rules.yaml b/config/processors/builtin/auggie-update-rules.yaml index e3526c476..97f8a9cd9 100644 --- a/config/processors/builtin/auggie-update-rules.yaml +++ b/config/processors/builtin/auggie-update-rules.yaml @@ -1,43 +1,37 @@ ########################################################################################## -# Builtin processor: updates Augment rules files from ongoing conversation insights. +# Builtin processor: updates Augment rules files when a conversation is archived. # # Companion to auggie-manage-rules.yaml (which handles initial generation). -# This processor fires once the agent goes idle (its message queue is drained), throttled -# by cadence (every 6 turns or 15K tokens or 5 minutes), to review recent conversation -# history and update the .augment/rules/ files with new patterns, conventions, and lessons -# learned. Firing on idle ensures the auxiliary agent sees the full exchange rather than a -# partial mid-burst turn. +# Fires once when the session enters the closed (archived or deleted) state — either +# through a manual archive from the UI, an automatic archive triggered by inactivity, +# or a session delete. The processor asks a workspace-scoped auxiliary AI agent to +# review the full archived conversation and update the .augment/rules/ files with new +# patterns, conventions, and lessons learned. +# +# Running at close time (rather than every few turns) means the reviewer sees the +# completed exchange in full instead of a mid-burst snapshot, and only one auxiliary +# LLM invocation is spent per session. # # Only activates when .augment/rules/ already exist (initial generation already done). # -# Fire-and-forget: the auxiliary agent works in the background while the main -# conversation continues normally. +# Fire-and-forget: the auxiliary agent works in the background after the main +# conversation is already archived. # # Enabled by default — disable per workspace in the Workspaces dialog or .mittorc. # Only activates when the ACP server is Auggie. ########################################################################################## name: auggie-update-rules -description: "Update .augment/rules/ files based on conversation insights and lessons learned" +description: "Update .augment/rules/ files based on insights from an archived conversation" enabled: true when: - on: agentIdle + on: conversationClosed match: all - stopReasons: [end_turn] - cadence: - everyNTurns: 6 - everyNTokens: 15000 - afterInterval: 5m priority: 200 timeout: 300s onError: skip -parameters: - - name: HistoryLimit - type: text - description: "How many recent user/agent messages the auxiliary agent reviews" - default: "10" - -# Only for Auggie sessions, skip loop prompts, and only when rules already exist +# Only for Auggie sessions, skip loop conversations, and only when rules already exist +# (initial generation already done). enabledWhen: 'ACP.MatchesServerType("augment") && !Session.IsLoop && DirExists(".augment/rules")' prompt: | @@ -83,25 +77,38 @@ prompt: | - Make rules actionable for coding decisions - Include common pitfalls and anti-pattern examples - Reference existing repo docs rather than duplicating - - === Recent Conversation === - - Use the `mitto_conversation_history` MCP tool to retrieve the recent conversation. - Call it with: + - **Allowed content**: facts about *this codebase* (architecture, patterns, + conventions, APIs, gotchas, lessons learned from code changes) and generic + developer procedures that apply to any contributor working on this project + (how to build, run tests, lint, project-wide tooling commands). + - **Forbidden content**: never write anything personal or user-specific. + Do NOT include: + - Absolute paths containing usernames or home directories + (e.g. `/Users//...`, `/home//...`, `C:\Users\\...`). + - Locations of the current user's credentials, SSH keys, GitHub tokens, + API keys, or any other secrets. + - Machine-specific configuration (hostnames, local port numbers currently + in use, personal editor/IDE settings, shell aliases). + - Anything that could identify or leak information about the current user + or their local environment. + If in doubt, omit it — rules are shared across every contributor. + If the recent conversation includes such personal/user-specific material, + do NOT copy it into any rules file — either omit it or generalize it into + a project-wide fact. + + === Conversation === + + Use the `mitto_conversation_history` MCP tool to retrieve the archived session's + full history. Call it with: - `self_id`: your session ID (from `mitto_conversation_get_current`) - `conversation_id`: "{{ .Session.ID }}" - `event_types`: ["user_prompt", "agent_message"] - - `last_n`: ${HistoryLimit:-10} + - `last_n`: 200 Review these messages for patterns, conventions, and lessons learned. - ## Notification - - After completing your work, if you updated any rules files, call - `mitto_ui_notify` with: - - `self_id`: "{{ .Session.ID }}" - - `title`: "✏️ Rules Updated" - - `message`: a brief summary, e.g. "Updated 3 rules files with new patterns" - - `style`: "success" + ## Silent completion - If no changes were needed, do NOT send any notification — stay completely silent. + This is a background clean-up processor. Do NOT call `mitto_ui_notify` and do NOT + post any user-visible summary — the session is already archived and there is no + active UI to notify. If no changes were needed, just exit silently. diff --git a/config/processors/builtin/beads-prime.yaml b/config/processors/builtin/beads-prime.yaml index b44b6ac9f..7326cb8f7 100644 --- a/config/processors/builtin/beads-prime.yaml +++ b/config/processors/builtin/beads-prime.yaml @@ -35,4 +35,4 @@ input: none output: prepend outputFormat: raw priority: 92 -timeout: 10s +timeout: 30s diff --git a/config/processors/builtin/claude-update-memory.yaml b/config/processors/builtin/claude-update-memory.yaml index 821349ad2..102a7f2b4 100644 --- a/config/processors/builtin/claude-update-memory.yaml +++ b/config/processors/builtin/claude-update-memory.yaml @@ -1,37 +1,37 @@ ########################################################################################## -# Builtin processor: updates Claude Code memory files from ongoing conversation insights. +# Builtin processor: updates Claude Code memory files when a conversation is archived. # # Companion to claude-manage-memory.yaml (which handles initial generation). -# This processor fires once the agent goes idle (its message queue is drained), throttled -# by cadence (every 6 turns or 15K tokens or 5 minutes), to review recent conversation -# history and update the CLAUDE.md / .claude/ files with new patterns, conventions, and -# lessons learned. Firing on idle ensures the auxiliary agent sees the full exchange rather -# than a partial mid-burst turn. +# Fires once when the session enters the closed (archived or deleted) state — either +# through a manual archive from the UI, an automatic archive triggered by inactivity, +# or a session delete. The processor asks a workspace-scoped auxiliary AI agent to +# review the full archived conversation and update the CLAUDE.md / .claude/ files with +# new patterns, conventions, and lessons learned. +# +# Running at close time (rather than every few turns) means the reviewer sees the +# completed exchange in full instead of a mid-burst snapshot, and only one auxiliary +# LLM invocation is spent per session. # # Only activates when memory files already exist (initial generation already done). # -# Fire-and-forget: the auxiliary agent works in the background while the main -# conversation continues normally. +# Fire-and-forget: the auxiliary agent works in the background after the main +# conversation is already archived. # # Enabled by default — disable per workspace in the Workspaces dialog or .mittorc. # Only activates when the ACP server is Claude Code. ########################################################################################## name: claude-update-memory -description: "Update Claude Code memory files based on conversation insights and lessons learned" +description: "Update Claude Code memory files based on insights from an archived conversation" enabled: true when: - on: agentIdle + on: conversationClosed match: all - stopReasons: [end_turn] - cadence: - everyNTurns: 6 - everyNTokens: 15000 - afterInterval: 5m priority: 200 timeout: 300s onError: skip -# Only for Claude Code sessions, skip loop prompts, and only when memory files already exist +# Only for Claude Code sessions, skip loop conversations, and only when memory files +# already exist (initial generation already done). enabledWhen: 'ACP.MatchesServerType("claude-code") && !Session.IsLoop && (FileExists("CLAUDE.md") || DirExists(".claude"))' prompt: | @@ -83,24 +83,19 @@ prompt: | - Include anti-pattern examples - Reference existing repo docs rather than duplicating - === Recent Conversation === + === Conversation === - Use the `mitto_conversation_history` MCP tool to retrieve the recent conversation. - Call it with: + Use the `mitto_conversation_history` MCP tool to retrieve the archived session's + full history. Call it with: - `self_id`: your session ID (from `mitto_conversation_get_current`) - `conversation_id`: "{{ .Session.ID }}" - `event_types`: ["user_prompt", "agent_message"] - - `last_n`: 30 + - `last_n`: 200 Review these messages for patterns, conventions, and lessons learned. - ## Notification - - After completing your work, if you updated any memory files, call - `mitto_ui_notify` with: - - `self_id`: "{{ .Session.ID }}" - - `title`: "🧠 Memory Updated" - - `message`: a brief summary, e.g. "Updated 2 memory files with new patterns" - - `style`: "success" + ## Silent completion - If no changes were needed, do NOT send any notification — stay completely silent. + This is a background clean-up processor. Do NOT call `mitto_ui_notify` and do NOT + post any user-visible summary — the session is already archived and there is no + active UI to notify. If no changes were needed, just exit silently. diff --git a/config/processors/builtin/curate-memories-on-close.yaml b/config/processors/builtin/curate-memories-on-close.yaml new file mode 100644 index 000000000..e26837cc8 --- /dev/null +++ b/config/processors/builtin/curate-memories-on-close.yaml @@ -0,0 +1,114 @@ +########################################################################################## +# Builtin processor: prunes obsolete entries from the beads memory store on close. +# +# Complements `extract-memories-on-close` (which ADDS memories via `bd remember`) by +# REMOVING durable-memory entries that have become obsolete or superseded. Runs at +# conversation close as a fire-and-forget auxiliary agent; the session is already +# archived and there is no UI to notify. +# +# Approach B — auto-forget only on STRONG signal: +# - `obsolete` : the memory body self-declares SUPERSEDED / FALSIFIED / RETRACTED / +# CORRECTED / WITHDRAWN → auto `bd forget `. +# - `superseded` : another memory in the store clearly supersedes this one AND that +# survivor has been verified via `bd recall ` to inline +# the essential facts → auto `bd forget ` (subject to guardrails). +# - `uncertain` : refers to files, PRs, issue IDs, or branches that may no longer +# apply → file a `bd create --type task --priority p3` for human +# triage instead of auto-forgetting. +# - `keep` : everything else (default when unsure). +# +# Activation gates (all must hold): +# 1. `bd` command exists on PATH (CommandExists CEL gate) +# 2. The workspace contains a `.beads` directory (DirExists CEL gate) +# 3. The session is NOT a loop conversation (loops routinely open/close and would +# otherwise thrash the memories store on every iteration) +# +# Priority 210 — runs AFTER extract-memories-on-close (200) so the curator can also +# consider any memories that processor may have just added in this session. +# +# Enabled by default — disable per workspace in the Workspaces dialog or .mittorc. +########################################################################################## +name: curate-memories-on-close +description: "Prunes obsolete/superseded entries from the bd memory store on close (auto-forget on strong signal; uncertain cases become bd tasks)" +enabled: true +when: + on: conversationClosed + match: all +priority: 210 +timeout: 300s +onError: skip + +# Only fire when beads is initialised in this workspace, and never for loop sessions +# (they archive/unarchive frequently and would repeatedly re-scan the same store). +enabledWhen: 'CommandExists("bd") && DirExists(".beads") && !Session.IsLoop' + +prompt: | + You curate the beads memory store for {{ .Workspace.Folder }} after a conversation + has closed. Your ONLY job is to remove entries that are DEMONSTRABLY obsolete — + nothing else. When in doubt, KEEP the memory. + + ## 1. Load the current memories + + Run `bd memories --json` to get the full store. If it is empty or has fewer than + 3 memories, exit silently. + + ## 2. Classify each memory + + For each key, decide exactly one of: + + - `obsolete` — the memory body self-declares SUPERSEDED, FALSIFIED, RETRACTED, + CORRECTED, or WITHDRAWN (typically near the start of the body). + - `superseded` — another, NEWER memory in the store clearly covers the same topic + and either names this key as superseded/downgraded or plainly + subsumes its essential facts. + - `uncertain` — the memory refers to concrete files, PRs, issue IDs, branches, or + ticket numbers that may no longer exist / be current, but you + cannot confidently confirm it is stale. + - `keep` — anything else. This is the default. + + ## 3. Enforce guardrails BEFORE acting + + Apply ALL of these; violating any single one means the memory stays: + + - **KEEP is the default.** When in any doubt, keep the memory. + - **Cap forgets at 3 this run.** If more strong-signal candidates exist, downgrade + the excess to `uncertain` and file review tasks instead of forgetting them. + - **Never forget the newest memory of a topical cluster.** If two or more memories + overlap and it is not clear which is later / authoritative, keep them all. + - **Verify the survivor before forgetting a `superseded` candidate.** Run + `bd recall ` and confirm the survivor's body inlines the essential + facts of the candidate. If it does not, keep both. + - **Do not forget any memory you consulted this run** to make a classification + decision — that memory is by definition still influencing the store's behaviour. + - **Do not forget memories younger than 7 days.** Recent entries may still be + evolving; too fresh to be confidently "superseded". Use the memory's created / + updated timestamp from `bd memories --json`. + - **No secrets in `bd create` titles or descriptions.** When filing an `uncertain` + review task, copy the memory KEY only — never paste the memory body verbatim + into the beads issue. + + ## 4. Act + + Resolve every candidate to exactly one of the actions below. + + - For each `obsolete` and `superseded` candidate that PASSES every guardrail above + (up to the cap of 3 total this run): + + bd forget + + - For each `uncertain` candidate: + + bd create --type task --priority p3 \ + --title "Review possibly-stale memory: " \ + --description "Memory key may no longer apply. Run \ + 'bd recall ' and either 'bd forget ' or keep it. \ + Reasoning: ." + + - For `keep`: do nothing. + + ## 5. Silent completion + + This is a background clean-up processor. Do NOT call `mitto_ui_notify` and do NOT + post any user-visible summary — the session is already archived and there is no + active UI to notify. If nothing needed doing (no obsolete/superseded/uncertain + candidates, or every candidate failed a guardrail), just exit silently. diff --git a/config/processors/builtin/delegate-playwright.yaml b/config/processors/builtin/delegate-playwright.yaml index a4a0fce99..2da91cf72 100644 --- a/config/processors/builtin/delegate-playwright.yaml +++ b/config/processors/builtin/delegate-playwright.yaml @@ -1,20 +1,22 @@ ########################################################################################## # Builtin processor: delegates Playwright browser automation to a faster model. # -# When using a premium reasoning model (Opus, o3, etc.) and Playwright MCP tools -# are available (browser_* tools), this processor instructs the agent to delegate -# browser automation tasks to a cheaper/faster model via a child session. +# When the session's CURRENTLY ACTIVE model is a premium reasoning model and +# Playwright MCP tools are available (browser_* tools), this processor instructs +# the agent to delegate browser automation tasks to a cheaper/faster model via +# a child session. # # This avoids wasting expensive reasoning tokens on mechanical browser interactions # like clicking, typing, navigating, and taking screenshots. # # Activation conditions: -# 1. ACP server is a "smart" model (tagged "reasoning"/"thinking" or name matches) -# 2. Playwright MCP tools are present (browser_* pattern) -# 3. Mitto conversation tools are available (for spawning child sessions) +# 1. The active model carries a "Reasoning" or "Smartest" capability tag +# (from the `models:` profiles — e.g. Claude Opus, GPT-5). +# 2. Playwright MCP tools are present (browser_* pattern). +# 3. Mitto conversation tools are available (for spawning child sessions). # -# To activate, tag your premium ACP servers with "reasoning" in your mitto config, -# or use a name containing "opus", "o3", etc. +# Model tags track the live model (session/set_model aware), unlike ACP server +# tags/names, which are a static property of the server binary. ########################################################################################## name: delegate-playwright description: "Delegates Playwright browser automation to a faster model when using a premium reasoning model" @@ -29,16 +31,14 @@ when: mutate: append priority: 91 enabledWhen: >- - (ACP.Tags.exists(t, t == "reasoning") - || ACP.Tags.exists(t, t == "thinking") - || ACP.Name.matches("(?i)opus|o3|deep-research|codex")) + (Session.HasModelTag("Reasoning") || Session.HasModelTag("Smartest")) && Tools.HasAllPatterns(["browser_*", "mitto_conversation_*"]) text: | --- [Playwright Delegation Guidance] You have Playwright browser automation tools available (browser_navigate, browser_click, browser_snapshot, etc.), but you are running on a premium - reasoning model ({{ .ACP.Name }}) where these tools are wastefully expensive. + reasoning model ({{ .ACP.Name }}{{ if .Session.ModelName }}, model {{ .Session.ModelName }}{{ end }}) where these tools are wastefully expensive. IMPORTANT: Do NOT use the browser_* tools directly. Instead, delegate ALL browser automation tasks to a cheaper/faster child session: diff --git a/config/processors/builtin/delegate-to-coder.yaml b/config/processors/builtin/delegate-to-coder.yaml index 0f75dabff..da515c0ef 100644 --- a/config/processors/builtin/delegate-to-coder.yaml +++ b/config/processors/builtin/delegate-to-coder.yaml @@ -1,15 +1,29 @@ ########################################################################################## # Builtin processor: suggests delegating coding tasks to faster models. # -# This processor activates only for ACP servers tagged with "reasoning" or whose -# name contains keywords associated with premium reasoning models. It uses a CEL -# expression for flexible matching. +# Activates when the session is running a premium reasoning/thinking model. +# Two signals are checked (either fires the gate): # -# To activate this processor, tag your premium ACP servers with "reasoning" in -# your mitto config, or rename them to include a keyword like "opus", "o3", etc. +# 1) Session.HasModelTag(...) — matches the CURRENTLY ACTIVE model's canonical +# capability tags from the `models:` profiles (config.DefaultModelProfiles). +# Claude Opus is tagged [Smartest, Reasoning, Thinking, Deep, Slow, Expensive]; +# GPT-5 is tagged [Smart, Reasoning, Thinking, Deep, Coding]. This is the +# preferred signal but requires the agent to advertise its model via ACP +# session config options (category=model). Claude Code does; Auggie doesn't. +# +# 2) ACP.Tags.exists(...) — matches the workspace-configured tags on the +# current ACP server (settings.json acp[i].tags). Provides the fallback for +# agents that don't advertise a model (notably Auggie). The tag literals +# here are the same canonical vocabulary as (1) so users configuring their +# ACP server tags can reuse the same names (case-insensitive match). +# +# The prior condition gated on ACP server tags / server-name regex, which is +# the wrong grain: the same server (e.g. auggie, claude-code) can run any of +# Opus / Sonnet / Haiku / GPT / Gemini, and can switch models mid-session via +# session/set_model. Session.HasModelTag(...) tracks the live model instead; +# ACP.Tags is the fallback for agents that don't expose the live model. # # Uses the same CEL context as prompt enabledWhen expressions. -# Also requires mitto_conversation_* MCP tools to be available (Tools.HasPattern CEL). ########################################################################################## name: delegate-to-coder description: "Suggests delegating coding tasks to a faster model when using a premium reasoning model" @@ -24,13 +38,16 @@ when: mutate: append priority: 90 enabledWhen: >- - ACP.Tags.exists(t, t == "reasoning") - || ACP.Tags.exists(t, t == "thinking") - || ACP.Name.matches("(?i)opus|o3|deep-research|codex") + Session.HasModelTag("Reasoning") || Session.HasModelTag("Smartest") || + Session.HasModelTag("Thinking") || Session.HasModelTag("Deep") || + ACP.Tags.exists(t, t in [ + "Reasoning", "Smartest", "Thinking", "Deep", "Slow", "Expensive", + "reasoning", "smartest", "thinking", "deep", "slow", "expensive" + ]) text: | --- [Multi-Agent Delegation Guidance] - You are running on a premium reasoning model ({{ .ACP.Name }}). + You are running on a premium reasoning model ({{ .ACP.Name }}{{ if .Session.ModelName }}, model {{ .Session.ModelName }}{{ end }}). Your session ID is: {{ .Session.ID }} For tasks that involve extensive coding changes (writing code, refactoring, diff --git a/config/processors/builtin/extract-memories-on-close.yaml b/config/processors/builtin/extract-memories-on-close.yaml new file mode 100644 index 000000000..faff721ee --- /dev/null +++ b/config/processors/builtin/extract-memories-on-close.yaml @@ -0,0 +1,104 @@ +########################################################################################## +# Builtin processor: extracts durable project memories when a conversation is archived. +# +# Fires once when the session enters the closed (archived) state — either through a +# manual archive from the UI or an automatic archive triggered by inactivity. The +# processor asks a workspace-scoped auxiliary AI agent to review the conversation and +# save any durable, project-level knowledge (architectural decisions, gotchas, non- +# obvious behaviours) as beads memories via `bd remember`. +# +# Fire-and-forget: the auxiliary agent works in the background after the main +# conversation is already archived. Nothing is written to the closed session; the +# extracted knowledge lives on the workspace side (beads database) so it survives +# and is available to future sessions. +# +# Activation gates (all must hold): +# 1. `bd` command exists on PATH (CommandExists CEL gate) +# 2. The workspace contains a `.beads` directory (DirExists CEL gate) +# 3. The session is NOT a loop conversation (loops routinely open/close and would +# otherwise thrash the memories store on every iteration) +# +# Enabled by default — disable per workspace in the Workspaces dialog or .mittorc. +########################################################################################## +name: extract-memories-on-close +description: "Extracts durable project memories via bd remember when a conversation is archived" +enabled: true +when: + on: conversationClosed + match: all +priority: 200 +timeout: 300s +onError: skip + +# Only fire when beads is initialised in this workspace, and never for loop sessions +# (they archive/unarchive frequently and would repeatedly re-scan the same history). +enabledWhen: 'CommandExists("bd") && DirExists(".beads") && !Session.IsLoop' + +prompt: | + You review a Mitto conversation that has just been archived and preserve any durable, + project-level knowledge as beads memories so future sessions can benefit from it. + + Working directory: {{ .Workspace.Folder }} + Archived session ID: {{ .Session.ID }} + + ## 1. Read the conversation + + Use the `mitto_conversation_history` MCP tool to retrieve the archived session's + history. Call it with: + - `self_id`: your session ID (from `mitto_conversation_get_current`) + - `conversation_id`: "{{ .Session.ID }}" + - `event_types`: ["user_prompt", "agent_message"] + - `last_n`: 50 + + If the history is empty or trivially short (fewer than ~4 real exchanges), do nothing + and exit silently. + + ## 2. Extract durable, project-level memories + + Look for knowledge that would help a *future* agent working in this same project. + Good candidates: + - **Architectural decisions**: why a component is structured a particular way, why + an alternative was rejected. + - **Non-obvious behaviours or gotchas**: subtle bugs discovered, order-of-operations + requirements, race conditions, quirks in third-party libraries. + - **Conventions confirmed during this conversation**: naming, layering, testing style, + that the project follows (as evidenced by the exchange, not merely stated). + - **Discovered facts about the codebase**: where important logic lives, how + subsystems fit together, invariants that must hold. + + Do NOT record: + - Personal user preferences (those belong in the user-preferences file, not memories). + - One-off task descriptions ("fix bug X", "implement feature Y"). + - Ephemeral debugging notes that don't outlive the immediate task. + - Anything that's already obvious from the code itself. + - Anything you already remembered in a previous session (check first — see below). + + Be conservative. It is better to remember nothing than to pollute the store with + noise. When in doubt, skip. + + ## 3. Deduplicate against existing memories + + Before adding anything, run `bd memory list` (or equivalent) to see what has already + been recorded. Skip any memory that is already captured, and prefer refining an + existing entry over adding a near-duplicate. + + ## 4. Persist via `bd remember` + + For each genuinely new memory, call: + + ``` + bd remember "" + ``` + + Keep each memory: + - **Self-contained** — future readers won't have the conversation context. + - **Specific** — reference concrete files, functions, or subsystems where useful. + - **Durable** — phrase it as a lasting fact about the project, not a task narrative. + - **Short** — one to three sentences per memory, never more. + + ## 5. Silent success + + This is a background clean-up processor. Do NOT call `mitto_ui_notify` and do NOT + post any user-visible summary — the session is already archived and there is no + active UI to notify. If you added no memories (nothing worth preserving, or all + candidates were already recorded), just exit silently. diff --git a/config/processors/builtin/memorize-preferences.yaml b/config/processors/builtin/memorize-preferences.yaml index bcf3e91a3..6009741fe 100644 --- a/config/processors/builtin/memorize-preferences.yaml +++ b/config/processors/builtin/memorize-preferences.yaml @@ -1,16 +1,21 @@ ########################################################################################## -# Builtin processor: automatically extracts user preferences from conversations. +# Builtin processor: extracts personal user preferences when a conversation is archived. # -# This prompt-mode processor watches user messages for PERSONAL preferences about -# how this individual user likes to work — the kind of thing that is specific to -# them and would not necessarily be shared with other people working on the same -# project. When it finds such a preference, it instructs an auxiliary AI agent to -# save it in a target file. That file is auto-detected from the project's rules -# directory (.augment/rules/, .cursor/rules/, or .codex/rules/) unless -# PreferencesFile is set; if none is found, nothing is written. +# Fires once when the session enters the closed (archived or deleted) state — either +# through a manual archive from the UI, an automatic archive triggered by inactivity, +# or a session delete. The processor asks a workspace-scoped auxiliary AI agent to +# review the full conversation and save any PERSONAL preferences about how this +# individual user likes to work — the kind of thing that is specific to them and +# would not necessarily be shared with other people working on the same project. # -# This is a fire-and-forget processor: the prompt is dispatched to a workspace-scoped -# auxiliary ACP session and the pipeline continues immediately without waiting. +# The target file is auto-detected from the project's rules directory +# (.augment/rules/, .cursor/rules/, or .codex/rules/) unless PreferencesFile is set; +# if none is found, nothing is written. +# +# Fire-and-forget: the auxiliary agent works in the background after the main +# conversation is already archived. Running at close time (rather than every few turns) +# means the reviewer sees the completed exchange in full instead of a mid-burst +# snapshot, and only one auxiliary LLM invocation is spent per session. # # Enabled by default — disable in the Workspaces dialog or .mittorc if you want # to turn off automatic preference tracking. @@ -42,21 +47,17 @@ # conservatively — when in doubt, an entry is kept. ########################################################################################## name: memorize-preferences -description: "Extracts personal, user-specific preferences (not project/code conventions) from conversations and saves them to a target file (auto-detected from .augment/rules/, .cursor/rules/, or .codex/rules/ unless PreferencesFile is set)" +description: "Extracts personal, user-specific preferences (not project/code conventions) from an archived conversation and saves them to a target file (auto-detected from .augment/rules/, .cursor/rules/, or .codex/rules/ unless PreferencesFile is set)" enabled: true when: - on: agentIdle + on: conversationClosed match: all - stopReasons: [end_turn] - cadence: - everyNTurns: 3 - everyNTokens: 8000 - afterInterval: 3m priority: 200 -timeout: 120s +timeout: 300s onError: skip -# Skip loop prompts — only process real user messages +# Skip loop conversations (they archive/unarchive frequently and would repeatedly +# re-scan the same history). enabledWhen: '!Session.IsLoop' parameters: @@ -154,26 +155,21 @@ prompt: | If there are NO new preferences AND nothing needs garbage-collecting or compacting, do nothing — do NOT modify any files. - === User Messages === + === Conversation === - Use the `mitto_conversation_history` MCP tool to retrieve recent user messages. - Call it with: + Use the `mitto_conversation_history` MCP tool to retrieve the archived session's + full history. Call it with: - `self_id`: your session ID (from `mitto_conversation_get_current`) - `conversation_id`: "{{ .Session.ID }}" - `event_types`: ["user_prompt", "agent_message"] - - `last_n`: 12 - - Review these messages for user preferences and conventions. + - `last_n`: 200 - ## Notification + Review these messages for personal user preferences. - After completing your work, if you changed {{ $file }} (added, removed, or compacted - preferences), call `mitto_ui_notify` with: - - `self_id`: "{{ .Session.ID }}" - - `title`: "📝 Preferences Updated" - - `message`: a brief summary, e.g. "Added 1, removed 2 stale, merged 3" - - `style`: "success" + ## Silent completion - If nothing changed and no files were modified, do NOT send any notification — stay - completely silent. + This is a background clean-up processor. Do NOT call `mitto_ui_notify` and do NOT + post any user-visible summary — the session is already archived and there is no + active UI to notify. If nothing changed and no files were modified, just exit + silently. {{- end -}} diff --git a/config/prompts/builtin/beads-issue-loop-fixing-bug.prompt.yaml b/config/prompts/builtin/beads-issue-loop-fixing-bug.prompt.yaml index a56a896df..faeb864ce 100644 --- a/config/prompts/builtin/beads-issue-loop-fixing-bug.prompt.yaml +++ b/config/prompts/builtin/beads-issue-loop-fixing-bug.prompt.yaml @@ -198,16 +198,20 @@ prompt: | and stops. The next scheduled run of this driver will observe `fixed` and take the Done branch (Step 3d). - ### Step 3d — Done (`fixed` present) — handled inline + ### Step 3d — Done (`fixed` present) — handled inline, EVALUATE FIRST - All three phases are complete. Close the bead and self-terminate — **no phase - dispatch on this branch**, since there is no further phase work to do: + **This branch is checked before every other Step 3 branch**, against the fresh + `bd show --json` from Step 1. All three phases are complete: **unconditionally close + the bead** and **unconditionally disable this conversation's loop flag** — no + phase dispatch on this branch, since there is no further phase work to do + (mitto-i5k: never treat close or `loop_enabled: false` as optional here — a + soft-gated Done branch lets the loop re-fire past the terminal label). ```bash - bd close {{ $target }} --reason "" # optional but recommended + bd close {{ $target }} --reason "" ``` - Then stop this conversation from re-running: + Then, unconditionally, stop this conversation from re-running: ``` mitto_conversation_update(self_id: "{{ .Session.ID }}", conversation_id: "self", loop_enabled: false) diff --git a/config/prompts/builtin/beads-issue-loop-fixing-bugs.prompt.yaml b/config/prompts/builtin/beads-issue-loop-fixing-bugs.prompt.yaml index 4e3d1c09b..7ad2e5d3b 100644 --- a/config/prompts/builtin/beads-issue-loop-fixing-bugs.prompt.yaml +++ b/config/prompts/builtin/beads-issue-loop-fixing-bugs.prompt.yaml @@ -16,7 +16,7 @@ prompt: | Available ACP servers: `{{ .ACP.AvailableText }}` Existing children: `{{ .Children.MCPText }}` - # Beads: Iterate Fixing Bugs (list-level orchestrator) + # Beads: Loop Fixing Bugs (list-level orchestrator) Beads is a CLI issue tracker (`bd`). Issues are called "beads" and have IDs like `bd-xyz`. @@ -27,7 +27,7 @@ prompt: | one, spawn ONE child conversation to fix it, wait for that child to finish, clean it up, then move to the next bug. Stop when the eligible set is empty or a per-run budget is hit. - - **INNER loop (already shipped: the `Iterate fixing bug` prompt, mitto-gap.1).** Each + - **INNER loop (already shipped: the `Loop fixing bug` prompt, mitto-gap.1).** Each child runs that per-bug driver as an `onCompletion` loop, advancing one `researched → reproduced → fixed` label per re-fire, then self-terminating. @@ -46,7 +46,7 @@ prompt: | `mitto_children_tasks_wait` and any child-directed sends. Also: only a **top-level** (non-child) conversation may create conversations. The - child driver (`Iterate fixing bug`) already runs in-place and never spawns, giving a + child driver (`Loop fixing bug`) already runs in-place and never spawns, giving a strict 2-level nesting. This orchestrator therefore refuses to run from a child. If either preflight fails at runtime (a tool call errors because a flag is disabled, @@ -54,21 +54,21 @@ prompt: | single `mitto_ui_notify` explaining which flag / role is missing and what to enable, and STOP. Do **not** touch `bd`, do **not** spawn anything, do **not** loop. - ## Step 1 — Fetch the per-bug driver body ONCE + ## Step 1 — Validate the per-bug driver prompt is available ONCE - Every child you spawn seeds and re-fires from the **same** body: the per-bug driver - prompt (`Iterate fixing bug`, mitto-gap.1). Fetch it once at the top of this run so - the same string is reused for every child (no drift between children in the same - run): + Every child you spawn is seeded and re-fired by **name** from the per-bug driver + prompt (`Loop fixing bug`, mitto-gap.1). Confirm it resolves before spawning any + child so we fail cleanly rather than mid-loop: ``` - mitto_prompt_get(self_id: "{{ .Session.ID }}", name: "Iterate fixing bug") + mitto_prompt_get(self_id: "{{ .Session.ID }}", name: "Loop fixing bug") ``` - Bind the response's `.prompt` (or `body`) field to a local variable — call it - **``** below. It is a Go-template string with `.Args.IssueID`, - `.Args.Commit`, and `.Session.BeadsIssue` placeholders; the runtime renders them - at each child dispatch (seed run and every `onCompletion` re-fire). + You do **not** need to bind or reuse the body — Step 4 passes `prompt_name:` and the + runtime expands the named prompt server-side on every seed and every `onCompletion` + re-fire. Fetching here is purely a preflight ("does the name resolve, is it + enabled?"); server-side expansion cannot be truncated to a placeholder by the LLM + composing multiple spawn calls in the same turn (mitto-dj9). If this fetch fails (name not found, prompt disabled), STOP with a `mitto_ui_notify` explaining that the per-bug driver is unavailable and no fixes were @@ -100,7 +100,15 @@ prompt: | for the human), OR - matches any *existing* child conversation's `beads_issue` in `{{ .Children.MCPText }}` (a spawn already exists from an earlier run of this - orchestrator; do not spawn a duplicate). + orchestrator; do not spawn a duplicate), OR + - has any `dependency_type: parent-child` dependency whose parent bead was **closed + within this outer run** (a *recently-closed* parent — mitto-fko). Rationale: when a + parent feature/bug closes, `bd ready` promotes its sub-issues to first-class + candidates immediately, but their premise may no longer hold (the parent's design + changed, the sub-issue is now redundant, etc.). Track the set of bead IDs this + outer run has closed so far (starting empty at Step 1) and consult each + candidate's `dependencies` array via `bd show --json`; drop any candidate + whose parent-child parent appears in that recently-closed set. Order the surviving set deterministically: by declared priority (`critical` → `high` → `medium` → `low`), then by bead ID ascending for stability. @@ -124,19 +132,19 @@ prompt: | ## Step 4 — Spawn ONE child for the highest-priority eligible bug Take the first entry from Step 2's ordered set — call it **``**. Spawn exactly - ONE child conversation whose seed AND loop re-fire are both ``. Link - it to `` via `beads_issue` so the per-bug driver resolves its target durably from - `.Session.BeadsIssue` on every re-fire (this is why the child does not need to know - its own `IssueID` after the seed run): + ONE child conversation whose seed AND loop re-fire are both the named per-bug driver + prompt (`Loop fixing bug`). Link it to `` via `beads_issue` so the driver + resolves its target durably from `.Session.BeadsIssue` on every re-fire (this is why + the child does not need to know its own `IssueID` after the seed run): ``` mitto_conversation_new( self_id: "{{ .Session.ID }}", title: "Fix ", beads_issue: "", - initial_prompt: , + prompt_name: "Loop fixing bug", arguments: { "IssueID": "", "Commit": "{{ if eq .Args.Commit "false" }}false{{ else }}true{{ end }}" }, - loop_prompt: , + loop_prompt_name: "Loop fixing bug", loop_trigger: "onCompletion", loop_completion_delay_seconds: 30, loop_max_iterations: 20, @@ -146,16 +154,16 @@ prompt: | Notes on why each argument is what it is: - - **`initial_prompt: ` + `arguments`** — `mitto_conversation_new` does - NOT auto-apply a fetched prompt's own `loop:` block. To make the child - self-drive its state machine, the same body must be provided as both the seed AND - the loop re-fire. The `arguments` map fills the driver's template - placeholders on the seed run. - - **`loop_prompt: `** — every `onCompletion` re-fire uses this text - verbatim. It resolves its target from `.Session.BeadsIssue` (set here via - `beads_issue`), so it does not need `IssueID`/`Commit` in the arguments map on - re-fires. The initial `Commit` argument only affects whichever run dispatches the - Fix phase; later phases don't consume it. + - **`prompt_name: "Loop fixing bug"` + `arguments`** — the runtime expands the named + prompt server-side on the seed run, filling the driver's template placeholders + from `arguments`. Passing the name (not the body) closes the mitto-dj9 short-circuit + where the LLM composing multiple `mitto_conversation_new` calls in one turn + collapses subsequent `initial_prompt` values to `[Same driver body]`. + - **`loop_prompt_name: "Loop fixing bug"`** — every `onCompletion` re-fire re-expands + the same named prompt server-side. It resolves its target from + `.Session.BeadsIssue` (set here via `beads_issue`), so it does not need + `IssueID`/`Commit` in the arguments map on re-fires. The initial `Commit` argument + only affects whichever run dispatches the Fix phase; later phases don't consume it. - **30 / 20 / 14400** — mirror the per-bug driver's own advertised budget (`delay: 30`, `maxIterations: 20`, `maxDuration: "4h" = 14400s`). Passing them explicitly makes the child's schedule identical whether the runtime applies the @@ -225,7 +233,7 @@ prompt: | ``` mitto_ui_notify( self_id: "{{ .Session.ID }}", - title: "Iterate fixing bugs — done", + title: "Loop fixing bugs — done", message: ". Reason: .>", style: "success" ) @@ -244,7 +252,7 @@ prompt: | ```bash bd update --add-label needs-human --defer +1d - bd comment "Orchestrator: could not spawn per-bug worker. What I tried: . What I need from you: . How to resume: clear needs-human then re-run 'Iterate fixing bugs'." + bd comment "Orchestrator: could not spawn per-bug worker. What I tried: . What I need from you: . How to resume: clear needs-human then re-run 'Loop fixing bugs'." ``` Then post the closing notification (Step 7) explaining that iteration stopped due diff --git a/config/prompts/builtin/beads-issue-loop-implementing-feature.prompt.yaml b/config/prompts/builtin/beads-issue-loop-implementing-feature.prompt.yaml index 4a9d254d2..444e6b689 100644 --- a/config/prompts/builtin/beads-issue-loop-implementing-feature.prompt.yaml +++ b/config/prompts/builtin/beads-issue-loop-implementing-feature.prompt.yaml @@ -220,17 +220,21 @@ prompt: | closes the bead, and stops. The next scheduled run of this driver will observe `verified` and take the Done branch (Step 3e). - ### Step 3e — Done (`verified` present) — handled inline + ### Step 3e — Done (`verified` present) — handled inline, EVALUATE FIRST - All four phases are complete. Close the bead (if the review phase has not already) - and self-terminate — **no phase dispatch on this branch**, since there is no further - phase work to do: + **This branch is checked before every other Step 3 branch**, against the fresh + `bd show --json` from Step 1. All four phases are complete: **unconditionally close + the bead** (if the review phase has not already) and **unconditionally disable this + conversation's loop flag** — no phase dispatch on this branch, since there is no + further phase work to do (mitto-i5k: never treat close or `loop_enabled: false` as + optional here — a soft-gated Done branch lets the loop re-fire past the terminal + label). ```bash - bd close {{ $target }} --reason "" # optional but recommended + bd close {{ $target }} --reason "" ``` - Then self-terminate so this conversation stops re-running: + Then, unconditionally, self-terminate so this conversation stops re-running: ``` mitto_conversation_update(self_id: "{{ .Session.ID }}", conversation_id: "self", loop_enabled: false) @@ -247,26 +251,53 @@ prompt: | shared files, or work with interdependencies — those go through the normal single self-send phase. Cap at **3–4** parallel children. - 1. Define each part completely and self-contained: the exact sub-scope (ideally a - sub-issue ID), the **disjoint** set of files/modules it owns (confirm the sets do not - overlap), acceptance criteria, and a definition of done. + 1. Define each part completely: the exact sub-scope (ideally a sub-issue ID), the + **disjoint** set of files/modules it owns (confirm the sets do not overlap), + acceptance criteria, and a definition of done. Record each part's IssueID/scope so + they can be passed to a **named** worker prompt below (mitto-6am: never inline- + compose a full free-text worker body from inside a driver — spawning + grand-children from an already-scheduled loop is structurally more exposed to the + mitto-dj9 short-circuit than the outer serial spawn). 2. For each part, reuse a suitable **idle** child from `{{ .Children.MCPText }}` when possible, otherwise create one with `mitto_conversation_new` (prefer a - faster/cheaper Coding-tier agent), seeding a fully self-contained worker prompt that - ends by reporting via `mitto_children_tasks_report`. + faster/cheaper Coding-tier agent) using a **named** worker prompt registered in + the workspace — for example `Feature — implement subpart` — so the body is + expanded server-side and cannot short-circuit to a placeholder: + + ``` + mitto_conversation_new( + self_id: "{{ .Session.ID }}", + title: "Implement subpart of {{ $target }}", + beads_issue: "", + prompt_name: "Feature — implement subpart", + arguments: { "IssueID": "", "ParentIssueID": "{{ $target }}", "Scope": "", "OwnedPaths": "" } + ) + ``` + + The worker prompt must end by reporting via `mitto_children_tasks_report`. If the + named worker prompt is not yet registered in this workspace, do **not** improvise + an inline free-text body — abandon the split and fall back to the single + Implement phase (Step 3b) so the work still lands, and record a bead comment + naming the missing worker prompt so an operator can register it. 3. Block on all of them: `mitto_children_tasks_wait(self_id: "{{ .Session.ID }}", children_list: [], task_id: "impl-{{ $target }}", timeout_seconds: 3600)`. On timeout, retry the pending children with the **same** `task_id` (omit the prompt); after a second timeout treat those parts as failed and finish the remainder via a single Implement phase on the next run. - 4. **Synthesize** the reports and verify the combined increment (build/lint clean, the - parts integrate). If verified, record an `Implementation:` comment summarising the - distributed work, add the `implemented` label{{ if eq .Args.Commit "true" }} and - commit only the changed files (staged by path){{ end }}, archive the finished - children, and end the turn. The next scheduled run will observe `implemented` and - dispatch Test. If any part reports a conflict, abandon the split and finish via the - single Implement phase on the next run. If a spawn call errors because **Can start - conversation** is off, fall back to the single Implement phase (Step 3b). + 4. **Synthesize** the reports and verify the combined increment. **Fail closed** on + empty or non-matching reports: every child's report must have non-empty findings + AND a file-diff touching the paths that child was assigned. If any child reports + vacuously or without matching files, abandon the split and finish via the single + Implement phase on the next run (this is the mitto-6am post-condition check that + catches placeholder short-circuits before advancing the feature's state). If all + reports verify and the combined increment is build/lint clean, record an + `Implementation:` comment summarising the distributed work, add the `implemented` + label{{ if eq .Args.Commit "true" }} and commit only the changed files (staged by + path){{ end }}, archive the finished children, and end the turn. The next + scheduled run will observe `implemented` and dispatch Test. If any part reports a + conflict, abandon the split and finish via the single Implement phase on the next + run. If a spawn call errors because **Can start conversation** is off, fall back + to the single Implement phase (Step 3b). {{- else }} ## Step 1 — No target feature to work on diff --git a/config/prompts/builtin/beads-issue-loop-implementing-features.prompt.yaml b/config/prompts/builtin/beads-issue-loop-implementing-features.prompt.yaml index 846114f88..366e0126b 100644 --- a/config/prompts/builtin/beads-issue-loop-implementing-features.prompt.yaml +++ b/config/prompts/builtin/beads-issue-loop-implementing-features.prompt.yaml @@ -16,7 +16,7 @@ prompt: | Available ACP servers: `{{ .ACP.AvailableText }}` Existing children: `{{ .Children.MCPText }}` - # Beads: Iterate Implementing Features (list-level orchestrator) + # Beads: Loop Implementing Features (list-level orchestrator) Beads is a CLI issue tracker (`bd`). Issues are called "beads" and have IDs like `bd-xyz`. @@ -27,7 +27,7 @@ prompt: | highest-priority one, spawn ONE child conversation to implement it, wait for that child to finish, clean it up, then move to the next feature. Stop when the eligible set is empty or a per-run budget is hit. - - **INNER loop (already shipped: the `Iterate implementing feature` prompt, + - **INNER loop (already shipped: the `Loop implementing feature` prompt, mitto-gap.5).** Each child runs that per-feature driver as an `onCompletion` loop, advancing one `planned → implemented → tested → verified` label per re-fire, then self-terminating. @@ -47,7 +47,7 @@ prompt: | `mitto_children_tasks_wait` and any child-directed sends. Also: only a **top-level** (non-child) conversation may create conversations. The - child driver (`Iterate implementing feature`) already runs in-place and never spawns, + child driver (`Loop implementing feature`) already runs in-place and never spawns, giving a strict 2-level nesting. This orchestrator therefore refuses to run from a child. @@ -56,21 +56,21 @@ prompt: | single `mitto_ui_notify` explaining which flag / role is missing and what to enable, and STOP. Do **not** touch `bd`, do **not** spawn anything, do **not** loop. - ## Step 1 — Fetch the per-feature driver body ONCE + ## Step 1 — Validate the per-feature driver prompt is available ONCE - Every child you spawn seeds and re-fires from the **same** body: the per-feature - driver prompt (`Iterate implementing feature`, mitto-gap.5). Fetch it once at the top - of this run so the same string is reused for every child (no drift between children in - the same run): + Every child you spawn is seeded and re-fired by **name** from the per-feature driver + prompt (`Loop implementing feature`, mitto-gap.5). Confirm it resolves before + spawning any child so we fail cleanly rather than mid-loop: ``` - mitto_prompt_get(self_id: "{{ .Session.ID }}", name: "Iterate implementing feature") + mitto_prompt_get(self_id: "{{ .Session.ID }}", name: "Loop implementing feature") ``` - Bind the response's `.prompt` (or `body`) field to a local variable — call it - **``** below. It is a Go-template string with `.Args.IssueID`, - `.Args.Commit`, and `.Session.BeadsIssue` placeholders; the runtime renders them - at each child dispatch (seed run and every `onCompletion` re-fire). + You do **not** need to bind or reuse the body — Step 4 passes `prompt_name:` and the + runtime expands the named prompt server-side on every seed and every `onCompletion` + re-fire. Fetching here is purely a preflight ("does the name resolve, is it + enabled?"); server-side expansion cannot be truncated to a placeholder by the LLM + composing multiple spawn calls in the same turn (mitto-dj9). If this fetch fails (name not found, prompt disabled), STOP with a `mitto_ui_notify` explaining that the per-feature driver is unavailable and no @@ -102,7 +102,15 @@ prompt: | leave it for the human), OR - matches any *existing* child conversation's `beads_issue` in `{{ .Children.MCPText }}` (a spawn already exists from an earlier run of this - orchestrator; do not spawn a duplicate). + orchestrator; do not spawn a duplicate), OR + - has any `dependency_type: parent-child` dependency whose parent bead was **closed + within this outer run** (a *recently-closed* parent — mitto-fko). Rationale: when a + parent feature closes, `bd ready` promotes its sub-features to first-class + candidates immediately, but their premise may no longer hold (the parent's design + changed, the sub-feature is now redundant, etc.). Track the set of bead IDs this + outer run has closed so far (starting empty at Step 1) and consult each + candidate's `dependencies` array via `bd show --json`; drop any candidate + whose parent-child parent appears in that recently-closed set. Order the surviving set deterministically: by declared priority (`critical` → `high` → `medium` → `low`), then by bead ID ascending for stability. @@ -126,19 +134,19 @@ prompt: | ## Step 4 — Spawn ONE child for the highest-priority eligible feature Take the first entry from Step 2's ordered set — call it **``**. Spawn exactly - ONE child conversation whose seed AND loop re-fire are both ``. Link - it to `` via `beads_issue` so the per-feature driver resolves its target durably - from `.Session.BeadsIssue` on every re-fire (this is why the child does not need to - know its own `IssueID` after the seed run): + ONE child conversation whose seed AND loop re-fire are both the named per-feature + driver prompt (`Loop implementing feature`). Link it to `` via `beads_issue` so + the driver resolves its target durably from `.Session.BeadsIssue` on every re-fire + (this is why the child does not need to know its own `IssueID` after the seed run): ``` mitto_conversation_new( self_id: "{{ .Session.ID }}", title: "Implement : ", beads_issue: "", - initial_prompt: , + prompt_name: "Loop implementing feature", arguments: { "IssueID": "", "Commit": "{{ if eq .Args.Commit "false" }}false{{ else }}true{{ end }}" }, - loop_prompt: , + loop_prompt_name: "Loop implementing feature", loop_trigger: "onCompletion", loop_completion_delay_seconds: 30, loop_max_iterations: 30, @@ -148,16 +156,18 @@ prompt: | Notes on why each argument is what it is: - - **`initial_prompt: ` + `arguments`** — `mitto_conversation_new` does - NOT auto-apply a fetched prompt's own `loop:` block. To make the child - self-drive its state machine, the same body must be provided as both the seed AND - the loop re-fire. The `arguments` map fills the driver's template - placeholders on the seed run. - - **`loop_prompt: `** — every `onCompletion` re-fire uses this text - verbatim. It resolves its target from `.Session.BeadsIssue` (set here via - `beads_issue`), so it does not need `IssueID`/`Commit` in the arguments map on - re-fires. The initial `Commit` argument only affects whichever run dispatches the - review phase; earlier phases don't consume it. + - **`prompt_name: "Loop implementing feature"` + `arguments`** — the runtime expands + the named prompt server-side on the seed run, filling the driver's template + placeholders from `arguments`. Passing the name (not the body) closes the + mitto-dj9 short-circuit where the LLM composing multiple `mitto_conversation_new` + calls in one turn collapses subsequent `initial_prompt` values to + `[Same driver body]`. + - **`loop_prompt_name: "Loop implementing feature"`** — every `onCompletion` re-fire + re-expands the same named prompt server-side. It resolves its target from + `.Session.BeadsIssue` (set here via `beads_issue`), so it does not need + `IssueID`/`Commit` in the arguments map on re-fires. The initial `Commit` argument + only affects whichever run dispatches the review phase; earlier phases don't + consume it. - **30 / 30 / 28800** — mirror the per-feature driver's own advertised budget (`delay: 30`, `maxIterations: 30`, `maxDuration: "8h" = 28800s`). Passing them explicitly makes the child's schedule identical whether the runtime applies the @@ -228,7 +238,7 @@ prompt: | ``` mitto_ui_notify( self_id: "{{ .Session.ID }}", - title: "Iterate implementing features — done", + title: "Loop implementing features — done", message: ". Reason: .>", style: "success" ) @@ -247,7 +257,7 @@ prompt: | ```bash bd update --add-label needs-human --defer +1d - bd comment "Orchestrator: could not spawn per-feature worker. What I tried: . What I need from you: . How to resume: clear needs-human then re-run 'Iterate implementing features'." + bd comment "Orchestrator: could not spawn per-feature worker. What I tried: . What I need from you: . How to resume: clear needs-human then re-run 'Loop implementing features'." ``` Then post the closing notification (Step 7) explaining that iteration stopped due diff --git a/config/prompts/builtin/continue.prompt.yaml b/config/prompts/builtin/continue.prompt.yaml index 733938ef1..f162dce1a 100644 --- a/config/prompts/builtin/continue.prompt.yaml +++ b/config/prompts/builtin/continue.prompt.yaml @@ -1,10 +1,17 @@ icon: play name: Continue -description: Continue with the current task from where we left off +description: Continue with the current task from where we left off — optionally as a self-driving loop that stops once the task's intention is met group: Work flow menus: prompts, conversation backgroundColor: '#FFF9C4' enabledWhen: Session.HasMessages +loop: + mode: optional + default: false + trigger: onCompletion + delay: 60 + maxIterations: 20 + maxDuration: "4h" prompt: | Before taking any action, review the current state of the work by reading relevant files, checking git status, and understanding what has already been completed. @@ -33,6 +40,74 @@ prompt: | **Interactive mode** — a force-triggered run or a non-loop conversation; the user may be present. {{- end }} + {{- if .Session.IsLoop }} + + ## Task Intention (loop mode) + + In loop mode the loop's exit condition is **"the intention of the current task is complete"** — + not "the next step ran". You must derive that intention, keep it in view every iteration, and + stop the loop as soon as it is met. + {{- if .Iteration.IsFirst }} + + **This is the first iteration.** Determine and record the intention now: + + 1. Read the recent conversation, the linked bead (if any — see the "Beads issue" section below), + the relevant files, and `git status` / `git log` to infer **what the current task is actually + trying to achieve** — the *outcome*, not a list of steps. Prefer explicit sources (the linked + bead's description/acceptance criteria, the user's most recent instruction) over guesses. + 2. State the intention at the top of your response as a short block: + + ``` + TASK INTENTION: + DONE WHEN: closed", "file F contains function G and the linter is clean"> + ``` + + Keep it concrete and *observable* — something a later iteration can check by reading files or + running commands, not by re-reading your own claims. + {{- if .Session.BeadsIssue }} + 3. Persist it durably on the linked bead so later iterations (and a fresh-context restart) can + recover it exactly: + + ```bash + bd comment {{ .Session.BeadsIssue }} "Continue-loop intention: . Done when: ." + ``` + {{- else }} + 3. There is no linked bead; the intention block above is the durable record — restate it verbatim + at the top of every subsequent iteration so the loop keeps a stable target. + {{- end }} + {{- else }} + + **This is a continuation iteration.** Recover the intention **before** doing any work: + + 1. {{ if .Session.BeadsIssue -}} + Re-read the most recent `Continue-loop intention:` comment on `{{ .Session.BeadsIssue }}` + (`bd show {{ .Session.BeadsIssue }} --long --json --include-comments`). If none exists, + fall back to the intention block from the first iteration in this conversation. + {{- else -}} + Re-read the `TASK INTENTION` / `DONE WHEN` block from the first iteration of this + conversation. + {{- end }} + 2. Restate the recovered intention verbatim at the top of your response so it stays in view. + 3. Evaluate the `DONE WHEN` criteria against the **real** current state (files, tests, git, + `bd show`, PR status) — never against your own prior claims. If you cannot verify a criterion + is met, treat it as not yet met. + 4. If every `DONE WHEN` criterion is verifiably met, **the intention is complete** — disable the + loop and notify: + + ``` + mitto_conversation_update(self_id: "{{ .Session.ID }}", conversation_id: "self", loop_enabled: false) + mitto_ui_notify(self_id: "{{ .Session.ID }}", title: "Continue — task complete", + message: "", style: "success") + ``` + + Then stop; do not start new work. + 5. If the intention has **materially changed** since it was recorded (the user redirected the + task, or new information invalidated the old goal), update the record{{ if .Session.BeadsIssue }} + with a fresh `bd comment {{ .Session.BeadsIssue }} "Continue-loop intention: ..."` on the bead{{ end }} + and continue against the new intention. Do not silently drift. + {{- end }} + {{- end }} Rules: @@ -40,6 +115,10 @@ prompt: | 2. Identify the next immediate step 3. Execute that step 4. Report progress and what remains + {{- if .Session.IsLoop }} + 5. Do **one** concrete increment per run — do not try to finish the whole task in a single iteration. + After the increment, briefly restate how much of the `DONE WHEN` criteria are now met, then stop. + {{- end }} {{- if and .Session.IsLoop (not .Session.IsLoopForced) }} In scheduled mode, if blocked or unclear: notify and stop (see above) — do not ask. diff --git a/config/prompts/builtin/create-commits.prompt.yaml b/config/prompts/builtin/create-commits.prompt.yaml index a8f8e944e..a658e1ac7 100644 --- a/config/prompts/builtin/create-commits.prompt.yaml +++ b/config/prompts/builtin/create-commits.prompt.yaml @@ -90,7 +90,39 @@ prompt: | Per commit: `git add ` → `git commit -m ""`. Report results. + {{ if .Session.BeadsIssue -}} + ### 6. Close Linked Beads Issue + + This conversation is linked to beads issue `{{ .Session.BeadsIssue }}`. Now that the commits are made, check whether the issue is fully resolved. + + - Load the issue and its acceptance criteria: + + ```bash + bd show {{ .Session.BeadsIssue }} --long --json + ``` + + - Compare the acceptance criteria against the committed changes. + - **If nothing else is left to do** for this issue: + 1. Confirm with the user via + `mitto_ui_options(self_id: "{{ .Session.ID }}", question: "All work for {{ .Session.BeadsIssue }} appears complete. Close it now?", options: [{label: "Yes, close it"}, {label: "No, keep it open"}])`. + 2. If approved, close with a specific reason that references the commits: + + ```bash + bd close {{ .Session.BeadsIssue }} --reason "; tests pass'>" + ``` + + - **If work remains**, leave the issue open and append a short progress note so the finding is not lost: + + ```bash + bd update {{ .Session.BeadsIssue }} --append-notes "" + ``` + + Be conservative: when it is unclear whether every acceptance criterion is met, keep the issue open. + + ### 7. Update Agent Rules (Optional) + {{- else }} ### 6. Update Agent Rules (Optional) + {{- end }} If you discovered project conventions or the user corrected assumptions, update Agent rules/memories (with user approval). diff --git a/config/prompts/builtin/github-post-merge-cleanup.prompt.yaml b/config/prompts/builtin/github-post-merge-cleanup.prompt.yaml index d693d3249..4a41db503 100644 --- a/config/prompts/builtin/github-post-merge-cleanup.prompt.yaml +++ b/config/prompts/builtin/github-post-merge-cleanup.prompt.yaml @@ -5,25 +5,25 @@ parameters: - name: IssuesOnly type: boolean description: Triage only — file/update beads cleanup issues and notify, but never auto-fix or open PRs (leave unchecked to auto-fix small, low-risk items) -description: Auto-loop — after merges to the default branch, sweep for follow-up work (TODOs, deprecations, stale flags, doc gaps), track it in beads, auto-fix small low-risk items, and self-terminate when quiet +description: After merges to the default branch, sweep for follow-up work (TODOs, deprecations, stale flags, doc gaps), track it in beads, and auto-fix small low-risk items — one-shot by default; convert to a loop from the UI for continuous sweeping group: GitHub backgroundColor: '#BBDEFB' tags: -- loop - github - cleanup enabledWhen: '!Session.IsChild && FileExists(".git/config") && (Tools.HasPattern("github_*") || CommandExists("gh")) && CommandExists("bd") && DirExists(".beads") && Permissions.CanStartConversation' -loop: - mode: always - trigger: onCompletion - delay: 21600 - maxIterations: 20 - maxDuration: "168h" prompt: | - The auto-loop **post-merge cleanup sweeper**. After merges land on the - default branch, each run sweeps for follow-up work — deprecations, `TODO`/`FIXME`, - `// remove after`, stale feature flags, broken doc links, and explicit follow-ups - named in merged PRs/issues — **without blocking or touching the merge itself**. + **Post-merge cleanup sweeper.** After merges land on the default branch, this run + sweeps for follow-up work — deprecations, `TODO`/`FIXME`, `// remove after`, stale + feature flags, broken doc links, and explicit follow-ups named in merged PRs/issues — + **without blocking or touching the merge itself**. + + This is a **one-shot task by default**: run once against the current default-branch + head, file/fix what applies, then stop. If you want a continuous sweeper that keeps + waking up after new merges, convert this conversation into a loop from the UI + (`onCompletion` trigger + a several-hour delay) — the body below is written to work + in both modes, and the tracker epic's `lastSHA` marker makes each run resume where + the previous one stopped. Unlike the original pattern, **there is no `post-merge-state.md` file**: beads is the durable state store. A **tracker epic** bead holds the run state, each cleanup @@ -234,12 +234,13 @@ prompt: | Open cleanup beads = the "Pending" bucket; `deferred`-labelled beads = "Deferred (human decision)"; closed beads = "Completed". No separate file is needed. - ## Step 6 — Stop decision / self-terminate + ## Step 6 — Wrap up + {{- if .Session.IsLoop }} - Keep iterating (end this run; the next fires after `delay`) while there is anything - this loop can still advance — new merges arriving, or open auto-fixable beads not yet - at `pr-open`. `deferred` beads waiting on a human are **not** a reason to keep - spinning. + This conversation is a **loop**. Keep iterating (end this run; the next fires after + `delay`) while there is anything this loop can still advance — new merges arriving, + or open auto-fixable beads not yet at `pr-open`. `deferred` beads waiting on a human + are **not** a reason to keep spinning. When a run finds **no new merges** since `lastSHA` **and** no open auto-fixable cleanup beads remain (only `deferred`/`pr-open`/none), the sweep is quiet — @@ -253,6 +254,20 @@ prompt: | ``` The user can re-run this prompt anytime to sweep a fresh batch of merges. + {{- else }} + + This is a **one-shot** run. Finish with a short `mitto_ui_notify` summary of what + was filed, fixed, and deferred, then stop — do not re-fire yourself. + + ``` + mitto_ui_notify(self_id: "{{ .Session.ID }}", title: "Post-merge cleanup — done", + message: "", style: "success") + ``` + + The user can re-run this prompt anytime to sweep the next batch of merges (the + tracker epic's `lastSHA` marker means the next run resumes where this one stopped). + If you want this to run continuously, convert the conversation to a loop from the UI. + {{- end }} ## Guidelines @@ -270,5 +285,6 @@ prompt: | with external API consumers, or anything attempted twice without passing tests. - **Caps:** at most **2 auto-fix PRs per run**; check `{{ .Children.MCPText }}` and skip duplicate spawns; spawned conversations are one-off and **must never be loops**. - - **Notify only when it matters** on scheduled runs (filed / fixed / deferred / final - stop); stay quiet on no-op runs. Always log to the tracker with `bd comment`. + - **Notify only when it matters** (filed / fixed / deferred / final stop); stay quiet + on no-op runs — especially on scheduled loop runs. Always log to the tracker with + `bd comment`. diff --git a/config/prompts/builtin/loop-prompt-until.prompt.yaml b/config/prompts/builtin/loop-prompt-until.prompt.yaml new file mode 100644 index 000000000..9b9a21381 --- /dev/null +++ b/config/prompts/builtin/loop-prompt-until.prompt.yaml @@ -0,0 +1,93 @@ +icon: loop +name: Loop prompt until ... +menus: prompts, conversation +parameters: + - name: Prompt + type: prompts + required: false + description: The prompt to run each iteration + - name: Condition + type: text + required: true + description: The stop condition — keep iterating until this is true (e.g. "all tests pass and the linter is clean") + - name: Commit + type: boolean + description: Commit the changes made at the end of each iteration +description: Make this conversation a loop (on completion), run a picked prompt each iteration, and keep iterating until your condition is met — then self-terminate +backgroundColor: '#D1C4E9' +group: Work flow +enabledWhen: '!Session.IsChild && !Session.IsLoopConversation' +loop: + mode: always + trigger: onCompletion + delay: 30 + maxIterations: 20 + maxDuration: "4h" +prompt: | + ## Session Context + + Your session ID is `{{ .Session.ID }}` — use this as `self_id` for all `mitto_*` MCP tool calls. + + # Iterate a Picked Prompt Until a Condition Is Met + + This conversation is a self-driving loop: each iteration runs the picked prompt + below, then checks the stop condition. Each run fires automatically a short + while after you stop responding (an "on completion" trigger) and continues + unattended — you do not need to arm anything. + + **The stop condition is:** + + > {{ .Args.Condition }} + + ## Each run + {{- if .Iteration.IsUninterrupted }} + + Automated, unattended run — use `mitto_ui_notify` only; never call + `mitto_ui_options`, `mitto_ui_form`, or `mitto_ui_textbox`, and never ask the user + to make work decisions. Decide autonomously. + {{- end }} + + 1. Review the real current state — read the relevant files, run the relevant + checks, inspect git status. Never speculate about code you have not opened. + 2. Evaluate the stop condition against that observed state (test output, file + contents, command exit codes), never against your intentions. If you cannot + verify it is true, treat it as not yet met. + 3. If it is TRUE: disable the loop — + mitto_conversation_update(self_id: "{{ .Session.ID }}", conversation_id: "self", loop_enabled: false) — + then mitto_ui_notify(self_id: "{{ .Session.ID }}", title: "Iteration complete", + message: "", style: "success"). Do nothing further. + 4. If it is NOT met: execute the picked prompt's instructions (inlined below) + as ONE concrete increment toward the stop condition, verify that increment, + briefly note progress, then stop responding so the next run continues. + When that increment decomposes into two or more genuinely independent subtasks + (different files/modules, no ordering dependencies, no possible conflict) and + this conversation can spawn children (Can start conversation + Can Send Prompt + flags on), fan them out to run in parallel instead of serially: give each child a + self-contained task over a DISJOINT file set that ends by reporting via + mitto_children_tasks_report, block on them all with + mitto_children_tasks_wait(self_id: "{{ .Session.ID }}", children_list: [...], + task_id: "