diff --git a/README.md b/README.md index 3ecd4611f..68be0bf8b 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ CI runs `go test ./... -count=1`, `go run ./cmd/progress validate`, and `git dif | Phase | Status | Shipped | |-------|--------|---------| | Phase 1 — The Dashboard | ✅ | 4/4 subphases | -| Phase 2 — The Gateway | ✅ | 21/21 subphases | +| Phase 2 — The Gateway | 🔨 | 20/21 subphases | | Phase 3 — The Black Box (Memory) | ✅ | 15/15 subphases | | Phase 4 — The Brain Transplant | ✅ | 13/13 subphases | | Phase 5 — The Final Purge | 🔨 | 18/23 subphases | diff --git a/internal/gateway/render_test.go b/internal/gateway/render_test.go index f38461861..4df4ac5fd 100644 --- a/internal/gateway/render_test.go +++ b/internal/gateway/render_test.go @@ -130,7 +130,7 @@ func TestFormatToolProgressPlain_ToolTraceFixtureMatrix(t *testing.T) { } } -func TestFormatToolProgressPlain_TruncatedWebPreviewsKeepRightEdge(t *testing.T) { +func TestFormatToolProgressPlain_TruncatedWebPreviewsKeepLeftEdge(t *testing.T) { f := kernel.RenderFrame{SoulEvents: []kernel.SoulEntry{ {At: time.Now(), Text: "tool: web_extract: https://docs.openclaw.ai/concepts/multi-agent"}, {At: time.Now(), Text: "tool: browser_navigate: https://docs.openclaw.ai/concepts/multi-agent"}, @@ -139,12 +139,12 @@ func TestFormatToolProgressPlain_TruncatedWebPreviewsKeepRightEdge(t *testing.T) got := FormatToolProgressPlain(f) for _, want := range []string{ - `📄 web_extract: "...docs.openclaw.ai/concepts/multi-agent"`, - `🌐 browser_navigate: "...docs.openclaw.ai/concepts/multi-agent"`, - `🔍 web_search: "...concepts/multi-agent strings in tools"`, + `📄 web_extract: "https://docs.openclaw.ai/concepts/mul..."`, + `🌐 browser_navigate: "https://docs.openclaw.ai/concepts/mul..."`, + `🔍 web_search: "site:docs.openclaw.ai/concepts/multi-..."`, } { if !strings.Contains(got, want) { - t.Fatalf("FormatToolProgressPlain missing right-edge preview %q in:\n%s", want, got) + t.Fatalf("FormatToolProgressPlain missing left-edge preview %q in:\n%s", want, got) } } } @@ -191,8 +191,8 @@ func TestFormatToolProgressPlain_MineruGatewayTranscriptShape(t *testing.T) { for _, want := range []string{ `📚 skill_view: "gormes-hermes-parity"`, `⏰ cronjob: "list"`, - `🌐 browser_navigate: "...www.reddit.com/r/WebAfterAI/s/example"`, - `🌐 browser_navigate: "...old.reddit.com/r/WebAfterAI/s/example"`, + `🌐 browser_navigate: "https://www.reddit.com/r/WebAfterAI/s..."`, + `🌐 browser_navigate: "https://old.reddit.com/r/WebAfterAI/s..."`, `💻 terminal: "curl -L https://example.test/post.json"`, `📸 browser_snapshot...`, `💻 terminal: "curl -L https://example.test/post.json" (×2)`, diff --git a/internal/progress/progress_test.go b/internal/progress/progress_test.go index b5c7a6afc..e68ab95cd 100644 --- a/internal/progress/progress_test.go +++ b/internal/progress/progress_test.go @@ -459,8 +459,12 @@ func TestLoad_RealFile_Phase2ExecutionQueue(t *testing.T) { if routing.Priority != "P1" { t.Fatalf("Phase 2.B.5 priority = %q, want P1", routing.Priority) } - if got := routing.DerivedStatus(); got != StatusComplete { - t.Fatalf("Phase 2.B.5 = %q, want complete — all session-context and delivery-routing rows shipped", got) + switch got := routing.DerivedStatus(); got { + case StatusComplete, StatusInProgress: + // complete or reopened by a newly discovered gateway slash-handler + // gap such as `Gateway /model interactive provider/model picker` + default: + t.Fatalf("Phase 2.B.5 = %q, want complete or in_progress", got) } routingItems := itemsByName(routing.Items) topicCloseout := routingItems["Telegram topic mode off/help/auth/debounce closeout"] diff --git a/internal/tooltrace/tooltrace.go b/internal/tooltrace/tooltrace.go index 98a8e07bf..0bdf183e5 100644 --- a/internal/tooltrace/tooltrace.go +++ b/internal/tooltrace/tooltrace.go @@ -196,8 +196,13 @@ func quoteAndTruncate(toolName, s string, limit int) string { } func rightEdgePreviewTool(name string) bool { + // File-path arguments keep the tail (filename matters more than the + // directory chain). URLs and search queries keep the head: the domain + // or the first words of the query are the user-meaningful part, and a + // `"...et.com/market-data/..."` preview hides which site Gormes is + // actually visiting. switch strings.TrimSpace(name) { - case "web_search", "web_extract", "web_crawl", "browser_navigate", "read_file", "write_file", "patch": + case "read_file", "write_file", "patch": return true default: return false diff --git a/internal/tooltrace/tooltrace_preview_test.go b/internal/tooltrace/tooltrace_preview_test.go new file mode 100644 index 000000000..40b42d4ba --- /dev/null +++ b/internal/tooltrace/tooltrace_preview_test.go @@ -0,0 +1,55 @@ +package tooltrace + +import "testing" + +// Live regression 2026-05-10: an operator on Telegram saw tool-call previews +// rendered as `🔍 web_search: "...et docs websocket WSS Quickstart CLOB"` +// and `📄 web_extract: "...et.com/market-data/websocket/overview"`. Both +// hide the user-meaningful start (the search query's first words and the +// URL's domain) behind a leading "...". Search queries and URLs must keep +// their head; only file-path tool args (read_file/write_file/patch) keep +// the tail because the filename usually matters more than the directory +// chain. + +func TestFormatPlain_PreservesUrlAndQueryHead(t *testing.T) { + cases := []struct { + name string + in string + want string + }{ + { + name: "web_search keeps head of query", + in: "tool: web_search: polymarket docs websocket WSS Quickstart CLOB", + want: `🔍 web_search: "polymarket docs websocket WSS Quickst..."`, + }, + { + name: "web_extract keeps domain at head of URL", + in: "tool: web_extract: https://docs.polymarket.com/market-data/websocket/overview", + want: `📄 web_extract: "https://docs.polymarket.com/market-da..."`, + }, + { + name: "browser_navigate keeps domain at head of URL", + in: "tool: browser_navigate: https://docs.polymarket.com/market-data/websocket/overview", + want: `🌐 browser_navigate: "https://docs.polymarket.com/market-da..."`, + }, + { + name: "read_file keeps tail (filename)", + in: "tool: read_file: internal/channels/telegram/document_cache.go", + want: `📖 read_file: "...l/channels/telegram/document_cache.go"`, + }, + { + name: "write_file keeps tail (filename)", + in: "tool: write_file: internal/channels/telegram/document_cache.go", + want: `🔧 write_file: "...l/channels/telegram/document_cache.go"`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := FormatPlain(tc.in) + if got != tc.want { + t.Fatalf("FormatPlain(%q)\n got: %s\nwant: %s", tc.in, got, tc.want) + } + }) + } +} diff --git a/scripts/gormes-builder-cron.sh b/scripts/gormes-builder-cron.sh index b25e67093..36275f700 100755 --- a/scripts/gormes-builder-cron.sh +++ b/scripts/gormes-builder-cron.sh @@ -98,6 +98,85 @@ sync_development_branch() { die "$BRANCH diverged from origin/$BRANCH; skipping automated builder run" } +opencode_prompt() { + cat <<'PROMPT' +You are an autonomous coding agent (opencode + a local ollama model — typically +qwen3-coder:30b MoE) running from cron inside the Gormes repository. The +codexu/codex backend is rate-limited until 2026-05-14, so this loop is keeping +forward motion with a smaller local model. Bias toward small, safe, finished +work over big plans. + +Skill loading +- The repo's gormes-* skills are SKILL.md instruction files, not MCP servers. +- Use the `skill` tool to load a skill by name (e.g. `skill gormes-git`). The + tool returns the SKILL.md content; read it and follow the instructions + manually. Do NOT call `skill_mcp` for these — they have no MCP endpoint. +- The full skill catalog is under docs/development-skills/gormes-*/SKILL.md. + The same files are symlinked into ~/.config/opencode/skills/ for + discoverability. + +Tool-schema reminders (opencode is strict about these — wrong shape = no-op) +- `bash`: REQUIRES both `command` (the shell command) and `description` (one + short sentence describing what it does). Omitting `description` errors out. +- `read`: takes `file_path` (absolute path preferred). +- `edit`: takes `file_path`, `old_string`, `new_string`. The old_string must + match exactly (whitespace + newlines included) and be unique in the file. +- `write`: takes `file_path` and `content`. Use only for new files; prefer + `edit` for changes to existing files. +- When in doubt, run a single `bash` with `git status --short` first — it is + the cheapest way to ground yourself in the worktree state. + +Mandatory branch + safety rules +- Stay on the existing `development` branch only. Do not create branches or + worktrees. If HEAD is not `development`, stop and report the blocker. +- Preserve any user or parallel-agent changes you find. If you do not + understand untracked files (IDENTITY.md, SOUL.md, TOOLS.md, memory/*, etc.), + leave them alone — they are not your work. +- Never force-push. Never bypass hooks. Never recreate cmd/planner-loop or + cmd/builder-loop. + +Single focused goal per cycle (pick the FIRST that applies) + +1. Worktree is dirty with YOUR previous-cycle work? + Load `gormes-git`, follow it to commit + push only the files you authored + this cycle (skip unfamiliar untracked files). Then exit cleanly. + +2. Worktree is dirty with files you cannot attribute to yourself? + Do not touch them. Run `go test ./... -count=1` and report the result. + Exit cleanly. Leave the worktree as you found it. + +3. Worktree is clean and you can read progress.json? + Pick ONE small, builder-ready row from + docs/content/building-gormes/architecture_plan/progress.json (priority P1 + or P2, slice_size small, contract_status ready, blocked_by empty). Load + `gormes-builder` and `gormes-tdd-slice`, follow them to implement that + one row with a red→green→refactor loop. Run `go run ./cmd/progress + validate` and `git diff --check` afterwards. If validation fails, fix + the validation only (do not chase scope creep). + +4. You are unsure or any tool returns confusing output? + Stop. Write a one-line summary of what you tried and what blocked you. + Exit with status 0 — the next cron tick is in 60s, the user can read + the log. + +Hard constraints +- Do exactly ONE bounded thing this cycle. Do not chain row-implementation + + parity sweep + planner work. +- Do NOT add new progress.json rows. Do NOT do parity audits. Those are for + the codex backend which has the cognitive headroom. +- If a step requires more than ~5 tool calls to complete, you are over your + size budget. Save what you have and exit. +- Run validation gates immediately after edits, not at the end of a long + chain — partial green is better than untested wholeness. +- Do not edit files outside the row's `write_scope`. If the row's scope is + unclear, fall back to goal #4 (report and exit). + +When you cannot continue +- Use plain text "I am stopping because: ". Do not pretend success. +- Exit. The cron wrapper will retry in 60 seconds. +PROMPT +} + codex_prompt() { cat <<'PROMPT' You are Codex running from cron inside the Gormes repository. @@ -171,7 +250,11 @@ main() { if [[ "${1:-}" == "--dry-run" ]]; then log "dry run: would execute $BACKEND builder prompt with timeout $RUN_TIMEOUT" - codex_prompt + if [[ "$BACKEND" == "opencode" ]]; then + opencode_prompt + else + codex_prompt + fi exit 0 fi @@ -179,7 +262,7 @@ main() { local status if [[ "$BACKEND" == "opencode" ]]; then local opencode_log="$LOG_DIR/$RUN_ID.opencode.jsonl" - codex_prompt | timeout "$RUN_TIMEOUT" "$resolved_bin" run \ + opencode_prompt | timeout "$RUN_TIMEOUT" "$resolved_bin" run \ --model "$OPENCODE_MODEL" \ --dir "$REPO_ROOT" \ --format json \ diff --git a/webpages/docs/content/building-gormes/architecture_plan/_index.md b/webpages/docs/content/building-gormes/architecture_plan/_index.md index ed0011194..013a57b13 100644 --- a/webpages/docs/content/building-gormes/architecture_plan/_index.md +++ b/webpages/docs/content/building-gormes/architecture_plan/_index.md @@ -87,12 +87,12 @@ classification and row-backed implementation queue. ## Progress -**Overall:** 87/100 subphases shipped · 10 in progress · 3 planned +**Overall:** 86/100 subphases shipped · 11 in progress · 3 planned | Phase | Status | Shipped | |-------|--------|---------| | Phase 1 — The Dashboard | ✅ | 4/4 subphases | -| Phase 2 — The Gateway | ✅ | 21/21 subphases | +| Phase 2 — The Gateway | 🔨 | 20/21 subphases | | Phase 3 — The Black Box (Memory) | ✅ | 15/15 subphases | | Phase 4 — The Brain Transplant | ✅ | 13/13 subphases | | Phase 5 — The Final Purge | 🔨 | 18/23 subphases | @@ -136,7 +136,7 @@ classification and row-backed implementation queue. - [x] Skill-pack coverage audit for Hermes-in-Go completion - [x] Canonical development-skills directory and loader symlinks -## Phase 2 — The Gateway ✅ +## Phase 2 — The Gateway 🔨 *Go-native operator wiring harness: tools, Telegram, shared gateway chassis, shipped cron, and the first OS-AI spine slices before focused channel closeout* @@ -180,7 +180,7 @@ classification and row-backed implementation queue. - [x] WhatsApp outbound pairing gate + raw peer mapping - [x] WhatsApp reconnect backoff + send retry policy -### 2.B.5 — Session Context + Delivery Routing ✅ +### 2.B.5 — Session Context + Delivery Routing 🔨 - [x] Gateway session store + SessionSource parity - [x] Gateway manual reset session-boundary hooks @@ -261,6 +261,7 @@ classification and row-backed implementation queue. - [x] Telegram streaming edit Markdown safety - [x] Telegram guest mention allowlist bypass - [x] Gateway platform reconnect isolation + channel health limits +- [ ] Gateway /model interactive provider/model picker ### 2.B.10 — WeChat Adapter ✅ diff --git a/webpages/docs/content/building-gormes/architecture_plan/progress.json b/webpages/docs/content/building-gormes/architecture_plan/progress.json index 533a2e5f8..16837694d 100644 --- a/webpages/docs/content/building-gormes/architecture_plan/progress.json +++ b/webpages/docs/content/building-gormes/architecture_plan/progress.json @@ -7198,6 +7198,99 @@ "note": "2026-05-07 bounded all-topic weakness sweep selected this as the highest-priority builder-ready runtime row after the Telegram forum thread/retry row was downgraded to draft for missing SDK/thread delivery seams and the remaining open P1 event-bus row stayed a non-selectable umbrella parent. Complete: TDD added fakeable platform lifecycle helpers for HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT parsing, timeout-bounded startup, retry queue evidence, reconnect success clearing, nonretryable removal, retryable attempt increments, status updates, manifest-backed connected-platform checkers, tuned HTTP keepalive env parsing, and a WhatsApp bridge typing response cleanup policy fixture without live SDKs or network clients." }, "note": "Complete 2026-05-07: gateway lifecycle fixtures now prove Hermes-style platform startup isolation and retry state over fake connectors. The completed slice is a Go lifecycle/read-model seam; production channel startup can consume the helper in a later wiring row if broader manager integration is required." + }, + { + "name": "Gateway /model interactive provider/model picker", + "status": "planned", + "priority": "P1", + "contract": "Gateway `/model` with no arguments opens an interactive two-step provider→model picker on channels that support inline keyboards (Telegram first; Discord later) and writes a per-session model/provider override that the live-turn provider payload sees on the next message. The handler shares the Hermes-compatible `⚙ Model Configuration` / `Select a provider:` / `Select a model:` header text and reuses the curated provider list from the Hermes provider registry manifest. Channels without inline-keyboard support and the existing `/model ` typed-arg path keep the current static-text behavior. The picker is the gateway slash-handler port of `_handle_model_command`; it does not duplicate the completed `gormes model` Cobra CLI picker, which persists `hermes.model`/`hermes.provider` to config TOML and is governed by row `Gormes model interactive provider/model picker`.", + "contract_status": "draft", + "slice_size": "medium", + "execution_owner": "gateway", + "trust_class": [ + "operator", + "gateway", + "system" + ], + "degraded_mode": "On channels without inline keyboards the no-arg `/model` invocation continues to return the existing static `🤖 Model / 📡 Provider` text and the picker is reported as `gateway_model_picker_unavailable` evidence rather than silently dropping. If the provider registry manifest returns no providers, the handler falls back to the static text and reports `gateway_model_picker_empty_providers`. OpenRouter live-catalog filtering is out of scope for this slice: the picker uses the curated manifest models only and records `gateway_model_picker_openrouter_live_filter_pending` degraded evidence; a separate row may add live-catalog filtering parity with Hermes' `fetch_openrouter_models`. Picker selections apply as session-only overrides; the `--global` persistence path is reachable only through the typed-arg `/model --global` path covered by a future row.", + "fixture": "internal/gateway/model_picker_test.go; internal/channels/telegram/model_picker_buttons_test.go", + "source_refs": [ + "../hermes-agent/gateway/run.py@d4b26df8:_handle_model_command (no-arg picker dispatch)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:send_model_picker (provider stage text + buttons)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_build_model_keyboard (model pagination)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_handle_model_picker_callback (mp:/mm:/mg:/mb:/mx: routing)", + "../hermes-agent/hermes_cli/model_switch.py@d4b26df8:list_picker_providers (curated picker list)", + "internal/gateway/manager.go:1298:handleModelCommand current static-text handler", + "internal/channels/telegram/approval_buttons.go:87:handleCallbackQuery callback-router pattern Gormes already owns", + "internal/hermes/provider_registry_manifest.go:HermesProviderRegistryManifest curated provider list", + "internal/gateway/reasoning_command.go:SessionReasoningState per-session override precedent", + "docs/content/building-gormes/architecture_plan/progress.json:Gormes model interactive provider/model picker (CLI row 41975, coordinated)" + ], + "ready_when": [ + "Telegram inline approval buttons + callback auth is complete so the Telegram callback router already accepts a new `mp:/mm:/mg:/mb:/mx:` family alongside the existing `ea:` family without changing existing approval routing.", + "Hermes provider registry manifest is complete and exposes the curated provider+model list the picker needs.", + "Hermes CLI nested parser inventory refresh classifies `_handle_model_command` as the row-backed gateway-handler residual for this row.", + "Tests can drive a fake Telegram client capturing send-message payloads + inline keyboard markup and a fake callback-query stream without a live Bot API token." + ], + "not_ready_when": [ + "The slice mutates `hermes.model`/`hermes.provider` in TOML config (that is the `gormes model` Cobra row's job and the typed `/model --global` path's job, not this no-arg picker).", + "The slice runs OAuth, device-code, or vendor-CLI import flows for any provider (all auth flows belong to `gormes auth add`).", + "The slice ports OpenRouter live-catalog filtering, custom-endpoint config plumbing, or any non-Telegram channel beyond the inline-keyboard fallback contract.", + "The picker callback family collides with the existing `ea:` exec-approval callback family or routes approval callbacks through the picker resolver.", + "The Cobra `gormes model` CLI picker behavior is changed; its row (`Gormes model interactive provider/model picker`) remains the persistence-only path." + ], + "blocked_by": [ + "Telegram inline approval buttons + callback auth", + "Hermes provider registry manifest", + "Hermes CLI nested parser inventory refresh" + ], + "unblocks": [ + "Gateway /model typed-arg switch + --global persistence", + "Discord /model interactive picker parity" + ], + "acceptance": [ + "TestGatewayModelCommandNoArgsOpensTelegramPicker proves a no-arg `/model` from a Telegram source sends one message whose text contains `⚙ *Model Configuration*` and `Select a provider:` plus inline keyboard markup with one button per curated provider (`callback_data == \"mp:\"`) two per row and a final `mx` cancel button.", + "TestGatewayModelPickerProviderCallbackEditsToModelStage proves a `mp:` callback edits the same message in place to `Select a model:` text and replaces the keyboard with model buttons (`callback_data == \"mm:\"`) using the curated picker provider list, plus a `mb` back button and `mx` cancel.", + "TestGatewayModelPickerModelCallbackAppliesSessionOverride proves a `mm:` callback edits the message to a confirmation that names the chosen model+provider, applies a per-session override (new `SessionModelOverrideState` in the gateway manager) such that the next live-turn provider payload sees the chosen model/provider, and never writes to `hermes.model`/`hermes.provider` in TOML.", + "TestGatewayModelPickerCancelOrUnknownLeavesOverrideUnchanged proves `mx` cancel clears the pending picker without changing the session model override and proves `mp:` returns a redacted error answer without mutating state.", + "TestGatewayModelCommandStaticFallbackForNonInlineChannel proves a no-arg `/model` from a channel without inline-keyboard support keeps the existing `🤖 Model / 📡 Provider` static text and reports `gateway_model_picker_unavailable` evidence.", + "TestGatewayModelPickerCallbackFamiliesAreIsolated proves an `ea:` exec-approval callback continues to route through the existing approval resolver, an `mp:`/`mm:`/`mg:`/`mb`/`mx` callback does not invoke the approval resolver, and the new family does not eat unrelated callback data.", + "TestHermesCLIParityManifest reclassifies `_handle_model_command` from `gateway-handler:row-backed` to `gateway-handler:implemented` once the row ships." + ], + "write_scope": [ + "internal/gateway/manager.go", + "internal/gateway/model_picker.go", + "internal/gateway/model_picker_test.go", + "internal/channels/telegram/model_picker_buttons.go", + "internal/channels/telegram/model_picker_buttons_test.go", + "internal/channels/telegram/bot.go", + "internal/hermes/picker_providers.go", + "internal/hermes/picker_providers_test.go", + "cmd/gormes/hermes_cli_parity.go", + "docs/content/building-gormes/architecture_plan/progress.json" + ], + "test_commands": [ + "go test ./internal/gateway -run 'TestGatewayModel(Command|Picker)' -count=1", + "go test ./internal/channels/telegram -run 'TestTelegramModelPicker|TestCallback' -count=1", + "go test ./internal/hermes -run 'TestPickerProviders' -count=1", + "go test ./cmd/gormes -run 'TestHermesCLIParityManifest' -count=1", + "go test ./internal/gateway ./internal/channels/telegram ./internal/hermes ./cmd/gormes -count=1", + "go run ./cmd/progress validate", + "git diff --check" + ], + "done_signal": [ + "Gateway and Telegram fake-client fixtures prove the no-arg `/model` picker renders the Hermes-compatible header + two-stage inline keyboard, callback families stay isolated from `ea:` approvals, a chosen provider+model applies a per-session override (no TOML writes), cancel leaves the override unchanged, non-inline channels keep the static fallback, and the CLI parity manifest reclassifies `_handle_model_command` as implemented." + ], + "provenance": { + "origin_type": "hybrid", + "upstream_refs": [ + "hermes:d4b26df8 gateway/run.py:_handle_model_command", + "hermes:d4b26df8 gateway/platforms/telegram.py:send_model_picker + _handle_model_picker_callback", + "hermes:d4b26df8 hermes_cli/model_switch.py:list_picker_providers" + ], + "note": "Hermes shape: two-step inline-keyboard picker with mp:/mm:/mg:/mb:/mx: callback family editing the same message in place; selection applies a per-session model/provider override visible to the next live turn. Gormes-owned port: same callback family + header text + per-session override seam, with curated provider list from the existing Hermes provider registry manifest. OpenRouter live-catalog filtering, --global TOML persistence, and Discord picker are explicitly deferred to follow-on rows. This row is the gateway slash-handler counterpart of the complete Cobra-CLI row `Gormes model interactive provider/model picker`; the two share the Hermes-compatible header but write into different surfaces (per-session override vs. TOML config)." + }, + "note": "Authored 2026-05-10 from a gormes-hermes-parity sweep against hermes-agent d4b26df8 (current pinned submodule). User-reported regression: Gormes `/model` in a gateway channel showed only `🤖 Model: / 📡 Provider:

` text instead of Hermes' interactive `Select a provider:` keyboard. Evidence boundary: upstream submodule, repo source, and progress.json only; no live homes, secrets, or `~/.gormes` read. Dead-code observation: `internal/tui/model_picker.go` already implements a Hermes-compatible 2-step `ModelPickerState`/`RenderModelPicker`/`UpdateModelPicker` overlay but has zero non-test callers; that overlay is a TUI/Bubble Tea path, not the gateway/Telegram inline-keyboard path. Its state machine is a useful reference shape but it is not a direct dependency of this row, and wiring it through TUI `/model` is a separate behavior atom worth its own row." } ], "drift_state": { diff --git a/webpages/docs/content/building-gormes/builder-loop/blocked-slices.md b/webpages/docs/content/building-gormes/builder-loop/blocked-slices.md index dfbc5ce73..abcafdfb7 100644 --- a/webpages/docs/content/building-gormes/builder-loop/blocked-slices.md +++ b/webpages/docs/content/building-gormes/builder-loop/blocked-slices.md @@ -15,6 +15,7 @@ Use it to avoid assigning work before the dependency chain is ready. | Phase | Slice | Blocked by | Ready when | Unblocks | |---|---|---|---|---| +| 2 / 2.B.5 | Gateway /model interactive provider/model picker | Telegram inline approval buttons + callback auth, Hermes provider registry manifest, Hermes CLI nested parser inventory refresh | Telegram inline approval buttons + callback auth is complete so the Telegram callback router already accepts a new `mp:/mm:/mg:/mb:/mx:` family alongside the existing `ea:` family without changing existing approval routing., Hermes provider registry manifest is complete and exposes the curated provider+model list the picker needs., Hermes CLI nested parser inventory refresh classifies `_handle_model_command` as the row-backed gateway-handler residual for this row., Tests can drive a fake Telegram client capturing send-message payloads + inline keyboard markup and a fake callback-query stream without a live Bot API token. | Gateway /model typed-arg switch + --global persistence, Discord /model interactive picker parity | | 5 / 5.E | Go-native audio decoding (ffmpeg replacement) | Audio preprocessing and chunking pipeline | Row E 'Audio preprocessing and chunking pipeline' is complete and shipping ffmpeg-based preprocessing., Operator feedback or telemetry shows ffmpeg-absence is a real blocker on a target platform (Termux, Windows-portable, etc.). | - | | 8 / 8.A | TD social presence connected to blog feed | TD engineering blog scaffolded and live | TD blog (8.A row 1) is live and emitting a feed., Operator has chosen a social platform and created the account. | - | | 8 / 8.C | Engineering writeup #1: autonomous Hermes-porting loop | TD engineering blog scaffolded and live, Loop $/iteration cost metric in status file | TD blog (8.A row 1) is live., Loop $/iteration cost telemetry (8.F) has at least one week of data., Operator has decided the publication date and platform (HN/Lobsters/Reddit). | Engineering writeup #2: validation-gated agentic engineering, Engineering writeup #3: Gormes vs Hermes-Python benchmarks, HN launch post for Gormes 1.0 | diff --git a/webpages/docs/content/building-gormes/contract-readiness.md b/webpages/docs/content/building-gormes/contract-readiness.md index 2846ca6cc..d5eb181d3 100644 --- a/webpages/docs/content/building-gormes/contract-readiness.md +++ b/webpages/docs/content/building-gormes/contract-readiness.md @@ -161,6 +161,7 @@ through `gormes-interface-designer` first. | 2 / 2.B.5 | Telegram streaming edit Markdown safety — Telegram streamed edit delivery mirrors Hermes' finalization split: non-final editMessageText calls send the partial draft as plain text with no MarkdownV2 parse_mode so incomplete Markdown cannot break live streaming, while final edits keep MarkdownV2 formatting and parse-error plaintext fallback. The shared gateway coalescer already passes the finalize flag through gateway.FinalizingMessageEditor; this slice binds that interface in the Telegram adapter without changing generic coalescer semantics or Telegram send/reply behavior. | `validated` | `gateway` | `small` | operator, gateway | `internal/channels/telegram/bot_parse_mode_test.go::TestBot_EditMessageFinal_*` | If the adapter cannot distinguish streaming from final edits, Gormes may send partial Markdown through Telegram's MarkdownV2 parser and drop or delay visible streamed updates; final replies still fall back to a plain text send path. | | 2 / 2.B.5 | Telegram guest mention allowlist bypass — Telegram group admission matches Hermes guest_mode: when an operator configures an allowed_chat_id/allowed_chats allowlist and enables guest_mode, a group or supergroup outside that allowlist may reach the gateway only when the message explicitly mentions the bot by @username, text_mention user ID, or /command@botname entity. Replies to the bot, regex mention patterns, bare slash commands, and ordinary text must not bypass the allowlist. | `validated` | `gateway` | `small` | operator, gateway | `internal/channels/telegram/group_mention_binding_test.go::TestBot_ToInboundEvent_GuestMode* plus internal/gateway manager admission fixture` | If guest_mode is disabled or bot identity is unavailable, Gormes keeps the existing strict allowlist behavior and drops non-allowlisted group traffic. | | 2 / 2.B.5 | Gateway platform reconnect isolation + channel health limits — Gateway platform lifecycle matches Hermes' multi-channel resilience: each enabled platform connect is bounded by HERMES/GORMES gateway platform connect timeout and one timed-out platform is queued for retry without blocking later platforms; retryable startup/connect failures enter a reconnect watcher with attempts/next_retry state, successful retry installs the adapter and clears failure state, non-retryable fatal errors leave the queue, and retryable failures stay queued with incremented attempts. Connected-platform readouts cover every built-in platform via generic token/api-key checks or bespoke checkers that return bool without panics. Long-lived HTTP platform clients share tuned keepalive limits with env overrides and malformed override fallbacks, and WhatsApp typing releases HTTP responses structurally instead of leaking sockets. | `validated` | `gateway` | `medium` | operator, gateway, system | `internal/gateway/platform_reconnect_test.go; internal/gateway/platform_connected_checkers_test.go; internal/gateway/platform_http_client_limits_test.go` | If lifecycle resilience is unavailable, Gormes reports platform_reconnect_unavailable, platform_connected_checker_missing, or platform_http_client_limits_unavailable evidence while continuing healthy channels and preserving redacted startup failure state. | +| 2 / 2.B.5 | Gateway /model interactive provider/model picker — Gateway `/model` with no arguments opens an interactive two-step provider→model picker on channels that support inline keyboards (Telegram first; Discord later) and writes a per-session model/provider override that the live-turn provider payload sees on the next message. The handler shares the Hermes-compatible `⚙ Model Configuration` / `Select a provider:` / `Select a model:` header text and reuses the curated provider list from the Hermes provider registry manifest. Channels without inline-keyboard support and the existing `/model ` typed-arg path keep the current static-text behavior. The picker is the gateway slash-handler port of `_handle_model_command`; it does not duplicate the completed `gormes model` Cobra CLI picker, which persists `hermes.model`/`hermes.provider` to config TOML and is governed by row `Gormes model interactive provider/model picker`. | `draft` | `gateway` | `medium` | operator, gateway, system | `internal/gateway/model_picker_test.go; internal/channels/telegram/model_picker_buttons_test.go` | On channels without inline keyboards the no-arg `/model` invocation continues to return the existing static `🤖 Model / 📡 Provider` text and the picker is reported as `gateway_model_picker_unavailable` evidence rather than silently dropping. If the provider registry manifest returns no providers, the handler falls back to the static text and reports `gateway_model_picker_empty_providers`. OpenRouter live-catalog filtering is out of scope for this slice: the picker uses the curated manifest models only and records `gateway_model_picker_openrouter_live_filter_pending` degraded evidence; a separate row may add live-catalog filtering parity with Hermes' `fetch_openrouter_models`. Picker selections apply as session-only overrides; the `--global` persistence path is reachable only through the typed-arg `/model --global` path covered by a future row. | | 2 / 2.B.11 | Discord SessionSource guild/parent/message evidence — Discord ingress preserves guild, parent channel, and triggering message IDs in the gateway SessionSource without changing existing chat/thread routing | `validated` | `gateway` | `small` | gateway, operator | `internal/channels/discord/session_source_metadata_test.go` | Gateway session context omits unavailable Discord scope IDs explicitly rather than fabricating guild/thread/message metadata. | | 2 / 2.B.11 | Discord forum media + polish parity — Discord forum sends preserve Hermes forum-post semantics on top of the shipped Discord adapter: outbound text to a forum parent creates a starter thread post, follow-up chunks route through the created thread, file-bearing sends use create_thread(file=...) with deterministic thread names, failures are returned as SendResult warnings/errors, and forum thread metadata keeps parent_chat_id/thread_id/message_id intact for shared gateway delivery. | `validated` | `gateway` | `medium` | gateway, operator | `internal/channels/discord/forum_media_test.go` | Discord forum create/send/upload failures produce send_warning or send_failed evidence while non-forum channel sends continue through the existing shared-chassis contract. | | 2 / 2.B.12 | Channel-neutral native runtime turn adapter — Telegram, Slack, Discord, WhatsApp, BlueBubbles, and future channels enter the same native Gormes turn adapter so provider/runtime fixes preserve Hermes channel parity instead of hard-coding Telegram behavior | `validated` | `gateway` | `small` | gateway, operator, system | `internal/gateway/channel_neutral_turn_adapter_test.go` | Provider/runtime failures are rendered through the shared external-channel safe error helper without leaking raw error text and active turn state is cleared so subsequent inbound traffic is not blocked. | diff --git a/webpages/landing/legacy/go-renderer/internal/site/data/progress.json b/webpages/landing/legacy/go-renderer/internal/site/data/progress.json index 533a2e5f8..16837694d 100644 --- a/webpages/landing/legacy/go-renderer/internal/site/data/progress.json +++ b/webpages/landing/legacy/go-renderer/internal/site/data/progress.json @@ -7198,6 +7198,99 @@ "note": "2026-05-07 bounded all-topic weakness sweep selected this as the highest-priority builder-ready runtime row after the Telegram forum thread/retry row was downgraded to draft for missing SDK/thread delivery seams and the remaining open P1 event-bus row stayed a non-selectable umbrella parent. Complete: TDD added fakeable platform lifecycle helpers for HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT parsing, timeout-bounded startup, retry queue evidence, reconnect success clearing, nonretryable removal, retryable attempt increments, status updates, manifest-backed connected-platform checkers, tuned HTTP keepalive env parsing, and a WhatsApp bridge typing response cleanup policy fixture without live SDKs or network clients." }, "note": "Complete 2026-05-07: gateway lifecycle fixtures now prove Hermes-style platform startup isolation and retry state over fake connectors. The completed slice is a Go lifecycle/read-model seam; production channel startup can consume the helper in a later wiring row if broader manager integration is required." + }, + { + "name": "Gateway /model interactive provider/model picker", + "status": "planned", + "priority": "P1", + "contract": "Gateway `/model` with no arguments opens an interactive two-step provider→model picker on channels that support inline keyboards (Telegram first; Discord later) and writes a per-session model/provider override that the live-turn provider payload sees on the next message. The handler shares the Hermes-compatible `⚙ Model Configuration` / `Select a provider:` / `Select a model:` header text and reuses the curated provider list from the Hermes provider registry manifest. Channels without inline-keyboard support and the existing `/model ` typed-arg path keep the current static-text behavior. The picker is the gateway slash-handler port of `_handle_model_command`; it does not duplicate the completed `gormes model` Cobra CLI picker, which persists `hermes.model`/`hermes.provider` to config TOML and is governed by row `Gormes model interactive provider/model picker`.", + "contract_status": "draft", + "slice_size": "medium", + "execution_owner": "gateway", + "trust_class": [ + "operator", + "gateway", + "system" + ], + "degraded_mode": "On channels without inline keyboards the no-arg `/model` invocation continues to return the existing static `🤖 Model / 📡 Provider` text and the picker is reported as `gateway_model_picker_unavailable` evidence rather than silently dropping. If the provider registry manifest returns no providers, the handler falls back to the static text and reports `gateway_model_picker_empty_providers`. OpenRouter live-catalog filtering is out of scope for this slice: the picker uses the curated manifest models only and records `gateway_model_picker_openrouter_live_filter_pending` degraded evidence; a separate row may add live-catalog filtering parity with Hermes' `fetch_openrouter_models`. Picker selections apply as session-only overrides; the `--global` persistence path is reachable only through the typed-arg `/model --global` path covered by a future row.", + "fixture": "internal/gateway/model_picker_test.go; internal/channels/telegram/model_picker_buttons_test.go", + "source_refs": [ + "../hermes-agent/gateway/run.py@d4b26df8:_handle_model_command (no-arg picker dispatch)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:send_model_picker (provider stage text + buttons)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_build_model_keyboard (model pagination)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_handle_model_picker_callback (mp:/mm:/mg:/mb:/mx: routing)", + "../hermes-agent/hermes_cli/model_switch.py@d4b26df8:list_picker_providers (curated picker list)", + "internal/gateway/manager.go:1298:handleModelCommand current static-text handler", + "internal/channels/telegram/approval_buttons.go:87:handleCallbackQuery callback-router pattern Gormes already owns", + "internal/hermes/provider_registry_manifest.go:HermesProviderRegistryManifest curated provider list", + "internal/gateway/reasoning_command.go:SessionReasoningState per-session override precedent", + "docs/content/building-gormes/architecture_plan/progress.json:Gormes model interactive provider/model picker (CLI row 41975, coordinated)" + ], + "ready_when": [ + "Telegram inline approval buttons + callback auth is complete so the Telegram callback router already accepts a new `mp:/mm:/mg:/mb:/mx:` family alongside the existing `ea:` family without changing existing approval routing.", + "Hermes provider registry manifest is complete and exposes the curated provider+model list the picker needs.", + "Hermes CLI nested parser inventory refresh classifies `_handle_model_command` as the row-backed gateway-handler residual for this row.", + "Tests can drive a fake Telegram client capturing send-message payloads + inline keyboard markup and a fake callback-query stream without a live Bot API token." + ], + "not_ready_when": [ + "The slice mutates `hermes.model`/`hermes.provider` in TOML config (that is the `gormes model` Cobra row's job and the typed `/model --global` path's job, not this no-arg picker).", + "The slice runs OAuth, device-code, or vendor-CLI import flows for any provider (all auth flows belong to `gormes auth add`).", + "The slice ports OpenRouter live-catalog filtering, custom-endpoint config plumbing, or any non-Telegram channel beyond the inline-keyboard fallback contract.", + "The picker callback family collides with the existing `ea:` exec-approval callback family or routes approval callbacks through the picker resolver.", + "The Cobra `gormes model` CLI picker behavior is changed; its row (`Gormes model interactive provider/model picker`) remains the persistence-only path." + ], + "blocked_by": [ + "Telegram inline approval buttons + callback auth", + "Hermes provider registry manifest", + "Hermes CLI nested parser inventory refresh" + ], + "unblocks": [ + "Gateway /model typed-arg switch + --global persistence", + "Discord /model interactive picker parity" + ], + "acceptance": [ + "TestGatewayModelCommandNoArgsOpensTelegramPicker proves a no-arg `/model` from a Telegram source sends one message whose text contains `⚙ *Model Configuration*` and `Select a provider:` plus inline keyboard markup with one button per curated provider (`callback_data == \"mp:\"`) two per row and a final `mx` cancel button.", + "TestGatewayModelPickerProviderCallbackEditsToModelStage proves a `mp:` callback edits the same message in place to `Select a model:` text and replaces the keyboard with model buttons (`callback_data == \"mm:\"`) using the curated picker provider list, plus a `mb` back button and `mx` cancel.", + "TestGatewayModelPickerModelCallbackAppliesSessionOverride proves a `mm:` callback edits the message to a confirmation that names the chosen model+provider, applies a per-session override (new `SessionModelOverrideState` in the gateway manager) such that the next live-turn provider payload sees the chosen model/provider, and never writes to `hermes.model`/`hermes.provider` in TOML.", + "TestGatewayModelPickerCancelOrUnknownLeavesOverrideUnchanged proves `mx` cancel clears the pending picker without changing the session model override and proves `mp:` returns a redacted error answer without mutating state.", + "TestGatewayModelCommandStaticFallbackForNonInlineChannel proves a no-arg `/model` from a channel without inline-keyboard support keeps the existing `🤖 Model / 📡 Provider` static text and reports `gateway_model_picker_unavailable` evidence.", + "TestGatewayModelPickerCallbackFamiliesAreIsolated proves an `ea:` exec-approval callback continues to route through the existing approval resolver, an `mp:`/`mm:`/`mg:`/`mb`/`mx` callback does not invoke the approval resolver, and the new family does not eat unrelated callback data.", + "TestHermesCLIParityManifest reclassifies `_handle_model_command` from `gateway-handler:row-backed` to `gateway-handler:implemented` once the row ships." + ], + "write_scope": [ + "internal/gateway/manager.go", + "internal/gateway/model_picker.go", + "internal/gateway/model_picker_test.go", + "internal/channels/telegram/model_picker_buttons.go", + "internal/channels/telegram/model_picker_buttons_test.go", + "internal/channels/telegram/bot.go", + "internal/hermes/picker_providers.go", + "internal/hermes/picker_providers_test.go", + "cmd/gormes/hermes_cli_parity.go", + "docs/content/building-gormes/architecture_plan/progress.json" + ], + "test_commands": [ + "go test ./internal/gateway -run 'TestGatewayModel(Command|Picker)' -count=1", + "go test ./internal/channels/telegram -run 'TestTelegramModelPicker|TestCallback' -count=1", + "go test ./internal/hermes -run 'TestPickerProviders' -count=1", + "go test ./cmd/gormes -run 'TestHermesCLIParityManifest' -count=1", + "go test ./internal/gateway ./internal/channels/telegram ./internal/hermes ./cmd/gormes -count=1", + "go run ./cmd/progress validate", + "git diff --check" + ], + "done_signal": [ + "Gateway and Telegram fake-client fixtures prove the no-arg `/model` picker renders the Hermes-compatible header + two-stage inline keyboard, callback families stay isolated from `ea:` approvals, a chosen provider+model applies a per-session override (no TOML writes), cancel leaves the override unchanged, non-inline channels keep the static fallback, and the CLI parity manifest reclassifies `_handle_model_command` as implemented." + ], + "provenance": { + "origin_type": "hybrid", + "upstream_refs": [ + "hermes:d4b26df8 gateway/run.py:_handle_model_command", + "hermes:d4b26df8 gateway/platforms/telegram.py:send_model_picker + _handle_model_picker_callback", + "hermes:d4b26df8 hermes_cli/model_switch.py:list_picker_providers" + ], + "note": "Hermes shape: two-step inline-keyboard picker with mp:/mm:/mg:/mb:/mx: callback family editing the same message in place; selection applies a per-session model/provider override visible to the next live turn. Gormes-owned port: same callback family + header text + per-session override seam, with curated provider list from the existing Hermes provider registry manifest. OpenRouter live-catalog filtering, --global TOML persistence, and Discord picker are explicitly deferred to follow-on rows. This row is the gateway slash-handler counterpart of the complete Cobra-CLI row `Gormes model interactive provider/model picker`; the two share the Hermes-compatible header but write into different surfaces (per-session override vs. TOML config)." + }, + "note": "Authored 2026-05-10 from a gormes-hermes-parity sweep against hermes-agent d4b26df8 (current pinned submodule). User-reported regression: Gormes `/model` in a gateway channel showed only `🤖 Model: / 📡 Provider:

` text instead of Hermes' interactive `Select a provider:` keyboard. Evidence boundary: upstream submodule, repo source, and progress.json only; no live homes, secrets, or `~/.gormes` read. Dead-code observation: `internal/tui/model_picker.go` already implements a Hermes-compatible 2-step `ModelPickerState`/`RenderModelPicker`/`UpdateModelPicker` overlay but has zero non-test callers; that overlay is a TUI/Bubble Tea path, not the gateway/Telegram inline-keyboard path. Its state machine is a useful reference shape but it is not a direct dependency of this row, and wiring it through TUI `/model` is a separate behavior atom worth its own row." } ], "drift_state": { diff --git a/webpages/landing/src/data/progress.json b/webpages/landing/src/data/progress.json index 533a2e5f8..16837694d 100644 --- a/webpages/landing/src/data/progress.json +++ b/webpages/landing/src/data/progress.json @@ -7198,6 +7198,99 @@ "note": "2026-05-07 bounded all-topic weakness sweep selected this as the highest-priority builder-ready runtime row after the Telegram forum thread/retry row was downgraded to draft for missing SDK/thread delivery seams and the remaining open P1 event-bus row stayed a non-selectable umbrella parent. Complete: TDD added fakeable platform lifecycle helpers for HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT parsing, timeout-bounded startup, retry queue evidence, reconnect success clearing, nonretryable removal, retryable attempt increments, status updates, manifest-backed connected-platform checkers, tuned HTTP keepalive env parsing, and a WhatsApp bridge typing response cleanup policy fixture without live SDKs or network clients." }, "note": "Complete 2026-05-07: gateway lifecycle fixtures now prove Hermes-style platform startup isolation and retry state over fake connectors. The completed slice is a Go lifecycle/read-model seam; production channel startup can consume the helper in a later wiring row if broader manager integration is required." + }, + { + "name": "Gateway /model interactive provider/model picker", + "status": "planned", + "priority": "P1", + "contract": "Gateway `/model` with no arguments opens an interactive two-step provider→model picker on channels that support inline keyboards (Telegram first; Discord later) and writes a per-session model/provider override that the live-turn provider payload sees on the next message. The handler shares the Hermes-compatible `⚙ Model Configuration` / `Select a provider:` / `Select a model:` header text and reuses the curated provider list from the Hermes provider registry manifest. Channels without inline-keyboard support and the existing `/model ` typed-arg path keep the current static-text behavior. The picker is the gateway slash-handler port of `_handle_model_command`; it does not duplicate the completed `gormes model` Cobra CLI picker, which persists `hermes.model`/`hermes.provider` to config TOML and is governed by row `Gormes model interactive provider/model picker`.", + "contract_status": "draft", + "slice_size": "medium", + "execution_owner": "gateway", + "trust_class": [ + "operator", + "gateway", + "system" + ], + "degraded_mode": "On channels without inline keyboards the no-arg `/model` invocation continues to return the existing static `🤖 Model / 📡 Provider` text and the picker is reported as `gateway_model_picker_unavailable` evidence rather than silently dropping. If the provider registry manifest returns no providers, the handler falls back to the static text and reports `gateway_model_picker_empty_providers`. OpenRouter live-catalog filtering is out of scope for this slice: the picker uses the curated manifest models only and records `gateway_model_picker_openrouter_live_filter_pending` degraded evidence; a separate row may add live-catalog filtering parity with Hermes' `fetch_openrouter_models`. Picker selections apply as session-only overrides; the `--global` persistence path is reachable only through the typed-arg `/model --global` path covered by a future row.", + "fixture": "internal/gateway/model_picker_test.go; internal/channels/telegram/model_picker_buttons_test.go", + "source_refs": [ + "../hermes-agent/gateway/run.py@d4b26df8:_handle_model_command (no-arg picker dispatch)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:send_model_picker (provider stage text + buttons)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_build_model_keyboard (model pagination)", + "../hermes-agent/gateway/platforms/telegram.py@d4b26df8:_handle_model_picker_callback (mp:/mm:/mg:/mb:/mx: routing)", + "../hermes-agent/hermes_cli/model_switch.py@d4b26df8:list_picker_providers (curated picker list)", + "internal/gateway/manager.go:1298:handleModelCommand current static-text handler", + "internal/channels/telegram/approval_buttons.go:87:handleCallbackQuery callback-router pattern Gormes already owns", + "internal/hermes/provider_registry_manifest.go:HermesProviderRegistryManifest curated provider list", + "internal/gateway/reasoning_command.go:SessionReasoningState per-session override precedent", + "docs/content/building-gormes/architecture_plan/progress.json:Gormes model interactive provider/model picker (CLI row 41975, coordinated)" + ], + "ready_when": [ + "Telegram inline approval buttons + callback auth is complete so the Telegram callback router already accepts a new `mp:/mm:/mg:/mb:/mx:` family alongside the existing `ea:` family without changing existing approval routing.", + "Hermes provider registry manifest is complete and exposes the curated provider+model list the picker needs.", + "Hermes CLI nested parser inventory refresh classifies `_handle_model_command` as the row-backed gateway-handler residual for this row.", + "Tests can drive a fake Telegram client capturing send-message payloads + inline keyboard markup and a fake callback-query stream without a live Bot API token." + ], + "not_ready_when": [ + "The slice mutates `hermes.model`/`hermes.provider` in TOML config (that is the `gormes model` Cobra row's job and the typed `/model --global` path's job, not this no-arg picker).", + "The slice runs OAuth, device-code, or vendor-CLI import flows for any provider (all auth flows belong to `gormes auth add`).", + "The slice ports OpenRouter live-catalog filtering, custom-endpoint config plumbing, or any non-Telegram channel beyond the inline-keyboard fallback contract.", + "The picker callback family collides with the existing `ea:` exec-approval callback family or routes approval callbacks through the picker resolver.", + "The Cobra `gormes model` CLI picker behavior is changed; its row (`Gormes model interactive provider/model picker`) remains the persistence-only path." + ], + "blocked_by": [ + "Telegram inline approval buttons + callback auth", + "Hermes provider registry manifest", + "Hermes CLI nested parser inventory refresh" + ], + "unblocks": [ + "Gateway /model typed-arg switch + --global persistence", + "Discord /model interactive picker parity" + ], + "acceptance": [ + "TestGatewayModelCommandNoArgsOpensTelegramPicker proves a no-arg `/model` from a Telegram source sends one message whose text contains `⚙ *Model Configuration*` and `Select a provider:` plus inline keyboard markup with one button per curated provider (`callback_data == \"mp:\"`) two per row and a final `mx` cancel button.", + "TestGatewayModelPickerProviderCallbackEditsToModelStage proves a `mp:` callback edits the same message in place to `Select a model:` text and replaces the keyboard with model buttons (`callback_data == \"mm:\"`) using the curated picker provider list, plus a `mb` back button and `mx` cancel.", + "TestGatewayModelPickerModelCallbackAppliesSessionOverride proves a `mm:` callback edits the message to a confirmation that names the chosen model+provider, applies a per-session override (new `SessionModelOverrideState` in the gateway manager) such that the next live-turn provider payload sees the chosen model/provider, and never writes to `hermes.model`/`hermes.provider` in TOML.", + "TestGatewayModelPickerCancelOrUnknownLeavesOverrideUnchanged proves `mx` cancel clears the pending picker without changing the session model override and proves `mp:` returns a redacted error answer without mutating state.", + "TestGatewayModelCommandStaticFallbackForNonInlineChannel proves a no-arg `/model` from a channel without inline-keyboard support keeps the existing `🤖 Model / 📡 Provider` static text and reports `gateway_model_picker_unavailable` evidence.", + "TestGatewayModelPickerCallbackFamiliesAreIsolated proves an `ea:` exec-approval callback continues to route through the existing approval resolver, an `mp:`/`mm:`/`mg:`/`mb`/`mx` callback does not invoke the approval resolver, and the new family does not eat unrelated callback data.", + "TestHermesCLIParityManifest reclassifies `_handle_model_command` from `gateway-handler:row-backed` to `gateway-handler:implemented` once the row ships." + ], + "write_scope": [ + "internal/gateway/manager.go", + "internal/gateway/model_picker.go", + "internal/gateway/model_picker_test.go", + "internal/channels/telegram/model_picker_buttons.go", + "internal/channels/telegram/model_picker_buttons_test.go", + "internal/channels/telegram/bot.go", + "internal/hermes/picker_providers.go", + "internal/hermes/picker_providers_test.go", + "cmd/gormes/hermes_cli_parity.go", + "docs/content/building-gormes/architecture_plan/progress.json" + ], + "test_commands": [ + "go test ./internal/gateway -run 'TestGatewayModel(Command|Picker)' -count=1", + "go test ./internal/channels/telegram -run 'TestTelegramModelPicker|TestCallback' -count=1", + "go test ./internal/hermes -run 'TestPickerProviders' -count=1", + "go test ./cmd/gormes -run 'TestHermesCLIParityManifest' -count=1", + "go test ./internal/gateway ./internal/channels/telegram ./internal/hermes ./cmd/gormes -count=1", + "go run ./cmd/progress validate", + "git diff --check" + ], + "done_signal": [ + "Gateway and Telegram fake-client fixtures prove the no-arg `/model` picker renders the Hermes-compatible header + two-stage inline keyboard, callback families stay isolated from `ea:` approvals, a chosen provider+model applies a per-session override (no TOML writes), cancel leaves the override unchanged, non-inline channels keep the static fallback, and the CLI parity manifest reclassifies `_handle_model_command` as implemented." + ], + "provenance": { + "origin_type": "hybrid", + "upstream_refs": [ + "hermes:d4b26df8 gateway/run.py:_handle_model_command", + "hermes:d4b26df8 gateway/platforms/telegram.py:send_model_picker + _handle_model_picker_callback", + "hermes:d4b26df8 hermes_cli/model_switch.py:list_picker_providers" + ], + "note": "Hermes shape: two-step inline-keyboard picker with mp:/mm:/mg:/mb:/mx: callback family editing the same message in place; selection applies a per-session model/provider override visible to the next live turn. Gormes-owned port: same callback family + header text + per-session override seam, with curated provider list from the existing Hermes provider registry manifest. OpenRouter live-catalog filtering, --global TOML persistence, and Discord picker are explicitly deferred to follow-on rows. This row is the gateway slash-handler counterpart of the complete Cobra-CLI row `Gormes model interactive provider/model picker`; the two share the Hermes-compatible header but write into different surfaces (per-session override vs. TOML config)." + }, + "note": "Authored 2026-05-10 from a gormes-hermes-parity sweep against hermes-agent d4b26df8 (current pinned submodule). User-reported regression: Gormes `/model` in a gateway channel showed only `🤖 Model: / 📡 Provider:

` text instead of Hermes' interactive `Select a provider:` keyboard. Evidence boundary: upstream submodule, repo source, and progress.json only; no live homes, secrets, or `~/.gormes` read. Dead-code observation: `internal/tui/model_picker.go` already implements a Hermes-compatible 2-step `ModelPickerState`/`RenderModelPicker`/`UpdateModelPicker` overlay but has zero non-test callers; that overlay is a TUI/Bubble Tea path, not the gateway/Telegram inline-keyboard path. Its state machine is a useful reference shape but it is not a direct dependency of this row, and wiring it through TUI `/model` is a separate behavior atom worth its own row." } ], "drift_state": {