loki-mode 6.67.2 → 6.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +199 -2
- package/autonomy/run.sh +7 -0
- package/completions/_loki +4 -0
- package/completions/loki.bash +6 -1
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/web-app/server.py +117 -15
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.68.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -268,4 +268,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
268
268
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
269
269
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
270
270
|
|
|
271
|
-
**v6.
|
|
271
|
+
**v6.68.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.68.0
|
package/autonomy/loki
CHANGED
|
@@ -398,6 +398,7 @@ show_help() {
|
|
|
398
398
|
echo " run <issue> Issue-driven engineering (v6.0.0) - GitHub/GitLab/Jira/Azure DevOps"
|
|
399
399
|
echo " start [PRD] Start Loki Mode (optionally with PRD file)"
|
|
400
400
|
echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
|
|
401
|
+
echo " monitor [path] Monitor Docker Compose services with auto-fix (v6.67.0)"
|
|
401
402
|
echo " demo Run interactive demo (~60s simulated session)"
|
|
402
403
|
echo " init [name] Project scaffolding with 22 PRD templates"
|
|
403
404
|
echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
|
|
@@ -6987,11 +6988,12 @@ cmd_quick() {
|
|
|
6987
6988
|
|
|
6988
6989
|
local task_desc="$*"
|
|
6989
6990
|
local version=$(get_version)
|
|
6991
|
+
local max_iter="${LOKI_MAX_ITERATIONS:-3}"
|
|
6990
6992
|
|
|
6991
6993
|
echo -e "${BOLD}Loki Mode v$version - Quick Mode${NC}"
|
|
6992
6994
|
echo ""
|
|
6993
6995
|
echo -e "${CYAN}Task:${NC} $task_desc"
|
|
6994
|
-
echo -e "${DIM}Running lightweight execution (
|
|
6996
|
+
echo -e "${DIM}Running lightweight execution ($max_iter iterations max, no quality council)${NC}"
|
|
6995
6997
|
echo ""
|
|
6996
6998
|
|
|
6997
6999
|
# Create quick PRD from task description
|
|
@@ -7028,7 +7030,7 @@ QPRDEOF
|
|
|
7028
7030
|
echo ""
|
|
7029
7031
|
|
|
7030
7032
|
# Set lightweight execution environment
|
|
7031
|
-
export LOKI_MAX_ITERATIONS=
|
|
7033
|
+
export LOKI_MAX_ITERATIONS="$max_iter"
|
|
7032
7034
|
export LOKI_COMPLEXITY=simple
|
|
7033
7035
|
export LOKI_COUNCIL_ENABLED=false
|
|
7034
7036
|
export LOKI_PHASE_CODE_REVIEW=false
|
|
@@ -7048,6 +7050,198 @@ QPRDEOF
|
|
|
7048
7050
|
exec "$RUN_SH" "$quick_prd"
|
|
7049
7051
|
}
|
|
7050
7052
|
|
|
7053
|
+
# Docker Compose monitoring with auto-fix (v6.67.0)
|
|
7054
|
+
cmd_monitor() {
|
|
7055
|
+
local project_dir="${1:-.}"
|
|
7056
|
+
|
|
7057
|
+
# Resolve to absolute path
|
|
7058
|
+
if [[ ! "$project_dir" = /* ]]; then
|
|
7059
|
+
project_dir="$(cd "$project_dir" 2>/dev/null && pwd)" || {
|
|
7060
|
+
echo -e "${RED}Error: directory not found: $1${NC}"
|
|
7061
|
+
return 1
|
|
7062
|
+
}
|
|
7063
|
+
fi
|
|
7064
|
+
|
|
7065
|
+
if [[ ! -f "$project_dir/docker-compose.yml" ]] && [[ ! -f "$project_dir/docker-compose.yaml" ]]; then
|
|
7066
|
+
echo -e "${RED}No docker-compose.yml found in $project_dir${NC}"
|
|
7067
|
+
return 1
|
|
7068
|
+
fi
|
|
7069
|
+
|
|
7070
|
+
echo -e "${CYAN}AI-Powered Docker Monitor${NC}"
|
|
7071
|
+
echo -e "Project: ${BOLD}$project_dir${NC}"
|
|
7072
|
+
echo -e "Provider: ${GREEN}${LOKI_PROVIDER:-claude}${NC}"
|
|
7073
|
+
echo "Press Ctrl+C to stop."
|
|
7074
|
+
echo ""
|
|
7075
|
+
|
|
7076
|
+
local fix_count=0
|
|
7077
|
+
local max_fixes="${LOKI_MONITOR_MAX_FIXES:-10}"
|
|
7078
|
+
local poll_interval="${LOKI_MONITOR_INTERVAL:-10}"
|
|
7079
|
+
local consecutive_healthy=0
|
|
7080
|
+
|
|
7081
|
+
trap 'echo ""; echo -e "${CYAN}Monitor stopped.${NC}"; return 0' INT TERM
|
|
7082
|
+
|
|
7083
|
+
while true; do
|
|
7084
|
+
# 1. CAPTURE: Get service status (all services including stopped)
|
|
7085
|
+
local ps_output
|
|
7086
|
+
ps_output=$(cd "$project_dir" && docker compose ps -a --format json 2>/dev/null)
|
|
7087
|
+
|
|
7088
|
+
if [[ -z "$ps_output" ]]; then
|
|
7089
|
+
echo -e "${YELLOW}No services found. Waiting...${NC}"
|
|
7090
|
+
sleep "$poll_interval"
|
|
7091
|
+
continue
|
|
7092
|
+
fi
|
|
7093
|
+
|
|
7094
|
+
# Parse services and find failures
|
|
7095
|
+
local failed_services=""
|
|
7096
|
+
local all_healthy=true
|
|
7097
|
+
local service_summary=""
|
|
7098
|
+
|
|
7099
|
+
while IFS= read -r line; do
|
|
7100
|
+
[[ -z "$line" ]] && continue
|
|
7101
|
+
local svc_name svc_state svc_exit
|
|
7102
|
+
svc_name=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('Service',d.get('Name','')))" 2>/dev/null || echo "unknown")
|
|
7103
|
+
svc_state=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('State','unknown'))" 2>/dev/null || echo "unknown")
|
|
7104
|
+
svc_exit=$(echo "$line" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('ExitCode',0))" 2>/dev/null || echo "0")
|
|
7105
|
+
|
|
7106
|
+
service_summary="${service_summary}${svc_name}=${svc_state} "
|
|
7107
|
+
|
|
7108
|
+
if [[ "$svc_state" == "exited" ]] || [[ "$svc_state" == "dead" ]]; then
|
|
7109
|
+
all_healthy=false
|
|
7110
|
+
failed_services="${failed_services} ${svc_name}"
|
|
7111
|
+
fi
|
|
7112
|
+
done <<< "$ps_output"
|
|
7113
|
+
|
|
7114
|
+
if [[ "$all_healthy" == "true" ]]; then
|
|
7115
|
+
consecutive_healthy=$((consecutive_healthy + 1))
|
|
7116
|
+
printf "\r${GREEN}[healthy]${NC} %s (%s) " "$service_summary" "$(date +%H:%M:%S)"
|
|
7117
|
+
sleep "$poll_interval"
|
|
7118
|
+
continue
|
|
7119
|
+
fi
|
|
7120
|
+
|
|
7121
|
+
consecutive_healthy=0
|
|
7122
|
+
|
|
7123
|
+
# 2. CAPTURE LOGS and CONTEXT for each failed service, then feed to AI
|
|
7124
|
+
for svc_name in $failed_services; do
|
|
7125
|
+
if [[ $fix_count -ge $max_fixes ]]; then
|
|
7126
|
+
echo -e "\n${RED}Max fix attempts ($max_fixes) reached for this session.${NC}"
|
|
7127
|
+
echo "Manual intervention needed. Check: cd $project_dir && docker compose logs $svc_name"
|
|
7128
|
+
sleep "$poll_interval"
|
|
7129
|
+
continue 2
|
|
7130
|
+
fi
|
|
7131
|
+
|
|
7132
|
+
echo -e "\n${RED}[FAILURE] $svc_name is down${NC}"
|
|
7133
|
+
|
|
7134
|
+
local logs
|
|
7135
|
+
logs=$(cd "$project_dir" && docker compose logs --tail 50 "$svc_name" 2>/dev/null | head -c 3000)
|
|
7136
|
+
|
|
7137
|
+
# Gather docker-compose.yml for AI context (truncated to avoid ARG_MAX)
|
|
7138
|
+
local compose_content=""
|
|
7139
|
+
if [[ -f "$project_dir/docker-compose.yml" ]]; then
|
|
7140
|
+
compose_content=$(head -c 5000 "$project_dir/docker-compose.yml")
|
|
7141
|
+
elif [[ -f "$project_dir/docker-compose.yaml" ]]; then
|
|
7142
|
+
compose_content=$(head -c 5000 "$project_dir/docker-compose.yaml")
|
|
7143
|
+
fi
|
|
7144
|
+
|
|
7145
|
+
# Gather Dockerfile for the failing service (truncated)
|
|
7146
|
+
local dockerfile_content=""
|
|
7147
|
+
for df_path in "$project_dir/$svc_name/Dockerfile" "$project_dir/Dockerfile" "$project_dir/Dockerfile.$svc_name"; do
|
|
7148
|
+
if [[ -f "$df_path" ]]; then
|
|
7149
|
+
dockerfile_content=$(head -c 3000 "$df_path")
|
|
7150
|
+
break
|
|
7151
|
+
fi
|
|
7152
|
+
done
|
|
7153
|
+
|
|
7154
|
+
# 3. FEED TO AI: Construct a rich prompt with all context
|
|
7155
|
+
local ai_prompt="You are debugging a Docker Compose service that has failed.
|
|
7156
|
+
|
|
7157
|
+
SERVICE: $svc_name
|
|
7158
|
+
STATUS: exited/dead
|
|
7159
|
+
|
|
7160
|
+
DOCKER COMPOSE LOGS (last 50 lines):
|
|
7161
|
+
$logs
|
|
7162
|
+
|
|
7163
|
+
DOCKER-COMPOSE.YML:
|
|
7164
|
+
$compose_content"
|
|
7165
|
+
|
|
7166
|
+
if [[ -n "$dockerfile_content" ]]; then
|
|
7167
|
+
ai_prompt="${ai_prompt}
|
|
7168
|
+
|
|
7169
|
+
DOCKERFILE ($svc_name/Dockerfile):
|
|
7170
|
+
$dockerfile_content"
|
|
7171
|
+
fi
|
|
7172
|
+
|
|
7173
|
+
ai_prompt="${ai_prompt}
|
|
7174
|
+
|
|
7175
|
+
INSTRUCTIONS:
|
|
7176
|
+
1. Analyze the error in the logs above
|
|
7177
|
+
2. Identify the root cause
|
|
7178
|
+
3. Fix the issue by editing the necessary files (docker-compose.yml, Dockerfile, source code, package.json, requirements.txt, etc.)
|
|
7179
|
+
4. Make sure the fix works on any platform (Docker Desktop, Linux, Docker-in-Docker, Kubernetes)
|
|
7180
|
+
5. Do NOT just restart -- actually fix the underlying code/config problem
|
|
7181
|
+
6. After fixing, the system will rebuild with 'docker compose up --build'
|
|
7182
|
+
7. Common issues: named volumes for node_modules (use anonymous), missing dependencies, port conflicts, wrong commands"
|
|
7183
|
+
|
|
7184
|
+
echo -e "${CYAN}[AI] Analyzing $svc_name failure with ${LOKI_PROVIDER:-claude}...${NC}"
|
|
7185
|
+
|
|
7186
|
+
# 4. LET AI FIX: Run loki quick with the AI prompt
|
|
7187
|
+
# The AI provider (claude/codex/gemini/ollama) decides what to fix
|
|
7188
|
+
fix_count=$((fix_count + 1))
|
|
7189
|
+
echo -e "${CYAN}[FIX] Attempt $fix_count/$max_fixes${NC}"
|
|
7190
|
+
|
|
7191
|
+
(
|
|
7192
|
+
cd "$project_dir"
|
|
7193
|
+
LOKI_MAX_ITERATIONS=5 LOKI_AUTO_FIX=true \
|
|
7194
|
+
"$0" quick "$ai_prompt" 2>&1 | while IFS= read -r fline; do
|
|
7195
|
+
echo " $fline"
|
|
7196
|
+
done
|
|
7197
|
+
)
|
|
7198
|
+
|
|
7199
|
+
# 5. REBUILD: Docker compose up --build for the fixed service
|
|
7200
|
+
echo -e "${CYAN}[REBUILD] Rebuilding $svc_name...${NC}"
|
|
7201
|
+
(cd "$project_dir" && docker compose up -d --build --no-deps "$svc_name" 2>&1) | while IFS= read -r fline; do
|
|
7202
|
+
echo " $fline"
|
|
7203
|
+
done
|
|
7204
|
+
|
|
7205
|
+
# 6. VERIFY: Wait and check if the fix worked
|
|
7206
|
+
echo -e "${CYAN}[VERIFY] Waiting 15s for $svc_name to stabilize...${NC}"
|
|
7207
|
+
sleep 15
|
|
7208
|
+
|
|
7209
|
+
local verify_state
|
|
7210
|
+
verify_state=$(cd "$project_dir" && docker compose ps -a --format json 2>/dev/null | VERIFY_SVC="$svc_name" python3 -c "
|
|
7211
|
+
import json, sys, os
|
|
7212
|
+
svc = os.environ['VERIFY_SVC']
|
|
7213
|
+
raw = sys.stdin.read().strip()
|
|
7214
|
+
if not raw:
|
|
7215
|
+
print('unknown')
|
|
7216
|
+
sys.exit(0)
|
|
7217
|
+
# Handle both JSON array (v2.21+) and NDJSON formats
|
|
7218
|
+
try:
|
|
7219
|
+
parsed = json.loads(raw)
|
|
7220
|
+
services = parsed if isinstance(parsed, list) else [parsed]
|
|
7221
|
+
except json.JSONDecodeError:
|
|
7222
|
+
services = []
|
|
7223
|
+
for line in raw.split(chr(10)):
|
|
7224
|
+
try: services.append(json.loads(line))
|
|
7225
|
+
except: pass
|
|
7226
|
+
for s in services:
|
|
7227
|
+
if not isinstance(s, dict): continue
|
|
7228
|
+
if s.get('Service', s.get('Name','')) == svc:
|
|
7229
|
+
print(s.get('State','unknown'))
|
|
7230
|
+
sys.exit(0)
|
|
7231
|
+
print('unknown')
|
|
7232
|
+
" 2>/dev/null || echo "unknown")
|
|
7233
|
+
|
|
7234
|
+
if [[ "$verify_state" == "running" ]]; then
|
|
7235
|
+
echo -e "${GREEN}[SUCCESS] $svc_name is now running!${NC}"
|
|
7236
|
+
else
|
|
7237
|
+
echo -e "${YELLOW}[RETRY] $svc_name still not healthy (state: $verify_state). Will retry on next poll.${NC}"
|
|
7238
|
+
fi
|
|
7239
|
+
done
|
|
7240
|
+
|
|
7241
|
+
sleep "$poll_interval"
|
|
7242
|
+
done
|
|
7243
|
+
}
|
|
7244
|
+
|
|
7051
7245
|
# Project scaffolding (v6.28.0)
|
|
7052
7246
|
cmd_init() {
|
|
7053
7247
|
# Guard: check if .loki/ already exists to avoid overwriting active session
|
|
@@ -10487,6 +10681,9 @@ main() {
|
|
|
10487
10681
|
quick)
|
|
10488
10682
|
cmd_quick "$@"
|
|
10489
10683
|
;;
|
|
10684
|
+
monitor)
|
|
10685
|
+
cmd_monitor "$@"
|
|
10686
|
+
;;
|
|
10490
10687
|
demo)
|
|
10491
10688
|
cmd_demo "$@"
|
|
10492
10689
|
;;
|
package/autonomy/run.sh
CHANGED
|
@@ -580,6 +580,11 @@ PHASE_UAT=${LOKI_PHASE_UAT:-true}
|
|
|
580
580
|
COMPLETION_PROMISE=${LOKI_COMPLETION_PROMISE:-""}
|
|
581
581
|
MAX_ITERATIONS=${LOKI_MAX_ITERATIONS:-1000}
|
|
582
582
|
ITERATION_COUNT=0
|
|
583
|
+
|
|
584
|
+
# If this is an auto-fix task, allow more iterations
|
|
585
|
+
if [[ "${LOKI_AUTO_FIX:-}" == "true" ]]; then
|
|
586
|
+
MAX_ITERATIONS="${LOKI_MAX_ITERATIONS:-5}"
|
|
587
|
+
fi
|
|
583
588
|
# Perpetual mode: never stop unless max iterations (ignores all completion signals)
|
|
584
589
|
PERPETUAL_MODE=${LOKI_PERPETUAL_MODE:-false}
|
|
585
590
|
|
|
@@ -625,6 +630,8 @@ if [ -f "$TELEMETRY_SCRIPT" ]; then
|
|
|
625
630
|
source "$TELEMETRY_SCRIPT"
|
|
626
631
|
fi
|
|
627
632
|
|
|
633
|
+
|
|
634
|
+
|
|
628
635
|
# 2026 Research Enhancements (minimal additions)
|
|
629
636
|
PROMPT_REPETITION=${LOKI_PROMPT_REPETITION:-true}
|
|
630
637
|
CONFIDENCE_ROUTING=${LOKI_CONFIDENCE_ROUTING:-true}
|
package/completions/_loki
CHANGED
|
@@ -91,6 +91,9 @@ function _loki {
|
|
|
91
91
|
completions)
|
|
92
92
|
_arguments '1:shell:(bash zsh)'
|
|
93
93
|
;;
|
|
94
|
+
monitor)
|
|
95
|
+
_directories
|
|
96
|
+
;;
|
|
94
97
|
context|ctx)
|
|
95
98
|
_loki_context
|
|
96
99
|
;;
|
|
@@ -107,6 +110,7 @@ function _loki_commands {
|
|
|
107
110
|
commands=(
|
|
108
111
|
'start:Start Loki Mode'
|
|
109
112
|
'quick:Quick single-task mode'
|
|
113
|
+
'monitor:Monitor Docker Compose services with auto-fix'
|
|
110
114
|
'demo:Interactive 60-second demo'
|
|
111
115
|
'init:Interactive PRD builder'
|
|
112
116
|
'stop:Stop execution'
|
package/completions/loki.bash
CHANGED
|
@@ -5,7 +5,7 @@ _loki_completion() {
|
|
|
5
5
|
_init_completion || return
|
|
6
6
|
|
|
7
7
|
# Main subcommands (must match autonomy/loki main case statement)
|
|
8
|
-
local main_commands="start quick demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share explain plan report test ci watch telemetry agent context code run export review optimize heal migrate cluster worktree trigger failover remote version completions help"
|
|
8
|
+
local main_commands="start quick monitor demo init stop pause resume status dashboard logs serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets doctor watchdog audit metrics syslog onboard share explain plan report test ci watch telemetry agent context code run export review optimize heal migrate cluster worktree trigger failover remote version completions help"
|
|
9
9
|
|
|
10
10
|
# 1. If we are on the first argument (subcommand)
|
|
11
11
|
if [[ $cword -eq 1 ]]; then
|
|
@@ -178,6 +178,11 @@ _loki_completion() {
|
|
|
178
178
|
fi
|
|
179
179
|
;;
|
|
180
180
|
|
|
181
|
+
monitor)
|
|
182
|
+
# Complete with directories
|
|
183
|
+
_filedir -d
|
|
184
|
+
;;
|
|
185
|
+
|
|
181
186
|
completions)
|
|
182
187
|
COMPREPLY=( $(compgen -W "bash zsh" -- "$cur") )
|
|
183
188
|
;;
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
package/web-app/server.py
CHANGED
|
@@ -1454,9 +1454,10 @@ class DevServerManager:
|
|
|
1454
1454
|
except Exception:
|
|
1455
1455
|
logger.debug("Docker context gathering for auto-fix failed", exc_info=True)
|
|
1456
1456
|
|
|
1457
|
-
# Save original command and multi_service flag before stop() removes the info dict
|
|
1457
|
+
# Save original command, framework, and multi_service flag before stop() removes the info dict
|
|
1458
1458
|
cmd = info.get("original_command")
|
|
1459
1459
|
is_multi_service = info.get("multi_service", False)
|
|
1460
|
+
framework = info.get("framework")
|
|
1460
1461
|
|
|
1461
1462
|
try:
|
|
1462
1463
|
auto_fix_env = {**os.environ}
|
|
@@ -1475,6 +1476,14 @@ class DevServerManager:
|
|
|
1475
1476
|
)
|
|
1476
1477
|
if result.returncode == 0:
|
|
1477
1478
|
logger.info("Auto-fix succeeded for session %s, restarting dev server", session_id)
|
|
1479
|
+
# For Docker projects, rebuild images before restarting
|
|
1480
|
+
# (fix may have changed Dockerfile, package.json, requirements.txt, etc.)
|
|
1481
|
+
if framework == "docker":
|
|
1482
|
+
await asyncio.to_thread(
|
|
1483
|
+
subprocess.run,
|
|
1484
|
+
["docker", "compose", "up", "-d", "--build", "--no-deps"],
|
|
1485
|
+
capture_output=True, cwd=str(target), timeout=120
|
|
1486
|
+
)
|
|
1478
1487
|
# Restart the dev server
|
|
1479
1488
|
await self.stop(session_id)
|
|
1480
1489
|
await asyncio.sleep(1)
|
|
@@ -1723,7 +1732,17 @@ class DevServerManager:
|
|
|
1723
1732
|
was_running = prev.get("status") in ("running", None)
|
|
1724
1733
|
now_failed = svc["state"] in ("exited", "dead")
|
|
1725
1734
|
|
|
1726
|
-
|
|
1735
|
+
# Detect services that need fixing:
|
|
1736
|
+
# 1. was_running and now_failed: service transitioned from running to exited
|
|
1737
|
+
# 2. Persistently failed: service is exited AND has never been successfully fixed
|
|
1738
|
+
# (fix_attempts == 0 or fix_status != "fixed")
|
|
1739
|
+
persistently_failed = (
|
|
1740
|
+
now_failed
|
|
1741
|
+
and prev.get("fix_status") != "fixed"
|
|
1742
|
+
and prev.get("status") in ("exited", "dead", None)
|
|
1743
|
+
)
|
|
1744
|
+
|
|
1745
|
+
if (was_running and now_failed) or persistently_failed:
|
|
1727
1746
|
svc_health["restarts"] = prev.get("restarts", 0) + 1
|
|
1728
1747
|
logger.warning("Docker service '%s' failed (exit %s)", name, svc.get("exit_code"))
|
|
1729
1748
|
|
|
@@ -1747,14 +1766,51 @@ class DevServerManager:
|
|
|
1747
1766
|
|
|
1748
1767
|
info["_auto_fixing"] = True
|
|
1749
1768
|
svc_logs = docker_ctx.get("service_logs", {}).get(name, "")
|
|
1750
|
-
diagnoses = _diagnose_errors(svc_logs)
|
|
1751
|
-
diag_text = "\n".join(f"- {d['diagnosis']}: {d['suggestion']}" for d in diagnoses)
|
|
1752
1769
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1770
|
+
# Read compose file for AI context
|
|
1771
|
+
compose_content = ""
|
|
1772
|
+
compose_file = project_dir / "docker-compose.yml"
|
|
1773
|
+
if not compose_file.exists():
|
|
1774
|
+
compose_file = project_dir / "docker-compose.yaml"
|
|
1775
|
+
if compose_file.exists():
|
|
1776
|
+
try:
|
|
1777
|
+
compose_content = compose_file.read_text(errors="replace")[:5000]
|
|
1778
|
+
except OSError:
|
|
1779
|
+
pass
|
|
1780
|
+
|
|
1781
|
+
# Read service Dockerfile for AI context
|
|
1782
|
+
dockerfile_content = ""
|
|
1783
|
+
for df_path in [project_dir / name / "Dockerfile", project_dir / "Dockerfile"]:
|
|
1784
|
+
if df_path.exists():
|
|
1785
|
+
try:
|
|
1786
|
+
dockerfile_content = df_path.read_text(errors="replace")[:3000]
|
|
1787
|
+
break
|
|
1788
|
+
except OSError:
|
|
1789
|
+
pass
|
|
1790
|
+
|
|
1791
|
+
fix_prompt = f"""You are debugging a Docker Compose service that has failed.
|
|
1792
|
+
|
|
1793
|
+
SERVICE: {name}
|
|
1794
|
+
STATUS: exited/dead (exit code {svc.get('exit_code', 1)})
|
|
1795
|
+
|
|
1796
|
+
DOCKER COMPOSE LOGS (last 50 lines):
|
|
1797
|
+
{svc_logs[:3000]}
|
|
1798
|
+
|
|
1799
|
+
DOCKER-COMPOSE.YML:
|
|
1800
|
+
{compose_content}
|
|
1801
|
+
"""
|
|
1802
|
+
if dockerfile_content:
|
|
1803
|
+
fix_prompt += f"\nDOCKERFILE ({name}/Dockerfile):\n{dockerfile_content}\n"
|
|
1804
|
+
|
|
1805
|
+
fix_prompt += """
|
|
1806
|
+
INSTRUCTIONS:
|
|
1807
|
+
1. Analyze the error in the logs above
|
|
1808
|
+
2. Identify the root cause
|
|
1809
|
+
3. Fix the issue by editing the necessary files (docker-compose.yml, Dockerfile, source code, package.json, requirements.txt, etc.)
|
|
1810
|
+
4. Make sure the fix works on any platform (Docker Desktop, Linux, Docker-in-Docker, Kubernetes)
|
|
1811
|
+
5. Do NOT just restart -- actually fix the underlying code/config problem
|
|
1812
|
+
6. After fixing, the system will rebuild with 'docker compose up --build'
|
|
1813
|
+
7. Common issues: named volumes for node_modules (use anonymous), missing dependencies, port conflicts, wrong commands"""
|
|
1758
1814
|
|
|
1759
1815
|
svc_health["fix_attempts"] += 1
|
|
1760
1816
|
svc_health["fix_timestamps"] = recent_fixes + [now]
|
|
@@ -1794,24 +1850,70 @@ class DevServerManager:
|
|
|
1794
1850
|
return
|
|
1795
1851
|
|
|
1796
1852
|
try:
|
|
1853
|
+
fix_env = {**os.environ, **_load_secrets()}
|
|
1854
|
+
fix_env["LOKI_MAX_ITERATIONS"] = "5" # More iterations for complex Docker fixes
|
|
1855
|
+
|
|
1797
1856
|
proc = await asyncio.to_thread(
|
|
1798
1857
|
subprocess.run,
|
|
1799
1858
|
[loki, "quick", fix_prompt],
|
|
1800
1859
|
capture_output=True, text=True, cwd=project_dir, timeout=300,
|
|
1801
|
-
env=
|
|
1860
|
+
env=fix_env
|
|
1802
1861
|
)
|
|
1803
1862
|
|
|
1804
|
-
#
|
|
1863
|
+
# Rebuild the image (fix may have changed Dockerfile or package.json)
|
|
1864
|
+
# --no-deps prevents restarting healthy services, --build rebuilds with the fix
|
|
1805
1865
|
await asyncio.to_thread(
|
|
1806
1866
|
subprocess.run,
|
|
1807
|
-
["docker", "compose", "
|
|
1808
|
-
capture_output=True, cwd=project_dir, timeout=
|
|
1867
|
+
["docker", "compose", "up", "-d", "--build", "--no-deps", service_name],
|
|
1868
|
+
capture_output=True, cwd=project_dir, timeout=120
|
|
1809
1869
|
)
|
|
1810
1870
|
|
|
1871
|
+
# Wait for service to stabilize after rebuild
|
|
1872
|
+
await asyncio.sleep(10)
|
|
1873
|
+
|
|
1874
|
+
# Verify the fix actually worked
|
|
1875
|
+
fix_worked = False
|
|
1876
|
+
try:
|
|
1877
|
+
verify_proc = await asyncio.to_thread(
|
|
1878
|
+
subprocess.run,
|
|
1879
|
+
["docker", "compose", "ps", "-a", "--format", "json"],
|
|
1880
|
+
capture_output=True, text=True, cwd=project_dir, timeout=10
|
|
1881
|
+
)
|
|
1882
|
+
if verify_proc.returncode == 0 and verify_proc.stdout.strip():
|
|
1883
|
+
raw = verify_proc.stdout.strip()
|
|
1884
|
+
try:
|
|
1885
|
+
parsed = json.loads(raw)
|
|
1886
|
+
if not isinstance(parsed, list):
|
|
1887
|
+
parsed = [parsed]
|
|
1888
|
+
except json.JSONDecodeError:
|
|
1889
|
+
parsed = []
|
|
1890
|
+
for line in raw.split("\n"):
|
|
1891
|
+
if line.strip():
|
|
1892
|
+
try:
|
|
1893
|
+
parsed.append(json.loads(line))
|
|
1894
|
+
except json.JSONDecodeError:
|
|
1895
|
+
pass
|
|
1896
|
+
for svc in parsed:
|
|
1897
|
+
if isinstance(svc, dict) and svc.get("Name", svc.get("name", "")) == service_name:
|
|
1898
|
+
state = svc.get("State", svc.get("state", ""))
|
|
1899
|
+
fix_worked = state == "running"
|
|
1900
|
+
break
|
|
1901
|
+
except Exception:
|
|
1902
|
+
logger.debug("Post-fix verification failed for service '%s'", service_name, exc_info=True)
|
|
1903
|
+
|
|
1904
|
+
# Determine final fix status
|
|
1905
|
+
if proc.returncode == 0 and fix_worked:
|
|
1906
|
+
final_status = "fixed"
|
|
1907
|
+
elif proc.returncode == 0 and not fix_worked:
|
|
1908
|
+
final_status = "fix_failed"
|
|
1909
|
+
logger.warning("loki quick succeeded but service '%s' still not running", service_name)
|
|
1910
|
+
else:
|
|
1911
|
+
final_status = "fix_failed"
|
|
1912
|
+
|
|
1811
1913
|
# BUG-V64-005: Re-fetch from live info dict to avoid writing to detached dict
|
|
1812
1914
|
info = self.servers.get(session_id)
|
|
1813
1915
|
if info and "docker_service_health" in info and service_name in info["docker_service_health"]:
|
|
1814
|
-
info["docker_service_health"][service_name]["fix_status"] =
|
|
1916
|
+
info["docker_service_health"][service_name]["fix_status"] = final_status
|
|
1815
1917
|
except Exception as exc:
|
|
1816
1918
|
logger.error("Auto-fix for service '%s' failed: %s", service_name, exc)
|
|
1817
1919
|
# BUG-V64-005: Re-fetch from live info dict
|
|
@@ -1856,7 +1958,7 @@ async def _gather_docker_context(project_dir: Path) -> dict:
|
|
|
1856
1958
|
# Get service status via docker compose ps
|
|
1857
1959
|
try:
|
|
1858
1960
|
ps_proc = await loop.run_in_executor(None, lambda: subprocess.run(
|
|
1859
|
-
["docker", "compose", "ps", "--format", "json"],
|
|
1961
|
+
["docker", "compose", "ps", "-a", "--format", "json"],
|
|
1860
1962
|
capture_output=True, text=True, cwd=str(project_dir), timeout=10
|
|
1861
1963
|
))
|
|
1862
1964
|
if ps_proc.returncode == 0 and ps_proc.stdout.strip():
|