loki-mode 5.46.0 → 5.48.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 +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +11 -6
- package/autonomy/checklist-verify.py +3 -2
- package/autonomy/completion-council.sh +129 -2
- package/autonomy/loki +9 -1
- package/autonomy/playwright-verify.sh +0 -0
- package/autonomy/prd-analyzer.py +0 -0
- package/autonomy/prd-checklist.sh +163 -2
- package/autonomy/run.sh +14 -7
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +166 -18
- package/dashboard/static/index.html +161 -65
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/memory/embeddings.py +5 -0
- package/memory/engine.py +2 -1
- package/memory/retrieval.py +1 -0
- package/memory/token_economics.py +13 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](benchmarks/results/)
|
|
14
14
|
[](benchmarks/results/)
|
|
15
15
|
|
|
16
|
-
**Current Version: v5.
|
|
16
|
+
**Current Version: v5.47.0**
|
|
17
17
|
|
|
18
18
|
**[Autonomi](https://www.autonomi.dev/)** | **[Documentation](https://www.autonomi.dev/docs)** | **[GitHub](https://github.com/asklokesh/loki-mode)**
|
|
19
19
|
|
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 zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.
|
|
6
|
+
# Loki Mode v5.48.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -262,4 +262,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
262
262
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
263
263
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
264
264
|
|
|
265
|
-
**v5.
|
|
265
|
+
**v5.48.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.48.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -395,8 +395,9 @@ _install_node_deps() {
|
|
|
395
395
|
_install_python_deps() {
|
|
396
396
|
local dir="$1"
|
|
397
397
|
if [ -f "$dir/requirements.txt" ]; then
|
|
398
|
-
log_step "App Runner: installing Python dependencies
|
|
399
|
-
(cd "$dir" && pip install -r requirements.txt >> "$_APP_RUNNER_DIR/app.log" 2>&1)
|
|
398
|
+
log_step "App Runner: installing Python dependencies..."
|
|
399
|
+
(cd "$dir" && pip install -r requirements.txt >> "$_APP_RUNNER_DIR/app.log" 2>&1) || \
|
|
400
|
+
log_warn "App Runner: pip install failed, app may not start"
|
|
400
401
|
fi
|
|
401
402
|
}
|
|
402
403
|
|
|
@@ -425,14 +426,18 @@ app_runner_start() {
|
|
|
425
426
|
_rotate_app_log
|
|
426
427
|
|
|
427
428
|
# Start the process in a new process group
|
|
428
|
-
|
|
429
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
430
|
+
(cd "$dir" && setsid bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
431
|
+
else
|
|
432
|
+
(cd "$dir" && bash -c "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
433
|
+
fi
|
|
429
434
|
_APP_RUNNER_PID=$!
|
|
430
435
|
|
|
431
436
|
# Write PID file
|
|
432
437
|
echo "$_APP_RUNNER_PID" > "$_APP_RUNNER_DIR/app.pid"
|
|
433
438
|
|
|
434
439
|
# Capture initial git diff hash for change detection
|
|
435
|
-
_GIT_DIFF_HASH=$(cd "$dir" && git diff --stat 2>/dev/null | md5sum 2>/dev/null | awk '{print $1}' || echo "none")
|
|
440
|
+
_GIT_DIFF_HASH=$(cd "$dir" && git diff --stat 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | awk '{print $1}' || echo "none")
|
|
436
441
|
|
|
437
442
|
# Brief pause for process to initialize
|
|
438
443
|
sleep 2
|
|
@@ -557,7 +562,7 @@ app_runner_should_restart() {
|
|
|
557
562
|
|
|
558
563
|
# Get current git diff hash
|
|
559
564
|
local current_hash
|
|
560
|
-
current_hash=$(cd "$dir" && git diff --stat 2>/dev/null | md5sum 2>/dev/null | awk '{print $1}' || echo "none")
|
|
565
|
+
current_hash=$(cd "$dir" && git diff --stat 2>/dev/null | (md5sum 2>/dev/null || md5 -r 2>/dev/null) | awk '{print $1}' || echo "none")
|
|
561
566
|
|
|
562
567
|
# No change
|
|
563
568
|
if [ "$current_hash" = "$_GIT_DIFF_HASH" ]; then
|
|
@@ -631,7 +636,7 @@ app_runner_watchdog() {
|
|
|
631
636
|
# Clear PID and restart
|
|
632
637
|
rm -f "$_APP_RUNNER_DIR/app.pid"
|
|
633
638
|
_APP_RUNNER_PID=""
|
|
634
|
-
app_runner_start
|
|
639
|
+
app_runner_start || log_warn "App Runner: auto-restart failed"
|
|
635
640
|
}
|
|
636
641
|
|
|
637
642
|
#===============================================================================
|
|
@@ -32,7 +32,7 @@ from pathlib import Path
|
|
|
32
32
|
|
|
33
33
|
# Allowed characters in check paths and patterns (security: prevent injection)
|
|
34
34
|
_SAFE_PATH_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?]+$')
|
|
35
|
-
_SAFE_PATTERN_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?|\\()+^$\s]+$')
|
|
35
|
+
_SAFE_PATTERN_RE = re.compile(r'^[a-zA-Z0-9_\-./\*\[\]{}?|\\()+^$\s:=<>@#"\'`,;!&%]+$')
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _validate_path(path: str, project_dir: str) -> str:
|
|
@@ -170,7 +170,8 @@ def run_check(check: dict, project_dir: str, timeout: int) -> dict:
|
|
|
170
170
|
elif check_type == "http_check":
|
|
171
171
|
path = check.get("path", "/")
|
|
172
172
|
# Validate path is safe
|
|
173
|
-
|
|
173
|
+
stripped = path.lstrip("/")
|
|
174
|
+
if stripped and not _SAFE_PATH_RE.match(stripped):
|
|
174
175
|
result["passed"] = None
|
|
175
176
|
result["output"] = f"Unsafe path rejected: {path!r}"
|
|
176
177
|
else:
|
|
@@ -461,6 +461,124 @@ try:
|
|
|
461
461
|
except: print('Results unavailable')
|
|
462
462
|
" >> "$evidence_file" 2>/dev/null || echo "Playwright data unavailable" >> "$evidence_file"
|
|
463
463
|
fi
|
|
464
|
+
|
|
465
|
+
# Add hard gate status
|
|
466
|
+
if [ -f "$COUNCIL_STATE_DIR/gate-block.json" ]; then
|
|
467
|
+
echo "" >> "$evidence_file"
|
|
468
|
+
echo "## Hard Gate Status: BLOCKED" >> "$evidence_file"
|
|
469
|
+
echo "Critical checklist items are failing. Completion is blocked until resolved." >> "$evidence_file"
|
|
470
|
+
cat "$COUNCIL_STATE_DIR/gate-block.json" >> "$evidence_file"
|
|
471
|
+
fi
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#===============================================================================
|
|
475
|
+
# Council Reverify Checklist - Re-run checklist before evaluation
|
|
476
|
+
#===============================================================================
|
|
477
|
+
|
|
478
|
+
# Re-verify checklist before council evaluation to ensure fresh data
|
|
479
|
+
council_reverify_checklist() {
|
|
480
|
+
if type checklist_verify &>/dev/null && [ -f ".loki/checklist/checklist.json" ]; then
|
|
481
|
+
log_info "[Council] Re-verifying checklist before evaluation..."
|
|
482
|
+
checklist_verify 2>/dev/null || true
|
|
483
|
+
fi
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
#===============================================================================
|
|
487
|
+
# Council Checklist Hard Gate - Block completion on critical failures
|
|
488
|
+
#===============================================================================
|
|
489
|
+
|
|
490
|
+
# Council hard gate: blocks completion if critical checklist items are failing
|
|
491
|
+
# Returns 0 if gate passes (ok to complete), 1 if gate blocks (critical failures exist)
|
|
492
|
+
council_checklist_gate() {
|
|
493
|
+
local results_file=".loki/checklist/verification-results.json"
|
|
494
|
+
local waivers_file=".loki/checklist/waivers.json"
|
|
495
|
+
|
|
496
|
+
# No checklist = no gate (backwards compatible)
|
|
497
|
+
if [ ! -f "$results_file" ]; then
|
|
498
|
+
return 0
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
# Check for critical failures, excluding waived items
|
|
502
|
+
local gate_result
|
|
503
|
+
gate_result=$(_RESULTS_FILE="$results_file" _WAIVERS_FILE="$waivers_file" python3 -c "
|
|
504
|
+
import json, sys, os
|
|
505
|
+
|
|
506
|
+
results_file = os.environ['_RESULTS_FILE']
|
|
507
|
+
waivers_file = os.environ.get('_WAIVERS_FILE', '')
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
with open(results_file) as f:
|
|
511
|
+
results = json.load(f)
|
|
512
|
+
except (json.JSONDecodeError, IOError, KeyError):
|
|
513
|
+
print('PASS')
|
|
514
|
+
sys.exit(0)
|
|
515
|
+
|
|
516
|
+
# Load waivers
|
|
517
|
+
waived_ids = set()
|
|
518
|
+
if waivers_file and os.path.exists(waivers_file):
|
|
519
|
+
try:
|
|
520
|
+
with open(waivers_file) as f:
|
|
521
|
+
waivers = json.load(f)
|
|
522
|
+
waived_ids = {w['item_id'] for w in waivers.get('waivers', []) if w.get('active', True)}
|
|
523
|
+
except (json.JSONDecodeError, KeyError):
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
# Find critical failures not waived
|
|
527
|
+
critical_failures = []
|
|
528
|
+
for cat in results.get('categories', []):
|
|
529
|
+
for item in cat.get('items', []):
|
|
530
|
+
if item.get('priority') == 'critical' and item.get('status') == 'failing':
|
|
531
|
+
if item.get('id') not in waived_ids:
|
|
532
|
+
critical_failures.append(item.get('title', item.get('id', 'unknown')))
|
|
533
|
+
|
|
534
|
+
if critical_failures:
|
|
535
|
+
print('BLOCK:' + '|'.join(critical_failures[:5]))
|
|
536
|
+
sys.exit(0)
|
|
537
|
+
else:
|
|
538
|
+
print('PASS')
|
|
539
|
+
sys.exit(0)
|
|
540
|
+
" 2>/dev/null || echo "PASS")
|
|
541
|
+
|
|
542
|
+
if [[ "$gate_result" == BLOCK:* ]]; then
|
|
543
|
+
local failures="${gate_result#BLOCK:}"
|
|
544
|
+
log_warn "[Council] Hard gate BLOCKED: critical checklist failures: ${failures//|/, }"
|
|
545
|
+
|
|
546
|
+
# Write gate block to council state (atomic write via temp file)
|
|
547
|
+
local gate_file="$COUNCIL_STATE_DIR/gate-block.json"
|
|
548
|
+
local gate_tmp="${gate_file}.tmp"
|
|
549
|
+
local timestamp
|
|
550
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
551
|
+
local failures_json
|
|
552
|
+
failures_json=$(_FAILURES="$failures" python3 -c "
|
|
553
|
+
import json, os
|
|
554
|
+
items = os.environ['_FAILURES'].split('|')
|
|
555
|
+
print(json.dumps(items))
|
|
556
|
+
" 2>/dev/null || echo '[]')
|
|
557
|
+
local critical_count
|
|
558
|
+
critical_count=$(_FAILURES="$failures" python3 -c "
|
|
559
|
+
import os
|
|
560
|
+
print(len(os.environ['_FAILURES'].split('|')))
|
|
561
|
+
" 2>/dev/null || echo '0')
|
|
562
|
+
cat > "$gate_tmp" << GATE_EOF
|
|
563
|
+
{
|
|
564
|
+
"status": "blocked",
|
|
565
|
+
"blocked": true,
|
|
566
|
+
"blocked_at": "$timestamp",
|
|
567
|
+
"iteration": ${ITERATION_COUNT:-0},
|
|
568
|
+
"reason": "critical_checklist_failures",
|
|
569
|
+
"critical_failures": $critical_count,
|
|
570
|
+
"failures": $failures_json
|
|
571
|
+
}
|
|
572
|
+
GATE_EOF
|
|
573
|
+
mv "$gate_tmp" "$gate_file"
|
|
574
|
+
return 1
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
# Gate passes
|
|
578
|
+
if [ -f "$COUNCIL_STATE_DIR/gate-block.json" ]; then
|
|
579
|
+
rm -f "$COUNCIL_STATE_DIR/gate-block.json"
|
|
580
|
+
fi
|
|
581
|
+
return 0
|
|
464
582
|
}
|
|
465
583
|
|
|
466
584
|
#===============================================================================
|
|
@@ -1019,6 +1137,15 @@ council_evaluate() {
|
|
|
1019
1137
|
|
|
1020
1138
|
log_info "Running council evaluation pipeline (round $ITERATION_COUNT)..."
|
|
1021
1139
|
|
|
1140
|
+
# Phase 4: Re-verify checklist for fresh data
|
|
1141
|
+
council_reverify_checklist
|
|
1142
|
+
|
|
1143
|
+
# Phase 4: Hard gate check - block if critical checklist items failing
|
|
1144
|
+
if ! council_checklist_gate; then
|
|
1145
|
+
log_info "[Council] Completion blocked by checklist hard gate"
|
|
1146
|
+
return 1 # CONTINUE - can't complete with critical failures
|
|
1147
|
+
fi
|
|
1148
|
+
|
|
1022
1149
|
# Step 1: Aggregate votes from all members
|
|
1023
1150
|
local aggregate_result
|
|
1024
1151
|
aggregate_result=$(council_aggregate_votes)
|
|
@@ -1082,8 +1209,8 @@ council_should_stop() {
|
|
|
1082
1209
|
return 1 # Not time to check yet
|
|
1083
1210
|
fi
|
|
1084
1211
|
|
|
1085
|
-
# Run the council
|
|
1086
|
-
if
|
|
1212
|
+
# Run the council evaluation (includes hard gate + aggregate votes + devil's advocate)
|
|
1213
|
+
if council_evaluate; then
|
|
1087
1214
|
log_header "COMPLETION COUNCIL: PROJECT APPROVED"
|
|
1088
1215
|
log_info "The council has determined this project is complete."
|
|
1089
1216
|
|
package/autonomy/loki
CHANGED
|
@@ -820,6 +820,14 @@ cmd_resume() {
|
|
|
820
820
|
|
|
821
821
|
# Show current status
|
|
822
822
|
cmd_status() {
|
|
823
|
+
# Check for --json flag
|
|
824
|
+
while [[ $# -gt 0 ]]; do
|
|
825
|
+
case "$1" in
|
|
826
|
+
--json) cmd_status_json; return $? ;;
|
|
827
|
+
*) shift ;;
|
|
828
|
+
esac
|
|
829
|
+
done
|
|
830
|
+
|
|
823
831
|
require_jq
|
|
824
832
|
|
|
825
833
|
if [ ! -d "$LOKI_DIR" ]; then
|
|
@@ -3123,7 +3131,7 @@ cmd_api() {
|
|
|
3123
3131
|
if [ -n "${LOKI_TLS_CERT:-}" ] && [ -n "${LOKI_TLS_KEY:-}" ]; then
|
|
3124
3132
|
uvicorn_args="$uvicorn_args --ssl-certfile ${LOKI_TLS_CERT} --ssl-keyfile ${LOKI_TLS_KEY}"
|
|
3125
3133
|
fi
|
|
3126
|
-
LOKI_DIR="$LOKI_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3134
|
+
LOKI_DIR="$LOKI_DIR" PYTHONPATH="$SKILL_DIR" nohup python3 -m uvicorn dashboard.server:app $uvicorn_args > "$LOKI_DIR/logs/api.log" 2>&1 &
|
|
3127
3135
|
local new_pid=$!
|
|
3128
3136
|
echo "$new_pid" > "$pid_file"
|
|
3129
3137
|
|
|
File without changes
|
package/autonomy/prd-analyzer.py
CHANGED
|
File without changes
|
|
@@ -158,7 +158,9 @@ checklist_summary() {
|
|
|
158
158
|
return 0
|
|
159
159
|
fi
|
|
160
160
|
|
|
161
|
-
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE"
|
|
161
|
+
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" \
|
|
162
|
+
_CHECKLIST_WAIVERS="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json" \
|
|
163
|
+
python3 -c "
|
|
162
164
|
import json, sys, os
|
|
163
165
|
try:
|
|
164
166
|
fpath = os.environ.get('_CHECKLIST_RESULTS', '')
|
|
@@ -168,18 +170,39 @@ try:
|
|
|
168
170
|
verified = s.get('verified', 0)
|
|
169
171
|
failing = s.get('failing', 0)
|
|
170
172
|
pending = s.get('pending', 0)
|
|
173
|
+
|
|
174
|
+
# Load waivers
|
|
175
|
+
waived_ids = set()
|
|
176
|
+
waivers_path = os.environ.get('_CHECKLIST_WAIVERS', '')
|
|
177
|
+
if waivers_path and os.path.exists(waivers_path):
|
|
178
|
+
try:
|
|
179
|
+
with open(waivers_path) as wf:
|
|
180
|
+
wdata = json.load(wf)
|
|
181
|
+
for w in wdata.get('waivers', []):
|
|
182
|
+
if w.get('active', True):
|
|
183
|
+
waived_ids.add(w['item_id'])
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Count waived items and adjust failing list
|
|
188
|
+
waived_count = 0
|
|
171
189
|
if total == 0:
|
|
172
190
|
print('')
|
|
173
191
|
else:
|
|
174
192
|
failing_items = []
|
|
175
193
|
for cat in data.get('categories', []):
|
|
176
194
|
for item in cat.get('items', []):
|
|
195
|
+
item_id = item.get('id', '')
|
|
196
|
+
if item_id in waived_ids:
|
|
197
|
+
waived_count += 1
|
|
198
|
+
continue
|
|
177
199
|
if item.get('status') == 'failing' and item.get('priority') in ('critical', 'major'):
|
|
178
200
|
failing_items.append(item.get('title', item.get('id', '?')))
|
|
179
201
|
detail = ''
|
|
180
202
|
if failing_items:
|
|
181
203
|
detail = ' FAILING: ' + ', '.join(failing_items[:5])
|
|
182
|
-
|
|
204
|
+
waived_str = f', {waived_count} waived' if waived_count > 0 else ''
|
|
205
|
+
print(f'{verified}/{total} verified, {failing} failing{waived_str}, {pending} pending.{detail}')
|
|
183
206
|
except Exception:
|
|
184
207
|
print('', file=sys.stderr)
|
|
185
208
|
" 2>/dev/null || echo ""
|
|
@@ -221,3 +244,141 @@ except Exception:
|
|
|
221
244
|
" 2>/dev/null || echo "Checklist data unavailable"
|
|
222
245
|
} >> "${evidence_file:-/dev/stdout}"
|
|
223
246
|
}
|
|
247
|
+
|
|
248
|
+
#===============================================================================
|
|
249
|
+
# Waiver Support (Phase 4)
|
|
250
|
+
#===============================================================================
|
|
251
|
+
|
|
252
|
+
# Load waivers from .loki/checklist/waivers.json
|
|
253
|
+
# Returns waived item IDs (one per line) to stdout
|
|
254
|
+
checklist_waiver_load() {
|
|
255
|
+
local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
|
|
256
|
+
if [ ! -f "$waivers_file" ]; then
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
260
|
+
import json, sys, os
|
|
261
|
+
try:
|
|
262
|
+
waivers_file = os.environ['_WAIVERS_FILE']
|
|
263
|
+
with open(waivers_file) as f:
|
|
264
|
+
waivers = json.load(f)
|
|
265
|
+
for w in waivers.get('waivers', []):
|
|
266
|
+
if w.get('active', True):
|
|
267
|
+
print(w['item_id'])
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
" 2>/dev/null || true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Add a waiver for a checklist item
|
|
274
|
+
# Usage: checklist_waiver_add <item_id> <reason> [waived_by]
|
|
275
|
+
checklist_waiver_add() {
|
|
276
|
+
local item_id="${1:?item_id required}"
|
|
277
|
+
local reason="${2:?reason required}"
|
|
278
|
+
local waived_by="${3:-manual}"
|
|
279
|
+
local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
|
|
280
|
+
local timestamp
|
|
281
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
282
|
+
|
|
283
|
+
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
284
|
+
import json, os, sys
|
|
285
|
+
|
|
286
|
+
waivers_file = os.environ['_WAIVERS_FILE']
|
|
287
|
+
item_id = sys.argv[1]
|
|
288
|
+
reason = sys.argv[2]
|
|
289
|
+
waived_by = sys.argv[3]
|
|
290
|
+
timestamp = sys.argv[4]
|
|
291
|
+
|
|
292
|
+
# Load existing or create new
|
|
293
|
+
waivers = {'waivers': []}
|
|
294
|
+
if os.path.exists(waivers_file):
|
|
295
|
+
try:
|
|
296
|
+
with open(waivers_file) as f:
|
|
297
|
+
waivers = json.load(f)
|
|
298
|
+
except (json.JSONDecodeError, IOError):
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
# Check for duplicate
|
|
302
|
+
for w in waivers.get('waivers', []):
|
|
303
|
+
if w.get('item_id') == item_id and w.get('active', True):
|
|
304
|
+
print(f'Waiver already exists for {item_id}')
|
|
305
|
+
sys.exit(0)
|
|
306
|
+
|
|
307
|
+
# Add new waiver
|
|
308
|
+
waivers.setdefault('waivers', []).append({
|
|
309
|
+
'item_id': item_id,
|
|
310
|
+
'reason': reason,
|
|
311
|
+
'waived_by': waived_by,
|
|
312
|
+
'waived_at': timestamp,
|
|
313
|
+
'active': True
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
# Atomic write
|
|
317
|
+
tmp = waivers_file + '.tmp'
|
|
318
|
+
with open(tmp, 'w') as f:
|
|
319
|
+
json.dump(waivers, f, indent=2)
|
|
320
|
+
os.replace(tmp, waivers_file)
|
|
321
|
+
print(f'Waiver added for {item_id}')
|
|
322
|
+
" "$item_id" "$reason" "$waived_by" "$timestamp" 2>/dev/null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Remove (deactivate) a waiver for a checklist item
|
|
326
|
+
# Usage: checklist_waiver_remove <item_id>
|
|
327
|
+
checklist_waiver_remove() {
|
|
328
|
+
local item_id="${1:?item_id required}"
|
|
329
|
+
local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
|
|
330
|
+
|
|
331
|
+
if [ ! -f "$waivers_file" ]; then
|
|
332
|
+
echo "No waivers file found"
|
|
333
|
+
return 1
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
337
|
+
import json, os, sys
|
|
338
|
+
|
|
339
|
+
waivers_file = os.environ['_WAIVERS_FILE']
|
|
340
|
+
item_id = sys.argv[1]
|
|
341
|
+
|
|
342
|
+
with open(waivers_file) as f:
|
|
343
|
+
waivers = json.load(f)
|
|
344
|
+
|
|
345
|
+
found = False
|
|
346
|
+
for w in waivers.get('waivers', []):
|
|
347
|
+
if w.get('item_id') == item_id and w.get('active', True):
|
|
348
|
+
w['active'] = False
|
|
349
|
+
found = True
|
|
350
|
+
|
|
351
|
+
if not found:
|
|
352
|
+
print(f'No active waiver found for {item_id}')
|
|
353
|
+
sys.exit(1)
|
|
354
|
+
|
|
355
|
+
tmp = waivers_file + '.tmp'
|
|
356
|
+
with open(tmp, 'w') as f:
|
|
357
|
+
json.dump(waivers, f, indent=2)
|
|
358
|
+
os.replace(tmp, waivers_file)
|
|
359
|
+
print(f'Waiver removed for {item_id}')
|
|
360
|
+
" "$item_id" 2>/dev/null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# List all active waivers
|
|
364
|
+
checklist_waiver_list() {
|
|
365
|
+
local waivers_file="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json"
|
|
366
|
+
|
|
367
|
+
if [ ! -f "$waivers_file" ]; then
|
|
368
|
+
echo "No waivers configured"
|
|
369
|
+
return 0
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
373
|
+
import json, os
|
|
374
|
+
waivers_file = os.environ['_WAIVERS_FILE']
|
|
375
|
+
with open(waivers_file) as f:
|
|
376
|
+
waivers = json.load(f)
|
|
377
|
+
active = [w for w in waivers.get('waivers', []) if w.get('active', True)]
|
|
378
|
+
if not active:
|
|
379
|
+
print('No active waivers')
|
|
380
|
+
else:
|
|
381
|
+
for w in active:
|
|
382
|
+
print(f\" {w['item_id']}: {w.get('reason', 'no reason')} (by {w.get('waived_by', 'unknown')} at {w.get('waived_at', '?')})\")
|
|
383
|
+
" 2>/dev/null
|
|
384
|
+
}
|
package/autonomy/run.sh
CHANGED
|
@@ -698,7 +698,7 @@ print(json.dumps(event))
|
|
|
698
698
|
if [ -z "$json_event" ]; then
|
|
699
699
|
# Escape quotes and special chars for JSON
|
|
700
700
|
local escaped_data
|
|
701
|
-
escaped_data=$(
|
|
701
|
+
escaped_data=$(printf '%s' "$event_data" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n')
|
|
702
702
|
json_event="{\"timestamp\":\"$timestamp\",\"type\":\"$event_type\",\"data\":\"$escaped_data\"}"
|
|
703
703
|
fi
|
|
704
704
|
|
|
@@ -733,8 +733,8 @@ emit_event_json() {
|
|
|
733
733
|
if [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" =~ ^(true|false|null)$ ]]; then
|
|
734
734
|
json_data+="\"$key\":$value"
|
|
735
735
|
else
|
|
736
|
-
# Escape quotes in value
|
|
737
|
-
value=$(
|
|
736
|
+
# Escape backslashes, quotes, and special chars in value
|
|
737
|
+
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g')
|
|
738
738
|
json_data+="\"$key\":\"$value\""
|
|
739
739
|
fi
|
|
740
740
|
shift
|
|
@@ -3511,8 +3511,10 @@ update_agents_state() {
|
|
|
3511
3511
|
|
|
3512
3512
|
agents_json="${agents_json}]"
|
|
3513
3513
|
|
|
3514
|
-
# Write aggregated data
|
|
3515
|
-
|
|
3514
|
+
# Write aggregated data (atomic via temp file + mv)
|
|
3515
|
+
local tmp_file="${output_file}.tmp.$$"
|
|
3516
|
+
echo "$agents_json" > "$tmp_file"
|
|
3517
|
+
mv -f "$tmp_file" "$output_file" 2>/dev/null || rm -f "$tmp_file"
|
|
3516
3518
|
}
|
|
3517
3519
|
|
|
3518
3520
|
#===============================================================================
|
|
@@ -5874,7 +5876,7 @@ save_state() {
|
|
|
5874
5876
|
"status": "$status",
|
|
5875
5877
|
"lastExitCode": $exit_code,
|
|
5876
5878
|
"lastRun": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
5877
|
-
"prdPath": "${PRD_PATH:-}",
|
|
5879
|
+
"prdPath": "$(printf '%s' "${PRD_PATH:-}" | sed 's/\\/\\\\/g; s/"/\\"/g')",
|
|
5878
5880
|
"pid": $$,
|
|
5879
5881
|
"maxRetries": $MAX_RETRIES,
|
|
5880
5882
|
"baseWait": $BASE_WAIT
|
|
@@ -6843,7 +6845,9 @@ check_human_intervention() {
|
|
|
6843
6845
|
if [ -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED" ]; then
|
|
6844
6846
|
log_info "Council force-review requested from dashboard"
|
|
6845
6847
|
rm -f "$loki_dir/signals/COUNCIL_REVIEW_REQUESTED"
|
|
6846
|
-
if type
|
|
6848
|
+
if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
|
|
6849
|
+
log_info "Council force-review: blocked by checklist hard gate"
|
|
6850
|
+
elif type council_vote &>/dev/null && council_vote; then
|
|
6847
6851
|
log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
|
|
6848
6852
|
# BUG #17 fix: Write COMPLETED marker, generate council report, and
|
|
6849
6853
|
# run memory consolidation (matching the normal council approval path
|
|
@@ -7452,6 +7456,9 @@ main() {
|
|
|
7452
7456
|
audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
|
|
7453
7457
|
|
|
7454
7458
|
# Cleanup
|
|
7459
|
+
if type app_runner_cleanup &>/dev/null; then
|
|
7460
|
+
app_runner_cleanup
|
|
7461
|
+
fi
|
|
7455
7462
|
stop_dashboard
|
|
7456
7463
|
stop_status_monitor
|
|
7457
7464
|
rm -f .loki/loki.pid 2>/dev/null
|