loki-mode 5.32.2 → 5.33.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 +20 -28
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +371 -7
- package/autonomy/hooks/quality-gate.sh +1 -1
- package/autonomy/hooks/track-metrics.sh +2 -2
- package/autonomy/hooks/validate-bash.sh +24 -24
- package/autonomy/loki +41 -32
- package/autonomy/run.sh +49 -18
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +7 -3
- package/dashboard/server.py +6 -4
- package/dashboard/static/index.html +5 -5
- package/docs/INSTALLATION.md +1 -1
- package/events/emit.sh +16 -9
- package/memory/engine.py +24 -16
- package/memory/retrieval.py +28 -19
- package/memory/storage.py +29 -3
- package/memory/vector_index.py +5 -5
- package/package.json +1 -1
package/autonomy/loki
CHANGED
|
@@ -873,8 +873,8 @@ cmd_status() {
|
|
|
873
873
|
# Check budget
|
|
874
874
|
if [ -f "$LOKI_DIR/metrics/budget.json" ]; then
|
|
875
875
|
local budget_limit budget_used budget_remaining
|
|
876
|
-
budget_limit=$(python3 -c "import json; d=json.load(open(
|
|
877
|
-
budget_used=$(python3 -c "import json; d=json.load(open(
|
|
876
|
+
budget_limit=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(d.get('budget_limit', 0))" 2>/dev/null || echo "0")
|
|
877
|
+
budget_used=$(LOKI_BUDGET_FILE="$LOKI_DIR/metrics/budget.json" python3 -c "import json,os; d=json.load(open(os.environ['LOKI_BUDGET_FILE'])); print(round(d.get('budget_used', 0), 2))" 2>/dev/null || echo "0")
|
|
878
878
|
if [ "$budget_limit" != "0" ]; then
|
|
879
879
|
echo -e "${CYAN}Budget:${NC} \$$budget_used / \$$budget_limit"
|
|
880
880
|
fi
|
|
@@ -3109,8 +3109,10 @@ send_slack_notification() {
|
|
|
3109
3109
|
project_name=$(basename "$(pwd)")
|
|
3110
3110
|
local timestamp
|
|
3111
3111
|
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
3112
|
-
local message_escaped
|
|
3112
|
+
local message_escaped event_escaped project_escaped
|
|
3113
3113
|
message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3114
|
+
event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3115
|
+
project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3114
3116
|
|
|
3115
3117
|
# Build Slack payload with blocks for better formatting
|
|
3116
3118
|
local payload
|
|
@@ -3121,7 +3123,7 @@ send_slack_notification() {
|
|
|
3121
3123
|
"type": "header",
|
|
3122
3124
|
"text": {
|
|
3123
3125
|
"type": "plain_text",
|
|
3124
|
-
"text": "Loki Mode: $
|
|
3126
|
+
"text": "Loki Mode: $event_escaped"
|
|
3125
3127
|
}
|
|
3126
3128
|
},
|
|
3127
3129
|
{
|
|
@@ -3136,7 +3138,7 @@ send_slack_notification() {
|
|
|
3136
3138
|
"elements": [
|
|
3137
3139
|
{
|
|
3138
3140
|
"type": "mrkdwn",
|
|
3139
|
-
"text": "*Project:* $
|
|
3141
|
+
"text": "*Project:* $project_escaped | *Time:* $timestamp"
|
|
3140
3142
|
}
|
|
3141
3143
|
]
|
|
3142
3144
|
}
|
|
@@ -3165,8 +3167,10 @@ send_discord_notification() {
|
|
|
3165
3167
|
project_name=$(basename "$(pwd)")
|
|
3166
3168
|
local timestamp
|
|
3167
3169
|
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
3168
|
-
local message_escaped
|
|
3170
|
+
local message_escaped event_escaped project_escaped
|
|
3169
3171
|
message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3172
|
+
event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3173
|
+
project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3170
3174
|
|
|
3171
3175
|
# Build Discord embed payload
|
|
3172
3176
|
local payload
|
|
@@ -3174,11 +3178,11 @@ send_discord_notification() {
|
|
|
3174
3178
|
{
|
|
3175
3179
|
"embeds": [
|
|
3176
3180
|
{
|
|
3177
|
-
"title": "Loki Mode: $
|
|
3181
|
+
"title": "Loki Mode: $event_escaped",
|
|
3178
3182
|
"description": "$message_escaped",
|
|
3179
3183
|
"color": 5814783,
|
|
3180
3184
|
"footer": {
|
|
3181
|
-
"text": "Project: $
|
|
3185
|
+
"text": "Project: $project_escaped | $timestamp"
|
|
3182
3186
|
}
|
|
3183
3187
|
}
|
|
3184
3188
|
]
|
|
@@ -3206,19 +3210,22 @@ send_webhook_notification() {
|
|
|
3206
3210
|
project_name=$(basename "$(pwd)")
|
|
3207
3211
|
local timestamp
|
|
3208
3212
|
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
3209
|
-
local message_escaped
|
|
3213
|
+
local message_escaped event_escaped project_escaped cwd_escaped
|
|
3210
3214
|
message_escaped=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3215
|
+
event_escaped=$(printf '%s' "$event_type" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3216
|
+
project_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3217
|
+
cwd_escaped=$(printf '%s' "$(pwd)" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
3211
3218
|
|
|
3212
3219
|
# Build generic JSON payload
|
|
3213
3220
|
local payload
|
|
3214
3221
|
payload=$(cat <<EOF
|
|
3215
3222
|
{
|
|
3216
3223
|
"source": "loki-mode",
|
|
3217
|
-
"event": "$
|
|
3224
|
+
"event": "$event_escaped",
|
|
3218
3225
|
"message": "$message_escaped",
|
|
3219
|
-
"project": "$
|
|
3226
|
+
"project": "$project_escaped",
|
|
3220
3227
|
"timestamp": "$timestamp",
|
|
3221
|
-
"cwd": "$
|
|
3228
|
+
"cwd": "$cwd_escaped"
|
|
3222
3229
|
}
|
|
3223
3230
|
EOF
|
|
3224
3231
|
)
|
|
@@ -3405,12 +3412,13 @@ SESSEOF
|
|
|
3405
3412
|
|
|
3406
3413
|
# Mark session stopped
|
|
3407
3414
|
if [ -f "$LOKI_DIR/session.json" ]; then
|
|
3408
|
-
python3 -c "
|
|
3409
|
-
import json
|
|
3410
|
-
|
|
3415
|
+
LOKI_SESSION_FILE="$LOKI_DIR/session.json" python3 -c "
|
|
3416
|
+
import json, os
|
|
3417
|
+
p = os.environ['LOKI_SESSION_FILE']
|
|
3418
|
+
with open(p, 'r') as f:
|
|
3411
3419
|
d = json.load(f)
|
|
3412
3420
|
d['status'] = 'stopped'
|
|
3413
|
-
with open(
|
|
3421
|
+
with open(p, 'w') as f:
|
|
3414
3422
|
json.dump(d, f, indent=2)
|
|
3415
3423
|
" 2>/dev/null || true
|
|
3416
3424
|
fi
|
|
@@ -4169,11 +4177,11 @@ cmd_memory() {
|
|
|
4169
4177
|
case "$1" in
|
|
4170
4178
|
--limit|-l)
|
|
4171
4179
|
limit="${2:-20}"
|
|
4172
|
-
shift 2
|
|
4180
|
+
if [ $# -ge 2 ]; then shift 2; else shift; fi
|
|
4173
4181
|
;;
|
|
4174
4182
|
--project|-p)
|
|
4175
4183
|
project="${2:-}"
|
|
4176
|
-
shift 2
|
|
4184
|
+
if [ $# -ge 2 ]; then shift 2; else shift; fi
|
|
4177
4185
|
;;
|
|
4178
4186
|
*)
|
|
4179
4187
|
shift
|
|
@@ -4364,11 +4372,11 @@ for filename in ['patterns.jsonl', 'mistakes.jsonl', 'successes.jsonl']:
|
|
|
4364
4372
|
export)
|
|
4365
4373
|
local output="${2:-learnings-export.json}"
|
|
4366
4374
|
|
|
4367
|
-
python3 -c "
|
|
4375
|
+
LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
|
|
4368
4376
|
import json
|
|
4369
4377
|
import os
|
|
4370
4378
|
|
|
4371
|
-
learnings_dir = '
|
|
4379
|
+
learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
|
|
4372
4380
|
result = {'patterns': [], 'mistakes': [], 'successes': []}
|
|
4373
4381
|
|
|
4374
4382
|
for category in ['patterns', 'mistakes', 'successes']:
|
|
@@ -4392,12 +4400,12 @@ print(f'Exported to $output')
|
|
|
4392
4400
|
echo -e "${BOLD}Learning Statistics${NC}"
|
|
4393
4401
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4394
4402
|
|
|
4395
|
-
python3 -c "
|
|
4403
|
+
LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
|
|
4396
4404
|
import json
|
|
4397
4405
|
import os
|
|
4398
4406
|
from collections import Counter
|
|
4399
4407
|
|
|
4400
|
-
learnings_dir = '
|
|
4408
|
+
learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
|
|
4401
4409
|
projects = Counter()
|
|
4402
4410
|
categories = Counter()
|
|
4403
4411
|
|
|
@@ -4431,13 +4439,13 @@ for proj, count in projects.most_common(10):
|
|
|
4431
4439
|
echo -e "${BOLD}Deduplicating Learnings${NC}"
|
|
4432
4440
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
4433
4441
|
|
|
4434
|
-
python3 -c "
|
|
4442
|
+
LOKI_LEARNINGS_DIR="$learnings_dir" python3 -c "
|
|
4435
4443
|
import json
|
|
4436
4444
|
import os
|
|
4437
4445
|
import hashlib
|
|
4438
4446
|
from datetime import datetime, timezone
|
|
4439
4447
|
|
|
4440
|
-
learnings_dir = '
|
|
4448
|
+
learnings_dir = os.environ['LOKI_LEARNINGS_DIR']
|
|
4441
4449
|
|
|
4442
4450
|
def dedupe_file(filepath):
|
|
4443
4451
|
'''Deduplicate a JSONL file, keeping first occurrence of each unique description'''
|
|
@@ -4526,13 +4534,14 @@ except Exception as e:
|
|
|
4526
4534
|
consolidate)
|
|
4527
4535
|
# Run consolidation pipeline
|
|
4528
4536
|
local hours="${2:-24}"
|
|
4529
|
-
python3 -c "
|
|
4537
|
+
LOKI_HOURS="$hours" python3 -c "
|
|
4538
|
+
import os
|
|
4530
4539
|
try:
|
|
4531
4540
|
from memory.consolidation import ConsolidationPipeline
|
|
4532
4541
|
from memory.storage import MemoryStorage
|
|
4533
4542
|
storage = MemoryStorage('.loki/memory')
|
|
4534
4543
|
pipeline = ConsolidationPipeline(storage)
|
|
4535
|
-
result = pipeline.consolidate(since_hours
|
|
4544
|
+
result = pipeline.consolidate(since_hours=int(os.environ.get('LOKI_HOURS', '24')))
|
|
4536
4545
|
print('Consolidation complete:')
|
|
4537
4546
|
print(f' Patterns created: {result.patterns_created}')
|
|
4538
4547
|
print(f' Patterns merged: {result.patterns_merged}')
|
|
@@ -5326,9 +5335,9 @@ cmd_council() {
|
|
|
5326
5335
|
fi
|
|
5327
5336
|
|
|
5328
5337
|
if [ -f "$council_dir/state.json" ]; then
|
|
5329
|
-
python3 -c "
|
|
5330
|
-
import json
|
|
5331
|
-
with open(
|
|
5338
|
+
LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
|
|
5339
|
+
import json, os
|
|
5340
|
+
with open(os.environ['LOKI_COUNCIL_STATE']) as f:
|
|
5332
5341
|
state = json.load(f)
|
|
5333
5342
|
print(f\"Enabled: {state.get('initialized', False)}\")
|
|
5334
5343
|
print(f\"Total votes: {state.get('total_votes', 0)}\")
|
|
@@ -5354,9 +5363,9 @@ else:
|
|
|
5354
5363
|
echo -e "${CYAN}=== Council Decision Log ===${NC}"
|
|
5355
5364
|
|
|
5356
5365
|
if [ -f "$council_dir/state.json" ]; then
|
|
5357
|
-
python3 -c "
|
|
5358
|
-
import json
|
|
5359
|
-
with open(
|
|
5366
|
+
LOKI_COUNCIL_STATE="$council_dir/state.json" python3 -c "
|
|
5367
|
+
import json, os
|
|
5368
|
+
with open(os.environ['LOKI_COUNCIL_STATE']) as f:
|
|
5360
5369
|
state = json.load(f)
|
|
5361
5370
|
verdicts = state.get('verdicts', [])
|
|
5362
5371
|
if not verdicts:
|
package/autonomy/run.sh
CHANGED
|
@@ -1067,14 +1067,16 @@ import_github_issues() {
|
|
|
1067
1067
|
local pending_file=".loki/queue/pending.json"
|
|
1068
1068
|
local task_count=0
|
|
1069
1069
|
|
|
1070
|
-
#
|
|
1070
|
+
# BUG #14 fix: Normalize to bare [] format (consistent with init_loki_dir
|
|
1071
|
+
# and all other queue consumers). Previously used {"tasks":[]} wrapper here
|
|
1072
|
+
# but bare [] everywhere else, causing format mismatch.
|
|
1071
1073
|
if [ ! -f "$pending_file" ]; then
|
|
1072
|
-
echo '
|
|
1073
|
-
elif jq -e 'type == "
|
|
1074
|
-
# Normalize
|
|
1074
|
+
echo '[]' > "$pending_file"
|
|
1075
|
+
elif jq -e 'type == "object"' "$pending_file" &>/dev/null; then
|
|
1076
|
+
# Normalize {"tasks":[...]} wrapper to bare array
|
|
1075
1077
|
local _tmp_normalize
|
|
1076
1078
|
_tmp_normalize=$(mktemp)
|
|
1077
|
-
jq '
|
|
1079
|
+
jq 'if type == "object" then .tasks // [] else . end' "$pending_file" > "$_tmp_normalize" && mv "$_tmp_normalize" "$pending_file"
|
|
1078
1080
|
rm -f "$_tmp_normalize"
|
|
1079
1081
|
fi
|
|
1080
1082
|
|
|
@@ -1094,8 +1096,8 @@ import_github_issues() {
|
|
|
1094
1096
|
url=$(echo "$issue" | jq -r '.url')
|
|
1095
1097
|
labels=$(echo "$issue" | jq -c '[.labels[].name]')
|
|
1096
1098
|
|
|
1097
|
-
# Check if task already exists
|
|
1098
|
-
if jq -e ".
|
|
1099
|
+
# Check if task already exists (bare array format)
|
|
1100
|
+
if jq -e ".[] | select(.github_issue == $number)" "$pending_file" &>/dev/null; then
|
|
1099
1101
|
log_info "Issue #$number already imported, skipping"
|
|
1100
1102
|
continue
|
|
1101
1103
|
fi
|
|
@@ -1137,10 +1139,10 @@ import_github_issues() {
|
|
|
1137
1139
|
created_at: $created
|
|
1138
1140
|
}')
|
|
1139
1141
|
|
|
1140
|
-
# Append to pending.json with temp file cleanup on error
|
|
1142
|
+
# Append to pending.json (bare array format) with temp file cleanup on error
|
|
1141
1143
|
local temp_file
|
|
1142
1144
|
temp_file=$(mktemp)
|
|
1143
|
-
if jq ".
|
|
1145
|
+
if jq ". += [$task_json]" "$pending_file" > "$temp_file" && mv "$temp_file" "$pending_file"; then
|
|
1144
1146
|
log_info "Imported issue #$number: $title"
|
|
1145
1147
|
task_count=$((task_count + 1))
|
|
1146
1148
|
else
|
|
@@ -1293,8 +1295,8 @@ export_tasks_to_github() {
|
|
|
1293
1295
|
return 0
|
|
1294
1296
|
fi
|
|
1295
1297
|
|
|
1296
|
-
# Export non-GitHub tasks as issues
|
|
1297
|
-
jq -c '.tasks[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
|
|
1298
|
+
# Export non-GitHub tasks as issues (handles both bare array and wrapper formats)
|
|
1299
|
+
jq -c 'if type == "object" then .tasks // [] else . end | .[] | select(.source != "github")' "$pending_file" 2>/dev/null | while read -r task; do
|
|
1298
1300
|
local title desc
|
|
1299
1301
|
title=$(echo "$task" | jq -r '.title')
|
|
1300
1302
|
desc=$(echo "$task" | jq -r '.description // ""')
|
|
@@ -2415,13 +2417,20 @@ write_dashboard_state() {
|
|
|
2415
2417
|
local project_path=$(pwd)
|
|
2416
2418
|
local _tmp_state="${output_file}.tmp"
|
|
2417
2419
|
|
|
2420
|
+
# BUG #49 fix: Escape project path/name for JSON to handle special chars
|
|
2421
|
+
# (spaces, quotes, backslashes in directory names)
|
|
2422
|
+
local project_name_escaped
|
|
2423
|
+
local project_path_escaped
|
|
2424
|
+
project_name_escaped=$(printf '%s' "$project_name" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
2425
|
+
project_path_escaped=$(printf '%s' "$project_path" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
2426
|
+
|
|
2418
2427
|
cat > "$_tmp_state" << EOF
|
|
2419
2428
|
{
|
|
2420
2429
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
2421
2430
|
"version": "$version",
|
|
2422
2431
|
"project": {
|
|
2423
|
-
"name": "$
|
|
2424
|
-
"path": "$
|
|
2432
|
+
"name": "$project_name_escaped",
|
|
2433
|
+
"path": "$project_path_escaped"
|
|
2425
2434
|
},
|
|
2426
2435
|
"mode": "$mode",
|
|
2427
2436
|
"provider": "${PROVIDER_NAME:-claude}",
|
|
@@ -4665,12 +4674,12 @@ build_prompt() {
|
|
|
4665
4674
|
fi
|
|
4666
4675
|
|
|
4667
4676
|
# Human directive injection (from HUMAN_INPUT.md)
|
|
4677
|
+
# NOTE: Do NOT unset LOKI_HUMAN_INPUT here - build_prompt runs in a subshell
|
|
4678
|
+
# (command substitution) so unset would not affect the parent shell.
|
|
4679
|
+
# The caller (run_autonomous) clears it after consuming the prompt.
|
|
4668
4680
|
local human_directive=""
|
|
4669
4681
|
if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
|
|
4670
4682
|
human_directive="HUMAN_DIRECTIVE (PRIORITY): $LOKI_HUMAN_INPUT Execute this directive BEFORE continuing normal tasks."
|
|
4671
|
-
# Clear after consumption so it doesn't repeat every iteration
|
|
4672
|
-
unset LOKI_HUMAN_INPUT
|
|
4673
|
-
rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
|
|
4674
4683
|
fi
|
|
4675
4684
|
|
|
4676
4685
|
# Queue task injection (from dashboard or API)
|
|
@@ -4795,6 +4804,15 @@ run_autonomous() {
|
|
|
4795
4804
|
|
|
4796
4805
|
local prompt=$(build_prompt $retry "$prd_path" $ITERATION_COUNT)
|
|
4797
4806
|
|
|
4807
|
+
# BUG #5 fix: Clear LOKI_HUMAN_INPUT in the parent shell after build_prompt
|
|
4808
|
+
# consumed it. build_prompt runs in a subshell (command substitution), so
|
|
4809
|
+
# any unset inside it does not affect the parent. Clear here to prevent
|
|
4810
|
+
# the same directive from repeating every iteration.
|
|
4811
|
+
if [ -n "${LOKI_HUMAN_INPUT:-}" ]; then
|
|
4812
|
+
unset LOKI_HUMAN_INPUT
|
|
4813
|
+
rm -f "${TARGET_DIR:-.}/.loki/HUMAN_INPUT.md"
|
|
4814
|
+
fi
|
|
4815
|
+
|
|
4798
4816
|
echo ""
|
|
4799
4817
|
log_header "Attempt $((retry + 1)) of $MAX_RETRIES"
|
|
4800
4818
|
log_info "Prompt: $prompt"
|
|
@@ -5234,11 +5252,19 @@ check_human_intervention() {
|
|
|
5234
5252
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
5235
5253
|
|
|
5236
5254
|
# Check for PAUSE file
|
|
5255
|
+
# BUG #4 fix: Check handle_pause return value before deleting PAUSE file.
|
|
5256
|
+
# handle_pause returns 1 if STOP was requested during the pause, so we must
|
|
5257
|
+
# propagate that as return 2 (stop) instead of always returning 1 (continue).
|
|
5237
5258
|
if [ -f "$loki_dir/PAUSE" ]; then
|
|
5238
5259
|
log_warn "PAUSE file detected - pausing execution"
|
|
5239
5260
|
notify_intervention_needed "Execution paused via PAUSE file"
|
|
5240
5261
|
handle_pause
|
|
5262
|
+
local pause_result=$?
|
|
5241
5263
|
rm -f "$loki_dir/PAUSE"
|
|
5264
|
+
if [ "$pause_result" -eq 1 ]; then
|
|
5265
|
+
# STOP was requested during pause
|
|
5266
|
+
return 2
|
|
5267
|
+
fi
|
|
5242
5268
|
return 1
|
|
5243
5269
|
fi
|
|
5244
5270
|
|
|
@@ -5287,8 +5313,13 @@ check_human_intervention() {
|
|
|
5287
5313
|
rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
|
|
5288
5314
|
if type council_vote &>/dev/null && council_vote; then
|
|
5289
5315
|
log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
|
|
5290
|
-
#
|
|
5291
|
-
|
|
5316
|
+
# BUG #17 fix: Write COMPLETED marker, generate council report, and
|
|
5317
|
+
# run memory consolidation (matching the normal council approval path
|
|
5318
|
+
# in council_should_stop).
|
|
5319
|
+
echo "Council force-review approved at iteration $ITERATION_COUNT on $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$loki_dir/COMPLETED"
|
|
5320
|
+
if type council_write_report &>/dev/null; then
|
|
5321
|
+
council_write_report
|
|
5322
|
+
fi
|
|
5292
5323
|
log_info "Running memory consolidation..."
|
|
5293
5324
|
run_memory_consolidation
|
|
5294
5325
|
notify_all_complete
|
package/dashboard/__init__.py
CHANGED
package/dashboard/control.py
CHANGED
|
@@ -56,10 +56,13 @@ app = FastAPI(
|
|
|
56
56
|
version="1.0.0"
|
|
57
57
|
)
|
|
58
58
|
|
|
59
|
-
# CORS middleware for dashboard frontend
|
|
59
|
+
# CORS middleware for dashboard frontend - restricted to localhost by default.
|
|
60
|
+
# Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
|
|
61
|
+
_cors_default = "http://localhost:57374,http://127.0.0.1:57374"
|
|
62
|
+
_cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
|
|
60
63
|
app.add_middleware(
|
|
61
64
|
CORSMiddleware,
|
|
62
|
-
allow_origins=[
|
|
65
|
+
allow_origins=[o.strip() for o in _cors_origins if o.strip()],
|
|
63
66
|
allow_credentials=True,
|
|
64
67
|
allow_methods=["*"],
|
|
65
68
|
allow_headers=["*"],
|
|
@@ -511,4 +514,5 @@ async def asyncio_sleep(seconds: float):
|
|
|
511
514
|
if __name__ == "__main__":
|
|
512
515
|
import uvicorn
|
|
513
516
|
port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
|
|
514
|
-
|
|
517
|
+
host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
|
|
518
|
+
uvicorn.run(app, host=host, port=port)
|
package/dashboard/server.py
CHANGED
|
@@ -209,9 +209,10 @@ app = FastAPI(
|
|
|
209
209
|
lifespan=lifespan,
|
|
210
210
|
)
|
|
211
211
|
|
|
212
|
-
# Add CORS middleware -
|
|
213
|
-
#
|
|
214
|
-
|
|
212
|
+
# Add CORS middleware - restricted to localhost by default.
|
|
213
|
+
# Set LOKI_DASHBOARD_CORS to override (comma-separated origins).
|
|
214
|
+
_cors_default = "http://localhost:57374,http://127.0.0.1:57374"
|
|
215
|
+
_cors_origins = os.environ.get("LOKI_DASHBOARD_CORS", _cors_default).split(",")
|
|
215
216
|
app.add_middleware(
|
|
216
217
|
CORSMiddleware,
|
|
217
218
|
allow_origins=[o.strip() for o in _cors_origins if o.strip()],
|
|
@@ -2046,6 +2047,7 @@ async def get_agents():
|
|
|
2046
2047
|
@app.post("/api/agents/{agent_id}/kill")
|
|
2047
2048
|
async def kill_agent(agent_id: str):
|
|
2048
2049
|
"""Kill a specific agent by ID."""
|
|
2050
|
+
agent_id = _sanitize_agent_id(agent_id)
|
|
2049
2051
|
agents_file = _get_loki_dir() / "state" / "agents.json"
|
|
2050
2052
|
if not agents_file.exists():
|
|
2051
2053
|
raise HTTPException(404, "No agents file found")
|
|
@@ -2296,7 +2298,7 @@ def run_server(host: str = None, port: int = None) -> None:
|
|
|
2296
2298
|
"""Run the dashboard server."""
|
|
2297
2299
|
import uvicorn
|
|
2298
2300
|
if host is None:
|
|
2299
|
-
# Default to localhost-only
|
|
2301
|
+
# Default to localhost-only for security
|
|
2300
2302
|
host = os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1")
|
|
2301
2303
|
if port is None:
|
|
2302
2304
|
port = int(os.environ.get("LOKI_DASHBOARD_PORT", "57374"))
|
|
@@ -3750,7 +3750,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3750
3750
|
${t}
|
|
3751
3751
|
</div>
|
|
3752
3752
|
</div>
|
|
3753
|
-
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(e.innerHTML=`
|
|
3753
|
+
`,this._attachEventListeners()}_attachEventListeners(){let e=this.shadowRoot.getElementById("time-range-select");e&&e.addEventListener("change",r=>this._setFilter("timeRange",r.target.value));let t=this.shadowRoot.getElementById("signal-type-select");t&&t.addEventListener("change",r=>this._setFilter("signalType",r.target.value));let a=this.shadowRoot.getElementById("source-select");a&&a.addEventListener("change",r=>this._setFilter("source",r.target.value));let i=this.shadowRoot.getElementById("refresh-btn");i&&i.addEventListener("click",()=>this._loadData());let s=this.shadowRoot.getElementById("close-detail");s&&s.addEventListener("click",()=>this._closeDetail()),this.shadowRoot.querySelectorAll(".list-item").forEach(r=>{r.addEventListener("click",()=>{let n=r.dataset.type,d=r.dataset.id,p=this._findItemData(n,d);p&&this._selectMetric(n,p)}),r.addEventListener("keydown",n=>{(n.key==="Enter"||n.key===" ")&&(n.preventDefault(),r.click())})})}_findItemData(e,t){if(!this._metrics?.aggregation)return null;switch(e){case"preference":return this._metrics.aggregation.preferences?.find(a=>a.preference_key===t);case"error_pattern":return this._metrics.aggregation.error_patterns?.find(a=>a.error_type===t);case"success_pattern":return this._metrics.aggregation.success_patterns?.find(a=>a.pattern_name===t);case"tool_efficiency":return this._metrics.aggregation.tool_efficiencies?.find(a=>a.tool_name===t);default:return null}}};customElements.get("loki-learning-dashboard")||customElements.define("loki-learning-dashboard",H);var me=[{id:"overview",label:"Overview"},{id:"decisions",label:"Decision Log"},{id:"convergence",label:"Convergence"},{id:"agents",label:"Agents"}],O=class extends c{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._loading=!1,this._error=null,this._api=null,this._activeTab="overview",this._pollInterval=null,this._councilState=null,this._verdicts=[],this._convergence=[],this._agents=[],this._selectedAgent=null,this._lastDataHash=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadData(),this._startPolling()}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling()}attributeChangedCallback(e,t,a){t!==a&&(e==="api-url"&&this._api&&(this._api.baseUrl=a,this._loadData()),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=g({baseUrl:e})}_startPolling(){this._pollInterval=setInterval(()=>this._loadData(),3e3),this._visibilityHandler=()=>{document.hidden?this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null):this._pollInterval||(this._loadData(),this._pollInterval=setInterval(()=>this._loadData(),3e3))},document.addEventListener("visibilitychange",this._visibilityHandler)}_stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null),this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null)}async _loadData(){try{let[t,a,i,s]=await Promise.allSettled([this._api._get("/api/council/state"),this._api._get("/api/council/verdicts"),this._api._get("/api/council/convergence"),this._api._get("/api/agents")]);t.status==="fulfilled"&&(this._councilState=t.value),a.status==="fulfilled"&&(this._verdicts=a.value.verdicts||[]),i.status==="fulfilled"&&(this._convergence=i.value.dataPoints||[]),s.status==="fulfilled"&&(this._agents=Array.isArray(s.value)?s.value:[]),this._error=null}catch(t){this._error=t.message}let e=JSON.stringify({s:this._councilState,v:this._verdicts,c:this._convergence,a:this._agents,e:this._error});e!==this._lastDataHash&&(this._lastDataHash=e,this.render())}async _forceReview(){try{await this._api._post("/api/council/force-review"),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"force-review"},bubbles:!0}))}catch(e){this._error=`Failed to force review: ${e.message}`,this.render()}}async _killAgent(e){if(confirm(`Kill agent ${e}?`))try{await this._api._post(`/api/agents/${e}/kill`),this.dispatchEvent(new CustomEvent("council-action",{detail:{action:"kill-agent",agentId:e},bubbles:!0})),await this._loadData()}catch(t){this._error=`Failed to kill agent: ${t.message}`,this.render()}}async _pauseAgent(e){try{await this._api._post(`/api/agents/${e}/pause`),await this._loadData()}catch(t){this._error=`Failed to pause agent: ${t.message}`,this.render()}}async _resumeAgent(e){try{await this._api._post(`/api/agents/${e}/resume`),await this._loadData()}catch(t){this._error=`Failed to resume agent: ${t.message}`,this.render()}}_setTab(e){this._activeTab=e,this.render()}_selectAgent(e){this._selectedAgent=this._selectedAgent?.id===e.id?null:e,this.render()}render(){let e=this.shadowRoot;e&&(this._pendingRaf&&(cancelAnimationFrame(this._pendingRaf),this._pendingRaf=null),e.innerHTML=`
|
|
3754
3754
|
<style>${this.getBaseStyles()}${this._getStyles()}</style>
|
|
3755
3755
|
<div class="council-dashboard">
|
|
3756
3756
|
<div class="council-header">
|
|
@@ -3758,7 +3758,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3758
3758
|
<h2 class="title">Completion Council</h2>
|
|
3759
3759
|
${this._councilState?.enabled!==!1?'<span class="badge badge-active">Active</span>':'<span class="badge badge-inactive">Disabled</span>'}
|
|
3760
3760
|
</div>
|
|
3761
|
-
<button class="btn btn-primary"
|
|
3761
|
+
<button class="btn btn-primary" id="force-review-btn">
|
|
3762
3762
|
Force Review
|
|
3763
3763
|
</button>
|
|
3764
3764
|
</div>
|
|
@@ -3767,7 +3767,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3767
3767
|
${me.map(t=>`
|
|
3768
3768
|
<button
|
|
3769
3769
|
class="tab ${this._activeTab===t.id?"active":""}"
|
|
3770
|
-
|
|
3770
|
+
data-tab="${t.id}"
|
|
3771
3771
|
>${t.label}</button>
|
|
3772
3772
|
`).join("")}
|
|
3773
3773
|
</div>
|
|
@@ -3778,7 +3778,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3778
3778
|
|
|
3779
3779
|
${this._error?`<div class="error-banner">${this._error}</div>`:""}
|
|
3780
3780
|
</div>
|
|
3781
|
-
|
|
3781
|
+
`,this._attachEventListeners())}_attachEventListeners(){let e=this.shadowRoot;if(!e)return;let t=e.getElementById("force-review-btn");t&&t.addEventListener("click",()=>this._forceReview()),e.querySelectorAll(".tab[data-tab]").forEach(a=>{a.addEventListener("click",()=>this._setTab(a.dataset.tab))})}_renderTabContent(){switch(this._activeTab){case"overview":return this._renderOverview();case"decisions":return this._renderDecisions();case"convergence":return this._renderConvergence();case"agents":return this._renderAgents();default:return""}}_renderOverview(){let e=this._councilState||{},t=e.consecutive_no_change||0,a=e.done_signals||0,i=e.total_votes||0,s=e.approve_votes||0,r=this._verdicts.length>0?this._verdicts[this._verdicts.length-1]:null,n=this._agents.filter(d=>d.alive).length;return`
|
|
3782
3782
|
<div class="overview-grid">
|
|
3783
3783
|
<div class="stat-card">
|
|
3784
3784
|
<div class="stat-label">Council Status</div>
|
|
@@ -3918,7 +3918,7 @@ var LokiDashboard=(()=>{var N=Object.defineProperty;var se=Object.getOwnProperty
|
|
|
3918
3918
|
</div>
|
|
3919
3919
|
`).join("")}
|
|
3920
3920
|
</div>
|
|
3921
|
-
`;return requestAnimationFrame(()=>{let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
|
|
3921
|
+
`;return this._pendingRaf=requestAnimationFrame(()=>{this._pendingRaf=null;let t=this.shadowRoot;t&&t.querySelectorAll(".agent-card[data-agent-index]").forEach(a=>{let i=parseInt(a.dataset.agentIndex,10),s=this._agents[i];s&&(a.addEventListener("click",()=>this._selectAgent(s)),a.querySelectorAll("[data-action]").forEach(r=>{r.addEventListener("click",n=>{n.stopPropagation();let d=r.dataset.action,p=r.dataset.agentId;d==="pause"?this._pauseAgent(p):d==="kill"?this._killAgent(p):d==="resume"&&this._resumeAgent(p)})}))})}),e}_formatTime(e){if(!e)return"";try{return new Date(e).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}catch{return e}}_getStyles(){return`
|
|
3922
3922
|
:host {
|
|
3923
3923
|
display: block;
|
|
3924
3924
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
package/docs/INSTALLATION.md
CHANGED
package/events/emit.sh
CHANGED
|
@@ -25,30 +25,37 @@ mkdir -p "$EVENTS_DIR"
|
|
|
25
25
|
TYPE="${1:-state}"
|
|
26
26
|
SOURCE="${2:-cli}"
|
|
27
27
|
ACTION="${3:-unknown}"
|
|
28
|
-
if [ $# -ge 3 ]; then shift 3; else shift
|
|
28
|
+
if [ "$#" -ge 3 ]; then shift 3; else shift "$#"; fi
|
|
29
29
|
|
|
30
30
|
# Generate event ID and timestamp
|
|
31
31
|
EVENT_ID=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
32
32
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
33
33
|
|
|
34
|
+
# JSON escape helper: handles \, ", and control characters
|
|
35
|
+
json_escape() {
|
|
36
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | tr -d '\n'
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
# Build payload JSON
|
|
35
|
-
|
|
40
|
+
ACTION_ESC=$(json_escape "$ACTION")
|
|
41
|
+
PAYLOAD="{\"action\":\"$ACTION_ESC\""
|
|
36
42
|
for arg in "$@"; do
|
|
37
43
|
key="${arg%%=*}"
|
|
38
44
|
value="${arg#*=}"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value\""
|
|
45
|
+
key_escaped=$(json_escape "$key")
|
|
46
|
+
value_escaped=$(json_escape "$value")
|
|
47
|
+
PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value_escaped\""
|
|
43
48
|
done
|
|
44
49
|
PAYLOAD="$PAYLOAD}"
|
|
45
50
|
|
|
46
|
-
# Build full event JSON
|
|
51
|
+
# Build full event JSON (escape type/source for safe embedding)
|
|
52
|
+
TYPE_ESC=$(json_escape "$TYPE")
|
|
53
|
+
SOURCE_ESC=$(json_escape "$SOURCE")
|
|
47
54
|
EVENT=$(cat <<EOF
|
|
48
55
|
{
|
|
49
56
|
"id": "$EVENT_ID",
|
|
50
|
-
"type": "$
|
|
51
|
-
"source": "$
|
|
57
|
+
"type": "$TYPE_ESC",
|
|
58
|
+
"source": "$SOURCE_ESC",
|
|
52
59
|
"timestamp": "$TIMESTAMP",
|
|
53
60
|
"payload": $PAYLOAD,
|
|
54
61
|
"version": "1.0"
|