diff --git a/.claude/context/PROJECT.md b/.claude/context/PROJECT.md
index 8872cf4b..6a64e7e4 100644
--- a/.claude/context/PROJECT.md
+++ b/.claude/context/PROJECT.md
@@ -10,7 +10,7 @@ Replace the sections below with information about your project.
---
-## Project: azlin
+## Project: task-unnamed-1774842804
## Overview
@@ -29,7 +29,6 @@ Replace the sections below with information about your project.
- **Language**: Python
- **Language**: JavaScript/TypeScript
- **Language**: Rust
-- **Language**: Go
- **Framework**: [Main framework if applicable]
- **Database**: [Database system if applicable]
diff --git a/.github/hooks/amplihack-hooks.json b/.github/hooks/amplihack-hooks.json
new file mode 100644
index 00000000..17b0c84a
--- /dev/null
+++ b/.github/hooks/amplihack-hooks.json
@@ -0,0 +1,47 @@
+{
+ "version": 1,
+ "hooks": {
+ "sessionStart": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/session-start",
+ "timeoutSec": 30
+ }
+ ],
+ "sessionEnd": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/session-stop",
+ "timeoutSec": 30
+ }
+ ],
+ "userPromptSubmitted": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/user-prompt-submit",
+ "timeoutSec": 10
+ }
+ ],
+ "preToolUse": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/pre-tool-use",
+ "timeoutSec": 15
+ }
+ ],
+ "postToolUse": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/post-tool-use",
+ "timeoutSec": 10
+ }
+ ],
+ "errorOccurred": [
+ {
+ "type": "command",
+ "bash": ".github/hooks/error-occurred",
+ "timeoutSec": 10
+ }
+ ]
+ }
+}
diff --git a/.github/hooks/error-occurred b/.github/hooks/error-occurred
new file mode 100755
index 00000000..9105c04d
--- /dev/null
+++ b/.github/hooks/error-occurred
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Copilot hook: error-occurred
+# Logs error to runtime log. No dedicated Python hook exists for this event;
+# error_protocol.py is a utility module, not a hook entry point.
+
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+LOG_DIR="$HOME/.amplihack/.claude/runtime/logs"
+
+# If a dedicated error_occurred.py hook exists, use it
+if [[ -f "${AMPLIHACK_HOOKS}/error_occurred.py" ]]; then
+ python3 "${AMPLIHACK_HOOKS}/error_occurred.py" "$@"
+elif REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/error_occurred.py" ]]; then
+ python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/error_occurred.py" "$@"
+else
+ # Fallback: log the error from stdin
+ mkdir -p "$LOG_DIR"
+ INPUT=$(cat)
+ ERROR_MSG=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error',{}).get('message','unknown'))" 2>/dev/null || echo "unknown")
+ echo "$(date -Iseconds): ERROR - $ERROR_MSG" >> "${LOG_DIR}/errors.log"
+ echo "{}"
+fi
diff --git a/.github/hooks/post-tool-use b/.github/hooks/post-tool-use
new file mode 100755
index 00000000..57e00d79
--- /dev/null
+++ b/.github/hooks/post-tool-use
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Copilot hook wrapper - generated by amplihack
+HOOK="post_tool_use.py"
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+
+if [[ -f "${AMPLIHACK_HOOKS}/${HOOK}" ]]; then
+ exec python3 "${AMPLIHACK_HOOKS}/${HOOK}" "$@"
+elif REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/${HOOK}" ]]; then
+ exec python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/${HOOK}" "$@"
+else
+ echo "{}"
+fi
diff --git a/.github/hooks/pre-tool-use b/.github/hooks/pre-tool-use
new file mode 100755
index 00000000..0d6d2fe4
--- /dev/null
+++ b/.github/hooks/pre-tool-use
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+# Copilot hook wrapper - generated by amplihack (python engine)
+# Aggregates amplihack and XPIA pre-tool validation into one JSON response
+REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || REPO_ROOT=""
+INPUT=$(cat)
+
+AMPLIHACK_OUTPUT="{}"
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+if [[ -f "${AMPLIHACK_HOOKS}/pre_tool_use.py" ]]; then
+ AMPLIHACK_OUTPUT=$(echo "$INPUT" | python3 "${AMPLIHACK_HOOKS}/pre_tool_use.py" "$@" 2>/dev/null || printf '{}')
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/pre_tool_use.py" ]]; then
+ AMPLIHACK_OUTPUT=$(echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/pre_tool_use.py" "$@" 2>/dev/null || printf '{}')
+fi
+
+XPIA_OUTPUT="{}"
+XPIA_HOOKS="$HOME/.amplihack/.claude/tools/xpia/hooks"
+if [[ -f "${XPIA_HOOKS}/pre_tool_use.py" ]]; then
+ XPIA_OUTPUT=$(echo "$INPUT" | python3 "${XPIA_HOOKS}/pre_tool_use.py" "$@" 2>/dev/null || printf '{}')
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/xpia/hooks/pre_tool_use.py" ]]; then
+ XPIA_OUTPUT=$(echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/xpia/hooks/pre_tool_use.py" "$@" 2>/dev/null || printf '{}')
+fi
+
+python3 - "$AMPLIHACK_OUTPUT" "$XPIA_OUTPUT" <<'PY'
+import json
+import sys
+
+
+def parse_payload(raw: str) -> dict:
+ raw = raw.strip()
+ if not raw:
+ return {}
+ for line in reversed(raw.splitlines()):
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ value = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ if isinstance(value, dict):
+ return value
+ return {}
+
+
+amplihack = parse_payload(sys.argv[1])
+xpia = parse_payload(sys.argv[2])
+
+permission = xpia.get("permissionDecision")
+if permission in {"allow", "deny", "ask"}:
+ print(json.dumps(xpia))
+ raise SystemExit(0)
+
+permission = amplihack.get("permissionDecision")
+if permission in {"allow", "deny", "ask"}:
+ print(json.dumps(amplihack))
+ raise SystemExit(0)
+
+if amplihack.get("block"):
+ print(
+ json.dumps(
+ {
+ "permissionDecision": "deny",
+ "message": amplihack.get(
+ "message",
+ "Blocked by amplihack pre-tool-use hook.",
+ ),
+ }
+ )
+ )
+ raise SystemExit(0)
+
+print("{}")
+PY
diff --git a/.github/hooks/session-start b/.github/hooks/session-start
new file mode 100755
index 00000000..15b287ec
--- /dev/null
+++ b/.github/hooks/session-start
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+# Copilot hook wrapper - generated by amplihack
+HOOK="session_start.py"
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+
+if [[ -f "${AMPLIHACK_HOOKS}/${HOOK}" ]]; then
+ exec python3 "${AMPLIHACK_HOOKS}/${HOOK}" "$@"
+elif REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/${HOOK}" ]]; then
+ exec python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/${HOOK}" "$@"
+else
+ echo "{}"
+fi
diff --git a/.github/hooks/session-stop b/.github/hooks/session-stop
new file mode 100755
index 00000000..2c369513
--- /dev/null
+++ b/.github/hooks/session-stop
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Copilot hook wrapper - generated by amplihack
+# Runs multiple hook scripts for this event
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || REPO_ROOT=""
+INPUT=$(cat)
+
+if [[ -f "${AMPLIHACK_HOOKS}/stop.py" ]]; then
+ echo "$INPUT" | python3 "${AMPLIHACK_HOOKS}/stop.py" "$@" 2>/dev/null || true
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/stop.py" ]]; then
+ echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/stop.py" "$@" 2>/dev/null || true
+fi
+
+if [[ -f "${AMPLIHACK_HOOKS}/session_stop.py" ]]; then
+ echo "$INPUT" | python3 "${AMPLIHACK_HOOKS}/session_stop.py" "$@" 2>/dev/null || true
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/session_stop.py" ]]; then
+ echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/session_stop.py" "$@" 2>/dev/null || true
+fi
diff --git a/.github/hooks/user-prompt-submit b/.github/hooks/user-prompt-submit
new file mode 100755
index 00000000..4f112305
--- /dev/null
+++ b/.github/hooks/user-prompt-submit
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Copilot hook wrapper - generated by amplihack
+# Runs multiple hook scripts for this event
+AMPLIHACK_HOOKS="$HOME/.amplihack/.claude/tools/amplihack/hooks"
+REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || REPO_ROOT=""
+INPUT=$(cat)
+
+if [[ -f "${AMPLIHACK_HOOKS}/user_prompt_submit.py" ]]; then
+ echo "$INPUT" | python3 "${AMPLIHACK_HOOKS}/user_prompt_submit.py" "$@" 2>/dev/null || true
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/user_prompt_submit.py" ]]; then
+ echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/user_prompt_submit.py" "$@" 2>/dev/null || true
+fi
+
+if [[ -f "${AMPLIHACK_HOOKS}/workflow_classification_reminder.py" ]]; then
+ echo "$INPUT" | python3 "${AMPLIHACK_HOOKS}/workflow_classification_reminder.py" "$@" 2>/dev/null || true
+elif [[ -n "$REPO_ROOT" ]] && [[ -f "${REPO_ROOT}/.claude/tools/amplihack/hooks/workflow_classification_reminder.py" ]]; then
+ echo "$INPUT" | python3 "${REPO_ROOT}/.claude/tools/amplihack/hooks/workflow_classification_reminder.py" "$@" 2>/dev/null || true
+fi
diff --git a/AGENTS.md b/AGENTS.md
index 503f315e..24ce10b3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,392 +12,4 @@ After the skill is activated, the next tool call must execute the `smart-orchest
Do not follow the workflow manually and do not fall back to legacy `ultrathink` behavior.
-## Auto-routing prompt
-
-
-Before responding, classify this prompt using parallel signal evaluation:
-
-```mermaid
-flowchart TD
- MSG[User Message] --> BYPASS{"Bypass?
'just answer' / 'skip workflow'"}
- BYPASS -->|yes| SKIP[SKIP: respect bypass]
-
- MSG --> EVAL{Detect all signals}
- EVAL --> S1["UNDERSTAND
explain, how does, why,
analyze, research, explore"]
- EVAL --> S2["IMPLEMENT
build, fix, add, create,
refactor, update, write"]
- EVAL --> S3["FILE_EDIT
any code/doc file will be
created / modified / deleted"]
- EVAL --> S4["SHELL_ONLY
run tests, git status,
check logs, show disk"]
- EVAL --> S5["QUESTION
what is, how do I,
explain, compare"]
-
- EVAL --> RESOLVE{Resolve by priority}
- RESOLVE -->|"UNDERSTAND + IMPLEMENT"| HYBRID[HYBRID → dev-orchestrator]
- RESOLVE -->|"SHELL_ONLY + IMPLEMENT"| HYBRID
- RESOLVE -->|"FILE_EDIT or IMPLEMENT alone"| DEV[DEV → dev-orchestrator]
- RESOLVE -->|"UNDERSTAND alone"| INVESTIGATE[INVESTIGATE → dev-orchestrator]
- RESOLVE -->|"SHELL_ONLY alone"| OPS[OPS: execute directly]
- RESOLVE -->|"QUESTION alone"| QA[Q&A: answer directly]
-```
-
-Resolution rules — detect all signals, then apply the FIRST matching rule:
-1. BYPASS → SKIP
-2. UNDERSTAND + IMPLEMENT both present → **HYBRID** (even if FILE_EDIT also present)
-3. SHELL_ONLY + IMPLEMENT both present → **HYBRID** (run command then fix)
-4. FILE_EDIT or IMPLEMENT present (no UNDERSTAND) → **DEV**
-5. UNDERSTAND present (no IMPLEMENT) → **INVESTIGATE**
-6. SHELL_ONLY present (no FILE_EDIT) → **OPS**
-7. QUESTION present (no FILE_EDIT) → **Q&A**
-8. Ambiguous → **DEV** (safe default)
-
-Calibration examples (use these to resolve edge cases):
- "run tests and fix failures" = HYBRID (SHELL_ONLY + IMPLEMENT → both phases)
- "investigate X then fix Y" = HYBRID
- "what does this error mean and how do I fix it" = HYBRID (UNDERSTAND + IMPLEMENT)
- "change the default model" = DEV. "update README" = DEV. "fix a typo" = DEV.
- "review this PR" = DEV. "make sure it works" = DEV. "write docs" = DEV.
- "run tests" = OPS. "git status" = OPS.
- "what is OAuth?" = Q&A. "how does X work?" = Q&A (no fix requested).
- "what's broken?" = INVESTIGATE. "why is CI failing?" = INVESTIGATE.
- "research rate limiting then implement it" = HYBRID.
- "analyze deps and remove circular imports" = HYBRID (UNDERSTAND + IMPLEMENT).
-
-Actions:
- DEV → Say "[auto-routed] DEV → launching dev-orchestrator" then Invoke Skill(skill="dev-orchestrator"). Do not answer inline.
- INVESTIGATE → Say "[auto-routed] INVESTIGATE → launching dev-orchestrator" then Invoke Skill(skill="dev-orchestrator"). Do not answer inline.
- HYBRID → Say "[auto-routed] HYBRID → launching dev-orchestrator" then Invoke Skill(skill="dev-orchestrator"). Do not answer inline.
- Q&A → Answer directly. No workflow needed.
- OPS → Execute directly. No workflow needed.
- SKIP → Respect the bypass.
-
-When in doubt, choose DEV. False positive costs minutes. False negative costs quality.
-
-
-## Execution Instructions
-
-**YOUR NEXT ACTION after reading this skill MUST include a Bash tool call that
-executes `run_recipe_by_name("smart-orchestrator")`.** If your runtime requires
-a `report_intent` call (e.g. Copilot CLI), emit it **in parallel** with the
-Bash launch — both tool calls in the same response. The constraint is that no
-_other_ substantive action (reading files, calling TodoWrite, invoking Agent,
-or typing a response) may precede or replace the Bash launch.
-
-If you find yourself doing anything else instead of launching the recipe
-runner, you are bypassing the workflow. Stop. Go back. Run the recipe runner.
-
-Development and Investigation tasks have exactly ONE execution path: the
-recipe runner. There is no "Option B." There is no "I'll follow the steps
-manually." There is no "Let me just quickly..." The recipe runner IS the
-workflow. Skipping it means skipping the 23-step process, the recursion
-guards, the goal verification, and the quality gates that exist for a reason.
-
-A PostToolUse hook monitors every tool call after this skill activates. If
-it detects 3 tool calls without recipe runner evidence, it will fire a hard
-WARNING. Do not wait for the warning — run the recipe runner immediately.
-
-When this skill is activated:
-
-### REQUIRED: Execute via Recipe Runner — IMMEDIATELY
-
-Your next tool call(s) must include the recipe runner launch (alongside
-`report_intent` if your runtime requires it).
-
-#### Default: Direct Execution
-
-The recipe runner is a plain subprocess — it does **not** require tmux.
-Call `run_recipe_by_name()` directly:
-
-```bash
-cd /path/to/repo && env -u CLAUDECODE \
- AMPLIHACK_HOME=/path/to/amplihack PYTHONPATH=src \
- python3 -c "
-from amplihack.recipes import run_recipe_by_name
-
-result = run_recipe_by_name(
- 'smart-orchestrator',
- user_context={
- 'task_description': '''TASK_DESCRIPTION_HERE''',
- 'repo_path': '.',
- },
- progress=True,
-)
-print(f'Recipe result: {result}')
-"
-```
-
-**Key points:**
-
-- `PYTHONPATH=src python3` — uses the interpreter on PATH while forcing imports from the checked-out repo source tree (do NOT hardcode `.venv/bin/python`)
-- `run_recipe_by_name` — delegates to the Rust binary via `subprocess.Popen`; no tmux involved
-- `progress=True` — streams recipe-runner stderr live so you see nested step activity
-- The recipe runner manages its own child processes (agent sessions, bash steps) as direct subprocesses
-
-This is the preferred execution mode for most scenarios. It is simpler, has
-no external dependencies beyond Python and the Rust binary, works on all
-platforms, and makes output capture straightforward.
-
-#### Durable Execution (tmux) — optional
-
-Use tmux **only** when:
-
-- The agent runtime may kill background processes after a timeout (e.g., some
- Claude Code hosted environments)
-- You need to survive SSH disconnects or terminal closures
-- You want to detach and monitor a long-running recipe interactively
-
-```bash
-LOG_FILE=$(mktemp /tmp/recipe-runner-output.XXXXXX.log)
-SCRIPT_FILE=$(mktemp /tmp/recipe-runner-script.XXXXXX.py)
-chmod 600 "$LOG_FILE" "$SCRIPT_FILE"
-cat > "$SCRIPT_FILE" << 'RECIPE_SCRIPT'
-from amplihack.recipes import run_recipe_by_name
-
-result = run_recipe_by_name(
- "smart-orchestrator",
- user_context={
- "task_description": """TASK_DESCRIPTION_HERE""",
- "repo_path": ".",
- },
- progress=True,
-)
-print(f"Recipe result: {result}")
-RECIPE_SCRIPT
-tmux new-session -d -s recipe-runner \
- "cd /path/to/repo && env -u CLAUDECODE \
- AMPLIHACK_HOME=/path/to/amplihack PYTHONPATH=src \
- python3 $SCRIPT_FILE 2>&1 | tee $LOG_FILE"
-echo "Recipe runner log: $LOG_FILE"
-```
-
-- The Python payload is written to a temp script to avoid nested quoting
- issues that cause silent launch failures (see issue #3215)
-- `chmod 600 "$LOG_FILE" "$SCRIPT_FILE"` — keeps both files private
-- `tmux new-session -d` — detached session, no timeout, survives disconnects
-- Monitor with: `tail -f "$LOG_FILE"` or `tmux attach -t recipe-runner`
-
-**Restarting a stale tmux session**: Some runtimes (e.g. Copilot CLI) block
-`tmux kill-session` because it does not target a numeric PID. Use one of these
-shell-policy-safe alternatives instead:
-
-```bash
-# Option A (preferred): use a unique session name per run to avoid collisions
-tmux new-session -d -s "recipe-$(date +%s)" "..."
-
-# Option B: locate the tmux server PID and terminate with numeric kill
-tmux list-sessions -F '#{pid}' 2>/dev/null | xargs -I{} kill {}
-
-# Option C: let tmux itself handle it — send exit to all panes
-tmux send-keys -t recipe-runner "exit" Enter 2>/dev/null; sleep 1
-```
-
-If using Option A, update the `tail -f` / `tmux attach` commands to use the
-same session name.
-
-**The recipe runner is the required execution path for Development and
-Investigation tasks.** Always try `smart-orchestrator` first.
-
-**Required environment variables** for the recipe runner:
-
-- `AMPLIHACK_HOME` — must point to the amplihack repo root (e.g.,
- `/home/user/src/amplihack`). The recipe runner uses this to find
- `amplifier-bundle/tools/orch_helper.py` and other orchestrator scripts.
-- Preserve `AMPLIHACK_AGENT_BINARY` — nested workflow agents read this env var
- to stay on the caller's active binary (for example, Copilot in Copilot CLI).
- The Python wrapper no longer forwards the removed `--agent-binary` CLI flag,
- so keeping this env var set is now the correct behavior.
-- Unset `CLAUDECODE` — required so nested Claude Code sessions can launch.
-
-**Fallback: Direct recipe invocation when smart-orchestrator fails.**
-
-Always try `smart-orchestrator` first — it handles classification, decomposition,
-and routing automatically. However, if `smart-orchestrator` fails at the
-**infrastructure level** (e.g., 0 workstreams from decomposition, missing env
-vars, Rust binary version mismatch), you MAY invoke the specific workflow
-recipe directly based on your classification:
-
-| Classification | Direct Recipe | When to Use |
-| -------------- | ------------------------ | --------------------------------------- |
-| Investigation | `investigation-workflow` | smart-orchestrator decomposition failed |
-| Development | `default-workflow` | smart-orchestrator decomposition failed |
-| Q&A (complex) | `qa-workflow` | Q&A needing multi-step research |
-| Consensus | `consensus-workflow` | Critical decisions needing validation |
-
-Example:
-
-```python
-run_recipe_by_name("investigation-workflow", user_context={
- 'task_description': task, 'repo_path': '.',
-}, progress=True)
-```
-
-This is NOT a license to bypass `smart-orchestrator`. Only use direct
-invocation after `smart-orchestrator` has failed at an infrastructure level
-(not because the task seems "too simple" or "too specific").
-
-**Handling hollow success** (recipe completes but agents produce no findings):
-
-If a recipe returns SUCCESS but the agent outputs indicate the agents could
-not access the codebase or produced empty/generic results (e.g., "no codebase
-exists", "cannot proceed without a target"), this is a **hollow success**.
-In this case:
-
-1. Check that `repo_path` and `AMPLIHACK_HOME` are correct
-2. Verify the working directory is the repo root
-3. Retry with explicit file paths in the `task_description`
-4. If retries also produce hollow results, report the infrastructure
- failure to the user with specifics
-
-**Common rationalizations that are NOT acceptable:**
-
-- "Let me first understand the codebase" — the recipe does that in Step 0
-- "I'll follow the workflow steps manually" — NO, the recipe enforces them
-- "The recipe runner might not work" — try it first, report errors if it fails
-- "This is a simple task" — simple or complex, the recipe runner handles both
-- "The recipe succeeded but didn't do anything useful, so I'll do it myself"
- — this is hollow success; retry with better context first
-
-**Q&A and Operations only** may bypass the recipe runner:
-
-- Q&A: Respond directly (analyzer agent)
-- Operations: Builder agent (direct execution, no workflow steps)
-
-### Error Recovery: Adaptive Strategy (NOT Degradation)
-
-When `smart-orchestrator` fails, **failures must be visible and surfaced** —
-never swallowed or silently degraded. The recipe handles error recovery
-automatically via its built-in adaptive strategy steps, but if you observe
-a failure outside the recipe, follow this protocol:
-
-**1. Surface the error with full context:**
-
-Report the exact error, the step that failed, and the log output. Never say
-"something went wrong" — always include the specific failure details.
-
-**2. File a bug with reproduction details:**
-
-For infrastructure failures (import errors, missing env vars, binary not found,
-decomposition producing invalid output), file a GitHub issue:
-
-```bash
-gh issue create \
- --title "smart-orchestrator infrastructure failure: " \
- --body "" \
- --label "bug"
-```
-
-**3. Evaluate alternative strategies:**
-
-If `smart-orchestrator` fails at the infrastructure level (not because the task
-is wrong), you MAY invoke the specific workflow recipe directly. This is an
-**adaptive strategy** — it must be announced explicitly, not done silently:
-
-| Classification | Direct Recipe | When Permitted |
-| -------------- | ------------------------ | --------------------------------------------------- |
-| Investigation | `investigation-workflow` | smart-orchestrator failed at parse/decompose/launch |
-| Development | `default-workflow` | smart-orchestrator failed at parse/decompose/launch |
-
-Example:
-
-```python
-# ANNOUNCE the strategy change first — never do this silently
-print("[ADAPTIVE] smart-orchestrator failed at parse-decomposition: ")
-print("[ADAPTIVE] Switching to direct investigation-workflow invocation")
-run_recipe_by_name("investigation-workflow", user_context={...}, progress=True)
-```
-
-**This is NOT a license to bypass smart-orchestrator.** Always try it first.
-Direct invocation is only permitted when smart-orchestrator fails at the
-infrastructure level. "The task seems simple" is NOT an infrastructure failure.
-
-**4. Detect hollow success:**
-
-A recipe can complete structurally (all steps exit 0) but produce empty or
-meaningless results — agents reporting "no codebase found" or reflection
-marking ACHIEVED when no work was done. After execution, check that:
-
-- Round results contain actual findings or code changes (not "I could not access...")
-- PR URLs or concrete outputs are present for Development tasks
-- At least one success criterion was verifiably evaluated
-
-If results are hollow, report this to the user with the specific empty outputs.
-Do not declare success when agents produced no meaningful work.
-
-### Required Environment Variables
-
-The recipe runner requires these environment variables to function:
-
-| Variable | Purpose | Default |
-| -------------------------- | ------------------------------------------------- | --------------- |
-| `AMPLIHACK_HOME` | Root of amplihack installation (for asset lookup) | Auto-detected |
-| `AMPLIHACK_AGENT_BINARY` | Which agent binary to use (claude, copilot, etc.) | Set by launcher |
-| `AMPLIHACK_MAX_DEPTH` | Max recursion depth for nested sessions | `3` |
-| `AMPLIHACK_NONINTERACTIVE` | Set to `1` to skip interactive prompts | Unset |
-
-If `AMPLIHACK_HOME` is not set and auto-detection fails, `parse-decomposition`
-and `activate-workflow` will fail with "orch_helper.py not found". Set it to
-the directory containing `amplifier-bundle/`.
-
-### After Execution: Reflect and verify
-
-After execution completes, verify the goal was achieved. If not:
-
-- For missing information: ask the user
-- For fixable gaps: re-invoke with the remaining work description
-- For infrastructure failures: file a bug and try adaptive strategy
-
-### Enforcement: PostToolUse Workflow Guard
-
-A PostToolUse hook (`workflow_enforcement_hook.py`) actively monitors every
-tool call after this skill is invoked. It tracks:
-
-- Whether `/dev` or `dev-orchestrator` was called (sets a flag)
-- Whether the recipe runner was actually executed (clears the flag)
-- How many tool calls have passed without workflow evidence
-
-If 3+ tool calls pass without evidence of recipe runner execution, the hook
-emits a hard WARNING. This is not a suggestion — it means you are violating
-the mandatory workflow. State is stored in `/tmp/amplihack-workflow-state/`.
-
-## User Preferences
-
-# User Preferences
-
-**MANDATORY**: These preferences MUST be followed by all agents. Priority #2 (only explicit user requirements override).
-
-## Autonomy
-
-Work autonomously. Follow workflows without asking permission between steps. Only ask when truly blocked on critical missing information.
-
-## Core Preferences
-
-| Setting | Value |
-| ------------------- | -------------------------- |
-| Verbosity | balanced |
-| Communication Style | (not set) |
-| Update Frequency | regular |
-| Priority Type | balanced |
-| Collaboration Style | autonomous and independent |
-| Auto Update | ask |
-| Neo4j Auto-Shutdown | ask |
-| Preferred Languages | (not set) |
-| Coding Standards | (not set) |
-
-## Workflow Configuration
-
-**Selected**: DEFAULT_WORKFLOW (`@~/.amplihack/.claude/workflows/DEFAULT_WORKFLOW.md`)
-**Consensus Depth**: balanced
-
-Use CONSENSUS_WORKFLOW for: ambiguous requirements, architectural changes, critical/security code, public APIs.
-
-## Behavioral Rules
-
-- **No sycophancy**: Be direct, challenge wrong ideas, point out flaws. Never use "Great idea!", "Excellent point!", etc. See `@~/.amplihack/.claude/context/TRUST.md`.
-- **Quality over speed**: Always prefer complete, high-quality work over fast delivery.
-
-## Learned Patterns
-
-
-
-## Managing Preferences
-
-Use `/amplihack:customize` to view or modify (`set`, `show`, `reset`, `learn`).
-
diff --git a/README.md b/README.md
index 159bc40c..7fee5afd 100644
--- a/README.md
+++ b/README.md
@@ -2395,4 +2395,4 @@ azlin COMMAND --help
---
-**For detailed API documentation and architecture, see [docs/](docs/)**
+**For detailed API documentation and architecture, see [docs/](docs/)** | **[Testing Guide](TESTING.md)**
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..31bf857e
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,192 @@
+# Testing Guide
+
+How to run azlin tests. For detailed specifications, see the linked docs.
+
+## Prerequisites
+
+- **Rust 1.85+** (edition 2021) — install via [rustup](https://rustup.rs/)
+- **Azure CLI** (`az`) — for live Azure tests only
+- **gadugi-agentic-test** — for YAML scenario tests only
+
+The workspace config at `rust/.cargo/config.toml` automatically sets
+`RUST_MIN_STACK=8388608` (8 MB stack), so no manual env setup is needed locally.
+
+## Quick Start
+
+```bash
+cd rust
+cargo test --all
+```
+
+This runs all unit and integration tests (excluding `#[ignore]`-gated live Azure tests).
+
+## Test Categories
+
+### Rust Unit Tests
+
+72 test groups in `rust/crates/azlin/src/tests/` plus 13 handler test groups in
+`rust/crates/azlin/src/handlers/tests/`. These test command parsing, output
+formatting, config handling, and business logic with mock data.
+
+```bash
+cd rust
+cargo test --all # all unit + integration tests
+cargo test --lib # unit tests only
+cargo test test_group_list # single test group by name
+```
+
+### Rust Integration Tests
+
+18 test files in `rust/crates/azlin/tests/` covering CLI invocation, config
+loading, session management, error handling, output formats, and end-to-end
+command flows.
+
+```bash
+cd rust
+cargo test --test cli_integration # single integration test file
+cargo test --test config_integration
+```
+
+Key integration test files:
+
+| File | Coverage |
+|------|----------|
+| `cli_integration.rs` | CLI arg parsing, help, version |
+| `config_integration.rs` | Config load/save/defaults |
+| `session_integration.rs` | Session persistence |
+| `local_e2e.rs` | End-to-end command flows (local, no Azure) |
+| `parity_integration.rs` | Python/Rust output parity |
+| `backup_dr_integration.rs` | Snapshot/backup CLI validation |
+| `azure_live_integration.rs` | Live Azure API calls (ignored) |
+| `live_commands_integration.rs` | Live command execution (ignored) |
+
+### Live Azure Tests
+
+Tests in `azure_live_integration.rs` and `live_commands_integration.rs` are
+marked `#[ignore]` and require real Azure credentials.
+
+```bash
+# Setup
+az login
+
+# Run ignored tests explicitly
+cd rust
+cargo test --test azure_live_integration -- --ignored
+cargo test --test live_commands_integration -- --ignored
+```
+
+These tests hit real Azure APIs against hardcoded resource groups and VMs.
+See [docs/REAL_AZURE_TESTING.md](docs/REAL_AZURE_TESTING.md) for manual
+testing procedures.
+
+### Agentic Scenario Tests
+
+YAML-based tests in `tests/agentic-scenarios/` using the gadugi test runner.
+These verify CLI behavior through scripted agent interactions.
+
+```bash
+# Point AZLIN_BIN at the debug or release binary
+export AZLIN_BIN=./rust/target/debug/azlin
+
+# Build first
+cd rust && cargo build && cd ..
+
+# Run scenarios
+gadugi-test run -d tests/agentic-scenarios
+```
+
+Scenarios:
+- `ssh-identity-key.yaml` — SSH key auto-resolution
+- `new-command-parity.yaml` — `azlin new` command parity checks
+
+See [docs/AGENTIC_INTEGRATION_TESTS.md](docs/AGENTIC_INTEGRATION_TESTS.md)
+for the full agentic test case specification.
+
+### E2E Tests
+
+End-to-end YAML scenarios in `tests/e2e/`:
+
+- `test_restore_multi_session.yaml` — Multi-session restore flow
+
+These also use the gadugi runner with `AZLIN_BIN`.
+
+### Benchmarks
+
+Python-based performance benchmarks in `benchmarks/`. These were written for
+the original Python implementation and measure Azure API and SSH operation
+latency.
+
+```bash
+pip install memory-profiler line-profiler pytest-benchmark
+python benchmarks/benchmark_vm_list.py
+python benchmarks/benchmark_parallel_vm_list.py
+```
+
+See [benchmarks/README.md](benchmarks/README.md) for full setup and
+baseline comparison workflows.
+
+### Backup & Disaster Recovery Tests
+
+[PLANNED — Implementation Pending]
+
+The backup and DR feature (Issue #439) adds four modules: BackupManager,
+ReplicationManager, VerificationManager, and DRTestManager. The test plan
+follows the testing pyramid:
+
+- **Unit tests (60%)** — 102+ methods across 4 test files covering backup
+ scheduling, cross-region replication, backup verification, and DR test
+ orchestration.
+- **Integration tests (30%)** — 12+ methods testing multi-module workflows
+ (backup → replicate → verify).
+- **E2E tests (10%)** — 6 methods covering complete user journeys including
+ region-failover and RTO validation (<15 min target).
+
+Current Rust integration tests in `backup_dr_integration.rs` validate the
+`snapshot` subcommand CLI surface. Full test specifications are in
+[docs/testing/backup-dr-test-coverage.md](docs/testing/backup-dr-test-coverage.md).
+
+## Environment Variables
+
+| Variable | Purpose | Required |
+|----------|---------|----------|
+| `RUST_MIN_STACK` | 8 MB stack for large CLI enum (set automatically by `.cargo/config.toml`) | Auto |
+| `AZLIN_BIN` | Path to azlin binary for agentic/E2E tests | Agentic tests |
+| `AZLIN_TEST_MODE` | Enables mock data in list commands | Some unit tests |
+| `ANTHROPIC_API_KEY` | Anthropic API access for `azlin do` commands | Agentic tests |
+| `AZURE_SUBSCRIPTION_ID` | Azure subscription (removed in test helpers to isolate) | Live Azure tests |
+| `AZURE_TENANT_ID` | Azure tenant (removed in test helpers to isolate) | Live Azure tests |
+
+## Linting
+
+```bash
+cd rust
+cargo clippy --all -- -D warnings
+```
+
+CI treats all clippy warnings as errors.
+
+## Test Coverage
+
+```bash
+cd rust
+cargo llvm-cov --all
+```
+
+Requires `cargo-llvm-cov` (`cargo install cargo-llvm-cov`).
+
+## CI Pipeline
+
+The GitHub Actions workflow at `.github/workflows/rust-ci.yml` runs on every
+push and PR touching `rust/**`:
+
+1. **Build** — `cargo build --release`
+2. **Test** — `cargo test --all` (with `RUST_MIN_STACK=8388608`)
+3. **Lint** — `cargo clippy --all -- -D warnings`
+
+## Detailed Documentation
+
+- [docs/TEST_SUITE_SPECIFICATION.md](docs/TEST_SUITE_SPECIFICATION.md) — Exhaustive CLI syntax test spec (300+ tests)
+- [docs/AGENTIC_INTEGRATION_TESTS.md](docs/AGENTIC_INTEGRATION_TESTS.md) — Agentic "do" mode test cases
+- [docs/REAL_AZURE_TESTING.md](docs/REAL_AZURE_TESTING.md) — Manual Azure testing procedures
+- [docs/testing/backup-dr-test-coverage.md](docs/testing/backup-dr-test-coverage.md) — Backup & DR TDD test plan (170+ tests)
+- [benchmarks/README.md](benchmarks/README.md) — Benchmark setup and comparison workflows
diff --git a/docs/index.md b/docs/index.md
index 1c77085b..66d7314d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -46,6 +46,19 @@
- [Tmux Session Status](./features/tmux-session-status.md)
- [VM Lifecycle Automation](./features/vm-lifecycle-automation.md)
+## Testing
+
+- [Testing Guide](../TESTING.md) — How to run all azlin tests (quick start, categories, env vars, CI)
+- [Test Suite Specification](./TEST_SUITE_SPECIFICATION.md) — 300+ CLI syntax tests
+- [Agentic Integration Tests](./AGENTIC_INTEGRATION_TESTS.md) — YAML-based scenario tests
+- [Real Azure Testing](./REAL_AZURE_TESTING.md) — Manual testing with live Azure credentials
+- [Backup & DR Test Coverage](./testing/backup-dr-test-coverage.md) — TDD test plan for backup and disaster recovery (170+ tests)
+- [Test Strategy](./testing/test_strategy.md) — Test pyramid, mocking patterns, TDD approach
+
+## Features (In Progress)
+
+- [Backup & Disaster Recovery](./backup-disaster-recovery.md) — Automated backup scheduling, cross-region replication, verification, and DR testing
+
## Monitoring
- [Monitoring Quick Reference](./monitoring-quick-reference.md) — Dashboard, alerts, metrics
diff --git a/pyproject.toml b/pyproject.toml
index 29bed8e0..8fd7a29d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "azlin"
-version = "2.7.0"
+version = "2.8.0"
description = "Azure VM fleet management CLI - provision, manage, and monitor development VMs"
requires-python = ">=3.11"
authors = [
diff --git a/rust/crates/azlin-cli/src/lib.rs b/rust/crates/azlin-cli/src/lib.rs
index 1c8334a2..ca5ec492 100644
--- a/rust/crates/azlin-cli/src/lib.rs
+++ b/rust/crates/azlin-cli/src/lib.rs
@@ -548,6 +548,20 @@ pub enum Commands {
action: SnapshotAction,
},
+ // ── Backup Commands ──────────────────────────────────────────────
+ /// Manage VM backup policies, retention, and cross-region replication
+ Backup {
+ #[command(subcommand)]
+ action: BackupAction,
+ },
+
+ // ── Disaster Recovery Commands ───────────────────────────────────
+ /// Disaster recovery testing and validation
+ Dr {
+ #[command(subcommand)]
+ action: DrAction,
+ },
+
// ── Storage Commands ──────────────────────────────────────────────
/// Manage NFS storage for shared home directories
Storage {
@@ -1491,6 +1505,194 @@ pub enum SnapshotAction {
},
}
+// ── Backup subcommands ────────────────────────────────────────────────────
+
+/// Backup tier for retention classification.
+#[derive(ValueEnum, Debug, Clone, Copy)]
+pub enum BackupTier {
+ /// Daily backups
+ Daily,
+ /// Weekly backups
+ Weekly,
+ /// Monthly backups
+ Monthly,
+}
+
+#[derive(Subcommand, Debug)]
+pub enum BackupAction {
+ /// Configure backup policy for a VM
+ Configure {
+ /// VM name
+ vm_name: String,
+ /// Number of daily backups to retain
+ #[arg(long)]
+ daily_retention: Option,
+ /// Number of weekly backups to retain
+ #[arg(long)]
+ weekly_retention: Option,
+ /// Number of monthly backups to retain
+ #[arg(long)]
+ monthly_retention: Option,
+ /// Enable cross-region replication
+ #[arg(long)]
+ cross_region: bool,
+ /// Target region for cross-region replication
+ #[arg(long)]
+ target_region: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Trigger an on-demand backup for a VM
+ Trigger {
+ /// VM name
+ vm_name: String,
+ /// Backup tier override
+ #[arg(long, value_enum)]
+ tier: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// List backups for a VM
+ List {
+ /// VM name
+ vm_name: String,
+ /// Filter by backup tier
+ #[arg(long, value_enum)]
+ tier: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Restore a VM from a backup
+ Restore {
+ /// VM name
+ vm_name: String,
+ /// Backup name to restore from
+ #[arg(long)]
+ backup: String,
+ /// Skip confirmation prompt
+ #[arg(long)]
+ force: bool,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Verify integrity of a specific backup
+ Verify {
+ /// Backup name to verify
+ backup_name: String,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Replicate a backup to another region
+ Replicate {
+ /// Backup name to replicate
+ backup_name: String,
+ /// Target region for replication
+ target_region: String,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Show current backup configuration for a VM
+ ConfigShow {
+ /// VM name
+ vm_name: String,
+ },
+ /// Disable backups for a VM
+ Disable {
+ /// VM name
+ vm_name: String,
+ },
+ /// Replicate all backups for a VM to another region
+ ReplicateAll {
+ /// VM name
+ vm_name: String,
+ /// Target region for replication
+ target_region: String,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Show replication status for a VM
+ ReplicationStatus {
+ /// VM name
+ vm_name: String,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// List replication jobs
+ ReplicationJobs {
+ /// Filter by job status
+ #[arg(long)]
+ status: Option,
+ /// Filter by VM name
+ #[arg(long)]
+ vm: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Verify all backups for a VM
+ VerifyAll {
+ /// VM name
+ vm_name: String,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Show backup verification report
+ VerificationReport {
+ /// Number of days to include in report
+ #[arg(long, default_value = "30")]
+ days: u32,
+ /// Filter by VM name
+ #[arg(long)]
+ vm: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+}
+
+// ── Disaster Recovery subcommands ─────────────────────────────────────────
+
+#[derive(Subcommand, Debug)]
+pub enum DrAction {
+ /// Run a disaster recovery test for a VM
+ Test {
+ /// VM name
+ vm_name: String,
+ /// Region to test recovery in
+ #[arg(long)]
+ test_region: String,
+ /// Specific backup to use for DR test
+ #[arg(long)]
+ backup: Option,
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Run DR tests for all VMs in a resource group
+ TestAll {
+ /// Test region for DR validation
+ #[arg(long)]
+ test_region: Option,
+ /// Resource group to test
+ #[arg(long, alias = "rg")]
+ resource_group: Option,
+ },
+ /// Show DR test history for a VM
+ TestHistory {
+ /// VM name
+ vm_name: String,
+ /// Number of days of history to show
+ #[arg(long, default_value = "30")]
+ days: u32,
+ },
+ /// Show DR test success rate
+ SuccessRate {
+ /// Filter by VM name
+ #[arg(long)]
+ vm: Option,
+ /// Number of days to include
+ #[arg(long, default_value = "90")]
+ days: u32,
+ },
+}
+
// ── Storage subcommands ───────────────────────────────────────────────────
#[derive(Subcommand, Debug)]
diff --git a/rust/crates/azlin/src/cmd_backup.rs b/rust/crates/azlin/src/cmd_backup.rs
new file mode 100644
index 00000000..7834d5e4
--- /dev/null
+++ b/rust/crates/azlin/src/cmd_backup.rs
@@ -0,0 +1,130 @@
+#[allow(unused_imports)]
+use super::*;
+use anyhow::Result;
+
+pub(crate) async fn dispatch(
+ command: azlin_cli::Commands,
+ verbose: bool,
+ output: &azlin_cli::OutputFormat,
+) -> Result<()> {
+ #[allow(unused_variables)]
+ let _ = (verbose, output);
+ match command {
+ azlin_cli::Commands::Backup { action } => match action {
+ azlin_cli::BackupAction::Configure {
+ vm_name,
+ daily_retention,
+ weekly_retention,
+ monthly_retention,
+ cross_region,
+ target_region,
+ resource_group,
+ } => {
+ crate::cmd_backup_ops::handle_backup_configure(
+ &vm_name,
+ daily_retention,
+ weekly_retention,
+ monthly_retention,
+ cross_region,
+ target_region.as_deref(),
+ resource_group.as_deref(),
+ )?;
+ }
+ azlin_cli::BackupAction::Trigger {
+ vm_name,
+ tier,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_trigger(&vm_name, tier, &rg).await?;
+ }
+ azlin_cli::BackupAction::List {
+ vm_name,
+ tier,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_list(&vm_name, tier, &rg).await?;
+ }
+ azlin_cli::BackupAction::Restore {
+ vm_name,
+ backup,
+ force,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_restore(&vm_name, &backup, force, &rg)
+ .await?;
+ }
+ azlin_cli::BackupAction::Verify {
+ backup_name,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_verify(&backup_name, &rg).await?;
+ }
+ azlin_cli::BackupAction::Replicate {
+ backup_name,
+ target_region,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_replicate(&backup_name, &target_region, &rg)
+ .await?;
+ }
+ azlin_cli::BackupAction::ConfigShow { vm_name } => {
+ crate::cmd_backup_ops::handle_backup_config_show(&vm_name)?;
+ }
+ azlin_cli::BackupAction::Disable { vm_name } => {
+ crate::cmd_backup_ops::handle_backup_disable(&vm_name)?;
+ }
+ azlin_cli::BackupAction::ReplicateAll {
+ vm_name,
+ target_region,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_replicate_all(&vm_name, &target_region, &rg)
+ .await?;
+ }
+ azlin_cli::BackupAction::ReplicationStatus {
+ vm_name,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_replication_status(&vm_name, &rg).await?;
+ }
+ azlin_cli::BackupAction::ReplicationJobs {
+ status,
+ vm,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_replication_jobs(
+ status.as_deref(),
+ vm.as_deref(),
+ &rg,
+ )
+ .await?;
+ }
+ azlin_cli::BackupAction::VerifyAll {
+ vm_name,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_backup_verify_all(&vm_name, &rg).await?;
+ }
+ azlin_cli::BackupAction::VerificationReport {
+ days,
+ vm,
+ resource_group,
+ } => {
+ let rg = resolve_resource_group(resource_group)?;
+ crate::cmd_backup_ops::handle_verification_report(days, vm.as_deref(), &rg)
+ .await?;
+ }
+ },
+ _ => unreachable!(),
+ }
+ Ok(())
+}
diff --git a/rust/crates/azlin/src/cmd_backup_ops.rs b/rust/crates/azlin/src/cmd_backup_ops.rs
new file mode 100644
index 00000000..39b21aef
--- /dev/null
+++ b/rust/crates/azlin/src/cmd_backup_ops.rs
@@ -0,0 +1,876 @@
+#[allow(unused_imports)]
+use super::*;
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+// ---------------------------------------------------------------------------
+// Backup config persistence (~/.azlin/backup/{vm_name}.toml)
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) struct BackupConfig {
+ pub vm_name: String,
+ pub daily_retention: Option,
+ pub weekly_retention: Option,
+ pub monthly_retention: Option,
+ pub cross_region: bool,
+ pub target_region: Option,
+ pub resource_group: Option,
+ pub created: String,
+}
+
+fn backup_config_dir() -> PathBuf {
+ dirs::home_dir()
+ .unwrap_or_else(|| PathBuf::from("."))
+ .join(".azlin")
+ .join("backup")
+}
+
+fn backup_config_path(vm_name: &str) -> PathBuf {
+ backup_config_dir().join(format!("{}.toml", vm_name))
+}
+
+fn load_backup_config(vm_name: &str) -> Result