loki-mode 6.71.0 → 6.72.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/README.md +9 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/hooks/migration-hooks.sh +26 -0
- package/autonomy/loki +429 -92
- package/autonomy/run.sh +219 -38
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +101 -19
- package/docs/INSTALLATION.md +20 -11
- package/docs/bug-fixes/agent-01-cli-fixes.md +101 -0
- package/docs/bug-fixes/agent-02-purplelab-fixes.md +88 -0
- package/docs/bug-fixes/agent-03-dashboard-fixes.md +119 -0
- package/docs/bug-fixes/agent-04-memory-fixes.md +105 -0
- package/docs/bug-fixes/agent-05-provider-fixes.md +86 -0
- package/docs/bug-fixes/agent-06-integration-fixes.md +101 -0
- package/docs/bug-fixes/agent-07-dash-run-fixes.md +101 -0
- package/docs/bug-fixes/agent-08-docker-fixes.md +164 -0
- package/docs/bug-fixes/agent-09-e2e-build-fixes.md +69 -0
- package/docs/bug-fixes/agent-10-e2e-fullstack-fixes.md +102 -0
- package/docs/bug-fixes/agent-11-e2e-session-fixes.md +70 -0
- package/docs/bug-fixes/agent-12-scenario-fixes.md +120 -0
- package/docs/bug-fixes/agent-13-enterprise-fixes.md +143 -0
- package/docs/bug-fixes/agent-14-uat-newuser-fixes.md +88 -0
- package/docs/bug-fixes/agent-15-uat-poweruser-fixes.md +132 -0
- package/docs/bug-fixes/agent-19-code-review.md +316 -0
- package/docs/bug-fixes/agent-20-architecture-review.md +331 -0
- package/docs/competitive/bolt-new-analysis.md +579 -0
- package/docs/competitive/emergence-others-analysis.md +605 -0
- package/docs/competitive/replit-lovable-analysis.md +622 -0
- package/docs/test-scenarios/edge-cases.md +813 -0
- package/docs/test-scenarios/enterprise-scenarios.md +732 -0
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +49 -5
- package/memory/consolidation.py +33 -0
- package/memory/embeddings.py +10 -1
- package/memory/engine.py +83 -38
- package/memory/retrieval.py +36 -0
- package/memory/storage.py +56 -4
- package/memory/token_economics.py +14 -2
- package/memory/vector_index.py +36 -7
- package/package.json +1 -1
- package/providers/gemini.sh +89 -2
- package/templates/README.md +1 -1
- package/templates/cli-tool.md +30 -0
- package/templates/dashboard.md +4 -0
- package/templates/data-pipeline.md +4 -0
- package/templates/discord-bot.md +47 -0
- package/templates/game.md +4 -0
- package/templates/microservice.md +4 -0
- package/templates/npm-library.md +4 -0
- package/templates/rest-api-auth.md +50 -20
- package/templates/rest-api.md +15 -0
- package/templates/saas-starter.md +1 -1
- package/templates/slack-bot.md +36 -0
- package/templates/static-landing-page.md +9 -1
- package/templates/web-scraper.md +4 -0
- package/web-app/dist/assets/Badge-CeBkFjo6.js +1 -0
- package/web-app/dist/assets/Button-yuhqo8Fq.js +1 -0
- package/web-app/dist/assets/{Card-BMw7NSaV.js → Card-BG17vsX0.js} +1 -1
- package/web-app/dist/assets/{HomePage-QyvNpyFv.js → HomePage-BMSQ7Apj.js} +3 -3
- package/web-app/dist/assets/{LoginPage-CG_DkANw.js → LoginPage-aH_6iolg.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-CHBJTLTi.js → NotFoundPage-Di8cNtB1.js} +1 -1
- package/web-app/dist/assets/ProjectPage-BtRssmw9.js +285 -0
- package/web-app/dist/assets/ProjectsPage-B-FTFagc.js +6 -0
- package/web-app/dist/assets/{SettingsPage-Dq-c6kXj.js → SettingsPage-DIJPBla4.js} +1 -1
- package/web-app/dist/assets/TeamsPage--19fNX7w.js +36 -0
- package/web-app/dist/assets/TemplatesPage-ChUQNOOv.js +11 -0
- package/web-app/dist/assets/TerminalOutput-Dwrzecyl.js +31 -0
- package/web-app/dist/assets/activity-BNRWeu9N.js +6 -0
- package/web-app/dist/assets/{arrow-left-Dw9yRwL8.js → arrow-left-Ce6g1_YE.js} +1 -1
- package/web-app/dist/assets/circle-alert-LIndawHL.js +11 -0
- package/web-app/dist/assets/clock-Bpj4VPlP.js +6 -0
- package/web-app/dist/assets/{external-link-DGtaQZrg.js → external-link-BhhdF0iQ.js} +1 -1
- package/web-app/dist/assets/folder-open-CM2LgfxI.js +11 -0
- package/web-app/dist/assets/index-8-KpWWq7.css +1 -0
- package/web-app/dist/assets/index-kPDW4e_b.js +236 -0
- package/web-app/dist/assets/lock-sAk3Xe54.js +16 -0
- package/web-app/dist/assets/search-CR-2i9by.js +6 -0
- package/web-app/dist/assets/server-DuFh4ymA.js +26 -0
- package/web-app/dist/assets/trash-2-BmkkT8V_.js +11 -0
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +1345 -55
- package/web-app/dist/assets/Badge-BFLpnFZM.js +0 -6
- package/web-app/dist/assets/Button-BYY9clv_.js +0 -16
- package/web-app/dist/assets/ProjectPage-q65bhy76.js +0 -217
- package/web-app/dist/assets/ProjectsPage-d4mY9ewI.js +0 -21
- package/web-app/dist/assets/TemplatesPage-BEpY-p-Q.js +0 -1
- package/web-app/dist/assets/TerminalOutput-CFy7MnPO.js +0 -51
- package/web-app/dist/assets/clock-D4pcK_Eq.js +0 -11
- package/web-app/dist/assets/index-BnNomb7B.js +0 -196
- package/web-app/dist/assets/index-D452pFGl.css +0 -1
package/autonomy/loki
CHANGED
|
@@ -126,6 +126,7 @@ SANDBOX_SH="$SKILL_DIR/autonomy/sandbox.sh"
|
|
|
126
126
|
EMIT_SH="$SKILL_DIR/events/emit.sh"
|
|
127
127
|
LEARNING_EMIT_SH="$SKILL_DIR/learning/emit.sh"
|
|
128
128
|
LOKI_DIR=".loki"
|
|
129
|
+
export LOKI_DIR
|
|
129
130
|
|
|
130
131
|
# Anonymous usage telemetry
|
|
131
132
|
PROJECT_DIR="$SKILL_DIR"
|
|
@@ -400,7 +401,7 @@ show_help() {
|
|
|
400
401
|
echo " quick \"task\" Quick single-task mode (lightweight, 3 iterations max)"
|
|
401
402
|
echo " monitor [path] Monitor Docker Compose services with auto-fix (v6.67.0)"
|
|
402
403
|
echo " demo Run interactive demo (~60s simulated session)"
|
|
403
|
-
echo " init [name] Project scaffolding with
|
|
404
|
+
echo " init [name] Project scaffolding with 21 PRD templates"
|
|
404
405
|
echo " issue <url|num> [DEPRECATED] Use 'loki run' instead"
|
|
405
406
|
echo " watch [prd] Auto-rerun on PRD file changes (v6.33.0)"
|
|
406
407
|
echo " export <format> Export session data: json|markdown|csv|timeline (v6.0.0)"
|
|
@@ -1092,6 +1093,24 @@ cmd_start() {
|
|
|
1092
1093
|
--outcome success \
|
|
1093
1094
|
--context "{\"provider\":\"$effective_provider\",\"prd_path\":\"${prd_file:-}\"}"
|
|
1094
1095
|
|
|
1096
|
+
# Pre-flight: check that the provider CLI is installed
|
|
1097
|
+
if ! command -v "$effective_provider" &>/dev/null; then
|
|
1098
|
+
echo -e "${RED}Error: Provider CLI '$effective_provider' is not installed.${NC}"
|
|
1099
|
+
echo ""
|
|
1100
|
+
echo "Install it first:"
|
|
1101
|
+
case "$effective_provider" in
|
|
1102
|
+
claude) echo " npm install -g @anthropic-ai/claude-code" ;;
|
|
1103
|
+
codex) echo " npm install -g @openai/codex" ;;
|
|
1104
|
+
gemini) echo " npm install -g @google/gemini-cli" ;;
|
|
1105
|
+
cline) echo " npm install -g @anthropic-ai/cline" ;;
|
|
1106
|
+
aider) echo " pip install aider-chat" ;;
|
|
1107
|
+
*) echo " Check the provider documentation for installation." ;;
|
|
1108
|
+
esac
|
|
1109
|
+
echo ""
|
|
1110
|
+
echo "Check your environment: loki doctor"
|
|
1111
|
+
exit 1
|
|
1112
|
+
fi
|
|
1113
|
+
|
|
1095
1114
|
exec "$RUN_SH" "${args[@]}"
|
|
1096
1115
|
}
|
|
1097
1116
|
|
|
@@ -1350,18 +1369,22 @@ cmd_stop() {
|
|
|
1350
1369
|
pkill -9 -f "loki-run-" 2>/dev/null || true
|
|
1351
1370
|
|
|
1352
1371
|
# Mark session.json as stopped (skill-invoked sessions)
|
|
1372
|
+
# BUG-ST-008: Atomic session.json update via temp file + mv (matches run.sh)
|
|
1353
1373
|
if [ -f "$LOKI_DIR/session.json" ]; then
|
|
1354
|
-
python3 -c "
|
|
1355
|
-
import json,
|
|
1374
|
+
_LOKI_SESSION_FILE="$LOKI_DIR/session.json" python3 -c "
|
|
1375
|
+
import json, os, tempfile
|
|
1376
|
+
sf = os.environ['_LOKI_SESSION_FILE']
|
|
1356
1377
|
try:
|
|
1357
|
-
|
|
1358
|
-
with open(p, 'r+') as f:
|
|
1378
|
+
with open(sf) as f:
|
|
1359
1379
|
d = json.load(f)
|
|
1360
|
-
|
|
1361
|
-
|
|
1380
|
+
d['status'] = 'stopped'
|
|
1381
|
+
sd = os.path.dirname(sf)
|
|
1382
|
+
fd, tmp = tempfile.mkstemp(dir=sd, suffix='.json')
|
|
1383
|
+
with os.fdopen(fd, 'w') as f:
|
|
1362
1384
|
json.dump(d, f)
|
|
1363
|
-
|
|
1364
|
-
|
|
1385
|
+
os.replace(tmp, sf)
|
|
1386
|
+
except (json.JSONDecodeError, OSError): pass
|
|
1387
|
+
" 2>/dev/null || true
|
|
1365
1388
|
fi
|
|
1366
1389
|
|
|
1367
1390
|
# Clean up control files
|
|
@@ -1738,12 +1761,15 @@ cmd_status() {
|
|
|
1738
1761
|
# Context window usage (token tracking)
|
|
1739
1762
|
if [ -f "$LOKI_DIR/state/context-usage.json" ]; then
|
|
1740
1763
|
local ctx_used ctx_total
|
|
1741
|
-
ctx_total=$(python3 -c "import json; d=json.load(open(
|
|
1742
|
-
ctx_used=$(python3 -c "import json; d=json.load(open(
|
|
1764
|
+
ctx_total=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
|
|
1765
|
+
ctx_used=$(_LOKI_CTX_FILE="$LOKI_DIR/state/context-usage.json" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
|
|
1743
1766
|
if type context_gauge &>/dev/null; then
|
|
1744
1767
|
context_gauge "$ctx_used" "$ctx_total" "Context"
|
|
1745
1768
|
else
|
|
1746
|
-
local ctx_pct
|
|
1769
|
+
local ctx_pct=0
|
|
1770
|
+
if [ "$ctx_total" -gt 0 ] 2>/dev/null; then
|
|
1771
|
+
ctx_pct=$((ctx_used * 100 / ctx_total))
|
|
1772
|
+
fi
|
|
1747
1773
|
echo -e "${CYAN}Context:${NC} ${ctx_pct}% (${ctx_used} / ${ctx_total} tokens)"
|
|
1748
1774
|
fi
|
|
1749
1775
|
fi
|
|
@@ -3151,6 +3177,22 @@ cmd_web() {
|
|
|
3151
3177
|
status)
|
|
3152
3178
|
cmd_web_status
|
|
3153
3179
|
;;
|
|
3180
|
+
logs)
|
|
3181
|
+
shift || true
|
|
3182
|
+
local log_lines="${1:-100}"
|
|
3183
|
+
local log_file="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
|
|
3184
|
+
if [ ! -f "$log_file" ]; then
|
|
3185
|
+
log_file="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
|
|
3186
|
+
fi
|
|
3187
|
+
if [ ! -f "$log_file" ]; then
|
|
3188
|
+
echo -e "${YELLOW}No Purple Lab log file found${NC}"
|
|
3189
|
+
return 0
|
|
3190
|
+
fi
|
|
3191
|
+
echo -e "${BOLD}Purple Lab Logs (last $log_lines lines)${NC}"
|
|
3192
|
+
echo -e "${DIM}Use 'loki web logs <N>' to show more/fewer lines${NC}"
|
|
3193
|
+
echo ""
|
|
3194
|
+
tail -n "$log_lines" "$log_file"
|
|
3195
|
+
;;
|
|
3154
3196
|
--help|-h|help)
|
|
3155
3197
|
cmd_web_help
|
|
3156
3198
|
;;
|
|
@@ -3171,6 +3213,7 @@ cmd_web_help() {
|
|
|
3171
3213
|
echo " start Start Purple Lab (default)"
|
|
3172
3214
|
echo " stop Stop Purple Lab server"
|
|
3173
3215
|
echo " status Show Purple Lab server status"
|
|
3216
|
+
echo " logs Show Purple Lab server logs"
|
|
3174
3217
|
echo " help Show this help"
|
|
3175
3218
|
echo ""
|
|
3176
3219
|
echo "Options (for start):"
|
|
@@ -3335,8 +3378,10 @@ cmd_web_start() {
|
|
|
3335
3378
|
|
|
3336
3379
|
# Wait for server to be ready
|
|
3337
3380
|
local retries=0
|
|
3381
|
+
local server_ready=false
|
|
3338
3382
|
while [ $retries -lt 15 ]; do
|
|
3339
3383
|
if curl -s "http://${PURPLE_LAB_DEFAULT_HOST}:${port}/api/session/status" > /dev/null 2>&1; then
|
|
3384
|
+
server_ready=true
|
|
3340
3385
|
break
|
|
3341
3386
|
fi
|
|
3342
3387
|
sleep 0.5
|
|
@@ -3351,21 +3396,33 @@ cmd_web_start() {
|
|
|
3351
3396
|
fi
|
|
3352
3397
|
|
|
3353
3398
|
local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
|
|
3354
|
-
|
|
3399
|
+
|
|
3400
|
+
if [ "$server_ready" = true ]; then
|
|
3401
|
+
echo -e "${GREEN}Purple Lab running at: $url${NC} (PID: $pid)"
|
|
3402
|
+
else
|
|
3403
|
+
echo -e "${YELLOW}Purple Lab starting at: $url${NC} (PID: $pid)"
|
|
3404
|
+
echo "Server may still be loading. Refresh the browser if it does not load immediately."
|
|
3405
|
+
fi
|
|
3355
3406
|
echo -e "Logs: $log_file"
|
|
3356
3407
|
echo -e "Stop with: ${CYAN}loki web stop${NC}"
|
|
3357
3408
|
|
|
3358
|
-
# Open browser
|
|
3409
|
+
# Open browser only after server is confirmed ready
|
|
3359
3410
|
if [ "$open_browser" = true ]; then
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
open
|
|
3363
|
-
|
|
3364
|
-
xdg-open
|
|
3365
|
-
|
|
3366
|
-
start
|
|
3411
|
+
if [ "$server_ready" = true ]; then
|
|
3412
|
+
echo ""
|
|
3413
|
+
if command -v open &> /dev/null; then
|
|
3414
|
+
open "$url"
|
|
3415
|
+
elif command -v xdg-open &> /dev/null; then
|
|
3416
|
+
xdg-open "$url"
|
|
3417
|
+
elif command -v start &> /dev/null; then
|
|
3418
|
+
start "$url"
|
|
3419
|
+
else
|
|
3420
|
+
echo "Please open in browser: $url"
|
|
3421
|
+
fi
|
|
3367
3422
|
else
|
|
3368
|
-
echo "
|
|
3423
|
+
echo ""
|
|
3424
|
+
echo -e "${YELLOW}Browser not opened because server is not ready yet.${NC}"
|
|
3425
|
+
echo "Open manually when ready: $url"
|
|
3369
3426
|
fi
|
|
3370
3427
|
fi
|
|
3371
3428
|
}
|
|
@@ -3388,13 +3445,20 @@ cmd_web_stop() {
|
|
|
3388
3445
|
local pid
|
|
3389
3446
|
pid=$(cat "$pid_file" 2>/dev/null)
|
|
3390
3447
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3448
|
+
# Verify the PID belongs to a Purple Lab/Python process (PID recycling guard)
|
|
3449
|
+
local pid_cmd
|
|
3450
|
+
pid_cmd=$(ps -p "$pid" -o comm= 2>/dev/null || true)
|
|
3451
|
+
if [[ "$pid_cmd" == *python* ]] || [[ "$pid_cmd" == *uvicorn* ]] || [[ "$pid_cmd" == *Purple* ]]; then
|
|
3452
|
+
kill "$pid" 2>/dev/null
|
|
3453
|
+
sleep 1
|
|
3454
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
3455
|
+
kill -9 "$pid" 2>/dev/null
|
|
3456
|
+
fi
|
|
3457
|
+
echo -e "${GREEN}Purple Lab stopped (PID: $pid)${NC}"
|
|
3458
|
+
stopped=true
|
|
3459
|
+
else
|
|
3460
|
+
echo -e "${YELLOW}PID $pid is not a Purple Lab process (found: $pid_cmd), skipping${NC}"
|
|
3395
3461
|
fi
|
|
3396
|
-
echo -e "${GREEN}Purple Lab stopped (PID: $pid)${NC}"
|
|
3397
|
-
stopped=true
|
|
3398
3462
|
fi
|
|
3399
3463
|
rm -f "$PURPLE_LAB_PID_FILE"
|
|
3400
3464
|
fi
|
|
@@ -3513,7 +3577,13 @@ cmd_web_stop() {
|
|
|
3513
3577
|
|
|
3514
3578
|
cmd_web_status() {
|
|
3515
3579
|
local port
|
|
3516
|
-
port=$(cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")
|
|
3580
|
+
port=$(cat "${PURPLE_LAB_STATE_DIR}/port" 2>/dev/null || cat "${LOKI_DIR}/purple-lab/port" 2>/dev/null || echo "$PURPLE_LAB_DEFAULT_PORT")
|
|
3581
|
+
|
|
3582
|
+
# Resolve log file path (check home-based state dir first, then CWD-based)
|
|
3583
|
+
local log_path="${PURPLE_LAB_STATE_DIR}/logs/purple-lab.log"
|
|
3584
|
+
if [ ! -f "$log_path" ]; then
|
|
3585
|
+
log_path="${LOKI_DIR}/purple-lab/logs/purple-lab.log"
|
|
3586
|
+
fi
|
|
3517
3587
|
|
|
3518
3588
|
# Check PID file
|
|
3519
3589
|
if [ -f "$PURPLE_LAB_PID_FILE" ]; then
|
|
@@ -3523,7 +3593,7 @@ cmd_web_status() {
|
|
|
3523
3593
|
echo -e "${GREEN}Purple Lab is running${NC}"
|
|
3524
3594
|
echo " PID: $pid"
|
|
3525
3595
|
echo " URL: http://${PURPLE_LAB_DEFAULT_HOST}:${port}"
|
|
3526
|
-
echo " Logs: $
|
|
3596
|
+
echo " Logs: $log_path"
|
|
3527
3597
|
return 0
|
|
3528
3598
|
fi
|
|
3529
3599
|
rm -f "$PURPLE_LAB_PID_FILE"
|
|
@@ -5031,6 +5101,22 @@ cmd_export() {
|
|
|
5031
5101
|
esac
|
|
5032
5102
|
}
|
|
5033
5103
|
|
|
5104
|
+
# Guard: check if output file exists and warn before overwriting
|
|
5105
|
+
_export_check_overwrite() {
|
|
5106
|
+
local output="$1"
|
|
5107
|
+
if [ -n "$output" ] && [ -f "$output" ]; then
|
|
5108
|
+
echo -e "${YELLOW}Warning: File already exists: $output${NC}"
|
|
5109
|
+
echo -n "Overwrite? [y/N] "
|
|
5110
|
+
local reply
|
|
5111
|
+
read -r reply
|
|
5112
|
+
case "$reply" in
|
|
5113
|
+
[yY]|[yY][eE][sS]) return 0 ;;
|
|
5114
|
+
*) echo "Export cancelled."; return 1 ;;
|
|
5115
|
+
esac
|
|
5116
|
+
fi
|
|
5117
|
+
return 0
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5034
5120
|
_export_json() {
|
|
5035
5121
|
local output="$1"
|
|
5036
5122
|
|
|
@@ -5102,6 +5188,7 @@ EXPORT_JSON
|
|
|
5102
5188
|
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
5103
5189
|
return 1
|
|
5104
5190
|
fi
|
|
5191
|
+
_export_check_overwrite "$output" || return 0
|
|
5105
5192
|
echo "$json_output" > "$output"
|
|
5106
5193
|
echo -e "${GREEN}Exported to $output${NC}"
|
|
5107
5194
|
else
|
|
@@ -5149,6 +5236,7 @@ MDEOF
|
|
|
5149
5236
|
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
5150
5237
|
return 1
|
|
5151
5238
|
fi
|
|
5239
|
+
_export_check_overwrite "$output" || return 0
|
|
5152
5240
|
echo "$md_output" > "$output"
|
|
5153
5241
|
echo -e "${GREEN}Exported to $output${NC}"
|
|
5154
5242
|
else
|
|
@@ -5199,6 +5287,7 @@ EXPORT_CSV
|
|
|
5199
5287
|
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
5200
5288
|
return 1
|
|
5201
5289
|
fi
|
|
5290
|
+
_export_check_overwrite "$output" || return 0
|
|
5202
5291
|
echo "$csv_output" > "$output"
|
|
5203
5292
|
echo -e "${GREEN}Exported to $output${NC}"
|
|
5204
5293
|
else
|
|
@@ -5252,6 +5341,7 @@ EXPORT_TIMELINE
|
|
|
5252
5341
|
echo -e "${RED}Error: Output path must not contain '..'${NC}"
|
|
5253
5342
|
return 1
|
|
5254
5343
|
fi
|
|
5344
|
+
_export_check_overwrite "$output" || return 0
|
|
5255
5345
|
echo "$timeline_output" > "$output"
|
|
5256
5346
|
echo -e "${GREEN}Exported to $output${NC}"
|
|
5257
5347
|
else
|
|
@@ -5399,7 +5489,15 @@ cmd_config_set() {
|
|
|
5399
5489
|
fi
|
|
5400
5490
|
;;
|
|
5401
5491
|
*)
|
|
5402
|
-
echo -e "${
|
|
5492
|
+
echo -e "${RED}Unknown configuration key: '$key'${NC}"
|
|
5493
|
+
echo ""
|
|
5494
|
+
echo "Valid keys: maxTier, provider, issue.provider, blind_validation,"
|
|
5495
|
+
echo " adversarial_testing, spawn_timeout, spawn_retries, budget,"
|
|
5496
|
+
echo " model.planning, model.development, model.fast,"
|
|
5497
|
+
echo " notify.slack, notify.discord"
|
|
5498
|
+
echo ""
|
|
5499
|
+
echo "Run 'loki config' for details on each key."
|
|
5500
|
+
return 1
|
|
5403
5501
|
;;
|
|
5404
5502
|
esac
|
|
5405
5503
|
|
|
@@ -5470,7 +5568,8 @@ cmd_config_get() {
|
|
|
5470
5568
|
return 0
|
|
5471
5569
|
fi
|
|
5472
5570
|
|
|
5473
|
-
|
|
5571
|
+
local result
|
|
5572
|
+
result=$(LOKI_CFG_FILE="$config_store" LOKI_CFG_KEY="$key" \
|
|
5474
5573
|
python3 << 'GET_CONFIG'
|
|
5475
5574
|
import json, os
|
|
5476
5575
|
cfg_file = os.environ["LOKI_CFG_FILE"]
|
|
@@ -5490,7 +5589,11 @@ for part in parts:
|
|
|
5490
5589
|
|
|
5491
5590
|
print(current if not isinstance(current, dict) else json.dumps(current, indent=2))
|
|
5492
5591
|
GET_CONFIG
|
|
5493
|
-
|
|
5592
|
+
) 2>/dev/null || {
|
|
5593
|
+
echo -e "${RED}Error reading config key '$key'${NC}"
|
|
5594
|
+
return 1
|
|
5595
|
+
}
|
|
5596
|
+
echo "$result"
|
|
5494
5597
|
}
|
|
5495
5598
|
|
|
5496
5599
|
cmd_config_show() {
|
|
@@ -5873,6 +5976,40 @@ cmd_doctor() {
|
|
|
5873
5976
|
doctor_check "Gemini CLI" gemini optional || true
|
|
5874
5977
|
doctor_check "Cline CLI" cline optional || true
|
|
5875
5978
|
doctor_check "Aider CLI" aider optional || true
|
|
5979
|
+
|
|
5980
|
+
# Check if at least one provider is installed
|
|
5981
|
+
local _any_provider=false
|
|
5982
|
+
for _dp in claude codex gemini cline aider; do
|
|
5983
|
+
command -v "$_dp" &>/dev/null && _any_provider=true && break
|
|
5984
|
+
done
|
|
5985
|
+
if ! $_any_provider; then
|
|
5986
|
+
echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
|
|
5987
|
+
echo -e " ${YELLOW}Install: npm install -g @anthropic-ai/claude-code${NC}"
|
|
5988
|
+
fail_count=$((fail_count + 1))
|
|
5989
|
+
fi
|
|
5990
|
+
echo ""
|
|
5991
|
+
|
|
5992
|
+
echo -e "${CYAN}API Keys:${NC}"
|
|
5993
|
+
# Note: CLI tools use their own login sessions outside Docker/K8s.
|
|
5994
|
+
# This section helps first-time users verify they can authenticate.
|
|
5995
|
+
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
5996
|
+
echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
|
|
5997
|
+
pass_count=$((pass_count + 1))
|
|
5998
|
+
elif command -v claude &>/dev/null; then
|
|
5999
|
+
echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
|
|
6000
|
+
fi
|
|
6001
|
+
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
6002
|
+
echo -e " ${GREEN}PASS${NC} OPENAI_API_KEY is set"
|
|
6003
|
+
pass_count=$((pass_count + 1))
|
|
6004
|
+
elif command -v codex &>/dev/null; then
|
|
6005
|
+
echo -e " ${DIM} -- ${NC} OPENAI_API_KEY not set (Codex CLI uses its own login)"
|
|
6006
|
+
fi
|
|
6007
|
+
if [ -n "${GOOGLE_API_KEY:-${GEMINI_API_KEY:-}}" ]; then
|
|
6008
|
+
echo -e " ${GREEN}PASS${NC} GOOGLE_API_KEY is set"
|
|
6009
|
+
pass_count=$((pass_count + 1))
|
|
6010
|
+
elif command -v gemini &>/dev/null; then
|
|
6011
|
+
echo -e " ${DIM} -- ${NC} GOOGLE_API_KEY not set (Gemini CLI uses its own login)"
|
|
6012
|
+
fi
|
|
5876
6013
|
echo ""
|
|
5877
6014
|
|
|
5878
6015
|
echo -e "${CYAN}Skills:${NC}"
|
|
@@ -6187,7 +6324,37 @@ for name, info in result.items():
|
|
|
6187
6324
|
|
|
6188
6325
|
# Show recent logs
|
|
6189
6326
|
cmd_logs() {
|
|
6190
|
-
local lines="
|
|
6327
|
+
local lines="50"
|
|
6328
|
+
local follow=false
|
|
6329
|
+
|
|
6330
|
+
# Parse arguments
|
|
6331
|
+
while [[ $# -gt 0 ]]; do
|
|
6332
|
+
case "$1" in
|
|
6333
|
+
--tail|-n) lines="$2"; shift 2 ;;
|
|
6334
|
+
--tail=*) lines="${1#--tail=}"; shift ;;
|
|
6335
|
+
-n=*) lines="${1#-n=}"; shift ;;
|
|
6336
|
+
--follow|-f) follow=true; shift ;;
|
|
6337
|
+
--all|-a) lines="+1"; shift ;;
|
|
6338
|
+
--help|-h)
|
|
6339
|
+
echo "Usage: loki logs [options]"
|
|
6340
|
+
echo ""
|
|
6341
|
+
echo "Options:"
|
|
6342
|
+
echo " --tail, -n N Show last N lines (default: 50)"
|
|
6343
|
+
echo " --all, -a Show all lines"
|
|
6344
|
+
echo " --follow, -f Follow log output (like tail -f)"
|
|
6345
|
+
echo " --help, -h Show this help"
|
|
6346
|
+
return 0
|
|
6347
|
+
;;
|
|
6348
|
+
*)
|
|
6349
|
+
# Legacy: treat plain number as line count
|
|
6350
|
+
if [[ "$1" =~ ^[0-9]+$ ]]; then
|
|
6351
|
+
lines="$1"
|
|
6352
|
+
fi
|
|
6353
|
+
shift
|
|
6354
|
+
;;
|
|
6355
|
+
esac
|
|
6356
|
+
done
|
|
6357
|
+
|
|
6191
6358
|
local log_file="$LOKI_DIR/logs/session.log"
|
|
6192
6359
|
|
|
6193
6360
|
if [ ! -f "$log_file" ]; then
|
|
@@ -6195,9 +6362,16 @@ cmd_logs() {
|
|
|
6195
6362
|
exit 0
|
|
6196
6363
|
fi
|
|
6197
6364
|
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6365
|
+
if [ "$follow" = true ]; then
|
|
6366
|
+
echo -e "${BOLD}Following logs (Ctrl+C to stop)${NC}"
|
|
6367
|
+
echo ""
|
|
6368
|
+
tail -f "$log_file"
|
|
6369
|
+
else
|
|
6370
|
+
echo -e "${BOLD}Recent Logs (last $lines lines)${NC}"
|
|
6371
|
+
echo -e "${DIM}Use --all to show all lines, --follow to stream${NC}"
|
|
6372
|
+
echo ""
|
|
6373
|
+
tail -n "$lines" "$log_file"
|
|
6374
|
+
fi
|
|
6201
6375
|
}
|
|
6202
6376
|
|
|
6203
6377
|
# API server management (delegates to unified FastAPI dashboard server)
|
|
@@ -6997,8 +7171,10 @@ cmd_quick() {
|
|
|
6997
7171
|
echo ""
|
|
6998
7172
|
|
|
6999
7173
|
# Create quick PRD from task description
|
|
7174
|
+
# BUG-PU-005: Use unique filename to prevent race conditions when
|
|
7175
|
+
# multiple simultaneous `loki quick` commands run in the same project
|
|
7000
7176
|
mkdir -p "$LOKI_DIR"
|
|
7001
|
-
local quick_prd="$LOKI_DIR/quick-prd
|
|
7177
|
+
local quick_prd="$LOKI_DIR/quick-prd-$$.md"
|
|
7002
7178
|
cat > "$quick_prd" << QPRDEOF
|
|
7003
7179
|
# Quick Task
|
|
7004
7180
|
|
|
@@ -7046,6 +7222,25 @@ QPRDEOF
|
|
|
7046
7222
|
# Emit event
|
|
7047
7223
|
emit_event session cli quick_start "task=$(echo "$task_desc" | head -c 100)"
|
|
7048
7224
|
|
|
7225
|
+
# Pre-flight: check that the provider CLI is installed
|
|
7226
|
+
local _quick_provider="${LOKI_PROVIDER:-claude}"
|
|
7227
|
+
if ! command -v "$_quick_provider" &>/dev/null; then
|
|
7228
|
+
echo -e "${RED}Error: Provider CLI '$_quick_provider' is not installed.${NC}"
|
|
7229
|
+
echo ""
|
|
7230
|
+
echo "Install your AI provider CLI first:"
|
|
7231
|
+
case "$_quick_provider" in
|
|
7232
|
+
claude) echo " npm install -g @anthropic-ai/claude-code" ;;
|
|
7233
|
+
codex) echo " npm install -g @openai/codex" ;;
|
|
7234
|
+
gemini) echo " npm install -g @google/gemini-cli" ;;
|
|
7235
|
+
cline) echo " npm install -g @anthropic-ai/cline" ;;
|
|
7236
|
+
aider) echo " pip install aider-chat" ;;
|
|
7237
|
+
*) echo " Check the provider documentation for installation." ;;
|
|
7238
|
+
esac
|
|
7239
|
+
echo ""
|
|
7240
|
+
echo "Then verify: loki doctor"
|
|
7241
|
+
exit 1
|
|
7242
|
+
fi
|
|
7243
|
+
|
|
7049
7244
|
# Run the orchestrator with quick settings
|
|
7050
7245
|
exec "$RUN_SH" "$quick_prd"
|
|
7051
7246
|
}
|
|
@@ -7364,7 +7559,7 @@ cmd_init() {
|
|
|
7364
7559
|
local list_mode=false
|
|
7365
7560
|
local json_mode=false
|
|
7366
7561
|
|
|
7367
|
-
#
|
|
7562
|
+
# 21 built-in template names (order: simple, standard, complex)
|
|
7368
7563
|
local TEMPLATE_NAMES=(
|
|
7369
7564
|
simple-todo-app
|
|
7370
7565
|
static-landing-page
|
|
@@ -7384,7 +7579,6 @@ cmd_init() {
|
|
|
7384
7579
|
npm-library
|
|
7385
7580
|
microservice
|
|
7386
7581
|
mobile-app
|
|
7387
|
-
saas-app
|
|
7388
7582
|
saas-starter
|
|
7389
7583
|
e-commerce
|
|
7390
7584
|
ai-chatbot
|
|
@@ -7411,7 +7605,6 @@ cmd_init() {
|
|
|
7411
7605
|
npm-library) echo "npm Library" ;;
|
|
7412
7606
|
microservice) echo "Microservice" ;;
|
|
7413
7607
|
mobile-app) echo "Mobile App" ;;
|
|
7414
|
-
saas-app) echo "SaaS Application" ;;
|
|
7415
7608
|
saas-starter) echo "SaaS Starter Kit" ;;
|
|
7416
7609
|
e-commerce) echo "E-Commerce Store" ;;
|
|
7417
7610
|
ai-chatbot) echo "AI Chatbot (RAG)" ;;
|
|
@@ -7469,7 +7662,7 @@ cmd_init() {
|
|
|
7469
7662
|
echo " .gitignore Git ignore rules (with git init)"
|
|
7470
7663
|
echo ""
|
|
7471
7664
|
echo "Options:"
|
|
7472
|
-
echo " --template, -t TYPE Template name (e.g., saas-
|
|
7665
|
+
echo " --template, -t TYPE Template name (e.g., saas-starter, cli-tool)"
|
|
7473
7666
|
echo " --no-git Skip git init"
|
|
7474
7667
|
echo " --stdout Print PRD to stdout instead of writing files"
|
|
7475
7668
|
echo " --list List all available templates"
|
|
@@ -7478,7 +7671,7 @@ cmd_init() {
|
|
|
7478
7671
|
echo " --help, -h Show this help"
|
|
7479
7672
|
echo ""
|
|
7480
7673
|
echo "Examples:"
|
|
7481
|
-
echo " loki init my-saas --template saas-
|
|
7674
|
+
echo " loki init my-saas --template saas-starter Create my-saas/ with SaaS PRD"
|
|
7482
7675
|
echo " loki init --template cli-tool Scaffold current dir with CLI PRD"
|
|
7483
7676
|
echo " loki init my-app Create my-app/ (prompts for template)"
|
|
7484
7677
|
echo " loki init --list Show all $template_count templates"
|
|
@@ -7748,11 +7941,17 @@ ENDGITIGNORE
|
|
|
7748
7941
|
echo -e "${RED}Directory already exists: $target_dir${NC}"
|
|
7749
7942
|
exit 1
|
|
7750
7943
|
fi
|
|
7751
|
-
mkdir -p "$target_dir"
|
|
7944
|
+
if ! mkdir -p "$target_dir"; then
|
|
7945
|
+
echo -e "${RED}Failed to create directory: $target_dir${NC}"
|
|
7946
|
+
exit 1
|
|
7947
|
+
fi
|
|
7752
7948
|
fi
|
|
7753
7949
|
|
|
7754
7950
|
# Create .loki config directory
|
|
7755
|
-
mkdir -p "$target_dir/.loki"
|
|
7951
|
+
if ! mkdir -p "$target_dir/.loki"; then
|
|
7952
|
+
echo -e "${RED}Failed to create .loki directory in: $target_dir${NC}"
|
|
7953
|
+
exit 1
|
|
7954
|
+
fi
|
|
7756
7955
|
|
|
7757
7956
|
# Write files
|
|
7758
7957
|
echo "$prd_content" > "$target_dir/prd.md"
|
|
@@ -7799,6 +7998,25 @@ ENDGITIGNORE
|
|
|
7799
7998
|
echo -e " 1. Review and edit: ${BOLD}prd.md${NC}"
|
|
7800
7999
|
echo -e " 2. Run: ${BOLD}loki start prd.md${NC}"
|
|
7801
8000
|
fi
|
|
8001
|
+
|
|
8002
|
+
# Check if an AI provider CLI is available
|
|
8003
|
+
local _has_provider=false
|
|
8004
|
+
for _pcli in claude codex gemini cline aider; do
|
|
8005
|
+
if command -v "$_pcli" &>/dev/null; then
|
|
8006
|
+
_has_provider=true
|
|
8007
|
+
break
|
|
8008
|
+
fi
|
|
8009
|
+
done
|
|
8010
|
+
if ! $_has_provider; then
|
|
8011
|
+
echo ""
|
|
8012
|
+
echo -e "${YELLOW}Note: No AI provider CLI detected.${NC}"
|
|
8013
|
+
echo " Install at least one before running 'loki start':"
|
|
8014
|
+
echo " npm install -g @anthropic-ai/claude-code (recommended)"
|
|
8015
|
+
echo " npm install -g @openai/codex"
|
|
8016
|
+
echo " npm install -g @google/gemini-cli"
|
|
8017
|
+
echo ""
|
|
8018
|
+
echo " Then verify your setup: ${BOLD}loki doctor${NC}"
|
|
8019
|
+
fi
|
|
7802
8020
|
}
|
|
7803
8021
|
|
|
7804
8022
|
# Dogfooding statistics
|
|
@@ -9166,8 +9384,8 @@ for f in data.get('frictions', []):
|
|
|
9166
9384
|
fi
|
|
9167
9385
|
echo -e "${BOLD}Healing Report${NC}"
|
|
9168
9386
|
echo ""
|
|
9169
|
-
echo " Friction map: $(python3 -c "import json; print(len(json.load(open(
|
|
9170
|
-
echo " Failure modes: $(python3 -c "import json; print(len(json.load(open(
|
|
9387
|
+
echo " Friction map: $(_HEAL_FILE="$heal_dir/friction-map.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('frictions', [])))" 2>/dev/null || echo '0') points"
|
|
9388
|
+
echo " Failure modes: $(_HEAL_FILE="$heal_dir/failure-modes.json" python3 -c "import json, os; print(len(json.load(open(os.environ['_HEAL_FILE'])).get('modes', [])))" 2>/dev/null || echo '0') cataloged"
|
|
9171
9389
|
echo " Institutional knowledge: $(wc -l < "$heal_dir/institutional-knowledge.md" 2>/dev/null || echo '0') lines"
|
|
9172
9390
|
echo " Characterization tests: $(find "$heal_dir/characterization-tests/" -name "*.json" 2>/dev/null | wc -l | tr -d ' ') tests"
|
|
9173
9391
|
echo ""
|
|
@@ -9200,6 +9418,21 @@ for f in data.get('frictions', []):
|
|
|
9200
9418
|
local heal_dir="$codebase_path/.loki/healing"
|
|
9201
9419
|
mkdir -p "$heal_dir"/{behavioral-baseline,characterization-tests}
|
|
9202
9420
|
|
|
9421
|
+
# BUG-HEAL-004: Source migration hooks for healing enforcement
|
|
9422
|
+
local hooks_file
|
|
9423
|
+
hooks_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hooks/migration-hooks.sh"
|
|
9424
|
+
if [[ -f "$hooks_file" ]]; then
|
|
9425
|
+
# shellcheck source=hooks/migration-hooks.sh
|
|
9426
|
+
source "$hooks_file"
|
|
9427
|
+
load_migration_hook_config "$codebase_path"
|
|
9428
|
+
fi
|
|
9429
|
+
|
|
9430
|
+
# Export healing environment variables for hooks
|
|
9431
|
+
export LOKI_HEAL_MODE="true"
|
|
9432
|
+
export LOKI_HEAL_PHASE="$phase"
|
|
9433
|
+
export LOKI_HEAL_STRICT="$strict"
|
|
9434
|
+
export LOKI_CODEBASE_PATH="$codebase_path"
|
|
9435
|
+
|
|
9203
9436
|
# Initialize healing state files if they don't exist
|
|
9204
9437
|
[[ ! -f "$heal_dir/friction-map.json" ]] && echo '{"frictions":[]}' > "$heal_dir/friction-map.json"
|
|
9205
9438
|
[[ ! -f "$heal_dir/failure-modes.json" ]] && echo '{"modes":[]}' > "$heal_dir/failure-modes.json"
|
|
@@ -9223,6 +9456,20 @@ with open('$heal_dir/healing-progress.json', 'w') as f:
|
|
|
9223
9456
|
" || true
|
|
9224
9457
|
fi
|
|
9225
9458
|
|
|
9459
|
+
# BUG-HEAL-004: Validate phase gate when resuming from a previous phase
|
|
9460
|
+
if [ "$do_resume" = "true" ] && [[ -f "$heal_dir/healing-progress.json" ]] && type hook_healing_phase_gate &>/dev/null; then
|
|
9461
|
+
local prev_phase
|
|
9462
|
+
prev_phase=$(python3 -c "import json; print(json.load(open('$heal_dir/healing-progress.json')).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
|
|
9463
|
+
if [[ "$prev_phase" != "$phase" ]]; then
|
|
9464
|
+
local gate_result
|
|
9465
|
+
if ! gate_result=$(hook_healing_phase_gate "$prev_phase" "$phase" 2>&1); then
|
|
9466
|
+
echo -e "${RED}Phase gate check failed:${NC}"
|
|
9467
|
+
echo " $gate_result"
|
|
9468
|
+
return 1
|
|
9469
|
+
fi
|
|
9470
|
+
fi
|
|
9471
|
+
fi
|
|
9472
|
+
|
|
9226
9473
|
emit_event healing cli start "phase=$phase" "codebase=$codebase_path" "strict=$strict" 2>/dev/null || true
|
|
9227
9474
|
|
|
9228
9475
|
echo -e "${BOLD}Legacy System Healing${NC}"
|
|
@@ -9329,6 +9576,12 @@ except Exception: pass
|
|
|
9329
9576
|
# shellcheck disable=SC2086
|
|
9330
9577
|
(cd "$codebase_path" && aider --message "$heal_prompt" --yes-always --no-auto-commits --model "$aider_model" $aider_flags 2>&1) || heal_exit=$?
|
|
9331
9578
|
;;
|
|
9579
|
+
# BUG-HEAL-003: Unknown provider should error, not silently succeed
|
|
9580
|
+
*)
|
|
9581
|
+
echo -e "${RED}Error: Unknown provider: $provider${NC}"
|
|
9582
|
+
echo "Supported providers: claude, codex, gemini, cline, aider"
|
|
9583
|
+
return 1
|
|
9584
|
+
;;
|
|
9332
9585
|
esac
|
|
9333
9586
|
|
|
9334
9587
|
emit_event healing cli complete "phase=$phase" "exit=$heal_exit" 2>/dev/null || true
|
|
@@ -9852,7 +10105,7 @@ print(json.loads(p.read_text()).get('port', 7373) if p.exists() else 7373)
|
|
|
9852
10105
|
local sched_file=".loki/triggers/schedules.json"
|
|
9853
10106
|
if [[ -f "$sched_file" ]]; then
|
|
9854
10107
|
local count
|
|
9855
|
-
count=$(python3 -c "import json; d=json.load(open('
|
|
10108
|
+
count=$(_SCHED_FILE="$sched_file" python3 -c "import json, os; d=json.load(open(os.environ['_SCHED_FILE'])); print(len(d) if isinstance(d,list) else len(d.get('schedules',[])))" 2>/dev/null || echo "?")
|
|
9856
10109
|
echo "Schedules configured: $count"
|
|
9857
10110
|
else
|
|
9858
10111
|
echo "Schedules configured: 0"
|
|
@@ -10165,7 +10418,7 @@ with open('$failover_file', 'w') as f: json.dump(d, f, indent=2)
|
|
|
10165
10418
|
|
|
10166
10419
|
local chain_providers="claude,codex,gemini"
|
|
10167
10420
|
if [ -f "$failover_file" ]; then
|
|
10168
|
-
chain_providers=$(python3 -c "import json; print(','.join(json.load(open('
|
|
10421
|
+
chain_providers=$(_FAILOVER_FILE="$failover_file" python3 -c "import json, os; print(','.join(json.load(open(os.environ['_FAILOVER_FILE'])).get('chain', ['claude','codex','gemini'])))" 2>/dev/null || echo "claude,codex,gemini")
|
|
10169
10422
|
fi
|
|
10170
10423
|
|
|
10171
10424
|
local IFS=','
|
|
@@ -11477,9 +11730,10 @@ cmd_worktree() {
|
|
|
11477
11730
|
local status="running"
|
|
11478
11731
|
local stream_name=""
|
|
11479
11732
|
|
|
11480
|
-
# Check for merge signal
|
|
11481
|
-
if [[ "$branch" == loki-parallel-* ]]; then
|
|
11482
|
-
stream_name="${branch#
|
|
11733
|
+
# Check for merge signal (branches named parallel-<stream> by run.sh)
|
|
11734
|
+
if [[ "$branch" == parallel-* ]] || [[ "$branch" == loki-parallel-* ]]; then
|
|
11735
|
+
stream_name="${branch#parallel-}"
|
|
11736
|
+
stream_name="${stream_name#loki-}"
|
|
11483
11737
|
if [ -f ".loki/signals/MERGE_REQUESTED_${stream_name}" ]; then
|
|
11484
11738
|
status="merge-ready"
|
|
11485
11739
|
elif [ -f ".loki/signals/WORKTREE_FAILED_${stream_name}" ]; then
|
|
@@ -11536,13 +11790,27 @@ except Exception as e:
|
|
|
11536
11790
|
MERGE_VALIDATE_PY
|
|
11537
11791
|
then
|
|
11538
11792
|
echo -e "${RED}Invalid or corrupted merge signal file${NC}"
|
|
11539
|
-
|
|
11793
|
+
return 1
|
|
11540
11794
|
fi
|
|
11541
11795
|
|
|
11542
11796
|
local branch
|
|
11543
|
-
branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])")
|
|
11797
|
+
branch=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['branch'])" 2>/dev/null)
|
|
11544
11798
|
local worktree_path
|
|
11545
|
-
worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])")
|
|
11799
|
+
worktree_path=$(LOKI_SIGNAL_FILE="$signal_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_SIGNAL_FILE']))['worktree'])" 2>/dev/null)
|
|
11800
|
+
|
|
11801
|
+
# Validate branch was extracted successfully
|
|
11802
|
+
if [ -z "$branch" ]; then
|
|
11803
|
+
echo -e "${RED}Could not extract branch name from merge signal${NC}"
|
|
11804
|
+
return 1
|
|
11805
|
+
fi
|
|
11806
|
+
|
|
11807
|
+
# Verify branch exists before attempting merge
|
|
11808
|
+
if ! git rev-parse --verify "$branch" &>/dev/null; then
|
|
11809
|
+
echo -e "${RED}Branch '$branch' does not exist${NC}"
|
|
11810
|
+
echo "The worktree may have been cleaned up already."
|
|
11811
|
+
rm -f "$signal_file"
|
|
11812
|
+
return 1
|
|
11813
|
+
fi
|
|
11546
11814
|
|
|
11547
11815
|
if git merge --no-ff "$branch" -m "merge($name): auto-merge from parallel worktree"; then
|
|
11548
11816
|
echo -e "${GREEN}Merge successful${NC}"
|
|
@@ -11563,14 +11831,50 @@ MERGE_VALIDATE_PY
|
|
|
11563
11831
|
local removed=0
|
|
11564
11832
|
local main_wt
|
|
11565
11833
|
main_wt=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
|
|
11566
|
-
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11573
|
-
|
|
11834
|
+
# BUG-PU-001: Track branches to clean up after worktree removal
|
|
11835
|
+
local branches_to_delete=()
|
|
11836
|
+
local current_wt_clean=""
|
|
11837
|
+
local current_branch_clean=""
|
|
11838
|
+
while IFS= read -r line; do
|
|
11839
|
+
case "$line" in
|
|
11840
|
+
"worktree "*)
|
|
11841
|
+
current_wt_clean="${line#worktree }"
|
|
11842
|
+
;;
|
|
11843
|
+
"branch "*)
|
|
11844
|
+
current_branch_clean="${line#branch refs/heads/}"
|
|
11845
|
+
if [ -n "$current_wt_clean" ] && [ "$current_wt_clean" != "$main_wt" ]; then
|
|
11846
|
+
# Kill any running session in this worktree before removing
|
|
11847
|
+
local wt_pid_file="$current_wt_clean/.loki/loki.pid"
|
|
11848
|
+
if [ -f "$wt_pid_file" ]; then
|
|
11849
|
+
local wt_pid
|
|
11850
|
+
wt_pid=$(cat "$wt_pid_file" 2>/dev/null)
|
|
11851
|
+
if [ -n "$wt_pid" ] && kill -0 "$wt_pid" 2>/dev/null; then
|
|
11852
|
+
echo " Stopping session in $current_wt_clean (PID: $wt_pid)..."
|
|
11853
|
+
kill "$wt_pid" 2>/dev/null || true
|
|
11854
|
+
sleep 1
|
|
11855
|
+
kill -0 "$wt_pid" 2>/dev/null && kill -9 "$wt_pid" 2>/dev/null || true
|
|
11856
|
+
fi
|
|
11857
|
+
fi
|
|
11858
|
+
echo " Removing: $current_wt_clean"
|
|
11859
|
+
git worktree remove "$current_wt_clean" --force 2>/dev/null || true
|
|
11860
|
+
removed=$((removed + 1))
|
|
11861
|
+
# Queue branch for deletion (can only delete after worktree is gone)
|
|
11862
|
+
if [[ "$current_branch_clean" == loki-parallel-* ]] || \
|
|
11863
|
+
[[ "$current_branch_clean" == parallel-* ]]; then
|
|
11864
|
+
branches_to_delete+=("$current_branch_clean")
|
|
11865
|
+
fi
|
|
11866
|
+
fi
|
|
11867
|
+
current_wt_clean=""
|
|
11868
|
+
current_branch_clean=""
|
|
11869
|
+
;;
|
|
11870
|
+
esac
|
|
11871
|
+
done <<< "$(git worktree list --porcelain 2>/dev/null)"
|
|
11872
|
+
# Prune worktree metadata for any already-removed directories
|
|
11873
|
+
git worktree prune 2>/dev/null || true
|
|
11874
|
+
# Clean up orphaned parallel branches
|
|
11875
|
+
for branch in "${branches_to_delete[@]}"; do
|
|
11876
|
+
git branch -D "$branch" 2>/dev/null || true
|
|
11877
|
+
done
|
|
11574
11878
|
rm -f .loki/signals/MERGE_REQUESTED_* .loki/signals/WORKTREE_FAILED_* 2>/dev/null || true
|
|
11575
11879
|
echo -e "${GREEN}Cleanup complete ($removed worktrees removed)${NC}"
|
|
11576
11880
|
;;
|
|
@@ -11755,7 +12059,7 @@ cmd_audit() {
|
|
|
11755
12059
|
if [ ! -f "$audit_file" ]; then
|
|
11756
12060
|
echo -e "${YELLOW}No audit log found at $audit_file${NC}"
|
|
11757
12061
|
echo "Agent action auditing records entries during loki sessions."
|
|
11758
|
-
|
|
12062
|
+
return 0
|
|
11759
12063
|
fi
|
|
11760
12064
|
local lines="${2:-50}"
|
|
11761
12065
|
echo -e "${BOLD}Agent Audit Log${NC} (last $lines entries)"
|
|
@@ -11781,7 +12085,7 @@ except: print(f' {sys.argv[1]}')
|
|
|
11781
12085
|
count)
|
|
11782
12086
|
if [ ! -f "$audit_file" ]; then
|
|
11783
12087
|
echo -e "${YELLOW}No audit log found${NC}"
|
|
11784
|
-
|
|
12088
|
+
return 0
|
|
11785
12089
|
fi
|
|
11786
12090
|
echo -e "${BOLD}Agent Action Counts${NC}"
|
|
11787
12091
|
echo "---"
|
|
@@ -11823,15 +12127,15 @@ print(f' {\"TOTAL\":25s} {sum(counts.values())}')
|
|
|
11823
12127
|
echo " --preset NAME Compliance preset (default|healthcare|fintech|government)"
|
|
11824
12128
|
echo " --export Save report to .loki/quality/report-{date}.json"
|
|
11825
12129
|
echo ""
|
|
11826
|
-
|
|
12130
|
+
return 0
|
|
11827
12131
|
;;
|
|
11828
|
-
*) echo -e "${RED}Unknown option: $1${NC}";
|
|
12132
|
+
*) echo -e "${RED}Unknown option: $1${NC}"; return 1 ;;
|
|
11829
12133
|
esac
|
|
11830
12134
|
done
|
|
11831
12135
|
|
|
11832
12136
|
case "$preset" in
|
|
11833
12137
|
default|healthcare|fintech|government) ;;
|
|
11834
|
-
*) echo -e "${RED}Error: Invalid preset '$preset'. Must be one of: default, healthcare, fintech, government${NC}";
|
|
12138
|
+
*) echo -e "${RED}Error: Invalid preset '$preset'. Must be one of: default, healthcare, fintech, government${NC}"; return 1 ;;
|
|
11835
12139
|
esac
|
|
11836
12140
|
|
|
11837
12141
|
local port="${LOKI_DASHBOARD_PORT:-57374}"
|
|
@@ -11848,12 +12152,12 @@ print(f' {\"TOTAL\":25s} {sum(counts.values())}')
|
|
|
11848
12152
|
if [ -z "$http_code" ] || [ "$http_code" = "000" ]; then
|
|
11849
12153
|
echo -e "${RED}Error: Could not connect to dashboard API at http://${host}:${port}${NC}"
|
|
11850
12154
|
echo "Make sure the dashboard is running: loki serve"
|
|
11851
|
-
|
|
12155
|
+
return 1
|
|
11852
12156
|
fi
|
|
11853
12157
|
if [ "$http_code" -ge 400 ] 2>/dev/null; then
|
|
11854
12158
|
echo -e "${RED}Error: Dashboard API returned HTTP $http_code${NC}"
|
|
11855
12159
|
[ -n "$response" ] && echo "$response"
|
|
11856
|
-
|
|
12160
|
+
return 1
|
|
11857
12161
|
fi
|
|
11858
12162
|
|
|
11859
12163
|
if ! command -v python3 &>/dev/null; then
|
|
@@ -12294,7 +12598,7 @@ if count == 0:
|
|
|
12294
12598
|
*)
|
|
12295
12599
|
echo -e "${RED}Unknown type: $type${NC}"
|
|
12296
12600
|
echo "Valid types: patterns, mistakes, successes, all"
|
|
12297
|
-
|
|
12601
|
+
return 1
|
|
12298
12602
|
;;
|
|
12299
12603
|
esac
|
|
12300
12604
|
;;
|
|
@@ -12317,11 +12621,13 @@ if count == 0:
|
|
|
12317
12621
|
_search_py="python3.12"
|
|
12318
12622
|
fi
|
|
12319
12623
|
|
|
12320
|
-
#
|
|
12321
|
-
|
|
12624
|
+
# BUG-PU-010: Pass query via environment variable instead of embedding
|
|
12625
|
+
# directly in heredoc to prevent shell/Python injection via triple-quotes
|
|
12626
|
+
export LOKI_MEM_QUERY="$query"
|
|
12627
|
+
$_search_py << 'PYEOF'
|
|
12322
12628
|
import os, json, sys, glob
|
|
12323
12629
|
|
|
12324
|
-
query =
|
|
12630
|
+
query = os.environ.get('LOKI_MEM_QUERY', '')
|
|
12325
12631
|
results = []
|
|
12326
12632
|
|
|
12327
12633
|
# Keyword search across all memory files
|
|
@@ -12450,7 +12756,7 @@ PYEOF
|
|
|
12450
12756
|
*)
|
|
12451
12757
|
echo -e "${RED}Unknown type: $type${NC}"
|
|
12452
12758
|
echo "Valid types: patterns, mistakes, successes"
|
|
12453
|
-
|
|
12759
|
+
return 1
|
|
12454
12760
|
;;
|
|
12455
12761
|
esac
|
|
12456
12762
|
fi
|
|
@@ -12886,7 +13192,7 @@ except Exception as e:
|
|
|
12886
13192
|
if ls -1 .loki/memory/vectors/*.npz 2>/dev/null | head -1 >/dev/null 2>&1; then
|
|
12887
13193
|
for f in .loki/memory/vectors/*.npz; do
|
|
12888
13194
|
if [ -f "$f" ]; then
|
|
12889
|
-
count=$(python3 -c "import numpy as np; d=np.load('
|
|
13195
|
+
count=$(_NPZ_FILE="$f" python3 -c "import numpy as np, os; d=np.load(os.environ['_NPZ_FILE']); print(len(d['ids']))" 2>/dev/null || echo "error")
|
|
12890
13196
|
echo " $(basename "$f"): $count vectors"
|
|
12891
13197
|
fi
|
|
12892
13198
|
done
|
|
@@ -14837,6 +15143,18 @@ cmd_telemetry() {
|
|
|
14837
15143
|
local endpoint="${LOKI_OTEL_ENDPOINT:-}"
|
|
14838
15144
|
if [ -n "$endpoint" ] && [ "$persistently_disabled" = false ]; then
|
|
14839
15145
|
echo -e " Endpoint: ${GREEN}$endpoint${NC}"
|
|
15146
|
+
# BUG-PU-004: Actually test connectivity to the endpoint instead of
|
|
15147
|
+
# failing silently when Jaeger/collector is down
|
|
15148
|
+
local health_url="${endpoint}/v1/traces"
|
|
15149
|
+
local health_code
|
|
15150
|
+
health_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$health_url" 2>/dev/null) || health_code="000"
|
|
15151
|
+
if [ "$health_code" = "000" ]; then
|
|
15152
|
+
echo -e " Reachable: ${RED}NO (connection failed - is the collector running?)${NC}"
|
|
15153
|
+
elif [ "$health_code" -ge 400 ] 2>/dev/null && [ "$health_code" -ne 405 ]; then
|
|
15154
|
+
echo -e " Reachable: ${YELLOW}HTTP $health_code (may not be accepting traces)${NC}"
|
|
15155
|
+
else
|
|
15156
|
+
echo -e " Reachable: ${GREEN}YES${NC}"
|
|
15157
|
+
fi
|
|
14840
15158
|
elif [ "$persistently_disabled" = true ]; then
|
|
14841
15159
|
echo -e " Endpoint: ${YELLOW}ignored (opted out)${NC}"
|
|
14842
15160
|
else
|
|
@@ -14861,7 +15179,7 @@ try {
|
|
|
14861
15179
|
local config_file=".loki/config.json"
|
|
14862
15180
|
if [ -f "$config_file" ]; then
|
|
14863
15181
|
local saved_endpoint
|
|
14864
|
-
saved_endpoint=$(python3 -c "import json; print(json.load(open('
|
|
15182
|
+
saved_endpoint=$(_CFG_FILE="$config_file" python3 -c "import json, os; print(json.load(open(os.environ['_CFG_FILE'])).get('otel_endpoint',''))" 2>/dev/null || echo "")
|
|
14865
15183
|
if [ -n "$saved_endpoint" ]; then
|
|
14866
15184
|
echo -e " Saved: $saved_endpoint"
|
|
14867
15185
|
fi
|
|
@@ -14886,7 +15204,9 @@ try {
|
|
|
14886
15204
|
mkdir -p .loki
|
|
14887
15205
|
local config_file=".loki/config.json"
|
|
14888
15206
|
if [ -f "$config_file" ]; then
|
|
14889
|
-
|
|
15207
|
+
# BUG-PU-011: Use separate if/then/fi to prevent python3 failure
|
|
15208
|
+
# from falling through to else and overwriting config
|
|
15209
|
+
if ! LOKI_TELEM_CFG="$config_file" LOKI_TELEM_ENDPOINT="$endpoint" \
|
|
14890
15210
|
python3 << 'TELEM_ENABLE_PY'
|
|
14891
15211
|
import json, os
|
|
14892
15212
|
cfg = os.environ['LOKI_TELEM_CFG']
|
|
@@ -14897,6 +15217,10 @@ config['otel_endpoint'] = ep
|
|
|
14897
15217
|
with open(cfg, 'w') as f:
|
|
14898
15218
|
json.dump(config, f, indent=2)
|
|
14899
15219
|
TELEM_ENABLE_PY
|
|
15220
|
+
then
|
|
15221
|
+
echo -e "${YELLOW}Warning: Could not update existing config, recreating${NC}"
|
|
15222
|
+
echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
|
|
15223
|
+
fi
|
|
14900
15224
|
else
|
|
14901
15225
|
echo "{\"otel_endpoint\": \"$endpoint\"}" > "$config_file"
|
|
14902
15226
|
fi
|
|
@@ -15771,17 +16095,20 @@ _context_show() {
|
|
|
15771
16095
|
local ctx_file="$loki_dir/state/context-usage.json"
|
|
15772
16096
|
if [ -f "$ctx_file" ]; then
|
|
15773
16097
|
local total_tokens used_tokens input_tokens output_tokens cache_tokens
|
|
15774
|
-
total_tokens=$(python3 -c "import json; d=json.load(open('
|
|
15775
|
-
used_tokens=$(python3 -c "import json; d=json.load(open('
|
|
15776
|
-
input_tokens=$(python3 -c "import json; d=json.load(open('
|
|
15777
|
-
output_tokens=$(python3 -c "import json; d=json.load(open('
|
|
15778
|
-
cache_tokens=$(python3 -c "import json; d=json.load(open('
|
|
16098
|
+
total_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('window_size', 200000))" 2>/dev/null || echo "200000")
|
|
16099
|
+
used_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('used_tokens', 0))" 2>/dev/null || echo "0")
|
|
16100
|
+
input_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('input_tokens', 0))" 2>/dev/null || echo "0")
|
|
16101
|
+
output_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('output_tokens', 0))" 2>/dev/null || echo "0")
|
|
16102
|
+
cache_tokens=$(_LOKI_CTX_FILE="$ctx_file" python3 -c "import json, os; d=json.load(open(os.environ['_LOKI_CTX_FILE'])); print(d.get('cache_read_tokens', 0))" 2>/dev/null || echo "0")
|
|
15779
16103
|
|
|
15780
16104
|
# Display gauge
|
|
15781
16105
|
if type context_gauge &>/dev/null; then
|
|
15782
16106
|
context_gauge "$used_tokens" "$total_tokens" "Window"
|
|
15783
16107
|
else
|
|
15784
|
-
local pct
|
|
16108
|
+
local pct=0
|
|
16109
|
+
if [ "$total_tokens" -gt 0 ] 2>/dev/null; then
|
|
16110
|
+
pct=$((used_tokens * 100 / total_tokens))
|
|
16111
|
+
fi
|
|
15785
16112
|
echo -e " ${CYAN}Context:${NC} ${pct}% used (${used_tokens} / ${total_tokens} tokens)"
|
|
15786
16113
|
fi
|
|
15787
16114
|
echo ""
|
|
@@ -15802,8 +16129,8 @@ _context_show() {
|
|
|
15802
16129
|
echo ""
|
|
15803
16130
|
echo -e " ${BOLD}Cost${NC}"
|
|
15804
16131
|
local budget_used budget_limit
|
|
15805
|
-
budget_used=$(python3 -c "import json; d=json.load(open('
|
|
15806
|
-
budget_limit=$(python3 -c "import json; d=json.load(open('
|
|
16132
|
+
budget_used=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 4))" 2>/dev/null || echo "0")
|
|
16133
|
+
budget_limit=$(_BUDGET_FILE="$budget_file" python3 -c "import json, os; d=json.load(open(os.environ['_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
|
|
15807
16134
|
|
|
15808
16135
|
if [ "$budget_limit" != "0" ]; then
|
|
15809
16136
|
echo -e " ${DIM}Spent:${NC} \$${budget_used} / \$${budget_limit}"
|
|
@@ -16431,7 +16758,15 @@ for a in agents:
|
|
|
16431
16758
|
return 1
|
|
16432
16759
|
fi
|
|
16433
16760
|
|
|
16434
|
-
|
|
16761
|
+
# BUG-PU-003: Separate persona from user prompt with clear delimiter
|
|
16762
|
+
# so the AI provider can distinguish role instruction from task
|
|
16763
|
+
local full_prompt="You are acting as the following specialist agent:
|
|
16764
|
+
|
|
16765
|
+
${persona}
|
|
16766
|
+
|
|
16767
|
+
---
|
|
16768
|
+
|
|
16769
|
+
USER TASK: ${prompt}"
|
|
16435
16770
|
echo -e "${BOLD}Running as: $agent_type${NC}"
|
|
16436
16771
|
echo -e "${DIM}Persona: ${persona:0:80}...${NC}"
|
|
16437
16772
|
echo ""
|
|
@@ -16508,13 +16843,15 @@ for a in agents:
|
|
|
16508
16843
|
cmd_start "$prd"
|
|
16509
16844
|
else
|
|
16510
16845
|
# Treat as inline prompt - create temp PRD
|
|
16511
|
-
|
|
16512
|
-
|
|
16846
|
+
# BUG-PU-003: Use .loki/ directory instead of /tmp so run.sh
|
|
16847
|
+
# can find it after exec replaces this process. The temp file
|
|
16848
|
+
# in /tmp was never cleaned because cmd_start uses exec.
|
|
16849
|
+
mkdir -p "$LOKI_DIR"
|
|
16850
|
+
local tmp_prd="$LOKI_DIR/agent-prd-$$.md"
|
|
16513
16851
|
echo "# Agent Task: $agent_type" > "$tmp_prd"
|
|
16514
16852
|
echo "" >> "$tmp_prd"
|
|
16515
16853
|
echo "$prd" >> "$tmp_prd"
|
|
16516
16854
|
cmd_start "$tmp_prd"
|
|
16517
|
-
rm -f "$tmp_prd"
|
|
16518
16855
|
fi
|
|
16519
16856
|
;;
|
|
16520
16857
|
|
|
@@ -17183,7 +17520,7 @@ except: pass
|
|
|
17183
17520
|
{
|
|
17184
17521
|
"project": {
|
|
17185
17522
|
"name": "$project_name",
|
|
17186
|
-
"description": $(python3 -c "import json; print(json.dumps('
|
|
17523
|
+
"description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
|
|
17187
17524
|
"version": "$project_version",
|
|
17188
17525
|
"path": "$target_path"
|
|
17189
17526
|
},
|
|
@@ -20280,7 +20617,7 @@ cmd_share() {
|
|
|
20280
20617
|
|
|
20281
20618
|
# Upload as gist
|
|
20282
20619
|
echo "Uploading session report..."
|
|
20283
|
-
local gist_desc="Loki Mode session report ($(date +%Y-%m-%
|
|
20620
|
+
local gist_desc="Loki Mode session report ($(date +%Y-%m-%dT%H:%M:%S))"
|
|
20284
20621
|
local gist_url
|
|
20285
20622
|
gist_url=$(gh gist create "$tmpfile" --desc "$gist_desc" $visibility 2>&1)
|
|
20286
20623
|
local gist_exit=$?
|