diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index d6d42e7..6665e6e 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -93,6 +93,13 @@ Plans:
**Delivers:** Production readiness
+**Plans:** 3 plans
+
+Plans:
+- [ ] 05-01-PLAN.md — Remove NLU nodes and add keyword routing with persistent menu
+- [ ] 05-02-PLAN.md — Standardize error messages and migrate credentials
+- [ ] 05-03-PLAN.md — Write deployment README and end-to-end testing
+
**Status:** 🔲 Not started
---
diff --git a/.planning/phases/05-polish-deploy/05-01-PLAN.md b/.planning/phases/05-polish-deploy/05-01-PLAN.md
new file mode 100644
index 0000000..dba635c
--- /dev/null
+++ b/.planning/phases/05-polish-deploy/05-01-PLAN.md
@@ -0,0 +1,172 @@
+---
+phase: 05-polish-deploy
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified: [n8n-workflow.json]
+autonomous: true
+
+must_haves:
+ truths:
+ - "User sees persistent menu buttons in Telegram"
+ - "Typing 'status' triggers container list (no Claude API call)"
+ - "Typing 'start plex' triggers start flow (no Claude API call)"
+ - "Unknown commands show menu instead of error"
+ artifacts:
+ - path: "n8n-workflow.json"
+ provides: "Keyword Router Switch node"
+ contains: "Keyword Router"
+ - path: "n8n-workflow.json"
+ provides: "Persistent menu reply markup"
+ contains: "is_persistent"
+ key_links:
+ - from: "Telegram Trigger"
+ to: "Keyword Router"
+ via: "Route Message path"
+ pattern: "Keyword Router"
+ - from: "Keyword Router"
+ to: "Docker List Containers"
+ via: "status output"
+ pattern: "status.*Docker List"
+---
+
+
+Replace Claude/NLU nodes with keyword-based routing and add persistent Telegram menu buttons.
+
+Purpose: Remove external Claude API dependency, enable offline-first operation with simple keyword matching.
+Output: Working keyword routing with persistent menu, no Claude API calls in workflow.
+
+
+
+@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
+@/home/luc/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/05-polish-deploy/05-CONTEXT.md
+@.planning/phases/05-polish-deploy/05-RESEARCH.md
+@n8n-workflow.json
+
+
+
+
+
+ Task 1: Remove NLU/Claude nodes and add Keyword Router
+ n8n-workflow.json
+
+Remove these nodes from the workflow:
+- Prepare Claude Request
+- Claude Intent Parser (HTTP Request node calling api.anthropic.com)
+- Parse Intent (Code node)
+- Intent Router (old Switch node routing on parsed intent)
+- Send Unknown Intent
+- Send Intent Error
+- Remove Anthropic API Key credential reference
+
+Replace with a single Switch node called "Keyword Router" with these rules (case-insensitive matching on message.text):
+- Contains "status" -> output "status" -> connect to Docker List Containers
+- Contains "start" -> output "start" -> connect to Parse Action Command (with action set to start)
+- Contains "stop" -> output "stop" -> connect to Parse Action Command (with action set to stop)
+- Contains "restart" -> output "restart" -> connect to Parse Action Command (with action set to restart)
+- Contains "update" -> output "update" -> connect to existing update flow entry point
+- Contains "logs" -> output "logs" -> connect to existing logs flow entry point
+- Fallback (extra) -> connect to new "Show Menu" node
+
+Use the Switch node pattern from RESEARCH.md:
+```json
+{
+ "conditions": {
+ "options": { "caseSensitive": false },
+ "conditions": [{
+ "leftValue": "={{ $json.message.text }}",
+ "rightValue": "status",
+ "operator": { "type": "string", "operation": "contains" }
+ }]
+ }
+}
+```
+
+Update "Route Message" node to connect directly to "Keyword Router" instead of the old Claude flow.
+
+
+Open workflow in n8n UI. Send "status" - should trigger Docker List Containers path.
+Check that no nodes reference api.anthropic.com or Anthropic API Key credential.
+
+
+Keyword Router handles all 6 commands (status, start, stop, restart, update, logs) plus fallback.
+No Claude/NLU nodes remain in workflow.
+
+
+
+
+ Task 2: Add persistent Telegram menu
+ n8n-workflow.json
+
+Create a new "Show Menu" node (HTTP Request type, not native Telegram node - per project pattern for keyboards).
+
+Configure HTTP Request:
+- URL: `https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/sendMessage`
+- Method: POST
+- Body (JSON):
+```json
+{
+ "chat_id": "={{ $json.message.chat.id }}",
+ "text": "Use buttons below or type commands:",
+ "parse_mode": "HTML",
+ "reply_markup": {
+ "keyboard": [
+ [{"text": "Status"}],
+ [{"text": "Start"}, {"text": "Stop"}],
+ [{"text": "Restart"}, {"text": "Update"}],
+ [{"text": "Logs"}]
+ ],
+ "is_persistent": true,
+ "resize_keyboard": true,
+ "one_time_keyboard": false
+ }
+}
+```
+
+Connect Keyword Router fallback output to Show Menu node.
+
+NOTE: Research suggests emojis optional (Claude's discretion from CONTEXT.md). Start without emojis for cleaner keyword matching - button text "Status" matches "status" keyword.
+
+Also wire /start command to Show Menu:
+- In Keyword Router, add rule: Contains "/start" -> output "menu" -> connect to Show Menu
+
+
+Send any unknown command (e.g., "hello") - should receive menu with persistent keyboard.
+Send "/start" - should receive same menu.
+Keyboard should persist after sending other messages.
+
+
+Persistent menu visible in Telegram chat.
+Menu buttons trigger corresponding actions when tapped.
+/start and unknown commands show menu.
+
+
+
+
+
+
+1. All Claude/NLU nodes removed (grep for "anthropic", "Claude", "Intent" in workflow JSON)
+2. Keyword Router handles: status, start, stop, restart, update, logs, /start, fallback
+3. Persistent menu shows 6 buttons in grouped layout
+4. Button taps trigger corresponding keyword routes
+5. No errors in n8n execution logs for any command
+
+
+
+- Zero references to Anthropic API or Claude in workflow
+- All 6 container commands work via typed keywords
+- Persistent menu visible and functional
+- Unknown input shows menu (not error)
+
+
+
diff --git a/.planning/phases/05-polish-deploy/05-02-PLAN.md b/.planning/phases/05-polish-deploy/05-02-PLAN.md
new file mode 100644
index 0000000..2722fd4
--- /dev/null
+++ b/.planning/phases/05-polish-deploy/05-02-PLAN.md
@@ -0,0 +1,157 @@
+---
+phase: 05-polish-deploy
+plan: 02
+type: execute
+wave: 2
+depends_on: [05-01]
+files_modified: [n8n-workflow.json]
+autonomous: true
+
+must_haves:
+ truths:
+ - "Docker socket errors show 'Cannot connect to Docker' (not stack trace)"
+ - "Failed actions show 'Failed to X' format (not verbose details)"
+ - "User ID stored in n8n credentials (not hardcoded in workflow JSON)"
+ artifacts:
+ - path: "n8n-workflow.json"
+ provides: "Terse error messages in response nodes"
+ contains: "Cannot connect to Docker"
+ - path: "n8n-workflow.json"
+ provides: "Credential reference for user auth"
+ contains: "$credentials"
+ key_links:
+ - from: "IF User Authenticated"
+ to: "n8n credentials"
+ via: "credential reference expression"
+ pattern: "\\$credentials"
+---
+
+
+Harden error handling with minimal user-facing messages and migrate user ID to n8n credentials system.
+
+Purpose: Production-ready error UX and secure credential storage for workflow sharing.
+Output: Terse error messages, credential-based auth, exportable workflow without sensitive data.
+
+
+
+@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
+@/home/luc/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/05-polish-deploy/05-CONTEXT.md
+@.planning/phases/05-polish-deploy/05-RESEARCH.md
+@.planning/phases/05-polish-deploy/05-01-SUMMARY.md
+@n8n-workflow.json
+
+
+
+
+
+ Task 1: Standardize error messages
+ n8n-workflow.json
+
+Find all error response nodes and update their message text to follow minimal format:
+
+**Docker/infrastructure errors (detect "docker.sock" or "ECONNREFUSED" in error):**
+- Message: "Cannot connect to Docker"
+
+**Action failures:**
+- Pattern: "Failed to {action} {container}"
+- Examples: "Failed to start plex", "Failed to stop nginx"
+
+**No match errors:**
+- Keep existing "No container matching 'X'" format (already terse)
+
+Update these nodes to use terse format:
+- Send Docker Error -> "Cannot connect to Docker"
+- Send Action Result (error case) -> "Failed to {action} {container}"
+- Send Update Error -> "Failed to update {container}"
+- Send Logs Error -> "Failed to get logs for {container}"
+
+Do NOT include:
+- Stack traces
+- HTTP status codes
+- Docker API error details
+- Technical debugging info
+
+Per CONTEXT.md: "Minimal error messages - 'Failed to start plex' without verbose details"
+
+
+Grep workflow JSON for error messages - should be terse, no technical details.
+Manually test by stopping n8n's Docker socket access and sending command - should see "Cannot connect to Docker".
+
+
+All user-facing error messages follow terse format.
+Infrastructure errors show "Cannot connect to Docker".
+Action errors show "Failed to X Y" pattern.
+
+
+
+
+ Task 2: Migrate user ID to n8n credentials
+ n8n-workflow.json
+
+Currently the workflow has hardcoded user ID in IF nodes for auth check. Per CONTEXT.md and RESEARCH.md, migrate to n8n credentials system.
+
+**Step 1: Create credential type reference in workflow**
+
+n8n credentials will be created manually by user during deployment, but workflow must reference them.
+
+**Step 2: Update auth check nodes**
+
+Find "IF User Authenticated" and "IF Callback Authenticated" nodes.
+
+Change the condition from hardcoded ID comparison to credential reference:
+```
+// Old (hardcoded):
+$json.message.from.id === 123456789
+
+// New (credential reference):
+$json.message.from.id === parseInt($credentials.telegramAuth.userId)
+```
+
+Note: The credential name "telegramAuth" with field "userId" follows RESEARCH.md pattern.
+User will create this credential during deployment (documented in README from Plan 03).
+
+**Step 3: Clean up sensitive data**
+
+Search workflow JSON for any hardcoded numeric IDs that look like Telegram user IDs (8+ digit numbers).
+Remove or replace with credential references.
+
+Per CONTEXT.md: "Sensitive values (Telegram user ID) moved to n8n credentials system - not hardcoded in workflow JSON"
+
+
+Grep workflow JSON for 8+ digit numbers - should find none (except node position coordinates).
+Auth check nodes should reference $credentials.telegramAuth.userId.
+
+
+No hardcoded user ID in workflow JSON.
+Auth nodes use credential reference.
+Workflow can be safely exported/shared.
+
+
+
+
+
+
+1. All error messages terse (no stack traces, no verbose details)
+2. Docker socket errors produce "Cannot connect to Docker"
+3. Action failures produce "Failed to X Y" format
+4. No hardcoded Telegram user ID in workflow JSON
+5. Auth nodes reference $credentials.telegramAuth.userId
+6. Workflow exports cleanly without embedded secrets
+
+
+
+- Error messages fit in single Telegram line (no scrolling needed)
+- Zero hardcoded sensitive values in workflow JSON
+- grep -E '[0-9]{9,}' workflow.json returns only position coordinates
+
+
+
diff --git a/.planning/phases/05-polish-deploy/05-03-PLAN.md b/.planning/phases/05-polish-deploy/05-03-PLAN.md
new file mode 100644
index 0000000..4887602
--- /dev/null
+++ b/.planning/phases/05-polish-deploy/05-03-PLAN.md
@@ -0,0 +1,203 @@
+---
+phase: 05-polish-deploy
+plan: 03
+type: execute
+wave: 3
+depends_on: [05-02]
+files_modified: [README.md, n8n-workflow.json]
+autonomous: false
+
+must_haves:
+ truths:
+ - "README has step-by-step deployment instructions"
+ - "README documents credential creation in n8n"
+ - "README documents Docker socket setup for n8n container"
+ - "All 6 commands tested end-to-end via Telegram"
+ artifacts:
+ - path: "README.md"
+ provides: "Deployment guide"
+ min_lines: 50
+ contains: "Installation"
+ - path: "n8n-workflow.json"
+ provides: "Production-ready workflow"
+ key_links:
+ - from: "README.md"
+ to: "n8n credentials"
+ via: "documentation"
+ pattern: "telegramAuth"
+---
+
+
+Write deployment README and perform end-to-end testing of complete bot functionality.
+
+Purpose: Enable users to deploy the bot on their own Unraid servers with clear instructions.
+Output: Complete README, verified workflow, production-ready deployment package.
+
+
+
+@/home/luc/.claude/get-shit-done/workflows/execute-plan.md
+@/home/luc/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/05-polish-deploy/05-CONTEXT.md
+@.planning/phases/05-polish-deploy/05-RESEARCH.md
+@.planning/phases/05-polish-deploy/05-01-SUMMARY.md
+@.planning/phases/05-polish-deploy/05-02-SUMMARY.md
+@n8n-workflow.json
+
+
+
+
+
+ Task 1: Write deployment README
+ README.md
+
+Replace the stub README with a complete deployment guide. Per CONTEXT.md: "README only - step-by-step instructions in markdown" and "No troubleshooting section - focused on initial setup only".
+
+Structure (following RESEARCH.md template):
+
+# Docker Manager Bot
+
+One-line description: Telegram bot for managing Docker containers on Unraid.
+
+## Prerequisites
+
+- Unraid server with Docker enabled
+- n8n container running on Unraid
+- Telegram Bot Token (from @BotFather)
+- Your Telegram User ID (from @userinfobot)
+
+## Installation
+
+### 1. Configure n8n Container
+
+Document the Docker run flags needed:
+```bash
+docker run -d \
+ --name n8n \
+ --group-add 281 \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v /path/to/curl:/usr/bin/curl:ro \
+ n8nio/n8n
+```
+
+Explain:
+- `--group-add 281` for Docker socket access
+- Socket mount requirement
+- Static curl binary mount (hardened n8n image)
+
+### 2. Create n8n Credentials
+
+**Telegram API credential:**
+- Type: Telegram API
+- Access Token: your bot token from @BotFather
+
+**Telegram Auth credential:**
+- Type: Header Auth (or custom)
+- Field: userId = your Telegram user ID
+
+### 3. Import Workflow
+
+- Copy n8n-workflow.json to server
+- In n8n: Workflows -> Import from File
+- Map credentials when prompted
+
+### 4. Activate Workflow
+
+- Open workflow
+- Click Active toggle
+- Test with "status" message
+
+## Usage
+
+List the 6 commands:
+- status - View all containers
+- start - Start container
+- stop - Stop container
+- restart - Restart container
+- update - Pull and recreate container
+- logs [lines] - View container logs
+
+Mention persistent menu buttons available.
+
+Per CONTEXT.md: No troubleshooting section.
+
+
+README exists at root.
+Contains all 4 installation sections.
+No troubleshooting section.
+Markdown renders correctly.
+
+
+Complete deployment guide in README.md.
+Step-by-step instructions for fresh Unraid installation.
+
+
+
+
+ Task 2: End-to-end testing
+
+Complete Docker Manager Bot:
+- Keyword routing (no Claude dependency)
+- Persistent Telegram menu
+- All 6 container commands
+- Terse error messages
+- Credential-based auth
+
+
+Test each command via Telegram bot:
+
+1. **Menu:** Send /start or any unknown text
+ - Expected: Persistent keyboard appears with 6 buttons
+
+2. **Status:** Tap Status button or type "status"
+ - Expected: List of containers with status indicators
+
+3. **Start:** Type "start "
+ - Expected: Container starts, confirmation message
+
+4. **Stop:** Type "stop "
+ - Expected: Container stops, confirmation message
+
+5. **Restart:** Type "restart "
+ - Expected: Container restarts, confirmation message
+
+6. **Update:** Type "update "
+ - Expected: Image pulled, container recreated (or silent if no update)
+
+7. **Logs:** Type "logs "
+ - Expected: Last 50 log lines displayed
+
+8. **Error handling:** Stop n8n's Docker socket access briefly
+ - Expected: "Cannot connect to Docker" (not stack trace)
+
+9. **Auth:** Message bot from different Telegram account
+ - Expected: No response (silent ignore per Phase 1 decision)
+
+ Type "approved" if all tests pass, or describe which tests failed
+
+
+
+
+
+1. README exists and has all required sections
+2. All 6 commands work end-to-end
+3. Persistent menu functions correctly
+4. Error messages are terse
+5. Unauthorized users get no response
+6. Workflow exports without sensitive data
+
+
+
+- Fresh user can follow README to deploy bot
+- All container management commands functional
+- Bot ready for production use on Unraid
+
+
+