loki-mode 5.48.2 → 5.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +58 -4
- package/autonomy/hooks/validate-bash.sh +11 -1
- package/autonomy/run.sh +129 -0
- package/autonomy/sandbox.sh +9 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
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.49.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -263,4 +263,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
263
263
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
264
264
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
265
265
|
|
|
266
|
-
**v5.
|
|
266
|
+
**v5.49.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.49.0
|
|
@@ -45,6 +45,14 @@ COUNCIL_MIN_ITERATIONS=${LOKI_COUNCIL_MIN_ITERATIONS:-3}
|
|
|
45
45
|
COUNCIL_CONVERGENCE_WINDOW=${LOKI_COUNCIL_CONVERGENCE_WINDOW:-3}
|
|
46
46
|
COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}
|
|
47
47
|
|
|
48
|
+
# Error budget: severity-aware completion (v5.49.0)
|
|
49
|
+
# SEVERITY_THRESHOLD: minimum severity that blocks completion (critical, high, medium, low)
|
|
50
|
+
# "critical" = only critical issues block (most permissive)
|
|
51
|
+
# "low" = all issues block (strictest, default for backwards compat)
|
|
52
|
+
# ERROR_BUDGET: fraction of non-blocking issues allowed (0.0 = none, 0.1 = 10% tolerance)
|
|
53
|
+
COUNCIL_SEVERITY_THRESHOLD=${LOKI_COUNCIL_SEVERITY_THRESHOLD:-low}
|
|
54
|
+
COUNCIL_ERROR_BUDGET=${LOKI_COUNCIL_ERROR_BUDGET:-0.0}
|
|
55
|
+
|
|
48
56
|
# Internal state
|
|
49
57
|
COUNCIL_STATE_DIR=""
|
|
50
58
|
COUNCIL_PRD_PATH=""
|
|
@@ -235,6 +243,38 @@ council_vote() {
|
|
|
235
243
|
local vote_result
|
|
236
244
|
vote_result=$(echo "$verdict" | grep -oE "VOTE:\s*(APPROVE|REJECT)" | grep -oE "APPROVE|REJECT" | head -1)
|
|
237
245
|
|
|
246
|
+
# Extract severity-categorized issues (v5.49.0 error budget)
|
|
247
|
+
local member_issues=""
|
|
248
|
+
member_issues=$(echo "$verdict" | grep -oE "ISSUES:\s*(CRITICAL|HIGH|MEDIUM|LOW):.*" || true)
|
|
249
|
+
|
|
250
|
+
# If error budget is active and member rejected, check if rejection
|
|
251
|
+
# is based only on issues below the severity threshold
|
|
252
|
+
if [ "$vote_result" = "REJECT" ] && [ "$COUNCIL_SEVERITY_THRESHOLD" != "low" ] && [ -n "$member_issues" ]; then
|
|
253
|
+
local has_blocking_issue=false
|
|
254
|
+
local severity_order="critical high medium low"
|
|
255
|
+
local threshold_reached=false
|
|
256
|
+
|
|
257
|
+
while IFS= read -r issue_line; do
|
|
258
|
+
local issue_severity
|
|
259
|
+
issue_severity=$(echo "$issue_line" | grep -oE "(CRITICAL|HIGH|MEDIUM|LOW)" | head -1 | tr '[:upper:]' '[:lower:]')
|
|
260
|
+
# Check if this severity meets or exceeds the threshold
|
|
261
|
+
for sev in $severity_order; do
|
|
262
|
+
if [ "$sev" = "$COUNCIL_SEVERITY_THRESHOLD" ]; then
|
|
263
|
+
threshold_reached=true
|
|
264
|
+
fi
|
|
265
|
+
if [ "$sev" = "$issue_severity" ] && [ "$threshold_reached" = "false" ]; then
|
|
266
|
+
has_blocking_issue=true
|
|
267
|
+
break
|
|
268
|
+
fi
|
|
269
|
+
done
|
|
270
|
+
done <<< "$member_issues"
|
|
271
|
+
|
|
272
|
+
if [ "$has_blocking_issue" = "false" ]; then
|
|
273
|
+
log_info " Member $member ($role): REJECT overridden to APPROVE (issues below ${COUNCIL_SEVERITY_THRESHOLD} threshold)"
|
|
274
|
+
vote_result="APPROVE"
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
|
|
238
278
|
if [ "$vote_result" = "APPROVE" ]; then
|
|
239
279
|
((approve_count++))
|
|
240
280
|
log_info " Member $member ($role): APPROVE"
|
|
@@ -618,23 +658,37 @@ council_member_review() {
|
|
|
618
658
|
;;
|
|
619
659
|
esac
|
|
620
660
|
|
|
661
|
+
local severity_instruction=""
|
|
662
|
+
if [ "$COUNCIL_SEVERITY_THRESHOLD" != "low" ]; then
|
|
663
|
+
severity_instruction="
|
|
664
|
+
ERROR BUDGET: This council uses severity-aware evaluation.
|
|
665
|
+
- Categorize each issue as CRITICAL, HIGH, MEDIUM, or LOW severity
|
|
666
|
+
- Blocking threshold: ${COUNCIL_SEVERITY_THRESHOLD} and above
|
|
667
|
+
- Only issues at ${COUNCIL_SEVERITY_THRESHOLD} severity or above should cause REJECT
|
|
668
|
+
- Issues below threshold are acceptable (error budget: ${COUNCIL_ERROR_BUDGET})
|
|
669
|
+
- List issues as ISSUES: SEVERITY:description (one per line)"
|
|
670
|
+
fi
|
|
671
|
+
|
|
621
672
|
local prompt="You are a council member reviewing project completion.
|
|
622
673
|
|
|
623
674
|
${role_instruction}
|
|
624
675
|
|
|
625
676
|
EVIDENCE:
|
|
626
677
|
${evidence}
|
|
678
|
+
${severity_instruction}
|
|
627
679
|
|
|
628
680
|
INSTRUCTIONS:
|
|
629
681
|
1. Review the evidence carefully
|
|
630
682
|
2. Determine if the project meets completion criteria
|
|
631
683
|
3. Output EXACTLY one line starting with VOTE:APPROVE or VOTE:REJECT
|
|
632
684
|
4. Output EXACTLY one line starting with REASON: explaining your decision
|
|
633
|
-
5.
|
|
685
|
+
5. If issues found, output lines starting with ISSUES: SEVERITY:description
|
|
686
|
+
6. Be honest - do not approve incomplete work
|
|
634
687
|
|
|
635
|
-
Output format
|
|
688
|
+
Output format:
|
|
636
689
|
VOTE:APPROVE or VOTE:REJECT
|
|
637
|
-
REASON: your reasoning here
|
|
690
|
+
REASON: your reasoning here
|
|
691
|
+
ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
638
692
|
|
|
639
693
|
local verdict_file="$vote_dir/member-${member_id}.txt"
|
|
640
694
|
|
|
@@ -1300,5 +1354,5 @@ council_get_dashboard_state() {
|
|
|
1300
1354
|
state_json=$(cat "$COUNCIL_STATE_DIR/state.json" 2>/dev/null || echo "{}")
|
|
1301
1355
|
fi
|
|
1302
1356
|
|
|
1303
|
-
echo "\"council\": {\"enabled\": true, \"size\": $COUNCIL_SIZE, \"threshold\": $COUNCIL_THRESHOLD, \"check_interval\": $COUNCIL_CHECK_INTERVAL, \"consecutive_no_change\": $COUNCIL_CONSECUTIVE_NO_CHANGE, \"done_signals\": $COUNCIL_DONE_SIGNALS, \"iteration\": $ITERATION_COUNT, \"state\": $state_json}"
|
|
1357
|
+
echo "\"council\": {\"enabled\": true, \"size\": $COUNCIL_SIZE, \"threshold\": $COUNCIL_THRESHOLD, \"check_interval\": $COUNCIL_CHECK_INTERVAL, \"consecutive_no_change\": $COUNCIL_CONSECUTIVE_NO_CHANGE, \"done_signals\": $COUNCIL_DONE_SIGNALS, \"iteration\": $ITERATION_COUNT, \"severity_threshold\": \"$COUNCIL_SEVERITY_THRESHOLD\", \"error_budget\": $COUNCIL_ERROR_BUDGET, \"state\": $state_json}"
|
|
1304
1358
|
}
|
|
@@ -30,11 +30,21 @@ BLOCKED_PATTERNS=(
|
|
|
30
30
|
"wget.*\|.*sh"
|
|
31
31
|
"curl.*\|.*bash"
|
|
32
32
|
"wget.*\|.*bash"
|
|
33
|
+
# Config self-protection: prevent agents from corrupting internal state
|
|
34
|
+
"rm -rf \.loki"
|
|
35
|
+
"rm -rf \./\.loki"
|
|
36
|
+
"rm .*\.loki/council/"
|
|
37
|
+
"rm .*\.loki/config\.yaml"
|
|
38
|
+
"rm .*\.loki/logs/bash-audit"
|
|
39
|
+
"rm .*\.loki/session\.lock"
|
|
40
|
+
"> \.loki/council/"
|
|
41
|
+
"> \.loki/config\.yaml"
|
|
33
42
|
)
|
|
34
43
|
|
|
35
|
-
# Safe path patterns that override
|
|
44
|
+
# Safe path patterns that override blocked pattern matches
|
|
36
45
|
SAFE_PATTERNS=(
|
|
37
46
|
"rm -rf /tmp/"
|
|
47
|
+
"rm -rf \.loki/queue/dead-letter"
|
|
38
48
|
)
|
|
39
49
|
|
|
40
50
|
# Check for blocked patterns
|
package/autonomy/run.sh
CHANGED
|
@@ -5681,6 +5681,132 @@ load_handoff_context() {
|
|
|
5681
5681
|
fi
|
|
5682
5682
|
}
|
|
5683
5683
|
|
|
5684
|
+
# Write structured handoff document (v5.49.0)
|
|
5685
|
+
# Produces both JSON (machine-readable) and markdown (human-readable) handoffs
|
|
5686
|
+
# Called at end of session or before context clear
|
|
5687
|
+
write_structured_handoff() {
|
|
5688
|
+
local reason="${1:-session_end}"
|
|
5689
|
+
local handoff_dir=".loki/memory/handoffs"
|
|
5690
|
+
mkdir -p "$handoff_dir"
|
|
5691
|
+
|
|
5692
|
+
local timestamp
|
|
5693
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
5694
|
+
local file_ts
|
|
5695
|
+
file_ts=$(date +"%Y%m%d-%H%M%S")
|
|
5696
|
+
local handoff_json="$handoff_dir/${file_ts}.json"
|
|
5697
|
+
local handoff_md="$handoff_dir/${file_ts}.md"
|
|
5698
|
+
|
|
5699
|
+
# Gather structured data
|
|
5700
|
+
local files_modified=""
|
|
5701
|
+
files_modified=$(git diff --name-only HEAD 2>/dev/null | head -20 | tr '\n' ',' | sed 's/,$//')
|
|
5702
|
+
local recent_commits=""
|
|
5703
|
+
recent_commits=$(git log --oneline -5 2>/dev/null | tr '\n' '|' | sed 's/|$//')
|
|
5704
|
+
local pending_tasks=0
|
|
5705
|
+
local completed_tasks=0
|
|
5706
|
+
if [ -f ".loki/queue/pending.json" ]; then
|
|
5707
|
+
pending_tasks=$(_QF=".loki/queue/pending.json" python3 -c "import json,os;print(len(json.load(open(os.environ['_QF']))))" 2>/dev/null || echo "0")
|
|
5708
|
+
fi
|
|
5709
|
+
if [ -f ".loki/queue/completed.json" ]; then
|
|
5710
|
+
completed_tasks=$(_QF=".loki/queue/completed.json" python3 -c "import json,os;print(len(json.load(open(os.environ['_QF']))))" 2>/dev/null || echo "0")
|
|
5711
|
+
fi
|
|
5712
|
+
|
|
5713
|
+
# Write JSON handoff
|
|
5714
|
+
_H_TS="$timestamp" \
|
|
5715
|
+
_H_REASON="$reason" \
|
|
5716
|
+
_H_ITER="${ITERATION_COUNT:-0}" \
|
|
5717
|
+
_H_FILES="$files_modified" \
|
|
5718
|
+
_H_COMMITS="$recent_commits" \
|
|
5719
|
+
_H_PENDING="$pending_tasks" \
|
|
5720
|
+
_H_COMPLETED="$completed_tasks" \
|
|
5721
|
+
_H_JSON="$handoff_json" \
|
|
5722
|
+
python3 -c "
|
|
5723
|
+
import json, os
|
|
5724
|
+
handoff = {
|
|
5725
|
+
'schema_version': '1.0.0',
|
|
5726
|
+
'timestamp': os.environ['_H_TS'],
|
|
5727
|
+
'reason': os.environ['_H_REASON'],
|
|
5728
|
+
'iteration': int(os.environ['_H_ITER']),
|
|
5729
|
+
'files_modified': [f for f in os.environ['_H_FILES'].split(',') if f],
|
|
5730
|
+
'recent_commits': [c for c in os.environ['_H_COMMITS'].split('|') if c],
|
|
5731
|
+
'task_status': {
|
|
5732
|
+
'pending': int(os.environ['_H_PENDING']),
|
|
5733
|
+
'completed': int(os.environ['_H_COMPLETED'])
|
|
5734
|
+
},
|
|
5735
|
+
'open_questions': [],
|
|
5736
|
+
'key_decisions': [],
|
|
5737
|
+
'blockers': []
|
|
5738
|
+
}
|
|
5739
|
+
with open(os.environ['_H_JSON'], 'w') as f:
|
|
5740
|
+
json.dump(handoff, f, indent=2)
|
|
5741
|
+
" 2>/dev/null || log_warn "Failed to write structured handoff JSON"
|
|
5742
|
+
|
|
5743
|
+
# Write markdown companion
|
|
5744
|
+
cat > "$handoff_md" << HANDOFF_EOF
|
|
5745
|
+
# Session Handoff - $timestamp
|
|
5746
|
+
|
|
5747
|
+
**Reason:** $reason
|
|
5748
|
+
**Iteration:** ${ITERATION_COUNT:-0}
|
|
5749
|
+
|
|
5750
|
+
## Files Modified
|
|
5751
|
+
$files_modified
|
|
5752
|
+
|
|
5753
|
+
## Recent Commits
|
|
5754
|
+
$(git log --oneline -5 2>/dev/null || echo "none")
|
|
5755
|
+
|
|
5756
|
+
## Task Status
|
|
5757
|
+
- Pending: $pending_tasks
|
|
5758
|
+
- Completed: $completed_tasks
|
|
5759
|
+
|
|
5760
|
+
## Notes
|
|
5761
|
+
Session handoff generated automatically.
|
|
5762
|
+
HANDOFF_EOF
|
|
5763
|
+
|
|
5764
|
+
log_info "Structured handoff written to $handoff_json"
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
# Load recent handoffs for context (reads both JSON and markdown)
|
|
5768
|
+
load_handoff_context() {
|
|
5769
|
+
local handoff_content=""
|
|
5770
|
+
|
|
5771
|
+
# Prefer JSON handoffs (structured, v5.49.0+)
|
|
5772
|
+
local recent_json
|
|
5773
|
+
recent_json=$(find .loki/memory/handoffs -name "*.json" -mtime -1 2>/dev/null | sort -r | head -1)
|
|
5774
|
+
|
|
5775
|
+
if [ -n "$recent_json" ] && [ -f "$recent_json" ]; then
|
|
5776
|
+
handoff_content=$(_HF="$recent_json" python3 -c "
|
|
5777
|
+
import json, os
|
|
5778
|
+
try:
|
|
5779
|
+
h = json.load(open(os.environ['_HF']))
|
|
5780
|
+
parts = []
|
|
5781
|
+
parts.append(f\"Handoff from {h.get('timestamp','unknown')} (reason: {h.get('reason','unknown')})\")
|
|
5782
|
+
parts.append(f\"Iteration: {h.get('iteration',0)}\")
|
|
5783
|
+
files = h.get('files_modified', [])
|
|
5784
|
+
if files:
|
|
5785
|
+
parts.append(f\"Modified files: {', '.join(files[:10])}\")
|
|
5786
|
+
tasks = h.get('task_status', {})
|
|
5787
|
+
parts.append(f\"Tasks - pending: {tasks.get('pending',0)}, completed: {tasks.get('completed',0)}\")
|
|
5788
|
+
for q in h.get('open_questions', []):
|
|
5789
|
+
parts.append(f\"Open question: {q}\")
|
|
5790
|
+
for b in h.get('blockers', []):
|
|
5791
|
+
parts.append(f\"Blocker: {b}\")
|
|
5792
|
+
print(' | '.join(parts))
|
|
5793
|
+
except Exception as e:
|
|
5794
|
+
print(f'Error reading handoff: {e}')
|
|
5795
|
+
" 2>/dev/null)
|
|
5796
|
+
echo "$handoff_content"
|
|
5797
|
+
return
|
|
5798
|
+
fi
|
|
5799
|
+
|
|
5800
|
+
# Fallback to markdown handoffs (pre-v5.49.0)
|
|
5801
|
+
local recent_handoff
|
|
5802
|
+
recent_handoff=$(find .loki/memory/handoffs -name "*.md" -mtime -1 2>/dev/null | sort -r | head -1)
|
|
5803
|
+
|
|
5804
|
+
if [ -n "$recent_handoff" ] && [ -f "$recent_handoff" ]; then
|
|
5805
|
+
handoff_content=$(cat "$recent_handoff" | head -80)
|
|
5806
|
+
echo "$handoff_content"
|
|
5807
|
+
fi
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5684
5810
|
# Load relevant learnings
|
|
5685
5811
|
load_learnings_context() {
|
|
5686
5812
|
local learnings=""
|
|
@@ -7489,6 +7615,9 @@ main() {
|
|
|
7489
7615
|
--context "{\"provider\":\"${PROVIDER_NAME:-claude}\",\"iterations\":$ITERATION_COUNT,\"exit_code\":$result}"
|
|
7490
7616
|
fi
|
|
7491
7617
|
|
|
7618
|
+
# Write structured handoff for future sessions (v5.49.0)
|
|
7619
|
+
write_structured_handoff "session_end_result_${result}" 2>/dev/null || true
|
|
7620
|
+
|
|
7492
7621
|
# Create PR from agent branch if branch protection was enabled
|
|
7493
7622
|
create_session_pr
|
|
7494
7623
|
audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
|
package/autonomy/sandbox.sh
CHANGED
|
@@ -843,6 +843,15 @@ start_sandbox() {
|
|
|
843
843
|
docker_args+=("--volume" "$PROJECT_DIR:/workspace:rw")
|
|
844
844
|
fi
|
|
845
845
|
|
|
846
|
+
# Config self-protection: mount critical .loki/ paths as read-only
|
|
847
|
+
# Prevents agents from corrupting council state, config, or audit trail
|
|
848
|
+
if [[ -d "$PROJECT_DIR/.loki/council" ]]; then
|
|
849
|
+
docker_args+=("--volume" "$PROJECT_DIR/.loki/council:/workspace/.loki/council:ro")
|
|
850
|
+
fi
|
|
851
|
+
if [[ -f "$PROJECT_DIR/.loki/config.yaml" ]]; then
|
|
852
|
+
docker_args+=("--volume" "$PROJECT_DIR/.loki/config.yaml:/workspace/.loki/config.yaml:ro")
|
|
853
|
+
fi
|
|
854
|
+
|
|
846
855
|
# Mount git config (read-only) - mount to /home/loki since container runs as user loki
|
|
847
856
|
if [[ -f "$HOME/.gitconfig" ]]; then
|
|
848
857
|
docker_args+=("--volume" "$HOME/.gitconfig:/home/loki/.gitconfig:ro")
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED