loki-mode 7.77.0 → 7.78.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 CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.77.0
6
+ # Loki Mode v7.78.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
406
406
 
407
407
  ---
408
408
 
409
- **v7.77.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.78.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.77.0
1
+ 7.78.0
@@ -2,6 +2,15 @@
2
2
 
3
3
  This document describes how to integrate multi-channel notifications (Slack, Discord, Webhook) into run.sh.
4
4
 
5
+ > Note (v7.78.0): the convenience wrappers `notify_task_started`,
6
+ > `notify_task_completed`, `notify_task_failed`, `notify_phase_complete`, and
7
+ > `notify_all_complete` were never wired into the runtime and were removed as dead
8
+ > code. The live notification helpers are `notify_intervention_needed` and
9
+ > `notify_rate_limit` (plus the `notify_channels` snippets shown below). Snippets
10
+ > in this guide that reference the removed wrappers are illustrative of where a
11
+ > notification COULD be emitted; use `notify_channels` (or the live helpers) when
12
+ > actually wiring one.
13
+
5
14
  ## Quick Start
6
15
 
7
16
  ```bash
package/autonomy/run.sh CHANGED
@@ -40,7 +40,12 @@
40
40
  # LOKI_AUDIT_DISABLED - Disable audit logging (default: false)
41
41
  # LOKI_MAX_PARALLEL_AGENTS - Limit concurrent agent spawning (default: 10)
42
42
  # LOKI_SANDBOX_MODE - Run in sandboxed container (default: false, requires Docker)
43
- # LOKI_ALLOWED_PATHS - Comma-separated paths agents can modify (default: all)
43
+ # LOKI_ALLOWED_PATHS - Reserved: comma-separated path allowlist. NOT YET
44
+ # ENFORCED by the runtime (the value is read from
45
+ # config but no path-restriction check consumes it).
46
+ # Do not rely on it as a security control. Agent file
47
+ # writes are constrained by the OS user + container
48
+ # sandbox (LOKI_SANDBOX_MODE), not by this var.
44
49
  # LOKI_BLOCKED_COMMANDS - Comma-separated blocked shell commands (default: rm -rf /)
45
50
  #
46
51
  # OIDC / SSO Authentication (optional, works alongside token auth):
@@ -588,7 +593,7 @@ STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
588
593
  AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default)
589
594
  MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
590
595
  SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode (informational; the real dispatch reads LOKI_SANDBOX_MODE at autonomy/loki:1965 and execs sandbox.sh -- this var is not consumed in run.sh)
591
- ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # Empty = all paths allowed
596
+ ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # Reserved, NOT enforced (see header note). No runtime path check reads this; kept only so config plumbing + a future enforcement wiring have a single var to bind. Not a security control today.
592
597
  BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
593
598
 
594
599
  # Process Supervision (opt-in)
@@ -1902,41 +1907,7 @@ detect_complexity() {
1902
1907
  }
1903
1908
 
1904
1909
  # Get phases based on complexity tier
1905
- get_complexity_phases() {
1906
- case "$DETECTED_COMPLEXITY" in
1907
- simple)
1908
- echo "3"
1909
- ;;
1910
- standard)
1911
- echo "6"
1912
- ;;
1913
- complex)
1914
- echo "8"
1915
- ;;
1916
- *)
1917
- echo "6" # Default to standard
1918
- ;;
1919
- esac
1920
- }
1921
-
1922
1910
  # Get phase names based on complexity tier
1923
- get_phase_names() {
1924
- case "$DETECTED_COMPLEXITY" in
1925
- simple)
1926
- echo "IMPLEMENT TEST DEPLOY"
1927
- ;;
1928
- standard)
1929
- echo "RESEARCH DESIGN IMPLEMENT TEST REVIEW DEPLOY"
1930
- ;;
1931
- complex)
1932
- echo "RESEARCH ARCHITECTURE DESIGN IMPLEMENT TEST REVIEW SECURITY DEPLOY"
1933
- ;;
1934
- *)
1935
- echo "RESEARCH DESIGN IMPLEMENT TEST REVIEW DEPLOY"
1936
- ;;
1937
- esac
1938
- }
1939
-
1940
1911
  #===============================================================================
1941
1912
  # Dynamic Tier Selection (RARV-aware model routing)
1942
1913
  #===============================================================================
@@ -2629,32 +2600,6 @@ send_notification() {
2629
2600
  return 0
2630
2601
  }
2631
2602
 
2632
- # Convenience notification functions
2633
- notify_task_started() {
2634
- local task_name="$1"
2635
- send_notification "Task Started" "$task_name" "low"
2636
- }
2637
-
2638
- notify_task_completed() {
2639
- local task_name="$1"
2640
- send_notification "Task Completed" "$task_name" "normal"
2641
- }
2642
-
2643
- notify_task_failed() {
2644
- local task_name="$1"
2645
- local error="${2:-Unknown error}"
2646
- send_notification "Task Failed" "$task_name: $error" "critical"
2647
- }
2648
-
2649
- notify_phase_complete() {
2650
- local phase_name="$1"
2651
- send_notification "Phase Complete" "$phase_name" "normal"
2652
- }
2653
-
2654
- notify_all_complete() {
2655
- send_notification "All Tasks Complete" "Loki Mode has finished all tasks" "normal"
2656
- }
2657
-
2658
2603
  notify_intervention_needed() {
2659
2604
  local reason="$1"
2660
2605
  # Delegate-then-notify: this helper ONLY fires the (gated) desktop ping. It
@@ -3722,23 +3667,6 @@ init_parallel_streams() {
3722
3667
  }
3723
3668
 
3724
3669
  # Spawn feature worktree from task
3725
- spawn_feature_stream() {
3726
- local feature_name="$1"
3727
- local task_description="$2"
3728
-
3729
- # Check worktree limit
3730
- # BUG-PAR-012: Worktree count subtracts 1 for main (git worktree list includes main)
3731
- local worktree_count_raw=$(git -C "$TARGET_DIR" worktree list 2>/dev/null | wc -l)
3732
- local worktree_count=$((worktree_count_raw > 0 ? worktree_count_raw - 1 : 0))
3733
- if [ "$worktree_count" -ge "$MAX_WORKTREES" ]; then
3734
- log_warn "Max worktrees reached ($MAX_WORKTREES). Queuing feature: $feature_name"
3735
- return 1
3736
- fi
3737
-
3738
- create_worktree "feature-$feature_name" "feature/$feature_name"
3739
- spawn_worktree_session "feature-$feature_name" "$task_description"
3740
- }
3741
-
3742
3670
  # Cleanup all worktrees on exit
3743
3671
  cleanup_parallel_streams() {
3744
3672
  log_header "Cleaning Up Parallel Streams"
@@ -4352,55 +4280,6 @@ EOF
4352
4280
  LAST_KNOWN_PHASE=""
4353
4281
 
4354
4282
  # Set the current phase and emit event if changed
4355
- set_phase() {
4356
- local new_phase="$1"
4357
- local orch_file=".loki/state/orchestrator.json"
4358
-
4359
- mkdir -p .loki/state
4360
-
4361
- # Get current phase
4362
- local current_phase=""
4363
- if [ -f "$orch_file" ]; then
4364
- current_phase=$(python3 -c "import json; print(json.load(open('$orch_file')).get('currentPhase', ''))" 2>/dev/null || echo "")
4365
- fi
4366
-
4367
- # Only emit event if phase changed
4368
- if [ "$new_phase" != "$current_phase" ]; then
4369
- emit_event_json "phase_change" \
4370
- "from=$current_phase" \
4371
- "to=$new_phase" \
4372
- "iteration=$ITERATION_COUNT"
4373
-
4374
- log_info "Phase changed: $current_phase -> $new_phase"
4375
-
4376
- # Update orchestrator state (atomic via temp file + mv)
4377
- # BUG ARCH-001 fix: prevent state corruption if process is killed mid-write
4378
- if [ -f "$orch_file" ]; then
4379
- python3 -c "
4380
- import json, sys, os, tempfile
4381
- orch_file = sys.argv[1]
4382
- new_phase = sys.argv[2]
4383
- with open(orch_file, 'r') as f:
4384
- data = json.load(f)
4385
- data['currentPhase'] = new_phase
4386
- orch_dir = os.path.dirname(orch_file)
4387
- fd, tmp = tempfile.mkstemp(dir=orch_dir, suffix='.json')
4388
- with os.fdopen(fd, 'w') as f:
4389
- json.dump(data, f, indent=2)
4390
- os.replace(tmp, orch_file)
4391
- " "$orch_file" "$new_phase" 2>/dev/null || true
4392
- fi
4393
- fi
4394
-
4395
- LAST_KNOWN_PHASE="$new_phase"
4396
-
4397
- # v7.5.12: Append a structured log entry to the active iteration task so
4398
- # the dashboard shows per-phase progress (REASON / ACT / REFLECT / VERIFY).
4399
- # No-op if no iteration is active or queue file is missing/corrupt.
4400
- append_iteration_task_log "${ITERATION_COUNT:-0}" "$new_phase" "info" \
4401
- "Phase entered: $new_phase" 2>/dev/null || true
4402
- }
4403
-
4404
4283
  # v7.5.12: append a log entry to the iteration-N task in in-progress.json.
4405
4284
  # Args: iteration, phase, level, message. All silent on failure -- this
4406
4285
  # must NEVER kill the run.
@@ -5729,438 +5608,6 @@ stop_status_monitor() {
5729
5608
  # Web Dashboard
5730
5609
  #===============================================================================
5731
5610
 
5732
- generate_dashboard() {
5733
- # Copy dashboard from skill installation (v4.0.0 with Anthropic design language)
5734
- local skill_dashboard="$SCRIPT_DIR/.loki/dashboard/index.html"
5735
- local project_name=$(basename "$(pwd)")
5736
- local project_path=$(pwd)
5737
-
5738
- if [ -f "$skill_dashboard" ]; then
5739
- # v7.5.8: Escape sed-special chars in project_name/project_path before
5740
- # interpolating into the substitution RHS. Without this, a project
5741
- # whose path contains '|' (the chosen sed delimiter), '&' (RHS
5742
- # backref), '\' or '/' would either break the substitution or allow
5743
- # attacker-controlled text to be smuggled into the served HTML.
5744
- local project_name_sed project_path_sed
5745
- project_name_sed=$(printf '%s' "$project_name" | sed -e 's/[\\&|/]/\\&/g')
5746
- project_path_sed=$(printf '%s' "$project_path" | sed -e 's/[\\&|/]/\\&/g')
5747
-
5748
- # Copy and inject project info
5749
- sed -e "s|Loki Mode</title>|Loki Mode - ${project_name_sed}</title>|g" \
5750
- -e "s|<div class=\"project-name\" id=\"project-name\">--|<div class=\"project-name\" id=\"project-name\">${project_name_sed}|g" \
5751
- -e "s|<div class=\"project-path\" id=\"project-path\" title=\"\">--|<div class=\"project-path\" id=\"project-path\" title=\"${project_path_sed}\">${project_path_sed}|g" \
5752
- "$skill_dashboard" > .loki/dashboard/index.html
5753
- log_info "Dashboard copied from skill installation"
5754
- log_info "Project: $project_name ($project_path)"
5755
- return
5756
- fi
5757
-
5758
- # Fallback: Generate basic dashboard if external file not found
5759
- cat > .loki/dashboard/index.html << 'DASHBOARD_HTML'
5760
- <!DOCTYPE html>
5761
- <html lang="en">
5762
- <head>
5763
- <meta charset="UTF-8">
5764
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5765
- <title>Loki Mode Dashboard</title>
5766
- <style>
5767
- * { box-sizing: border-box; margin: 0; padding: 0; }
5768
- body {
5769
- font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
5770
- background: #FAF9F6;
5771
- color: #1A1A1A;
5772
- padding: 24px;
5773
- min-height: 100vh;
5774
- }
5775
- .header {
5776
- text-align: center;
5777
- padding: 32px 20px;
5778
- margin-bottom: 32px;
5779
- }
5780
- .header h1 {
5781
- color: #D97757;
5782
- font-size: 28px;
5783
- font-weight: 600;
5784
- letter-spacing: -0.5px;
5785
- margin-bottom: 8px;
5786
- }
5787
- .header .subtitle {
5788
- color: #666;
5789
- font-size: 14px;
5790
- font-weight: 400;
5791
- }
5792
- .header .phase {
5793
- display: inline-block;
5794
- margin-top: 16px;
5795
- padding: 8px 16px;
5796
- background: #FFF;
5797
- border: 1px solid #E5E3DE;
5798
- border-radius: 20px;
5799
- font-size: 13px;
5800
- color: #1A1A1A;
5801
- font-weight: 500;
5802
- }
5803
- .stats {
5804
- display: flex;
5805
- justify-content: center;
5806
- gap: 16px;
5807
- margin-bottom: 40px;
5808
- flex-wrap: wrap;
5809
- }
5810
- .stat {
5811
- background: #FFF;
5812
- border: 1px solid #E5E3DE;
5813
- border-radius: 12px;
5814
- padding: 20px 32px;
5815
- text-align: center;
5816
- min-width: 140px;
5817
- transition: box-shadow 0.2s ease;
5818
- }
5819
- .stat:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
5820
- .stat .number { font-size: 36px; font-weight: 600; margin-bottom: 4px; }
5821
- .stat .label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
5822
- .stat.pending .number { color: #D97757; }
5823
- .stat.progress .number { color: #5B8DEF; }
5824
- .stat.completed .number { color: #2E9E6E; }
5825
- .stat.failed .number { color: #D44F4F; }
5826
- .stat.agents .number { color: #9B6DD6; }
5827
- .section-header {
5828
- text-align: center;
5829
- font-size: 16px;
5830
- font-weight: 600;
5831
- color: #666;
5832
- margin: 40px 0 20px 0;
5833
- text-transform: uppercase;
5834
- letter-spacing: 1px;
5835
- }
5836
- .agents-grid {
5837
- display: grid;
5838
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
5839
- gap: 16px;
5840
- max-width: 1400px;
5841
- margin: 0 auto 40px auto;
5842
- }
5843
- .agent-card {
5844
- background: #FFF;
5845
- border: 1px solid #E5E3DE;
5846
- border-radius: 12px;
5847
- padding: 16px;
5848
- transition: box-shadow 0.2s ease, border-color 0.2s ease;
5849
- }
5850
- .agent-card:hover {
5851
- box-shadow: 0 4px 12px rgba(0,0,0,0.06);
5852
- border-color: #9B6DD6;
5853
- }
5854
- .agent-card .agent-header {
5855
- display: flex;
5856
- justify-content: space-between;
5857
- align-items: flex-start;
5858
- margin-bottom: 12px;
5859
- }
5860
- .agent-card .agent-id {
5861
- font-size: 11px;
5862
- color: #999;
5863
- font-family: monospace;
5864
- }
5865
- .agent-card .model-badge {
5866
- padding: 4px 10px;
5867
- border-radius: 6px;
5868
- font-size: 10px;
5869
- font-weight: 600;
5870
- text-transform: uppercase;
5871
- letter-spacing: 0.5px;
5872
- }
5873
- .agent-card .model-badge.sonnet {
5874
- background: #E8F0FD;
5875
- color: #5B8DEF;
5876
- }
5877
- .agent-card .model-badge.haiku {
5878
- background: #FFF4E6;
5879
- color: #F59E0B;
5880
- }
5881
- .agent-card .model-badge.opus {
5882
- background: #F3E8FF;
5883
- color: #9B6DD6;
5884
- }
5885
- .agent-card .agent-type {
5886
- font-size: 14px;
5887
- font-weight: 600;
5888
- color: #1A1A1A;
5889
- margin-bottom: 8px;
5890
- }
5891
- .agent-card .agent-status {
5892
- display: inline-block;
5893
- padding: 3px 8px;
5894
- border-radius: 4px;
5895
- font-size: 10px;
5896
- font-weight: 500;
5897
- margin-bottom: 12px;
5898
- }
5899
- .agent-card .agent-status.active {
5900
- background: #E6F5EE;
5901
- color: #2E9E6E;
5902
- }
5903
- .agent-card .agent-status.completed {
5904
- background: #F0EFEA;
5905
- color: #666;
5906
- }
5907
- .agent-card .agent-work {
5908
- font-size: 12px;
5909
- color: #666;
5910
- line-height: 1.5;
5911
- margin-bottom: 8px;
5912
- }
5913
- .agent-card .agent-meta {
5914
- display: flex;
5915
- gap: 12px;
5916
- font-size: 11px;
5917
- color: #999;
5918
- margin-top: 8px;
5919
- padding-top: 8px;
5920
- border-top: 1px solid #F0EFEA;
5921
- }
5922
- .agent-card .agent-meta span {
5923
- display: flex;
5924
- align-items: center;
5925
- gap: 4px;
5926
- }
5927
- .columns {
5928
- display: flex;
5929
- gap: 20px;
5930
- overflow-x: auto;
5931
- padding-bottom: 24px;
5932
- max-width: 1400px;
5933
- margin: 0 auto;
5934
- }
5935
- .column {
5936
- flex: 1;
5937
- min-width: 300px;
5938
- max-width: 350px;
5939
- background: #FFF;
5940
- border: 1px solid #E5E3DE;
5941
- border-radius: 12px;
5942
- padding: 20px;
5943
- }
5944
- .column h2 {
5945
- font-size: 13px;
5946
- font-weight: 600;
5947
- color: #666;
5948
- margin-bottom: 16px;
5949
- display: flex;
5950
- align-items: center;
5951
- gap: 10px;
5952
- text-transform: uppercase;
5953
- letter-spacing: 0.5px;
5954
- }
5955
- .column h2 .count {
5956
- background: #F0EFEA;
5957
- padding: 3px 10px;
5958
- border-radius: 12px;
5959
- font-size: 11px;
5960
- color: #1A1A1A;
5961
- }
5962
- .column.pending h2 .count { background: #FCEEE8; color: #D97757; }
5963
- .column.progress h2 .count { background: #E8F0FD; color: #5B8DEF; }
5964
- .column.completed h2 .count { background: #E6F5EE; color: #2E9E6E; }
5965
- .column.failed h2 .count { background: #FCE8E8; color: #D44F4F; }
5966
- .task {
5967
- background: #FAF9F6;
5968
- border: 1px solid #E5E3DE;
5969
- border-radius: 8px;
5970
- padding: 14px;
5971
- margin-bottom: 12px;
5972
- transition: border-color 0.2s ease;
5973
- }
5974
- .task:hover { border-color: #D97757; }
5975
- .task .id { font-size: 10px; color: #999; margin-bottom: 6px; font-family: monospace; }
5976
- .task .type {
5977
- display: inline-block;
5978
- background: #FCEEE8;
5979
- color: #D97757;
5980
- padding: 3px 10px;
5981
- border-radius: 4px;
5982
- font-size: 11px;
5983
- font-weight: 500;
5984
- margin-bottom: 8px;
5985
- }
5986
- .task .title { font-size: 13px; color: #1A1A1A; line-height: 1.5; }
5987
- .task .error {
5988
- font-size: 11px;
5989
- color: #D44F4F;
5990
- margin-top: 10px;
5991
- padding: 10px;
5992
- background: #FCE8E8;
5993
- border-radius: 6px;
5994
- font-family: monospace;
5995
- }
5996
- .refresh {
5997
- position: fixed;
5998
- bottom: 24px;
5999
- right: 24px;
6000
- background: #D97757;
6001
- color: white;
6002
- border: none;
6003
- padding: 12px 24px;
6004
- border-radius: 8px;
6005
- cursor: pointer;
6006
- font-size: 14px;
6007
- font-weight: 500;
6008
- transition: background 0.2s ease;
6009
- box-shadow: 0 4px 12px rgba(217, 119, 87, 0.3);
6010
- }
6011
- .refresh:hover { background: #C56747; }
6012
- .updated {
6013
- text-align: center;
6014
- color: #999;
6015
- font-size: 12px;
6016
- margin-top: 24px;
6017
- }
6018
- .empty {
6019
- color: #999;
6020
- font-size: 13px;
6021
- text-align: center;
6022
- padding: 24px;
6023
- font-style: italic;
6024
- }
6025
- .powered-by {
6026
- text-align: center;
6027
- margin-top: 40px;
6028
- padding-top: 24px;
6029
- border-top: 1px solid #E5E3DE;
6030
- color: #999;
6031
- font-size: 12px;
6032
- }
6033
- .powered-by span { color: #D97757; font-weight: 500; }
6034
- </style>
6035
- </head>
6036
- <body>
6037
- <div class="header">
6038
- <h1>LOKI MODE</h1>
6039
- <div class="subtitle">Autonomous Spec-to-Product System</div>
6040
- <div class="phase" id="phase">Loading...</div>
6041
- </div>
6042
- <div class="stats">
6043
- <div class="stat agents"><div class="number" id="agents-count">-</div><div class="label">Active Agents</div></div>
6044
- <div class="stat pending"><div class="number" id="pending-count">-</div><div class="label">Pending</div></div>
6045
- <div class="stat progress"><div class="number" id="progress-count">-</div><div class="label">In Progress</div></div>
6046
- <div class="stat completed"><div class="number" id="completed-count">-</div><div class="label">Completed</div></div>
6047
- <div class="stat failed"><div class="number" id="failed-count">-</div><div class="label">Failed</div></div>
6048
- </div>
6049
- <div class="section-header">Active Agents</div>
6050
- <div class="agents-grid" id="agents-grid"></div>
6051
- <div class="section-header">Task Queue</div>
6052
- <div class="columns">
6053
- <div class="column pending"><h2>Pending <span class="count" id="pending-badge">0</span></h2><div id="pending-tasks"></div></div>
6054
- <div class="column progress"><h2>In Progress <span class="count" id="progress-badge">0</span></h2><div id="progress-tasks"></div></div>
6055
- <div class="column completed"><h2>Completed <span class="count" id="completed-badge">0</span></h2><div id="completed-tasks"></div></div>
6056
- <div class="column failed"><h2>Failed <span class="count" id="failed-badge">0</span></h2><div id="failed-tasks"></div></div>
6057
- </div>
6058
- <div class="updated" id="updated">Last updated: -</div>
6059
- <div class="powered-by">Powered by <span>${PROVIDER_DISPLAY_NAME:-Claude}</span></div>
6060
- <button class="refresh" onclick="loadData()">Refresh</button>
6061
- <script>
6062
- async function loadJSON(path) {
6063
- try {
6064
- const res = await fetch(path + '?t=' + Date.now());
6065
- if (!res.ok) return [];
6066
- const text = await res.text();
6067
- if (!text.trim()) return [];
6068
- const data = JSON.parse(text);
6069
- return Array.isArray(data) ? data : (data.tasks || data.agents || []);
6070
- } catch { return []; }
6071
- }
6072
- function getModelClass(model) {
6073
- if (!model) return 'sonnet';
6074
- const m = model.toLowerCase();
6075
- if (m.includes('haiku')) return 'haiku';
6076
- if (m.includes('opus')) return 'opus';
6077
- return 'sonnet';
6078
- }
6079
- function formatDuration(isoDate) {
6080
- if (!isoDate) return 'Unknown';
6081
- const start = new Date(isoDate);
6082
- const now = new Date();
6083
- const seconds = Math.floor((now - start) / 1000);
6084
- if (seconds < 60) return seconds + 's';
6085
- if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
6086
- return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
6087
- }
6088
- function renderAgent(agent) {
6089
- const modelClass = getModelClass(agent.model);
6090
- const modelName = agent.model || 'Sonnet 4.5';
6091
- const agentType = agent.agent_type || 'general-purpose';
6092
- const status = agent.status === 'completed' ? 'completed' : 'active';
6093
- const currentTask = agent.current_task || (agent.tasks_completed && agent.tasks_completed.length > 0
6094
- ? 'Completed: ' + agent.tasks_completed.join(', ')
6095
- : 'Initializing...');
6096
- const duration = formatDuration(agent.spawned_at);
6097
- const tasksCount = agent.tasks_completed ? agent.tasks_completed.length : 0;
6098
-
6099
- return `
6100
- <div class="agent-card">
6101
- <div class="agent-header">
6102
- <div class="agent-id">${agent.agent_id || 'Unknown'}</div>
6103
- <div class="model-badge ${modelClass}">${modelName}</div>
6104
- </div>
6105
- <div class="agent-type">${agentType}</div>
6106
- <div class="agent-status ${status}">${status}</div>
6107
- <div class="agent-work">${currentTask}</div>
6108
- <div class="agent-meta">
6109
- <span>${duration}</span>
6110
- <span>${tasksCount} tasks</span>
6111
- </div>
6112
- </div>
6113
- `;
6114
- }
6115
- function renderTask(task) {
6116
- const payload = task.payload || {};
6117
- const title = payload.description || payload.action || task.type || 'Task';
6118
- const error = task.lastError ? `<div class="error">${task.lastError}</div>` : '';
6119
- return `<div class="task"><div class="id">${task.id}</div><span class="type">${task.type || 'general'}</span><div class="title">${title}</div>${error}</div>`;
6120
- }
6121
- async function loadData() {
6122
- const [pending, progress, completed, failed, agents] = await Promise.all([
6123
- loadJSON('../queue/pending.json'),
6124
- loadJSON('../queue/in-progress.json'),
6125
- loadJSON('../queue/completed.json'),
6126
- loadJSON('../queue/failed.json'),
6127
- loadJSON('../state/agents.json')
6128
- ]);
6129
-
6130
- // Agent stats
6131
- document.getElementById('agents-count').textContent = agents.length;
6132
- document.getElementById('agents-grid').innerHTML = agents.length
6133
- ? agents.map(renderAgent).join('')
6134
- : '<div class="empty">No active agents</div>';
6135
-
6136
- // Task stats
6137
- document.getElementById('pending-count').textContent = pending.length;
6138
- document.getElementById('progress-count').textContent = progress.length;
6139
- document.getElementById('completed-count').textContent = completed.length;
6140
- document.getElementById('failed-count').textContent = failed.length;
6141
- document.getElementById('pending-badge').textContent = pending.length;
6142
- document.getElementById('progress-badge').textContent = progress.length;
6143
- document.getElementById('completed-badge').textContent = completed.length;
6144
- document.getElementById('failed-badge').textContent = failed.length;
6145
- document.getElementById('pending-tasks').innerHTML = pending.length ? pending.map(renderTask).join('') : '<div class="empty">No pending tasks</div>';
6146
- document.getElementById('progress-tasks').innerHTML = progress.length ? progress.map(renderTask).join('') : '<div class="empty">No tasks in progress</div>';
6147
- document.getElementById('completed-tasks').innerHTML = completed.length ? completed.slice(-10).reverse().map(renderTask).join('') : '<div class="empty">No completed tasks</div>';
6148
- document.getElementById('failed-tasks').innerHTML = failed.length ? failed.map(renderTask).join('') : '<div class="empty">No failed tasks</div>';
6149
-
6150
- try {
6151
- const state = await fetch('../state/orchestrator.json?t=' + Date.now()).then(r => r.json());
6152
- document.getElementById('phase').textContent = 'Phase: ' + (state.currentPhase || 'UNKNOWN');
6153
- } catch { document.getElementById('phase').textContent = 'Phase: UNKNOWN'; }
6154
- document.getElementById('updated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
6155
- }
6156
- loadData();
6157
- setInterval(loadData, 3000);
6158
- </script>
6159
- </body>
6160
- </html>
6161
- DASHBOARD_HTML
6162
- }
6163
-
6164
5611
  update_agents_state() {
6165
5612
  # Aggregate agent information from .agent/sub-agents/*.json into .loki/state/agents.json
6166
5613
  local agents_dir=".agent/sub-agents"
@@ -6853,34 +6300,6 @@ init_learnings_db() {
6853
6300
  log_info "Learnings database initialized at: $learnings_dir"
6854
6301
  }
6855
6302
 
6856
- save_learning() {
6857
- # Save a learning to the cross-project database
6858
- local learning_type="$1" # pattern, mistake, success
6859
- local category="$2"
6860
- local description="$3"
6861
- local project="${4:-$(basename "$(pwd)")}"
6862
-
6863
- local learnings_dir="${HOME}/.loki/learnings"
6864
- local target_file="$learnings_dir/${learning_type}s.jsonl"
6865
-
6866
- if [ ! -d "$learnings_dir" ]; then
6867
- init_learnings_db
6868
- fi
6869
-
6870
- local learning_entry
6871
- if command -v jq >/dev/null 2>&1; then
6872
- learning_entry=$(jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg proj "$project" --arg cat "$category" --arg desc "$description" '{timestamp:$ts,project:$proj,category:$cat,description:$desc}')
6873
- else
6874
- local safe_proj safe_cat safe_desc
6875
- safe_proj=$(printf '%s' "$project" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
6876
- safe_cat=$(printf '%s' "$category" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
6877
- safe_desc=$(printf '%s' "$description" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
6878
- learning_entry="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":\"$safe_proj\",\"category\":\"$safe_cat\",\"description\":\"$safe_desc\"}"
6879
- fi
6880
- echo "$learning_entry" >> "$target_file"
6881
- log_info "Saved $learning_type: $category"
6882
- }
6883
-
6884
6303
  get_relevant_learnings() {
6885
6304
  # Get learnings relevant to the current context
6886
6305
  local context="$1"
@@ -7882,24 +7301,6 @@ with open(gate_file, 'w') as f:
7882
7301
  " 2>/dev/null || true
7883
7302
  }
7884
7303
 
7885
- get_gate_failure_count() {
7886
- local gate_name="$1"
7887
- local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
7888
- [ -f "$gate_file" ] || { echo "0"; return; }
7889
-
7890
- _GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
7891
- import json, os
7892
- gate_file = os.environ['_GATE_FILE']
7893
- gate_name = os.environ['_GATE_NAME']
7894
- try:
7895
- with open(gate_file) as f:
7896
- counts = json.load(f)
7897
- print(counts.get(gate_name, 0))
7898
- except (json.JSONDecodeError, FileNotFoundError, OSError):
7899
- print(0)
7900
- " 2>/dev/null || echo "0"
7901
- }
7902
-
7903
7304
  # ============================================================================
7904
7305
  # Hard Quality Gate: Test Coverage (v6.7.0)
7905
7306
  # Detects test runner and runs tests with coverage reporting
@@ -10102,182 +9503,6 @@ DA_AGG_PATCH
10102
9503
  # Only runs when complexity >= standard (6+ agents).
10103
9504
  #===============================================================================
10104
9505
 
10105
- run_adversarial_testing() {
10106
- local loki_dir="${TARGET_DIR:-.}/.loki"
10107
- local adversarial_dir="$loki_dir/quality/adversarial"
10108
- local test_id
10109
- test_id="adversarial-$(date -u +%Y%m%dT%H%M%SZ)-${ITERATION_COUNT:-0}"
10110
- mkdir -p "$adversarial_dir/$test_id"
10111
-
10112
- # Only run for Standard+ complexity
10113
- local complexity="${LOKI_COMPLEXITY:-auto}"
10114
- if [ "$complexity" = "simple" ]; then
10115
- log_debug "Adversarial testing skipped: simple complexity tier"
10116
- return 0
10117
- fi
10118
-
10119
- # Check if adversarial testing is disabled
10120
- if [ "${LOKI_ADVERSARIAL_TESTING:-true}" = "false" ]; then
10121
- log_debug "Adversarial testing disabled via LOKI_ADVERSARIAL_TESTING=false"
10122
- return 0
10123
- fi
10124
-
10125
- log_header "ADVERSARIAL TESTING: $test_id"
10126
-
10127
- # Get diff for adversarial analysis
10128
- local diff_content
10129
- diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached 2>/dev/null || echo "")
10130
- if [ -z "$diff_content" ]; then
10131
- log_info "Adversarial testing: No diff to test, skipping"
10132
- return 0
10133
- fi
10134
-
10135
- local changed_files
10136
- changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
10137
-
10138
- # Write analysis files -- use printf to prevent shell variable expansion (#78)
10139
- local diff_file="$adversarial_dir/$test_id/diff.txt"
10140
- local files_file="$adversarial_dir/$test_id/files.txt"
10141
- printf '%s\n' "$diff_content" > "$diff_file"
10142
- printf '%s\n' "$changed_files" > "$files_file"
10143
-
10144
- # Build adversarial prompt -- use heredoc with quoted delimiter to prevent
10145
- # shell variable expansion in diff content (fixes #78)
10146
- local files_content changed_content
10147
- files_content=$(cat "$files_file")
10148
- changed_content=$(head -500 "$diff_file")
10149
- local adversarial_prompt
10150
- read -r -d '' adversarial_prompt <<'ADVERSARIAL_EOF' || true
10151
- You are an ADVERSARIAL TESTER. Your goal is to BREAK the implementation.
10152
-
10153
- CHANGED FILES:
10154
- __FILES_PLACEHOLDER__
10155
-
10156
- DIFF:
10157
- __DIFF_PLACEHOLDER__
10158
-
10159
- YOUR MISSION:
10160
- 1. Find edge cases that will cause crashes or incorrect behavior
10161
- 2. Identify inputs that bypass validation
10162
- 3. Find race conditions or concurrency issues
10163
- 4. Discover security vulnerabilities (injection, auth bypass, SSRF)
10164
- 5. Find resource exhaustion vectors (unbounded loops, memory leaks)
10165
- 6. Identify error handling gaps (missing try/catch, unchecked returns)
10166
-
10167
- OUTPUT FORMAT (STRICT):
10168
- ATTACK_VECTORS:
10169
- - [severity] [category] description | reproduction steps
10170
- Severity: Critical, High, Medium, Low
10171
- Category: crash, security, correctness, performance, resource
10172
-
10173
- SUGGESTED_TESTS:
10174
- - Test description that would catch this issue
10175
-
10176
- OVERALL_RISK: HIGH or MEDIUM or LOW
10177
- ADVERSARIAL_EOF
10178
- # Substitute placeholders with actual content (safe from shell expansion)
10179
- adversarial_prompt="${adversarial_prompt/__FILES_PLACEHOLDER__/$files_content}"
10180
- adversarial_prompt="${adversarial_prompt/__DIFF_PLACEHOLDER__/$changed_content}"
10181
-
10182
- local result_file="$adversarial_dir/$test_id/result.txt"
10183
-
10184
- # Run adversarial agent
10185
- log_info "Spawning adversarial agent..."
10186
- case "${PROVIDER_NAME:-claude}" in
10187
- claude)
10188
- if command -v claude &>/dev/null; then
10189
- # EMBED 2 + 3 (v7.33.0). Adversarial probe subcall.
10190
- # $adversarial_prompt is fully self-contained (instructions +
10191
- # changed files + diff inlined via the heredoc above) and output
10192
- # is captured to $result_file. So:
10193
- # EMBED 2 (--bare): no hooks/LSP/CLAUDE.md/MCP needed; cheaper.
10194
- # Opt out LOKI_BARE_SUBCALLS=0.
10195
- # EMBED 3 (--disallowedTools): keep an adversarial agent from
10196
- # casually mutating the tree. Deny Edit/Write/NotebookEdit +
10197
- # git mutation forms (incl. git -C / --git-dir evasions);
10198
- # read-only git stays allowed. Guardrail, not a sandbox.
10199
- # Opt out LOKI_REVIEW_TOOL_GUARD=0.
10200
- local _adv_argv=("--dangerously-skip-permissions")
10201
- if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
10202
- _adv_argv+=("--bare")
10203
- fi
10204
- if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
10205
- _adv_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
10206
- fi
10207
- # EMBED 3b (--allowedTools, #167): positive least-privilege
10208
- # allowlist. DEFAULT OFF (opt-in LOKI_REVIEW_ALLOWLIST=1).
10209
- # Emitted ALONGSIDE the denylist (deny precedence verified
10210
- # live, holds under --dangerously-skip-permissions). See
10211
- # loki_review_allowlist.
10212
- if type loki_review_allowlist_enabled >/dev/null 2>&1 && loki_review_allowlist_enabled; then
10213
- _adv_argv+=("--allowedTools" "$(loki_review_allowlist)")
10214
- fi
10215
- # caveman HARD-SUPPRESS (parsed output): the adversarial probe's
10216
- # output is parsed for findings/severity. Disable caveman
10217
- # unconditionally (CAVEMAN_DEFAULT_MODE=off) so compression cannot
10218
- # drop or reword a finding. No-op when caveman is absent.
10219
- CAVEMAN_DEFAULT_MODE=off \
10220
- claude "${_adv_argv[@]}" -p "$adversarial_prompt" \
10221
- --output-format text > "$result_file" 2>/dev/null || true
10222
- fi
10223
- ;;
10224
- codex)
10225
- if command -v codex &>/dev/null; then
10226
- codex exec --sandbox workspace-write --skip-git-repo-check "$adversarial_prompt" \
10227
- > "$result_file" 2>/dev/null || true
10228
- fi
10229
- ;;
10230
- cline)
10231
- if command -v cline &>/dev/null; then
10232
- invoke_cline_capture "$adversarial_prompt" \
10233
- > "$result_file" 2>/dev/null || true
10234
- fi
10235
- ;;
10236
- aider)
10237
- if command -v aider &>/dev/null; then
10238
- invoke_aider_capture "$adversarial_prompt" \
10239
- > "$result_file" 2>/dev/null || true
10240
- fi
10241
- ;;
10242
- *)
10243
- echo "ATTACK_VECTORS: None (unknown provider)" > "$result_file"
10244
- echo "OVERALL_RISK: LOW" >> "$result_file"
10245
- ;;
10246
- esac
10247
-
10248
- if [ ! -s "$result_file" ]; then
10249
- log_warn "Adversarial agent produced no output"
10250
- return 0
10251
- fi
10252
-
10253
- # Parse risk level
10254
- local risk_level
10255
- risk_level=$(grep -i "OVERALL_RISK:" "$result_file" | head -1 | sed 's/.*OVERALL_RISK:[[:space:]]*//' | awk '{print toupper($1)}')
10256
-
10257
- # Count critical/high attack vectors
10258
- local critical_count high_count
10259
- critical_count=$(grep -ci "\[critical\]" "$result_file" 2>/dev/null || echo "0")
10260
- high_count=$(grep -ci "\[high\]" "$result_file" 2>/dev/null || echo "0")
10261
-
10262
- log_info "Adversarial testing complete: risk=$risk_level, critical=$critical_count, high=$high_count"
10263
-
10264
- emit_event_json "adversarial_test_complete" \
10265
- "test_id=$test_id" \
10266
- "risk_level=${risk_level:-UNKNOWN}" \
10267
- "critical_count=$critical_count" \
10268
- "high_count=$high_count" \
10269
- "iteration=$ITERATION_COUNT"
10270
-
10271
- # Block on critical findings
10272
- if [ "$critical_count" -gt 0 ]; then
10273
- log_error "ADVERSARIAL TEST BLOCKED: $critical_count critical attack vectors found"
10274
- log_error "Details: $adversarial_dir/$test_id/result.txt"
10275
- return 1
10276
- fi
10277
-
10278
- return 0
10279
- }
10280
-
10281
9506
  load_solutions_context() {
10282
9507
  # Load relevant structured solutions for the current task context
10283
9508
  local context="$1"
@@ -10567,118 +9792,6 @@ print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':
10567
9792
  _LAST_CHECKPOINT_ID="$checkpoint_id"
10568
9793
  }
10569
9794
 
10570
- rollback_to_checkpoint() {
10571
- # Rollback state files to a specific checkpoint
10572
- # Args: $1 = checkpoint_id
10573
- local checkpoint_id="$1"
10574
- local checkpoint_dir=".loki/state/checkpoints"
10575
-
10576
- # Validate checkpoint ID (prevent path traversal)
10577
- if [[ ! "$checkpoint_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
10578
- log_error "Invalid checkpoint ID: must be alphanumeric, hyphens, underscores only"
10579
- return 1
10580
- fi
10581
-
10582
- local cp_dir="${checkpoint_dir}/${checkpoint_id}"
10583
-
10584
- if [ ! -d "$cp_dir" ]; then
10585
- log_error "Checkpoint not found: ${checkpoint_id}"
10586
- return 1
10587
- fi
10588
-
10589
- # Read checkpoint metadata
10590
- local git_sha
10591
- git_sha=$(_CP_META="${cp_dir}/metadata.json" python3 -c "import json, os; print(json.load(open(os.environ['_CP_META']))['git_sha'])" 2>/dev/null || echo "")
10592
-
10593
- log_warn "Rolling back to checkpoint: ${checkpoint_id}"
10594
-
10595
- # R6 re-undoability invariant: force a pre-rollback snapshot of CURRENT state
10596
- # before overwriting, even if the git tree is clean (the .loki/ state we are
10597
- # about to clobber is not git-tracked). _LOKI_CP_FORCE bypasses the clean-tree
10598
- # guard. Capture the snapshot id so we can tell the user how to undo the undo.
10599
- _LOKI_CP_FORCE=1 create_checkpoint "pre-rollback snapshot (before restoring ${checkpoint_id})" "rollback"
10600
- local pre_rollback_id="${_LAST_CHECKPOINT_ID:-}"
10601
- if [ -n "$pre_rollback_id" ]; then
10602
- log_info "Saved prior state as ${pre_rollback_id} (undo this rollback with: loki rollback to ${pre_rollback_id})"
10603
- fi
10604
-
10605
- # Restore state files (R6: CONTINUITY.md restores iteration/conversation context)
10606
- for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json CONTINUITY.md; do
10607
- if [ -f "${cp_dir}/${f}" ]; then
10608
- local target_dir=".loki/$(dirname "$f")"
10609
- mkdir -p "$target_dir"
10610
- cp "${cp_dir}/${f}" ".loki/${f}" 2>/dev/null || true
10611
- fi
10612
- done
10613
-
10614
- # Log the rollback (use python3 for safe JSON serialization)
10615
- # v7.5.10: route through safe_append_event_jsonl() so parallel-worktree
10616
- # rollbacks cannot interleave partial JSONL lines (POSIX append is
10617
- # only atomic for <PIPE_BUF and not all platforms honor it).
10618
- local timestamp rb_event
10619
- timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
10620
- rb_event=$(_RB_CPID="$checkpoint_id" _RB_SHA="$git_sha" _RB_TS="$timestamp" \
10621
- python3 -c "
10622
- import json,os
10623
- print(json.dumps({'event':'rollback','checkpoint':os.environ['_RB_CPID'],'git_sha':os.environ['_RB_SHA'],'timestamp':os.environ['_RB_TS']}))
10624
- " 2>/dev/null) || rb_event=""
10625
- if [ -n "$rb_event" ]; then
10626
- # Source the emit lib once per call to get safe_append_event_jsonl.
10627
- # Lib-only mode skips the emit script's normal CLI execution.
10628
- # shellcheck disable=SC1091
10629
- if [ -z "${_LOKI_EMIT_LIB_LOADED:-}" ]; then
10630
- LOKI_EMIT_LIB_ONLY=1 . "$(dirname "${BASH_SOURCE[0]}")/../events/emit.sh" 2>/dev/null \
10631
- && _LOKI_EMIT_LIB_LOADED=1
10632
- fi
10633
- if declare -f safe_append_event_jsonl >/dev/null 2>&1; then
10634
- safe_append_event_jsonl ".loki/events.jsonl" "$rb_event" 2>/dev/null || true
10635
- else
10636
- # Last-resort fallback: bare append (preserves prior behavior).
10637
- printf '%s\n' "$rb_event" >> ".loki/events.jsonl" 2>/dev/null || true
10638
- fi
10639
- fi
10640
-
10641
- log_info "State files restored from checkpoint: ${checkpoint_id}"
10642
-
10643
- # R6: the prior hint `git reset --hard ${git_sha}` was MISLEADING. git_sha is
10644
- # HEAD (the last commit), and Loki does not commit per iteration, so a hard
10645
- # reset would discard the iteration's work rather than reconstruct it. The
10646
- # correct, durable recovery is the anchored working-tree snapshot, if present.
10647
- if [ -f "${cp_dir}/worktree-snapshot.txt" ]; then
10648
- log_info "To also restore the working tree to this checkpoint:"
10649
- log_info " git stash apply refs/loki/cp/${checkpoint_id}"
10650
- elif [ -n "$git_sha" ] && [ "$git_sha" != "no-git" ]; then
10651
- log_info "Git SHA at checkpoint (last commit): ${git_sha}"
10652
- log_info "Note: no working-tree snapshot was captured for this checkpoint;"
10653
- log_info "code changes since the last commit are not restorable from here."
10654
- fi
10655
- }
10656
-
10657
- list_checkpoints() {
10658
- # List recent checkpoints
10659
- local checkpoint_dir=".loki/state/checkpoints"
10660
- local index_file="${checkpoint_dir}/index.jsonl"
10661
- local limit="${1:-10}"
10662
-
10663
- if [ ! -f "$index_file" ]; then
10664
- echo "No checkpoints found."
10665
- return
10666
- fi
10667
-
10668
- tail -n "$limit" "$index_file" | python3 -c "
10669
- import sys, json
10670
- lines = sys.stdin.readlines()
10671
- for line in reversed(lines):
10672
- try:
10673
- cp = json.loads(line)
10674
- sha = cp.get('sha','')[:8]
10675
- task = cp.get('task','')[:60]
10676
- print(f\" {cp['id']} {cp['ts']} [{sha}] {task}\")
10677
- except:
10678
- continue
10679
- "
10680
- }
10681
-
10682
9795
  start_dashboard() {
10683
9796
  log_header "Starting Loki Dashboard"
10684
9797
 
@@ -11928,17 +11041,6 @@ except Exception as e:
11928
11041
  }
11929
11042
 
11930
11043
  # Load relevant learnings
11931
- load_learnings_context() {
11932
- local learnings=""
11933
-
11934
- # Get recent learnings (last 7 days)
11935
- for learning in $(find .loki/memory/learnings -name "*.md" -mtime -7 2>/dev/null | head -5); do
11936
- learnings+="$(head -30 "$learning")\n---\n"
11937
- done
11938
-
11939
- echo -e "$learnings"
11940
- }
11941
-
11942
11044
  # Load pre-computed relevant learnings from CLI startup (SYN-008)
11943
11045
  # Reads .loki/state/memory-context.json written by load_memory_context() in CLI
11944
11046
  # Note: Different from get_relevant_learnings() which writes to relevant-learnings.json
@@ -12700,75 +11802,7 @@ PYEOF
12700
11802
  #===============================================================================
12701
11803
 
12702
11804
  # Enrich prompt context with relevant cross-project patterns
12703
- enrich_from_knowledge_graph() {
12704
- local context="$1"
12705
- local max_patterns="${2:-5}"
12706
-
12707
- _LOKI_KG_CONTEXT="$context" _LOKI_KG_MAX="$max_patterns" \
12708
- _LOKI_PROJECT_DIR="$PROJECT_DIR" \
12709
- python3 << 'PYEOF' 2>/dev/null || echo ""
12710
- import sys
12711
- import os
12712
- import json
12713
-
12714
- project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
12715
- context = os.environ.get('_LOKI_KG_CONTEXT', '')
12716
- max_results = int(os.environ.get('_LOKI_KG_MAX', '5'))
12717
-
12718
- if not project_dir:
12719
- sys.exit(0)
12720
- sys.path.insert(0, project_dir)
12721
- try:
12722
- from memory.knowledge_graph import OrganizationKnowledgeGraph
12723
- kg = OrganizationKnowledgeGraph()
12724
- patterns = kg.query_patterns(context, max_results=max_results)
12725
- if patterns:
12726
- output = "\n## Cross-Project Knowledge (from knowledge graph)\n"
12727
- for p in patterns:
12728
- name = p.get('name', p.get('pattern', 'unnamed'))
12729
- category = p.get('category', '')
12730
- desc = p.get('description', '')
12731
- output += f"- **{name}** ({category}): {desc}\n"
12732
- print(output)
12733
- except Exception:
12734
- pass
12735
- PYEOF
12736
- }
12737
-
12738
11805
  # Store new patterns to the knowledge graph after successful iterations
12739
- store_to_knowledge_graph() {
12740
- local target_dir="${TARGET_DIR:-.}"
12741
-
12742
- _LOKI_PROJECT_DIR="$PROJECT_DIR" _LOKI_TARGET_DIR="$target_dir" \
12743
- python3 << 'PYEOF' 2>/dev/null || true
12744
- import sys
12745
- import os
12746
-
12747
- project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
12748
- target_dir = os.environ.get('_LOKI_TARGET_DIR', '.')
12749
-
12750
- sys.path.insert(0, project_dir)
12751
- try:
12752
- from memory.knowledge_graph import OrganizationKnowledgeGraph
12753
- from pathlib import Path
12754
-
12755
- kg = OrganizationKnowledgeGraph()
12756
- project_dirs = [Path(target_dir)]
12757
-
12758
- # Extract and store patterns
12759
- patterns = kg.extract_patterns(project_dirs)
12760
- if patterns:
12761
- patterns = kg.deduplicate_patterns(patterns)
12762
- kg.save_patterns(patterns)
12763
-
12764
- # Rebuild graph
12765
- kg.build_graph(project_dirs)
12766
- kg.save_graph()
12767
- except Exception:
12768
- pass
12769
- PYEOF
12770
- }
12771
-
12772
11806
  #===============================================================================
12773
11807
  # Save/Load Wrapper State
12774
11808
  #===============================================================================
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.77.0"
10
+ __version__ = "7.78.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.77.0
5
+ **Version:** v7.78.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.77.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.78.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.77.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
2
+ var QQ=Object.defineProperty;var ZQ=($)=>$;function zQ($,Q){this[$]=ZQ.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)QQ($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:zQ.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var h1={};b(h1,{lokiDir:()=>P,homeLokiDir:()=>i$,findRepoRootForVersion:()=>t$,REPO_ROOT:()=>g});import{resolve as a,dirname as r$}from"path";import{fileURLToPath as XQ}from"url";import{existsSync as R$}from"fs";import{homedir as KQ}from"os";function qQ(){let $=b1;for(let Q=0;Q<6;Q++){if(R$(a($,"VERSION"))&&R$(a($,"autonomy/run.sh")))return $;let Z=r$($);if(Z===$)break;$=Z}return a(b1,"..","..","..")}function t$($){let Q=$;for(let Z=0;Z<6;Z++){if(R$(a(Q,"VERSION"))&&R$(a(Q,"autonomy/run.sh")))return Q;let z=r$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function P(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function i$(){return a(KQ(),".loki")}var b1,g;var C=L(()=>{b1=r$(XQ(import.meta.url));g=qQ()});import{readFileSync as VQ}from"fs";import{resolve as JQ,dirname as UQ}from"path";import{fileURLToPath as WQ}from"url";function E$(){if(Q$!==null)return Q$;let $="7.78.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=UQ(WQ(import.meta.url)),Z=t$(Q);Q$=VQ(JQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var e$=L(()=>{C()});var g1={};b(g1,{runOrThrow:()=>HQ,run:()=>F,readStreamCapped:()=>m1,commandVersion:()=>BQ,commandExists:()=>f,ShellError:()=>$1,MAX_STDOUT_BYTES:()=>v1});async function m1($,Q=v1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:U}=await Z.read();if(K)break;if(!U)continue;if(q+=U.byteLength,q>Q){let J=U.byteLength-(q-Q);X+=z.decode(U.subarray(0,J),{stream:!0});break}X+=z.decode(U,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,U]=await Promise.all([m1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function HQ($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new $1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=GQ($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function GQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function BQ($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var v1=16777216,$1;var d=L(()=>{$1=class $1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return YQ?"":$}var YQ,O,S,_,_Z,I,k,h,V;var c=L(()=>{YQ=(process.env.NO_COLOR??"").length>0;O=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),k=s("\x1B[1m"),h=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as jQ}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(jQ($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var Y$;var V$=L(()=>{d()});var X0={};b(X0,{runStatus:()=>oQ});import{existsSync as y,readFileSync as U$,readdirSync as r1,statSync as t1}from"fs";import{resolve as D,basename as vQ}from"path";import{homedir as mQ}from"os";function i1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function e1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*N$/Q);if(X>N$)X=N$;let q=N$-X,K=S;if(z>=80)K=O;else if(z>=50)K=_;let U="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=i1($),W=i1(Q);return` ${k}${Z}${V} ${K}[${U}]${V} ${z}% (${J} / ${W})`}async function fQ(){if(await f("jq"))return!0;return process.stdout.write(`${O}Error: jq is required but not installed.${V}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -793,4 +793,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
793
793
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
794
794
  `),process.stderr.write($Q),2}}s1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
795
795
 
796
- //# debugId=3E9A351BF52A74EB64756E2164756E21
796
+ //# debugId=753B9888BD0ED74B64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.77.0'
60
+ __version__ = '7.78.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.77.0",
4
+ "version": "7.78.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.77.0",
5
+ "version": "7.78.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",