loki-mode 7.6.0 → 7.6.2
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/loki +145 -5
- package/autonomy/run.sh +95 -11
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +29 -4
- package/dashboard/static/index.html +15 -7
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +85 -85
- 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 a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.6.
|
|
6
|
+
# Loki Mode v7.6.2
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
381
|
|
|
382
382
|
---
|
|
383
383
|
|
|
384
|
-
**v7.6.
|
|
384
|
+
**v7.6.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.6.
|
|
1
|
+
7.6.2
|
package/autonomy/loki
CHANGED
|
@@ -1869,6 +1869,23 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
1869
1869
|
|
|
1870
1870
|
# Kill orphaned processes from crashed sessions
|
|
1871
1871
|
cmd_cleanup() {
|
|
1872
|
+
# v7.6.2 B-13 fix: --help must print help, not run cleanup as a side effect.
|
|
1873
|
+
case "${1:-}" in
|
|
1874
|
+
--help|-h|help)
|
|
1875
|
+
echo -e "${BOLD}Loki Mode -- cleanup orphaned processes${NC}"
|
|
1876
|
+
echo ""
|
|
1877
|
+
echo "Usage: loki cleanup [options]"
|
|
1878
|
+
echo ""
|
|
1879
|
+
echo "Scans \$LOKI_DIR/pids/ for stale PID registrations from prior runs"
|
|
1880
|
+
echo "and kills processes whose PID file points at a dead process."
|
|
1881
|
+
echo ""
|
|
1882
|
+
echo "Options:"
|
|
1883
|
+
echo " --help, -h Show this help and exit"
|
|
1884
|
+
echo ""
|
|
1885
|
+
echo "Side effects: kills processes registered under .loki/pids/."
|
|
1886
|
+
return 0
|
|
1887
|
+
;;
|
|
1888
|
+
esac
|
|
1872
1889
|
local pids_dir="$LOKI_DIR/pids"
|
|
1873
1890
|
local killed=0
|
|
1874
1891
|
local stale=0
|
|
@@ -1949,6 +1966,23 @@ cmd_cleanup() {
|
|
|
1949
1966
|
|
|
1950
1967
|
# Pause after current session
|
|
1951
1968
|
cmd_pause() {
|
|
1969
|
+
# v7.6.2 B-13 fix: --help must print help, not act on session state.
|
|
1970
|
+
case "${1:-}" in
|
|
1971
|
+
--help|-h|help)
|
|
1972
|
+
echo -e "${BOLD}Loki Mode -- pause active session${NC}"
|
|
1973
|
+
echo ""
|
|
1974
|
+
echo "Usage: loki pause [options]"
|
|
1975
|
+
echo ""
|
|
1976
|
+
echo "Marks the active session as paused. The runner detects this on"
|
|
1977
|
+
echo "the next iteration boundary and halts cleanly until 'loki resume'."
|
|
1978
|
+
echo ""
|
|
1979
|
+
echo "Options:"
|
|
1980
|
+
echo " --help, -h Show this help and exit"
|
|
1981
|
+
echo ""
|
|
1982
|
+
echo "See also: loki resume, loki stop"
|
|
1983
|
+
return 0
|
|
1984
|
+
;;
|
|
1985
|
+
esac
|
|
1952
1986
|
if [ ! -d "$LOKI_DIR" ]; then
|
|
1953
1987
|
echo -e "${YELLOW}No .loki directory found.${NC}"
|
|
1954
1988
|
echo "No active session to pause."
|
|
@@ -1990,6 +2024,23 @@ cmd_pause() {
|
|
|
1990
2024
|
|
|
1991
2025
|
# Resume paused execution
|
|
1992
2026
|
cmd_resume() {
|
|
2027
|
+
# v7.6.2 B-13 fix: --help must print help, not act on session state.
|
|
2028
|
+
case "${1:-}" in
|
|
2029
|
+
--help|-h|help)
|
|
2030
|
+
echo -e "${BOLD}Loki Mode -- resume paused session${NC}"
|
|
2031
|
+
echo ""
|
|
2032
|
+
echo "Usage: loki resume [options]"
|
|
2033
|
+
echo ""
|
|
2034
|
+
echo "Clears the paused flag and lets the autonomous runner continue"
|
|
2035
|
+
echo "from where 'loki pause' stopped it."
|
|
2036
|
+
echo ""
|
|
2037
|
+
echo "Options:"
|
|
2038
|
+
echo " --help, -h Show this help and exit"
|
|
2039
|
+
echo ""
|
|
2040
|
+
echo "See also: loki pause, loki start"
|
|
2041
|
+
return 0
|
|
2042
|
+
;;
|
|
2043
|
+
esac
|
|
1993
2044
|
if [ ! -d "$LOKI_DIR" ]; then
|
|
1994
2045
|
echo -e "${YELLOW}No .loki directory found.${NC}"
|
|
1995
2046
|
echo "No session to resume. Start a session with: loki start"
|
|
@@ -2428,6 +2479,25 @@ print(json.dumps(result, indent=2))
|
|
|
2428
2479
|
|
|
2429
2480
|
# Session statistics
|
|
2430
2481
|
cmd_stats() {
|
|
2482
|
+
# v7.6.2 B-13 fix: --help must print help, not render the stats panel.
|
|
2483
|
+
case "${1:-}" in
|
|
2484
|
+
--help|-h|help)
|
|
2485
|
+
echo -e "${BOLD}Loki Mode -- session statistics${NC}"
|
|
2486
|
+
echo ""
|
|
2487
|
+
echo "Usage: loki stats [options]"
|
|
2488
|
+
echo ""
|
|
2489
|
+
echo "Prints session-level statistics: tasks completed, iterations,"
|
|
2490
|
+
echo "tokens used, cost, council votes, gate verdicts."
|
|
2491
|
+
echo ""
|
|
2492
|
+
echo "Options:"
|
|
2493
|
+
echo " --json JSON output (machine-readable)"
|
|
2494
|
+
echo " --efficiency Include cost/token efficiency breakdown"
|
|
2495
|
+
echo " --help, -h Show this help and exit"
|
|
2496
|
+
echo ""
|
|
2497
|
+
echo "See also: loki kpis, loki status"
|
|
2498
|
+
return 0
|
|
2499
|
+
;;
|
|
2500
|
+
esac
|
|
2431
2501
|
local show_json=false
|
|
2432
2502
|
local show_efficiency=false
|
|
2433
2503
|
|
|
@@ -4083,6 +4153,23 @@ cmd_web_status() {
|
|
|
4083
4153
|
|
|
4084
4154
|
# Import GitHub issues
|
|
4085
4155
|
cmd_import() {
|
|
4156
|
+
# v7.6.2 B-13 fix: --help must print help, not start an import.
|
|
4157
|
+
case "${1:-}" in
|
|
4158
|
+
--help|-h|help)
|
|
4159
|
+
echo -e "${BOLD}Loki Mode -- import GitHub issues into the queue${NC}"
|
|
4160
|
+
echo ""
|
|
4161
|
+
echo "Usage: loki import [options]"
|
|
4162
|
+
echo ""
|
|
4163
|
+
echo "Imports issues from the current repo's GitHub project into"
|
|
4164
|
+
echo ".loki/queue/ so loki start picks them up as tasks."
|
|
4165
|
+
echo ""
|
|
4166
|
+
echo "Options:"
|
|
4167
|
+
echo " --help, -h Show this help and exit"
|
|
4168
|
+
echo ""
|
|
4169
|
+
echo "Side effects: makes GitHub API calls, writes to .loki/queue/."
|
|
4170
|
+
return 0
|
|
4171
|
+
;;
|
|
4172
|
+
esac
|
|
4086
4173
|
if [ ! -d "$LOKI_DIR" ]; then
|
|
4087
4174
|
mkdir -p "$LOKI_DIR/queue"
|
|
4088
4175
|
fi
|
|
@@ -6289,6 +6376,23 @@ cmd_config_path() {
|
|
|
6289
6376
|
|
|
6290
6377
|
# Set up skill symlinks for all providers
|
|
6291
6378
|
cmd_setup_skill() {
|
|
6379
|
+
# v7.6.2 B-13 fix: --help must print help, not install skills.
|
|
6380
|
+
case "${1:-}" in
|
|
6381
|
+
--help|-h|help)
|
|
6382
|
+
echo -e "${BOLD}Loki Mode -- install Loki skill into provider CLIs${NC}"
|
|
6383
|
+
echo ""
|
|
6384
|
+
echo "Usage: loki setup-skill [options]"
|
|
6385
|
+
echo ""
|
|
6386
|
+
echo "Installs the Loki Mode skill into each detected provider CLI's"
|
|
6387
|
+
echo "skills directory: ~/.claude/skills/, ~/.codex/skills/, etc."
|
|
6388
|
+
echo ""
|
|
6389
|
+
echo "Options:"
|
|
6390
|
+
echo " --help, -h Show this help and exit"
|
|
6391
|
+
echo ""
|
|
6392
|
+
echo "Side effects: writes symlinks/files under ~/.<provider>/skills/loki-mode/."
|
|
6393
|
+
return 0
|
|
6394
|
+
;;
|
|
6395
|
+
esac
|
|
6292
6396
|
echo -e "${BOLD}Loki Mode Skill Setup${NC}"
|
|
6293
6397
|
echo ""
|
|
6294
6398
|
|
|
@@ -6915,7 +7019,9 @@ cmd_doctor() {
|
|
|
6915
7019
|
|
|
6916
7020
|
# JSON output for loki doctor --json
|
|
6917
7021
|
cmd_doctor_json() {
|
|
6918
|
-
|
|
7022
|
+
local _loki_version
|
|
7023
|
+
_loki_version=$(get_version)
|
|
7024
|
+
LOKI_VERSION="$_loki_version" python3 -c "
|
|
6919
7025
|
import json, os, subprocess, sys, shutil
|
|
6920
7026
|
|
|
6921
7027
|
def get_version(cmd, args=None):
|
|
@@ -7008,6 +7114,7 @@ elif disk_status == 'fail': fail_count += 1
|
|
|
7008
7114
|
elif disk_status == 'warn': warn_count += 1
|
|
7009
7115
|
|
|
7010
7116
|
result = {
|
|
7117
|
+
'loki_mode_version': os.environ.get('LOKI_VERSION', 'unknown'),
|
|
7011
7118
|
'checks': checks,
|
|
7012
7119
|
'disk': {
|
|
7013
7120
|
'available_gb': disk_gb,
|
|
@@ -7228,6 +7335,18 @@ SENTRUX_RULES_EOF
|
|
|
7228
7335
|
|
|
7229
7336
|
# Show version
|
|
7230
7337
|
cmd_version() {
|
|
7338
|
+
# v7.6.2 B-13 fix: --help prints help; bare invocation prints version.
|
|
7339
|
+
case "${1:-}" in
|
|
7340
|
+
--help|-h|help)
|
|
7341
|
+
echo -e "${BOLD}Loki Mode -- print version${NC}"
|
|
7342
|
+
echo ""
|
|
7343
|
+
echo "Usage: loki version"
|
|
7344
|
+
echo ""
|
|
7345
|
+
echo "Prints 'Loki Mode vX.Y.Z' from the VERSION file shipped with"
|
|
7346
|
+
echo "the package. Equivalent to 'loki --version' and 'loki -v'."
|
|
7347
|
+
return 0
|
|
7348
|
+
;;
|
|
7349
|
+
esac
|
|
7231
7350
|
echo "Loki Mode v$(get_version)"
|
|
7232
7351
|
}
|
|
7233
7352
|
|
|
@@ -12132,10 +12251,10 @@ main() {
|
|
|
12132
12251
|
cmd_cleanup "$@"
|
|
12133
12252
|
;;
|
|
12134
12253
|
pause)
|
|
12135
|
-
cmd_pause
|
|
12254
|
+
cmd_pause "$@"
|
|
12136
12255
|
;;
|
|
12137
12256
|
resume)
|
|
12138
|
-
cmd_resume
|
|
12257
|
+
cmd_resume "$@"
|
|
12139
12258
|
;;
|
|
12140
12259
|
status)
|
|
12141
12260
|
cmd_status "$@"
|
|
@@ -12153,6 +12272,27 @@ main() {
|
|
|
12153
12272
|
cmd_logs "$@"
|
|
12154
12273
|
;;
|
|
12155
12274
|
serve)
|
|
12275
|
+
# v7.6.2 B-12 fix: 'serve --help' previously routed to 'cmd_api start --help'
|
|
12276
|
+
# which started the dashboard as a side effect instead of printing help.
|
|
12277
|
+
case "${1:-}" in
|
|
12278
|
+
--help|-h|help)
|
|
12279
|
+
echo -e "${BOLD}Loki Mode -- start dashboard API server${NC}"
|
|
12280
|
+
echo ""
|
|
12281
|
+
echo "Usage: loki serve [options]"
|
|
12282
|
+
echo ""
|
|
12283
|
+
echo "Alias for 'loki api start'. Starts the dashboard HTTP API"
|
|
12284
|
+
echo "server (FastAPI, port 57374 by default)."
|
|
12285
|
+
echo ""
|
|
12286
|
+
echo "Options:"
|
|
12287
|
+
echo " --host HOST Bind host (default: 127.0.0.1)"
|
|
12288
|
+
echo " --port PORT Bind port (default: 57374)"
|
|
12289
|
+
echo " --help, -h Show this help and exit"
|
|
12290
|
+
echo ""
|
|
12291
|
+
echo "Side effects: starts a long-running HTTP server bound to a TCP port."
|
|
12292
|
+
echo "Stop with: loki dashboard stop"
|
|
12293
|
+
return 0
|
|
12294
|
+
;;
|
|
12295
|
+
esac
|
|
12156
12296
|
cmd_api start "$@"
|
|
12157
12297
|
;;
|
|
12158
12298
|
api)
|
|
@@ -12165,7 +12305,7 @@ main() {
|
|
|
12165
12305
|
cmd_notify "$@"
|
|
12166
12306
|
;;
|
|
12167
12307
|
import)
|
|
12168
|
-
cmd_import
|
|
12308
|
+
cmd_import "$@"
|
|
12169
12309
|
;;
|
|
12170
12310
|
github)
|
|
12171
12311
|
cmd_github "$@"
|
|
@@ -12310,7 +12450,7 @@ main() {
|
|
|
12310
12450
|
cmd_code "$@"
|
|
12311
12451
|
;;
|
|
12312
12452
|
version|--version|-v)
|
|
12313
|
-
cmd_version
|
|
12453
|
+
cmd_version "$@"
|
|
12314
12454
|
;;
|
|
12315
12455
|
completions)
|
|
12316
12456
|
cmd_completions "$@"
|
package/autonomy/run.sh
CHANGED
|
@@ -5634,13 +5634,29 @@ enforce_static_analysis() {
|
|
|
5634
5634
|
if [ "$_ts_project_mode" -eq 1 ]; then
|
|
5635
5635
|
continue
|
|
5636
5636
|
fi
|
|
5637
|
+
# v7.6.2 B-18 fix: previously skipped TS/TSX files when
|
|
5638
|
+
# tsc wasn't on PATH, leaving them silently unchecked.
|
|
5639
|
+
# Now fall back to `npx --yes -p typescript@latest tsc`
|
|
5640
|
+
# (uses the cached npm install), then to `bun tsc`
|
|
5641
|
+
# (Bun has built-in TypeScript), before giving up.
|
|
5637
5642
|
if command -v tsc &>/dev/null; then
|
|
5638
5643
|
tsc --noEmit --allowJs --jsx preserve --target esnext "$f" 2>&1 || {
|
|
5639
5644
|
findings=$((findings + 1))
|
|
5640
5645
|
details="${details}TS syntax error: $f. "
|
|
5641
5646
|
}
|
|
5647
|
+
elif command -v bun &>/dev/null; then
|
|
5648
|
+
# Bun has built-in TypeScript via `bun --check`.
|
|
5649
|
+
bun --check "$f" 2>&1 || {
|
|
5650
|
+
findings=$((findings + 1))
|
|
5651
|
+
details="${details}TS syntax error (bun --check): $f. "
|
|
5652
|
+
}
|
|
5653
|
+
elif command -v npx &>/dev/null; then
|
|
5654
|
+
npx --yes -p typescript@latest tsc --noEmit --allowJs --jsx preserve --target esnext "$f" 2>&1 || {
|
|
5655
|
+
findings=$((findings + 1))
|
|
5656
|
+
details="${details}TS syntax error (npx tsc): $f. "
|
|
5657
|
+
}
|
|
5642
5658
|
else
|
|
5643
|
-
log_info "Static analysis: skipping $f (tsc
|
|
5659
|
+
log_info "Static analysis: skipping $f (no tsc, bun, or npx available)"
|
|
5644
5660
|
fi
|
|
5645
5661
|
;;
|
|
5646
5662
|
*)
|
|
@@ -11439,7 +11455,22 @@ if __name__ == "__main__":
|
|
|
11439
11455
|
# promise text appears in the iteration output.
|
|
11440
11456
|
# The check_completion_promise() helper encapsulates both.
|
|
11441
11457
|
# BUG-RUN-001: Use per-iteration output, not stale daily log.
|
|
11442
|
-
|
|
11458
|
+
#
|
|
11459
|
+
# v7.6.2 B-17 fix: completion was firing even when code review
|
|
11460
|
+
# BLOCKED the iteration with Critical/High findings. That's a false
|
|
11461
|
+
# success signal -- review-blocked iterations cannot be considered
|
|
11462
|
+
# complete. Check the gate_failures accumulator for code_review and
|
|
11463
|
+
# refuse completion until the review passes.
|
|
11464
|
+
local _gate_block_for_completion=""
|
|
11465
|
+
case "${gate_failures:-}" in
|
|
11466
|
+
*code_review,*|*code_review_ESCALATED*) _gate_block_for_completion="code_review" ;;
|
|
11467
|
+
esac
|
|
11468
|
+
if [ -n "$_gate_block_for_completion" ] && check_completion_promise "$iter_output"; then
|
|
11469
|
+
log_warn "Completion claim rejected: code review is BLOCKED for this iteration (Critical/High findings). Fix review issues before completion."
|
|
11470
|
+
log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
|
|
11471
|
+
_gate_block_for_completion=""
|
|
11472
|
+
# Fall through; the gate-failed loop continues normally
|
|
11473
|
+
elif check_completion_promise "$iter_output"; then
|
|
11443
11474
|
echo ""
|
|
11444
11475
|
if [ -n "$COMPLETION_PROMISE" ]; then
|
|
11445
11476
|
log_header "COMPLETION PROMISE FULFILLED: $COMPLETION_PROMISE"
|
|
@@ -11551,13 +11582,57 @@ LOKI_PROVIDER_ACTIVE=0
|
|
|
11551
11582
|
# v7.5.12: Kill provider pipeline children with SIGTERM, then SIGKILL escalation.
|
|
11552
11583
|
# Uses pkill -P $$ to target direct children only (the pipeline subshells).
|
|
11553
11584
|
# Returns 0 if anything was killed, 1 if no children present.
|
|
11585
|
+
#
|
|
11586
|
+
# v7.6.2 B-15 fix: previously `pkill -P $$` was indiscriminate -- it caught
|
|
11587
|
+
# the dashboard server (started via nohup but still parented to this shell
|
|
11588
|
+
# until the OS reparents it). The dashboard PID 29716 was killed mid-session
|
|
11589
|
+
# after "Aggregating verdicts", breaking the browser UI. Now we explicitly
|
|
11590
|
+
# exclude any PID registered in .loki/pids/ (dashboard, app-runner, etc.).
|
|
11554
11591
|
kill_provider_child() {
|
|
11555
11592
|
local killed=0
|
|
11556
|
-
|
|
11557
|
-
#
|
|
11558
|
-
|
|
11559
|
-
|
|
11593
|
+
local protected_pids=""
|
|
11594
|
+
# Build list of PIDs that MUST survive a provider kill (dashboard, app-runner,
|
|
11595
|
+
# status monitor, resource monitor). These are recorded as PID files when
|
|
11596
|
+
# cmd_dashboard_start / cmd_api_start spawn them.
|
|
11597
|
+
local pid_root="${TARGET_DIR:-.}/.loki/pids"
|
|
11598
|
+
if [ -d "$pid_root" ]; then
|
|
11599
|
+
local pid_file pid
|
|
11600
|
+
for pid_file in "$pid_root"/*.pid; do
|
|
11601
|
+
[ -f "$pid_file" ] || continue
|
|
11602
|
+
pid=$(cat "$pid_file" 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
11603
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
11604
|
+
protected_pids="${protected_pids} ${pid}"
|
|
11605
|
+
fi
|
|
11606
|
+
done
|
|
11607
|
+
fi
|
|
11608
|
+
# Also protect the dashboard PID file at .loki/dashboard/dashboard.pid (older path).
|
|
11609
|
+
local dash_pid_file="${TARGET_DIR:-.}/.loki/dashboard/dashboard.pid"
|
|
11610
|
+
if [ -f "$dash_pid_file" ]; then
|
|
11611
|
+
local dpid
|
|
11612
|
+
dpid=$(cat "$dash_pid_file" 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
11613
|
+
if [ -n "$dpid" ] && kill -0 "$dpid" 2>/dev/null; then
|
|
11614
|
+
protected_pids="${protected_pids} ${dpid}"
|
|
11615
|
+
fi
|
|
11560
11616
|
fi
|
|
11617
|
+
|
|
11618
|
+
# Helper: returns 0 if $1 is in protected_pids list.
|
|
11619
|
+
_is_protected() {
|
|
11620
|
+
local target="$1"
|
|
11621
|
+
local p
|
|
11622
|
+
for p in $protected_pids; do
|
|
11623
|
+
[ "$p" = "$target" ] && return 0
|
|
11624
|
+
done
|
|
11625
|
+
return 1
|
|
11626
|
+
}
|
|
11627
|
+
|
|
11628
|
+
# First pass: SIGTERM each direct child individually so we can skip protected PIDs.
|
|
11629
|
+
local child_pid
|
|
11630
|
+
for child_pid in $(pgrep -P $$ 2>/dev/null); do
|
|
11631
|
+
if _is_protected "$child_pid"; then
|
|
11632
|
+
continue
|
|
11633
|
+
fi
|
|
11634
|
+
kill -TERM "$child_pid" 2>/dev/null && killed=1
|
|
11635
|
+
done
|
|
11561
11636
|
# Also kill provider leaf processes by name in case they were reparented.
|
|
11562
11637
|
local proc
|
|
11563
11638
|
for proc in claude codex aider cline; do
|
|
@@ -11567,18 +11642,27 @@ kill_provider_child() {
|
|
|
11567
11642
|
# Brief wait for graceful exit (max ~2s).
|
|
11568
11643
|
local i=0
|
|
11569
11644
|
while [ $i -lt 20 ]; do
|
|
11570
|
-
|
|
11645
|
+
local survivors=""
|
|
11646
|
+
for child_pid in $(pgrep -P $$ 2>/dev/null); do
|
|
11647
|
+
if ! _is_protected "$child_pid"; then
|
|
11648
|
+
survivors="${survivors} ${child_pid}"
|
|
11649
|
+
fi
|
|
11650
|
+
done
|
|
11651
|
+
if [ -z "$survivors" ]; then
|
|
11571
11652
|
break
|
|
11572
11653
|
fi
|
|
11573
11654
|
sleep 0.1
|
|
11574
11655
|
i=$((i + 1))
|
|
11575
11656
|
done
|
|
11576
11657
|
|
|
11577
|
-
# Escalate to SIGKILL for
|
|
11578
|
-
|
|
11579
|
-
|
|
11658
|
+
# Escalate to SIGKILL for unprotected survivors only.
|
|
11659
|
+
for child_pid in $(pgrep -P $$ 2>/dev/null); do
|
|
11660
|
+
if _is_protected "$child_pid"; then
|
|
11661
|
+
continue
|
|
11662
|
+
fi
|
|
11663
|
+
kill -KILL "$child_pid" 2>/dev/null
|
|
11580
11664
|
killed=1
|
|
11581
|
-
|
|
11665
|
+
done
|
|
11582
11666
|
|
|
11583
11667
|
LOKI_PROVIDER_ACTIVE=0
|
|
11584
11668
|
if [ $killed -eq 1 ]; then
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -3023,9 +3023,20 @@ async def get_learning_metrics(
|
|
|
3023
3023
|
|
|
3024
3024
|
total_count = len(events) + len(all_signals)
|
|
3025
3025
|
|
|
3026
|
-
# Calculate average confidence across both sources
|
|
3027
|
-
|
|
3028
|
-
|
|
3026
|
+
# Calculate average confidence across both sources. Coerce values to float
|
|
3027
|
+
# because some legacy events/signals stored confidence as a string, which
|
|
3028
|
+
# made sum() raise TypeError: unsupported operand type(s) for +: 'int' and 'str'.
|
|
3029
|
+
# B-7 fix (v7.6.1): silently skip non-numeric confidence values.
|
|
3030
|
+
def _as_num(v: object) -> float:
|
|
3031
|
+
if isinstance(v, (int, float)):
|
|
3032
|
+
return float(v)
|
|
3033
|
+
try:
|
|
3034
|
+
return float(v) if v is not None else 0.0
|
|
3035
|
+
except (TypeError, ValueError):
|
|
3036
|
+
return 0.0
|
|
3037
|
+
|
|
3038
|
+
total_conf = sum(_as_num(e.get("data", {}).get("confidence", 0)) for e in events)
|
|
3039
|
+
total_conf += sum(_as_num(s.get("confidence", 0)) for s in all_signals)
|
|
3029
3040
|
|
|
3030
3041
|
# Load aggregation data from file if available
|
|
3031
3042
|
aggregation = {
|
|
@@ -6388,7 +6399,21 @@ async def get_escalation(filename: str):
|
|
|
6388
6399
|
# ---------------------------------------------------------------------------
|
|
6389
6400
|
@app.get("/{full_path:path}", include_in_schema=False)
|
|
6390
6401
|
async def serve_spa_catchall(full_path: str):
|
|
6391
|
-
"""Serve static files or fall back to index.html for SPA routing.
|
|
6402
|
+
"""Serve static files or fall back to index.html for SPA routing.
|
|
6403
|
+
|
|
6404
|
+
v7.6.1 B-10 fix: requests under /api/, /lab/api/, or /ws/ that fall through
|
|
6405
|
+
here are missing routes, not SPA navigation. Returning index.html (text/html)
|
|
6406
|
+
silently masks 404s and breaks JSON clients (the dashboard UI's loki-memory-browser
|
|
6407
|
+
pinged /api/learning/metrics expecting JSON and got an HTML SPA on prior
|
|
6408
|
+
failures). Return a JSON 404 instead so clients fail loud.
|
|
6409
|
+
"""
|
|
6410
|
+
# API paths that fell through are real 404s, not SPA routes.
|
|
6411
|
+
api_like = full_path.startswith("api/") or full_path.startswith("lab/api/") or full_path.startswith("ws/")
|
|
6412
|
+
if api_like:
|
|
6413
|
+
return JSONResponse(
|
|
6414
|
+
status_code=404,
|
|
6415
|
+
content={"error": "Not Found", "path": f"/{full_path}"},
|
|
6416
|
+
)
|
|
6392
6417
|
if STATIC_DIR:
|
|
6393
6418
|
static_root = os.path.realpath(STATIC_DIR)
|
|
6394
6419
|
# Try to serve the exact file first (e.g. /vite.svg, /manifest.json)
|
|
@@ -1339,14 +1339,22 @@ var LokiDashboard=(()=>{var wt=Object.defineProperty;var ae=Object.getOwnPropert
|
|
|
1339
1339
|
<div class="card-label">App Runner</div>
|
|
1340
1340
|
<div class="card-value small-text">${this._data.status==="running"||this._data.status==="autonomous"?"Waiting...":"Not started"}</div>
|
|
1341
1341
|
</div>
|
|
1342
|
-
`;let a={running:"var(--loki-green, #22c55e)",starting:"var(--loki-yellow, #f59e0b)",crashed:"var(--loki-red, #ef4444)",stopped:"var(--loki-text-muted, #a1a1aa)"}[t.status]||"var(--loki-text-muted)",i=t.status==="running"?"active":t.status==="crashed"?"error":"offline",s=(t.status||"unknown").toUpperCase(),r=t.port?`:${t.port}`:"";
|
|
1342
|
+
`;let a={running:"var(--loki-green, #22c55e)",starting:"var(--loki-yellow, #f59e0b)",crashed:"var(--loki-red, #ef4444)",stopped:"var(--loki-text-muted, #a1a1aa)"}[t.status]||"var(--loki-text-muted)",i=t.status==="running"?"active":t.status==="crashed"?"error":"offline",s=(t.status||"unknown").toUpperCase(),r=t.port?`:${t.port}`:"",o=t.url&&typeof t.url=="string"?t.url:null;!o&&t.port&&t.status==="running"&&(o=`http://localhost:${t.port}`);let n=`
|
|
1343
|
+
<div class="card-label">App Runner${o?' <span style="font-size:10px;color:var(--loki-text-muted);">(click to open)</span>':""}</div>
|
|
1344
|
+
<div class="card-value small-text">
|
|
1345
|
+
<span class="status-dot ${i}"></span>
|
|
1346
|
+
${s}${r}
|
|
1347
|
+
</div>
|
|
1348
|
+
${t.method?`<div style="font-size:10px;color:var(--loki-text-muted);margin-top:2px;">${this._escapeHtml(t.method)}</div>`:""}
|
|
1349
|
+
`;return o?`
|
|
1350
|
+
<a class="overview-card overview-card-link" href="${this._escapeHtml(o)}" target="_blank" rel="noopener noreferrer"
|
|
1351
|
+
style="text-decoration:none;color:inherit;display:block;cursor:pointer;"
|
|
1352
|
+
title="Open ${this._escapeHtml(o)} in new tab">
|
|
1353
|
+
${n}
|
|
1354
|
+
</a>
|
|
1355
|
+
`:`
|
|
1343
1356
|
<div class="overview-card">
|
|
1344
|
-
|
|
1345
|
-
<div class="card-value small-text">
|
|
1346
|
-
<span class="status-dot ${i}"></span>
|
|
1347
|
-
${s}${r}
|
|
1348
|
-
</div>
|
|
1349
|
-
${t.method?`<div style="font-size:10px;color:var(--loki-text-muted);margin-top:2px;">${this._escapeHtml(t.method)}</div>`:""}
|
|
1357
|
+
${n}
|
|
1350
1358
|
</div>
|
|
1351
1359
|
`}_renderPlaywrightCard(){let t=this._playwrightResults;if(!t||t==="null"||!t.verified_at)return`
|
|
1352
1360
|
<div class="overview-card">
|
package/docs/INSTALLATION.md
CHANGED
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var _7=Object.defineProperty;var I7=(K)=>K;function P7(K,$){this[K]=I7.bind(null,$)}var b=(K,$)=>{for(var z in $)_7(K,z,{get:$[z],enumerable:!0,configurable:!0,set:P7.bind($,z)})};var L=(K,$)=>()=>(K&&($=K(K=0)),$);var V1=import.meta.require;var e1={};b(e1,{lokiDir:()=>P,homeLokiDir:()=>k1,findRepoRootForVersion:()=>N1,REPO_ROOT:()=>u});import{resolve as f,dirname as S1}from"path";import{fileURLToPath as L7}from"url";import{existsSync as J1}from"fs";import{homedir as R7}from"os";function E7(){let K=i1;for(let $=0;$<6;$++){if(J1(f(K,"VERSION"))&&J1(f(K,"autonomy/run.sh")))return K;let z=S1(K);if(z===K)break;K=z}return f(i1,"..","..","..")}function N1(K){let $=K;for(let z=0;z<6;z++){if(J1(f($,"VERSION"))&&J1(f($,"autonomy/run.sh")))return $;let Q=S1($);if(Q===$)break;$=Q}return f(K,"..","..","..")}function P(){return process.env.LOKI_DIR??f(process.cwd(),".loki")}function k1(){return f(R7(),".loki")}var i1,u;var y=L(()=>{i1=S1(L7(import.meta.url));u=E7()});import{readFileSync as x7}from"fs";import{resolve as F7,dirname as w7}from"path";import{fileURLToPath as S7}from"url";function G1(){if(o!==null)return o;let K="7.6.2";if(typeof K==="string"&&K.length>0)return o=K,o;try{let $=w7(S7(import.meta.url)),z=N1($);o=x7(F7(z,"VERSION"),"utf-8").trim()}catch{o="unknown"}return o}var o=null;var D1=L(()=>{y()});var $0={};b($0,{runOrThrow:()=>N7,run:()=>w,commandVersion:()=>D7,commandExists:()=>h,ShellError:()=>C1});async function w(K,$={}){let z=Bun.spawn({cmd:[...K],stdout:"pipe",stderr:"pipe",env:$.env?{...process.env,...$.env}:process.env,cwd:$.cwd}),Q,X;if($.timeoutMs&&$.timeoutMs>0)Q=setTimeout(()=>{try{z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{z.kill("SIGKILL")}catch{}},2000)},$.timeoutMs);try{let[H,Z,q]=await Promise.all([new Response(z.stdout).text(),new Response(z.stderr).text(),z.exited]);return{stdout:H,stderr:Z,exitCode:q}}finally{if(Q)clearTimeout(Q);if(X)clearTimeout(X)}}async function N7(K,$={}){let z=await w(K,$);if(z.exitCode!==0)throw new C1(`command failed (${z.exitCode}): ${K.join(" ")}`,z.exitCode,z.stdout,z.stderr);return z}async function h(K){let $=k7(K),z=await w(["sh","-c",`command -v ${$}`],{timeoutMs:5000});if(z.exitCode===0)return z.stdout.trim()||null;return null}function k7(K){if(!/^[A-Za-z0-9._/-]+$/.test(K))throw Error(`refused to shell-escape suspect token: ${K}`);return K}async function D7(K,$="--version"){if(!await h(K))return null;let Q=await w([K,$],{timeoutMs:5000});if(Q.exitCode!==0)return null;return((Q.stdout||Q.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var C1;var p=L(()=>{C1=class C1 extends Error{message;exitCode;stdout;stderr;constructor(K,$,z,Q){super(K);this.message=K;this.exitCode=$;this.stdout=z;this.stderr=Q;this.name="ShellError"}}});function c(K){return C7?"":K}var C7,R,D,E,O6,O,k,x,W;var n=L(()=>{C7=(process.env.NO_COLOR??"").length>0;R=c("\x1B[0;31m"),D=c("\x1B[0;32m"),E=c("\x1B[1;33m"),O6=c("\x1B[0;34m"),O=c("\x1B[0;36m"),k=c("\x1B[1m"),x=c("\x1B[2m"),W=c("\x1B[0m")});import{existsSync as c7}from"fs";async function r(){if(z1!==void 0)return z1;let K="/opt/homebrew/bin/python3.12";if(c7(K))return z1=K,K;let $=await h("python3.12");if($)return z1=$,$;let z=await h("python3");return z1=z,z}async function a(K,$={}){let z=await r();if(!z)return{stdout:"",stderr:"python3 not found",exitCode:127};return w([z,"-c",K],$)}var z1;var Q1=L(()=>{p()});var G0={};b(G0,{runStatus:()=>z5});import{existsSync as S,readFileSync as Z1,readdirSync as H0,statSync as W0}from"fs";import{resolve as F,basename as a7}from"path";async function r7(){if(await h("jq"))return!0;return process.stdout.write(`${R}Error: jq is required but not installed.${W}
|
|
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)
|
|
6
6
|
`),process.stdout.write(` yum install jq (RHEL/CentOS)
|
|
7
|
-
`),!1}function
|
|
7
|
+
`),!1}function B1(K){if(!Number.isFinite(K)||K<=0)return!1;try{return process.kill(K,0),!0}catch{return!1}}function M1(K){if(!S(K))return null;try{let $=Z1(K,"utf-8").trim();if(!$)return null;let z=Number.parseInt($,10);return Number.isFinite(z)?z:null}catch{return null}}function t7(K){let $=[],z=M1(F(K,"loki.pid"));if(z!==null&&B1(z))$.push(`global:${z}`);let Q=F(K,"sessions");if(S(Q)){let X=[];try{X=H0(Q)}catch{X=[]}for(let H of X){let Z=F(Q,H);try{if(!W0(Z).isDirectory())continue}catch{continue}let q=F(Z,"loki.pid"),U=M1(q);if(U!==null&&B1(U))$.push(`${H}:${U}`)}}if(S(K)){let X=[];try{X=H0(K)}catch{X=[]}for(let H of X){if(!H.startsWith("run-")||!H.endsWith(".pid"))continue;let Z=F(K,H);try{if(!W0(Z).isFile())continue}catch{continue}let q=a7(H,".pid").slice(4),U=M1(Z);if(U!==null&&B1(U)){if(!$.some((G)=>G.startsWith(`${q}:`)))$.push(`${q}:${U}`)}}}return $}async function q0(K,$){let z=await w(["jq","-r",K,$]);if(z.exitCode!==0)return null;return z.stdout.trim()}function U0(K,$){try{let z=Z1(K,"utf-8"),X=JSON.parse(z)[$];if(typeof X==="number"){if($==="budget_used"){let H=Math.round(X*100)/100;if(Number.isInteger(H))return String(H);return String(H)}return String(X)}if(X===void 0||X===null)return"0";return String(X)}catch{return"0"}}function V0(K,$,z){try{let Q=Z1(K,"utf-8"),H=JSON.parse(Q)[$];if(typeof H==="number"&&Number.isFinite(H))return H;return z}catch{return z}}async function i7(){let K=P();if(!await r7())return 1;if(!S(K))return process.stdout.write(`${k}Loki Mode Status${W}
|
|
8
8
|
`),process.stdout.write(`
|
|
9
9
|
`),process.stdout.write(`${E}No active session found.${W}
|
|
10
10
|
`),process.stdout.write(`Loki Mode has not been initialized in this directory.
|
|
@@ -19,7 +19,7 @@ var j7=Object.defineProperty;var _7=(K)=>K;function I7(K,$){this[K]=_7.bind(null
|
|
|
19
19
|
`);let $="",z=F(K,"state","provider");if(S(z))try{$=Z1(z,"utf-8").trim()}catch{$=""}let Q=$||process.env.LOKI_PROVIDER||"claude",X="full features";switch(Q){case"codex":case"aider":X="degraded mode";break;case"cline":X="near-full mode";break;default:X="full features";break}process.stdout.write(`${O}Provider:${W} ${Q} (${X})
|
|
20
20
|
`),process.stdout.write(`${x} Switch with: loki provider set <claude|codex|cline|aider>${W}
|
|
21
21
|
`),process.stdout.write(`
|
|
22
|
-
`);let H=
|
|
22
|
+
`);let H=t7(K);if(H.length>0){process.stdout.write(`${D}Active Sessions: ${H.length}${W}
|
|
23
23
|
`);for(let J of H){let Y=J.indexOf(":"),j=Y>=0?J.slice(0,Y):J,_=Y>=0?J.slice(Y+1):"";if(j==="global")process.stdout.write(` ${O}[global]${W} PID ${_}
|
|
24
24
|
`);else process.stdout.write(` ${O}[#${j}]${W} PID ${_}
|
|
25
25
|
`)}process.stdout.write(`
|
|
@@ -34,27 +34,27 @@ var j7=Object.defineProperty;var _7=(K)=>K;function I7(K,$){this[K]=_7.bind(null
|
|
|
34
34
|
`),process.stdout.write(`
|
|
35
35
|
`);let Z=F(K,"STATUS.txt");if(S(Z)){process.stdout.write(`${O}Session Info:${W}
|
|
36
36
|
`);try{process.stdout.write(Z1(Z,"utf-8"))}catch{}process.stdout.write(`
|
|
37
|
-
`)}let
|
|
38
|
-
`);let J=await
|
|
39
|
-
`)}let
|
|
40
|
-
`)}let
|
|
37
|
+
`)}let q=F(K,"state","orchestrator.json");if(S(q)){process.stdout.write(`${O}Orchestrator State:${W}
|
|
38
|
+
`);let J=await q0('.currentPhase // "unknown"',q);process.stdout.write(`${J??"unknown"}
|
|
39
|
+
`)}let U=F(K,"queue","pending.json");if(S(U)){let J=await q0('if type == "array" then length elif .tasks then .tasks | length else 0 end',U);process.stdout.write(`${O}Pending Tasks:${W} ${J??"0"}
|
|
40
|
+
`)}let V=F(K,"metrics","budget.json");if(S(V)){let J=U0(V,"budget_limit"),Y=U0(V,"budget_used");if(J!=="0")process.stdout.write(`${O}Budget:${W} $${Y} / $${J}
|
|
41
41
|
`);else process.stdout.write(`${O}Cost:${W} $${Y} (no limit)
|
|
42
|
-
`)}let G=F(K,"state","context-usage.json");if(S(G)){let J=
|
|
43
|
-
`)}let B=F(K,"dashboard","dashboard.pid");if(S(B)){let J=
|
|
44
|
-
`)}}return await
|
|
42
|
+
`)}let G=F(K,"state","context-usage.json");if(S(G)){let J=V0(G,"window_size",200000),Y=V0(G,"used_tokens",0),j=0;if(J>0)j=Math.floor(Y*100/J);process.stdout.write(`${O}Context:${W} ${j}% (${Y} / ${J} tokens)
|
|
43
|
+
`)}let B=F(K,"dashboard","dashboard.pid");if(S(B)){let J=M1(B);if(J!==null&&B1(J)){let Y=process.env.LOKI_DASHBOARD_PORT||"57374";process.stdout.write(`${O}Dashboard:${W} http://127.0.0.1:${Y}/
|
|
44
|
+
`)}}return await e7(K),process.stdout.write(`
|
|
45
45
|
`),process.stdout.write(`${x} Tip: loki context show - detailed token breakdown${W}
|
|
46
46
|
`),process.stdout.write(`${x} Tip: loki code overview - codebase intelligence${W}
|
|
47
|
-
`),0}async function
|
|
47
|
+
`),0}async function e7(K){let $=F(K,"state"),z=K5($),Q=F($,"relevant-learnings.json"),X=F(K,"escalations"),H=z.length>0,Z=S(Q),q=S(X);if(!H&&!Z&&!q)return;if(process.stdout.write(`
|
|
48
48
|
${O}Phase 1 artifacts:${W}
|
|
49
|
-
`),H){let
|
|
50
|
-
`)}}if(Z){let
|
|
51
|
-
`)}}if(
|
|
52
|
-
`)}}function
|
|
53
|
-
`),1;let $=u,z=P(),Q=process.env.LOKI_DASHBOARD_PORT||"57374",X=process.env.LOKI_PROVIDER||"claude",H=await w([K,"-c",
|
|
54
|
-
`),1;return process.stdout.write(H.stdout),0}async function
|
|
49
|
+
`),H){let U=z[z.length-1],V=J0(U);if(V&&Array.isArray(V.findings)){let G={Critical:0,High:0,Medium:0,Low:0};for(let J of V.findings){let Y=String(J.severity??"");if(Y in G)G[Y]=(G[Y]??0)+1}let B=Object.entries(G).filter(([,J])=>J>0).map(([J,Y])=>`${Y} ${J.toLowerCase()}`).join(", ");process.stdout.write(` Findings (iter ${V.iteration??"?"}): ${B||"none"} -- ${V.findings.length} total
|
|
50
|
+
`)}}if(Z){let U=J0(Q);if(U&&Array.isArray(U.learnings)&&U.learnings.length>0){let V=new Map;for(let B of U.learnings){let J=String(B.trigger??"unknown");V.set(J,(V.get(J)??0)+1)}let G=[...V.entries()].sort((B,J)=>J[1]-B[1]).slice(0,3).map(([B,J])=>`${J} ${B}`).join(", ");process.stdout.write(` Learnings: ${U.learnings.length} total (${G})
|
|
51
|
+
`)}}if(q){let U=0,V="";try{let B=(await import("fs")).readdirSync(X).filter((J)=>J.endsWith(".md"));if(U=B.length,B.length>0)B.sort(),V=B[B.length-1]??""}catch{}if(U>0)process.stdout.write(` Escalations: ${U} handoff doc${U===1?"":"s"} (latest: ${V})
|
|
52
|
+
`)}}function K5(K){if(!S(K))return[];try{return V1("fs").readdirSync(K).filter((Q)=>/^findings-\d+\.json$/.test(Q)).sort((Q,X)=>{let H=Number.parseInt(Q.replace(/[^0-9]/g,""),10)||0,Z=Number.parseInt(X.replace(/[^0-9]/g,""),10)||0;return H-Z}).map((Q)=>F(K,Q))}catch{return[]}}function J0(K){try{let $=V1("fs");return JSON.parse($.readFileSync(K,"utf-8"))}catch{return null}}async function $5(){let K=await r();if(!K)return process.stderr.write(`{"error": "Failed to generate JSON status. Ensure python3 is available."}
|
|
53
|
+
`),1;let $=u,z=P(),Q=process.env.LOKI_DASHBOARD_PORT||"57374",X=process.env.LOKI_PROVIDER||"claude",H=await w([K,"-c",s7,$,z,Q,X],{timeoutMs:30000});if(H.exitCode!==0)return process.stderr.write(`{"error": "Failed to generate JSON status. Ensure python3 is available."}
|
|
54
|
+
`),1;return process.stdout.write(H.stdout),0}async function z5(K){let $=[...K];while($.length>0){let z=$[0];if(z==="--json")return $5();if(z==="--help"||z==="-h")return process.stdout.write(`Usage: loki status [--json]
|
|
55
55
|
`),0;return process.stdout.write(`${R}Unknown flag: ${z}${W}
|
|
56
56
|
`),process.stdout.write(`Usage: loki status [--json]
|
|
57
|
-
`),1}return
|
|
57
|
+
`),1}return i7()}var s7=`
|
|
58
58
|
import json, os, sys, time
|
|
59
59
|
|
|
60
60
|
skill_dir = sys.argv[1]
|
|
@@ -261,9 +261,9 @@ if os.path.isfile(gate_count_file):
|
|
|
261
261
|
result['phase1'] = phase1
|
|
262
262
|
|
|
263
263
|
print(json.dumps(result, indent=2))
|
|
264
|
-
`;var
|
|
265
|
-
`)}function
|
|
266
|
-
Start a session with: loki start <prd>`}}let X=
|
|
264
|
+
`;var B0=L(()=>{p();Q1();n();y()});var T0={};b(T0,{runStats:()=>q5,computeStats:()=>O0});import{readdirSync as M0,readFileSync as Q5,statSync as Y0}from"fs";import{join as l}from"path";function i(K){try{if(!Y0(K).isFile())return null;return JSON.parse(Q5(K,"utf-8"))}catch{return null}}function v1(K){try{return Y0(K).isDirectory()}catch{return!1}}function X5(K){if(!v1(K))return[];try{let $=M0(K).filter((z)=>z.startsWith("iteration-")&&z.endsWith(".json"));return $.sort(),$.map((z)=>l(K,z))}catch{return[]}}function e(K){return Math.trunc(K).toLocaleString("en-US")}function b1(K){let $=Math.trunc(K);if($<60)return`${$}s`;let z=Math.trunc($/3600),Q=Math.trunc($%3600/60),X=$%60;if(z>0)return`${z}h ${String(Q).padStart(2,"0")}m`;return`${Q}m ${String(X).padStart(2,"0")}s`}function d(K,$=0){let z=Math.pow(10,$);return Math.round(K*z)/z}function K1(K,$){return K.toFixed($)}function y1(K,$){return K.length>=$?K:K+" ".repeat($-K.length)}function Z5(K){let $="N/A",z=0,Q=i(l(K,"state","orchestrator.json"));if(Q&&typeof Q==="object"){if(typeof Q.currentPhase==="string")$=Q.currentPhase;if(typeof Q.currentIteration==="number")z=Q.currentIteration}let X=l(K,"metrics","efficiency"),H=X5(X),Z=[];for(let T of H){let I=i(T);if(I&&typeof I==="object")Z.push(I)}if(Z.length>0)z=Math.max(z,Z.length);let q=Z.reduce((T,I)=>T+(I.input_tokens??0),0),U=Z.reduce((T,I)=>T+(I.output_tokens??0),0),V=q+U,G=Z.reduce((T,I)=>T+(I.cost_usd??0),0),B=Z.reduce((T,I)=>T+(I.duration_seconds??0),0),J=0,Y=0,j=i(l(K,"metrics","budget.json"));if(j&&typeof j==="object"){if(typeof j.budget_limit==="number")J=j.budget_limit;if(typeof j.budget_used==="number")Y=j.budget_used}let _=0,C=0,g=i(l(K,"state","quality-gates.json"));if(g&&typeof g==="object"){if(Array.isArray(g)){for(let T of g)if(C+=1,T===!0)_+=1;else if(T&&typeof T==="object"){let I=T;if(I.passed===!0||I.status==="passed")_+=1}}else for(let T of Object.values(g))if(typeof T==="boolean"){if(C+=1,T)_+=1}else if(T&&typeof T==="object"){C+=1;let I=T;if(I.passed===!0||I.status==="passed")_+=1}}let m={},A=i(l(K,"quality","gate-failure-count.json"));if(A&&typeof A==="object"&&!Array.isArray(A)){let T={};for(let[I,v]of Object.entries(A))if(typeof v==="number")T[I]=v;m=T}let N=0,$1=0,r1=0,w1=l(K,"quality");if(v1(w1)){let T=[];try{T=M0(w1)}catch{T=[]}for(let I of T){if(!I.endsWith(".json")||I==="gate-failure-count.json")continue;let v=i(l(w1,I));if(!v||typeof v!=="object")continue;if(!(("verdict"in v)||("approved"in v)||("reviewers"in v)))continue;N+=1;let t1=(v.verdict??"").toString().toLowerCase();if(v.approved===!0||["approved","approve","pass"].includes(t1))$1+=1;else if(["revision","revise","changes_requested","reject"].includes(t1))r1+=1}}return{phase:$,iterationCount:z,iterations:Z,totalInput:q,totalOutput:U,totalTokens:V,totalCost:G,totalDuration:B,budgetLimit:J,budgetUsed:Y,gatesPassed:_,gatesTotal:C,gateFailures:m,reviewsTotal:N,reviewsApproved:$1,reviewsRevision:r1}}function H5(K,$){let z=K.iterationCount,Q={session:{iterations:z,duration_seconds:K.totalDuration,phase:K.phase},tokens:{input:K.totalInput,output:K.totalOutput,total:K.totalTokens,cost_usd:d(K.totalCost,2)},quality:{gates_passed:K.gatesPassed,gates_total:K.gatesTotal,reviews_total:K.reviewsTotal,reviews_approved:K.reviewsApproved,reviews_revision:K.reviewsRevision,gate_failures:K.gateFailures},efficiency:{avg_tokens_per_iteration:z>0?d(K.totalTokens/z,0):0,avg_cost_per_iteration:z>0?d(K.totalCost/z,2):0,avg_duration_per_iteration:z>0?d(K.totalDuration/z,1):0},budget:{used:d(K.budgetUsed,2),limit:K.budgetLimit,percent:K.budgetLimit>0?d(K.budgetUsed/K.budgetLimit*100,1):0}};if($)Q.iterations=K.iterations.map((Z,q)=>({number:q+1,input_tokens:Z.input_tokens??0,output_tokens:Z.output_tokens??0,cost_usd:d(Z.cost_usd??0,2),duration_seconds:Z.duration_seconds??0}));let X=JSON.stringify(Q,null,2);function H(Z,q){if(!q)return;let U=new RegExp(`("${Z}": )(-?\\d+)(,?)$`,"m");X=X.replace(U,(V,G,B,J)=>`${G}${B}.0${J}`)}if(H("avg_duration_per_iteration",z>0&&Number.isInteger(Q.efficiency.avg_duration_per_iteration)),H("percent",K.budgetLimit>0&&Number.isInteger(Q.budget.percent)),H("cost_usd",z>0&&Number.isInteger(Q.tokens.cost_usd)),$)X=X.replace(/("cost_usd": )(-?\d+)(,?)$/gm,(Z,q,U,V)=>`${q}${U}.0${V}`);return X}function W5(K,$){let z=[];if(z.push("Loki Mode Session Statistics"),z.push("============================"),z.push(""),z.push("Session"),z.push(` Iterations completed: ${K.iterationCount}`),z.push(` Duration: ${b1(K.totalDuration)}`),z.push(` Current phase: ${K.phase}`),z.push(""),z.push("Token Usage"),K.iterations.length>0)z.push(` Input tokens: ${e(K.totalInput)}`),z.push(` Output tokens: ${e(K.totalOutput)}`),z.push(` Total tokens: ${e(K.totalTokens)}`),z.push(` Estimated cost: $${K1(K.totalCost,2)}`);else z.push(" N/A (no iteration metrics found)");if(z.push(""),z.push("Quality Gates"),K.gatesTotal>0){let Q=Math.round(K.gatesPassed/K.gatesTotal*100);z.push(` Gates passed: ${K.gatesPassed}/${K.gatesTotal} (${Q}%)`)}else z.push(" Gates passed: N/A");if(K.reviewsTotal>0){let Q=[];if(K.reviewsApproved>0)Q.push(`${K.reviewsApproved} approved`);if(K.reviewsRevision>0)Q.push(`${K.reviewsRevision} revision requested`);let X=Q.length>0?Q.join(", "):"N/A";z.push(` Code reviews: ${K.reviewsTotal} (${X})`)}if(Object.keys(K.gateFailures).length>0){let Q=Object.entries(K.gateFailures).filter(([,X])=>X>0).map(([X,H])=>`${X} (${H})`);if(Q.length>0)z.push(` Gate failures: ${Q.join(", ")}`)}if(z.push(""),z.push("Efficiency"),K.iterationCount>0&&K.iterations.length>0){let Q=Math.round(K.totalTokens/K.iterationCount),X=K.totalCost/K.iterationCount,H=K.totalDuration/K.iterationCount;z.push(` Avg tokens/iteration: ${e(Q)}`),z.push(` Avg cost/iteration: $${K1(X,2)}`),z.push(` Avg duration/iteration: ${b1(H)}`)}else z.push(" N/A (no iteration metrics found)");if(z.push(""),z.push("Budget"),K.budgetLimit>0){let Q=d(K.budgetUsed/K.budgetLimit*100,1),X=Number.isInteger(Q)?`${Q}.0`:`${Q}`;z.push(` Used: $${K1(K.budgetUsed,2)} / $${K1(K.budgetLimit,2)} (${X}%)`)}else if(K.budgetUsed>0)z.push(` Used: $${K1(K.budgetUsed,2)} (no limit set)`);else z.push(" N/A");if($&&K.iterations.length>0)z.push(""),z.push("Per-Iteration Breakdown"),K.iterations.forEach((Q,X)=>{let H=X+1,Z=y1(e(Q.input_tokens??0),10),q=y1(e(Q.output_tokens??0),10),U=Q.cost_usd??0,V=b1(Q.duration_seconds??0),G=y1(`${H}`,3);z.push(` #${G} input: ${Z} output: ${q} cost: $${K1(U,2)} time: ${V}`)});return z.join(`
|
|
265
|
+
`)}function O0(K){let $=!1,z=!1;for(let Z of K)if(Z==="--json")$=!0;else if(Z==="--efficiency")z=!0;let Q=P();if(!v1(Q)){if($)return{exitCode:0,stdout:'{"error": "No active session"}'};return{exitCode:0,stdout:`${E}No active session found.${W}
|
|
266
|
+
Start a session with: loki start <prd>`}}let X=Z5(Q);return{exitCode:0,stdout:$?H5(X,z):W5(X,z)}}async function q5(K){let $=O0(K);return console.log($.stdout),$.exitCode}var A0=L(()=>{y();n()});var F0={};b(F0,{runDoctor:()=>I5,pythonImportOk:()=>f1,httpReachable:()=>g1,checkTool:()=>L0,checkSkills:()=>R0,checkDisk:()=>m1,buildDoctorJson:()=>x0,_setPythonImportOkForTest:()=>Y5});import{existsSync as U5,lstatSync as V5,readlinkSync as J5,statfsSync as G5}from"fs";import{homedir as _0}from"os";import{resolve as j0}from"path";function M5(K){let $=K.match(B5);return $?$[1]:null}async function I0(K){try{let $=await w([K,"--version"],{timeoutMs:5000}),z=($.stdout||$.stderr||"").trim();return M5(z)}catch{return null}}function P0(K,$){let z=K.split(".").map((X)=>parseInt(X,10)),Q=$.split(".").map((X)=>parseInt(X,10));while(z.length<2)z.push(0);while(Q.length<2)Q.push(0);for(let X=0;X<2;X++){let H=z[X]??0,Z=Q[X]??0;if(Number.isNaN(H)||Number.isNaN(Z))return 0;if(H!==Z)return H-Z}return 0}async function L0(K,$,z,Q=null){let X=await h($),H=X!==null,Z=H?await I0($):null,q="pass";if(!H)q=z==="required"?"fail":"warn";else if(Q&&Z){if(P0(Z,Q)<0)q=z==="required"?"fail":"warn"}return{name:K,command:$,found:H,version:Z,required:z,min_version:Q,status:q,path:X}}function m1(){let K=null;try{let z=G5(_0()),Q=Number(z.bavail)*Number(z.bsize);K=Math.round(Q/1073741824*10)/10}catch{K=null}let $="pass";if(K!==null){if(K<1)$="fail";else if(K<5)$="warn"}return{available_gb:K,status:$}}async function g1(K,$=2000){try{return(await fetch(K,{signal:AbortSignal.timeout($)})).ok}catch{return!1}}async function f1(K,$=!1){let z=`import ${K}`,Q=$?30000:5000;if(!$)return(await a(z,{timeoutMs:Q})).exitCode===0;let X=await r();if(!X)return!1;return(await w([X,"-c",z],{timeoutMs:Q})).exitCode===0}function Y5(K){T1.fn=K??f1}function R0(){let K=_0();return O5.map(({name:$,dir:z})=>{let Q=j0(K,z),X=Q,H=j0(Q,"SKILL.md");if(U5(H))return{name:$,path:X,status:"pass",detail:""};try{if(V5(Q).isSymbolicLink()){let q="unknown";try{q=J5(Q)}catch{}return{name:$,path:X,status:"fail",detail:`(broken symlink -> ${q})`}}}catch{}return{name:$,path:X,status:"warn",detail:"(not found - run 'loki setup-skill')"}})}async function E0(){return Promise.all(T5.map(async(K)=>{return{...await L0(K.jsonName,K.cmd,K.required,K.min??null),displayName:K.displayName}}))}async function A5(){let $=await h("sentrux")!==null,z=$?await I0("sentrux"):null;return{found:$,version:z,status:$?"pass":"warn",required:"optional"}}async function x0(){let $=(await E0()).map(({displayName:q,...U})=>U),z=m1(),Q=await A5(),X=0,H=0,Z=0;for(let q of $)if(q.status==="pass")X++;else if(q.status==="fail")H++;else Z++;if(z.status==="pass")X++;else if(z.status==="fail")H++;else Z++;return{loki_mode_version:G1(),checks:$,disk:z,sentrux:Q,summary:{passed:X,failed:H,warnings:Z,ok:H===0}}}function M(K){switch(K){case"pass":return`${D}PASS${W}`;case"fail":return`${R}FAIL${W}`;case"warn":return`${E}WARN${W}`}}function Y1(K){let $=K.version?` (v${K.version})`:"",z=K.displayName;if(!K.found){let Q=K.required==="required"?"not found":K.required==="recommended"?"not found (recommended)":"not found (optional)";return` ${M(K.status)} ${z} - ${Q}`}if(K.min_version&&K.version&&P0(K.version,K.min_version)<0){let Q=K.required==="required"?"requires":"recommended";return` ${M(K.status)} ${z}${$} - ${Q} >= ${K.min_version}`}return` ${M(K.status)} ${z}${$}`}function O1(K,$){if($==="pass")K.pass++;else if($==="fail")K.fail++;else K.warn++}function j5(){process.stdout.write(`${k}loki doctor${W} - Check system prerequisites
|
|
267
267
|
|
|
268
268
|
`),process.stdout.write(`Usage: loki doctor [--json]
|
|
269
269
|
|
|
@@ -272,43 +272,43 @@ Start a session with: loki start <prd>`}}let X=X5(Q);return{exitCode:0,stdout:$?
|
|
|
272
272
|
|
|
273
273
|
`),process.stdout.write(`Checks: node, python3, jq, git, curl, bash version,
|
|
274
274
|
`),process.stdout.write(` claude/codex CLIs, and disk space.
|
|
275
|
-
`)}async function
|
|
275
|
+
`)}async function _5(){process.stdout.write(`${k}Loki Mode Doctor${W}
|
|
276
276
|
|
|
277
277
|
`),process.stdout.write(`Checking system prerequisites...
|
|
278
278
|
|
|
279
|
-
`);let K={pass:0,fail:0,warn:0},$=await
|
|
280
|
-
`);for(let A of["node","python3","jq","git","curl"]){let N=z.get(A);process.stdout.write(
|
|
281
|
-
`),
|
|
279
|
+
`);let K={pass:0,fail:0,warn:0},$=await E0(),z=new Map($.map((A)=>[A.command,A]));process.stdout.write(`${O}Required:${W}
|
|
280
|
+
`);for(let A of["node","python3","jq","git","curl"]){let N=z.get(A);process.stdout.write(Y1(N)+`
|
|
281
|
+
`),O1(K,N.status)}process.stdout.write(`
|
|
282
282
|
`),process.stdout.write(`${O}AI Providers:${W}
|
|
283
|
-
`);let Q=["claude","codex","cline","aider"],X=!1;for(let A of Q){let N=z.get(A);if(process.stdout.write(
|
|
284
|
-
`),
|
|
283
|
+
`);let Q=["claude","codex","cline","aider"],X=!1;for(let A of Q){let N=z.get(A);if(process.stdout.write(Y1(N)+`
|
|
284
|
+
`),O1(K,N.status),N.found)X=!0}if(!X)process.stdout.write(` ${M("fail")} No AI provider CLI installed -- at least one is required
|
|
285
285
|
`),process.stdout.write(` ${E}Install: npm install -g @anthropic-ai/claude-code${W}
|
|
286
286
|
`),K.fail++;process.stdout.write(`
|
|
287
287
|
`),process.stdout.write(`${O}API Keys:${W}
|
|
288
|
-
`);let H=z.get("claude")?.found??!1,Z=z.get("codex")?.found??!1,
|
|
288
|
+
`);let H=z.get("claude")?.found??!1,Z=z.get("codex")?.found??!1,q=process.env;if(q.ANTHROPIC_API_KEY)process.stdout.write(` ${M("pass")} ANTHROPIC_API_KEY is set
|
|
289
289
|
`),K.pass++;else if(H)process.stdout.write(` ${x} -- ${W} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)
|
|
290
|
-
`);if(
|
|
290
|
+
`);if(q.OPENAI_API_KEY)process.stdout.write(` ${M("pass")} OPENAI_API_KEY is set
|
|
291
291
|
`),K.pass++;else if(Z)process.stdout.write(` ${x} -- ${W} OPENAI_API_KEY not set (Codex CLI uses its own login)
|
|
292
|
-
`);if(
|
|
293
|
-
`),K.pass++,!
|
|
294
|
-
`),K.warn++;else process.stdout.write(` ${M("pass")} LOKI_MODEL_OVERRIDE: ${
|
|
292
|
+
`);if(q.ANTHROPIC_BASE_URL){let A=q.ANTHROPIC_BASE_URL;if(process.stdout.write(` ${M("pass")} ANTHROPIC_BASE_URL: ${A}
|
|
293
|
+
`),K.pass++,!q.LOKI_MODEL_OVERRIDE)process.stdout.write(` ${M("warn")} LOKI_MODEL_OVERRIDE not set -- opus/sonnet/haiku aliases may not resolve on alt-provider
|
|
294
|
+
`),K.warn++;else process.stdout.write(` ${M("pass")} LOKI_MODEL_OVERRIDE: ${q.LOKI_MODEL_OVERRIDE}
|
|
295
295
|
`),K.pass++}process.stdout.write(`
|
|
296
296
|
`),process.stdout.write(`${O}Skills:${W}
|
|
297
|
-
`);for(let A of
|
|
297
|
+
`);for(let A of R0())if(A.status==="pass")process.stdout.write(` ${M("pass")} ${A.name} ${x}${A.path}${W}
|
|
298
298
|
`),K.pass++;else if(A.status==="fail")process.stdout.write(` ${M("fail")} ${A.name} ${x}${A.detail}${W}
|
|
299
299
|
`),process.stdout.write(` ${E}Fix: loki setup-skill${W}
|
|
300
300
|
`),K.fail++;else process.stdout.write(` ${M("warn")} ${A.name} ${x}${A.detail}${W}
|
|
301
301
|
`),K.warn++;process.stdout.write(`
|
|
302
302
|
`),process.stdout.write(`${O}Integrations:${W}
|
|
303
|
-
`);let[
|
|
303
|
+
`);let[U,V,G]=await Promise.all([T1.fn("mcp"),T1.fn("numpy",!0),T1.fn("sentence_transformers",!0)]);if(U)process.stdout.write(` ${M("pass")} MCP SDK (Python)
|
|
304
304
|
`),K.pass++;else process.stdout.write(` ${M("warn")} MCP SDK - not installed (pip3 install mcp)
|
|
305
|
-
`),K.warn++;if(
|
|
305
|
+
`),K.warn++;if(V)process.stdout.write(` ${M("pass")} numpy (vector search)
|
|
306
306
|
`),K.pass++;else process.stdout.write(` ${M("warn")} numpy - not installed (pip3 install numpy)
|
|
307
307
|
`),K.warn++;if(G)process.stdout.write(` ${M("pass")} sentence-transformers (embeddings)
|
|
308
308
|
`),K.pass++;else process.stdout.write(` ${M("warn")} sentence-transformers - not installed (loki memory vectors setup)
|
|
309
|
-
`),K.warn++;if(await
|
|
309
|
+
`),K.warn++;if(await g1("http://localhost:8100/api/v2/heartbeat"))process.stdout.write(` ${M("pass")} ChromaDB server (port 8100)
|
|
310
310
|
`),K.pass++;else process.stdout.write(` ${M("warn")} ChromaDB - not running (docker start loki-chroma)
|
|
311
|
-
`),K.warn++;let B=process.env.LOKI_MIROFISH_URL;if(B)if(await
|
|
311
|
+
`),K.warn++;let B=process.env.LOKI_MIROFISH_URL;if(B)if(await g1(`${B}/health`))process.stdout.write(` ${M("pass")} MiroFish server (${B})
|
|
312
312
|
`),K.pass++;else process.stdout.write(` ${M("warn")} MiroFish - not running (loki start --mirofish-docker <image>)
|
|
313
313
|
`),K.warn++;if(process.env.LOKI_OTEL_ENDPOINT)process.stdout.write(` ${M("pass")} OTEL endpoint: ${process.env.LOKI_OTEL_ENDPOINT}
|
|
314
314
|
`),K.pass++;else process.stdout.write(` ${M("warn")} OTEL - not configured (set LOKI_OTEL_ENDPOINT)
|
|
@@ -316,9 +316,9 @@ Start a session with: loki start <prd>`}}let X=X5(Q);return{exitCode:0,stdout:$?
|
|
|
316
316
|
`),K.pass++}else process.stdout.write(` ${M("warn")} sentrux - not installed (optional, brew install sentrux/tap/sentrux)
|
|
317
317
|
`),K.warn++;process.stdout.write(`
|
|
318
318
|
`),process.stdout.write(`${O}System:${W}
|
|
319
|
-
`);let J=z.get("bash");process.stdout.write(
|
|
320
|
-
`),
|
|
321
|
-
`),
|
|
319
|
+
`);let J=z.get("bash");process.stdout.write(Y1(J)+`
|
|
320
|
+
`),O1(K,J.status);let Y=z.get("bun");if(Y)process.stdout.write(Y1(Y)+`
|
|
321
|
+
`),O1(K,Y.status);let j=m1(),_=j.available_gb===null?null:Math.floor(j.available_gb);if(_===null)process.stdout.write(` ${M("warn")} Disk space: unable to determine
|
|
322
322
|
`),K.warn++;else if(j.status==="fail")process.stdout.write(` ${M("fail")} Disk space: ${_}GB available (need >= 1GB)
|
|
323
323
|
`),K.fail++;else if(j.status==="warn")process.stdout.write(` ${M("warn")} Disk space: ${_}GB available (low)
|
|
324
324
|
`),K.warn++;else process.stdout.write(` ${M("pass")} Disk space: ${_}GB available
|
|
@@ -339,15 +339,15 @@ Start a session with: loki start <prd>`}}let X=X5(Q);return{exitCode:0,stdout:$?
|
|
|
339
339
|
`),process.stdout.write(`Install missing dependencies and run 'loki doctor' again.
|
|
340
340
|
`),1;if(K.warn>0)return process.stdout.write(`${E}All required checks passed with some warnings.${W}
|
|
341
341
|
`),0;return process.stdout.write(`${D}All checks passed. System is ready for Loki Mode.${W}
|
|
342
|
-
`),0}async function
|
|
342
|
+
`),0}async function I5(K){let $=!1;for(let z of K)if(z==="--json")$=!0;else if(z==="--help"||z==="-h")return j5(),0;else return process.stderr.write(`${R}Unknown option: ${z}${W}
|
|
343
343
|
`),process.stderr.write(`Usage: loki doctor [--json]
|
|
344
|
-
`),1;if($){let z=await
|
|
345
|
-
`),0}return
|
|
346
|
-
`)}var
|
|
344
|
+
`),1;if($){let z=await x0();return process.stdout.write(JSON.stringify(z,null,2)+`
|
|
345
|
+
`),0}return _5()}var B5,T1,O5,T5;var w0=L(()=>{p();Q1();n();D1();B5=/(\d+\.\d+(?:\.\d+)*)/;T1={fn:f1};O5=[{name:"Claude Code",dir:".claude/skills/loki-mode"},{name:"Codex CLI",dir:".codex/skills/loki-mode"},{name:"Cline CLI",dir:".cline/skills/loki-mode"},{name:"Aider CLI",dir:".aider/skills/loki-mode"}];T5=[{displayName:"Node.js (>= 18)",jsonName:"Node.js",cmd:"node",required:"required",min:"18.0"},{displayName:"Python 3 (>= 3.8)",jsonName:"Python 3",cmd:"python3",required:"required",min:"3.8"},{displayName:"jq",jsonName:"jq",cmd:"jq",required:"required"},{displayName:"git",jsonName:"git",cmd:"git",required:"required"},{displayName:"curl",jsonName:"curl",cmd:"curl",required:"required"},{displayName:"bash (>= 4.0)",jsonName:"bash",cmd:"bash",required:"recommended",min:"4.0"},{displayName:"Bun (>= 1.3)",jsonName:"Bun",cmd:"bun",required:"recommended",min:"1.3"},{displayName:"Claude CLI",jsonName:"Claude CLI",cmd:"claude",required:"optional"},{displayName:"Codex CLI",jsonName:"Codex CLI",cmd:"codex",required:"optional"},{displayName:"Cline CLI",jsonName:"Cline CLI",cmd:"cline",required:"optional"},{displayName:"Aider CLI",jsonName:"Aider CLI",cmd:"aider",required:"optional"}]});import{existsSync as k0,mkdirSync as e6,readdirSync as P5,readFileSync as D0,renameSync as K8,writeFileSync as $8}from"fs";import{dirname as L5,join as R5,resolve as E5}from"path";import{fileURLToPath as x5}from"url";function F5(){try{let K=L5(x5(import.meta.url)),$=E5(K,"..","..","data","model-pricing.json");if(!k0($))return H1;let Q=JSON.parse(D0($,"utf8")).pricing;if(!Q||typeof Q!=="object")return H1;let X={};for(let[H,Z]of Object.entries(Q))if(Z!==null&&typeof Z==="object"&&typeof Z.input==="number"&&typeof Z.output==="number")X[H]={input:Z.input,output:Z.output};for(let H of Object.keys(H1))if(!(H in X))return H1;return X}catch{return H1}}function w5(K){return Math.round((K+Number.EPSILON)*1e4)/1e4}function S5(K){let $=(K??N0).toLowerCase();return S0[$]??S0[N0]}function C0(K){let $=0;for(let z of K){if(typeof z.cost_usd==="number"&&Number.isFinite(z.cost_usd)){$+=z.cost_usd;continue}let Q=S5(z.model),X=typeof z.input_tokens==="number"?z.input_tokens:0,H=typeof z.output_tokens==="number"?z.output_tokens:0;$+=X/1e6*Q.input+H/1e6*Q.output}return w5($)}function h0(K){if(!k0(K))return[];let $=[],z;try{z=P5(K)}catch{return[]}for(let Q of z){if(!Q.endsWith(".json"))continue;let X=R5(K,Q);try{let H=D0(X,"utf8"),Z=JSON.parse(H);if(Z&&typeof Z==="object")$.push(Z)}catch{}}return $}var H1,S0,N0="sonnet";var b0=L(()=>{y();H1={opus:{input:5,output:25},sonnet:{input:3,output:15},haiku:{input:1,output:5},"gpt-5.3-codex":{input:1.5,output:12}};S0=Object.freeze(F5())});import{existsSync as A1,readdirSync as N5,readFileSync as k5,statSync as D5}from"fs";import{join as j1}from"path";function C5(K){let $=[],z=j1(K,"votes");if(!A1(z))return $;let Q;try{Q=N5(z)}catch{return $}for(let X of Q){if(!X.startsWith("round-")||!X.endsWith(".json"))continue;try{let H=j1(z,X);if(!D5(H).isFile())continue;let Z=JSON.parse(k5(H,"utf8"));$.push({iteration:typeof Z.iteration==="number"?Z.iteration:void 0,verdict:typeof Z.verdict==="string"?Z.verdict:void 0,complete_votes:typeof Z.complete_votes==="number"?Z.complete_votes:void 0,total_members:typeof Z.total_members==="number"?Z.total_members:void 0,threshold:typeof Z.threshold==="number"?Z.threshold:void 0})}catch{}}return $}function h5(){return{iteration_count:0,total_cost_usd:0,avg_cost_per_iteration:null,total_input_tokens:0,total_output_tokens:0,total_duration_ms:0,avg_duration_ms_per_iteration:null,model_breakdown:{},phase_breakdown:{},status_breakdown:{}}}function b5(){return{council_rounds:0,unanimous_rate:null,approval_rate:null,iteration_success_rate:null}}function y5(K){let $=h5();if(K.length===0)return $;$.iteration_count=K.length,$.total_cost_usd=Math.round(C0(K)*1e4)/1e4;for(let z of K){if(typeof z.input_tokens==="number")$.total_input_tokens+=z.input_tokens;if(typeof z.output_tokens==="number")$.total_output_tokens+=z.output_tokens;let Q=z;if(typeof Q.duration_ms==="number")$.total_duration_ms+=Q.duration_ms;if(typeof z.model==="string")$.model_breakdown[z.model]=($.model_breakdown[z.model]??0)+1;if(typeof Q.phase==="string")$.phase_breakdown[Q.phase]=($.phase_breakdown[Q.phase]??0)+1;if(typeof Q.status==="string")$.status_breakdown[Q.status]=($.status_breakdown[Q.status]??0)+1}return $.avg_cost_per_iteration=Math.round($.total_cost_usd/$.iteration_count*1e4)/1e4,$.avg_duration_ms_per_iteration=Math.round($.total_duration_ms/$.iteration_count),$}function v5(K,$,z){let Q=b5();if(Q.council_rounds=K.length,K.length>0){let X=0,H=0;for(let Z of K){if(typeof Z.complete_votes==="number"&&typeof Z.total_members==="number"&&Z.total_members>0&&Z.complete_votes===Z.total_members)X+=1;if(Z.verdict==="COMPLETE")H+=1}Q.unanimous_rate=Math.round(X/K.length*1e4)/1e4,Q.approval_rate=Math.round(H/K.length*1e4)/1e4}if(z>0)Q.iteration_success_rate=Math.round($/z*1e4)/1e4;return Q}function y0(K){let $=[],z=j1(K,"metrics","efficiency"),Q=j1(K,"council"),X=A1(z)?h0(z):[];if(!A1(z))$.push("no .loki/metrics/efficiency/ dir (efficiency KPIs zeroed)");else if(X.length===0)$.push(".loki/metrics/efficiency/ exists but no iteration files found");let H=C5(Q);if(!A1(Q))$.push("no .loki/council/ dir (accuracy KPIs zeroed)");else if(H.length===0)$.push(".loki/council/ exists but no round-N.json files found");let Z=y5(X),q=Z.status_breakdown.success??0,U=v5(H,q,Z.iteration_count);return{schema_version:1,generated_at:new Date().toISOString(),loki_dir:K,efficiency:Z,accuracy:U,notes:$}}function v0(K){return JSON.stringify(K,null,2)}function g0(K){let $=[];$.push(`Loki Mode KPIs (snapshot at ${K.generated_at})`),$.push(`Source: ${K.loki_dir}`),$.push(""),$.push("Efficiency"),$.push(` Iterations: ${K.efficiency.iteration_count}`),$.push(` Total cost USD: ${K.efficiency.total_cost_usd}`),$.push(` Avg cost per iter: ${K.efficiency.avg_cost_per_iteration??"n/a"}`),$.push(` Total input tokens: ${K.efficiency.total_input_tokens}`),$.push(` Total output tokens: ${K.efficiency.total_output_tokens}`),$.push(` Total duration (ms): ${K.efficiency.total_duration_ms}`),$.push(` Avg duration / iter: ${K.efficiency.avg_duration_ms_per_iteration??"n/a"}`);let z=Object.entries(K.efficiency.model_breakdown).sort((H,Z)=>H[0].localeCompare(Z[0]));if(z.length>0)$.push(` Model breakdown: ${z.map(([H,Z])=>`${H}=${Z}`).join(", ")}`);let Q=Object.entries(K.efficiency.phase_breakdown).sort((H,Z)=>H[0].localeCompare(Z[0]));if(Q.length>0)$.push(` Phase breakdown: ${Q.map(([H,Z])=>`${H}=${Z}`).join(", ")}`);let X=Object.entries(K.efficiency.status_breakdown).sort((H,Z)=>H[0].localeCompare(Z[0]));if(X.length>0)$.push(` Status breakdown: ${X.map(([H,Z])=>`${H}=${Z}`).join(", ")}`);if($.push(""),$.push("Accuracy"),$.push(` Council rounds: ${K.accuracy.council_rounds}`),$.push(` Unanimous rate: ${K.accuracy.unanimous_rate??"n/a"}`),$.push(` Approval rate: ${K.accuracy.approval_rate??"n/a"}`),$.push(` Iter success rate: ${K.accuracy.iteration_success_rate??"n/a"}`),K.notes.length>0){$.push(""),$.push("Notes");for(let H of K.notes)$.push(` - ${H}`)}return $.join(`
|
|
346
|
+
`)}var m0=L(()=>{b0()});var f0={};b(f0,{runKpis:()=>m5});function m5(K){let $=!1;for(let Q of K){if(Q==="--help"||Q==="-h"||Q==="help")return process.stdout.write(g5),0;if(Q==="--json"){$=!0;continue}return process.stderr.write(`loki kpis: unknown arg: ${Q}
|
|
347
347
|
Run 'loki kpis --help' for usage.
|
|
348
|
-
`),1}let z=
|
|
349
|
-
`:
|
|
350
|
-
`),0}var
|
|
348
|
+
`),1}let z=y0(P());return process.stdout.write($?v0(z)+`
|
|
349
|
+
`:g0(z)+`
|
|
350
|
+
`),0}var g5=`loki kpis -- accuracy + efficiency KPI snapshot (v7.5.28 MVP)
|
|
351
351
|
|
|
352
352
|
Usage:
|
|
353
353
|
loki kpis Pretty-print KPI snapshot
|
|
@@ -367,23 +367,23 @@ iteration success rate.
|
|
|
367
367
|
This is the Phase K MVP -- read-only derivation. Per-iteration
|
|
368
368
|
emission, dashboard panel, and the loki-bench harness are deferred
|
|
369
369
|
follow-ups (see project_v7_5_18_arc_status.md).
|
|
370
|
-
`;var
|
|
371
|
-
`),
|
|
370
|
+
`;var u0=L(()=>{m0();y()});import{closeSync as B8,fstatSync as M8,lstatSync as Y8,mkdirSync as f5,openSync as O8,readSync as T8,renameSync as u5,rmSync as A8,statSync as j8,unlinkSync as _8,writeFileSync as p5,writeSync as I8}from"fs";import{dirname as c5}from"path";function W1(K,$){f5(c5(K),{recursive:!0});let z=`${K}.tmp.${process.pid}.${++l5}`;p5(z,`${JSON.stringify($,null,2)}
|
|
371
|
+
`),u5(z,K)}async function p0(K,$){let z=_1.get(K)??Promise.resolve(),Q=()=>{},X=new Promise((Z)=>{Q=Z}),H=z.catch(()=>{}).then(()=>X);_1.set(K,H);try{return await z.catch(()=>{}),await $()}finally{if(Q(),_1.get(K)===H)_1.delete(K)}}var l5=0,_1;var I1=L(()=>{_1=new Map});import{existsSync as P1,mkdirSync as d5,copyFileSync as o5,readFileSync as n5,readdirSync as a5,statSync as s5,writeFileSync as x8,renameSync as r5,appendFileSync as F8,rmSync as w8}from"fs";import{join as s,dirname as t5}from"path";function L1(K){return s(K,"state","checkpoints")}function e5(K){let $=L1(K);if(!P1($))return[];return a5($).filter((z)=>z.startsWith("cp-")).filter((z)=>{try{return s5(s($,z)).isDirectory()}catch{return!1}})}function KK(K){return[...K].sort(($,z)=>{let Q=c0($),X=c0(z);return Q-X})}function c0(K){let $=K.split("-");if($.length<3)return 0;let z=$[$.length-1],Q=Number.parseInt(z??"0",10);return Number.isFinite(Q)?Q:0}function p1(K){let $=K??P(),z=KK(e5($)),Q=[];for(let X of z){let H=l0($,X);if(H)Q.push(H)}return Q}function l0(K,$){let z=s(L1(K),$,"metadata.json");if(!P1(z))return null;try{let Q=JSON.parse(n5(z,"utf-8"));return $K(Q,z)}catch{return null}}function $K(K,$){let z=XK(K,$);return z.ok?z.value:null}function XK(K,$){if(K===null||typeof K!=="object")return console.warn(`[checkpoint] invalid metadata at ${$}: not an object`),{ok:!1,reason:"invalid_type",field:"<root>"};let z=K,Q=["id","timestamp","task_id","task_description","git_sha","git_branch","provider","phase"];for(let X of Q){if(!(X in z))return console.warn(`[checkpoint] invalid metadata at ${$}: field "${X}" missing`),{ok:!1,reason:"missing_field",field:X};if(typeof z[X]!=="string")return console.warn(`[checkpoint] invalid metadata at ${$}: field "${X}" not a string`),{ok:!1,reason:"invalid_type",field:X}}if(!Object.prototype.hasOwnProperty.call(z,"iteration"))return console.warn(`[checkpoint] invalid metadata at ${$}: field "iteration" missing`),{ok:!1,reason:"missing_field",field:"iteration"};if(typeof z.iteration!=="number"||!Number.isFinite(z.iteration))return console.warn(`[checkpoint] invalid metadata at ${$}: field "iteration" not a finite number`),{ok:!1,reason:"invalid_type",field:"iteration"};for(let X of QK){let H=z[X];if(zK.test(H))return console.warn(`[checkpoint] invalid metadata at ${$}: field "${X}" contains control characters`),{ok:!1,reason:"control_chars",field:X}}return{ok:!0,value:{id:z.id,timestamp:z.timestamp,iteration:z.iteration,task_id:z.task_id,task_description:z.task_description,git_sha:z.git_sha,git_branch:z.git_branch,provider:z.provider,phase:z.phase}}}function c1(K,$){if(!ZK.test(K))throw new d0(K);let z=$??P(),Q=s(L1(z),K);if(!P1(Q))throw new u1(K);let X=l0(z,K);if(!X)throw new u1(K);return X}function o0(K,$){let z=c1(K,$),Q=$??P(),X=s(L1(Q),K),H=[];for(let Z of HK){let q=s(X,Z);if(!P1(q))continue;H.push({from:q,to:s(Q,Z)})}return{id:K,metadata:z,restore:H}}function n0(K){let $=[],z=0;for(let Q of K.restore)try{d5(t5(Q.to),{recursive:!0});let X=`${Q.to}.tmp.${process.pid}.${++i5}`;o5(Q.from,X),r5(X,Q.to),z+=1}catch(X){$.push(`${Q.from} -> ${Q.to}: ${X.message}`)}return{restored:z,errors:$}}var C8,i5=0,zK,QK,ZK,u1,d0,HK;var a0=L(()=>{y();p();I1();C8=Promise.resolve();zK=/[\x00-\x08\x0a-\x1f\x7f-\x9f]/,QK=["id","task_id","git_sha","git_branch","provider","phase"];ZK=/^[a-zA-Z0-9_-]+$/;u1=class u1 extends Error{id;constructor(K){super(`Checkpoint not found: ${K}`);this.id=K;this.name="CheckpointNotFoundError"}};d0=class d0 extends Error{id;constructor(K){super(`Invalid checkpoint ID: must be alphanumeric, hyphens, underscores only (got: ${K})`);this.id=K;this.name="InvalidCheckpointIdError"}};HK=["state/orchestrator.json","queue/pending.json","queue/completed.json","queue/in-progress.json","queue/current-task.json"]});var t0={};b(t0,{runRollback:()=>WK});async function WK(K){let $=K[0],z=K.slice(1);if($===void 0||$==="help"||$==="--help"||$==="-h")return process.stdout.write(s0),$===void 0?1:0;switch($){case"list":{let Q=[...p1()].reverse();if(Q.length===0)return process.stdout.write(`${E}No checkpoints found.${W}
|
|
372
372
|
`),0;process.stdout.write(`${k}Checkpoints${W} (${Q.length}, newest first):
|
|
373
373
|
`);for(let X of Q)process.stdout.write(` ${O}${X.id}${W} iter=${X.iteration} ${X.git_branch||"(no branch)"}@${(X.git_sha||"").slice(0,7)} ${X.timestamp}
|
|
374
374
|
`);return 0}case"show":{let Q=z[0];if(!Q)return process.stderr.write(`${R}Missing checkpoint id.${W} Use \`loki rollback list\`.
|
|
375
|
-
`),2;try{let X=
|
|
375
|
+
`),2;try{let X=c1(Q);return process.stdout.write(`${JSON.stringify(X,null,2)}
|
|
376
376
|
`),0}catch(X){return process.stderr.write(`${R}Failed to read checkpoint:${W} ${X.message}
|
|
377
377
|
`),1}}case"to":{let Q=z[0];if(!Q)return process.stderr.write(`${R}Missing checkpoint id.${W} Use \`loki rollback list\`.
|
|
378
|
-
`),2;return
|
|
378
|
+
`),2;return r0(Q)}case"latest":{let Q=p1(),X=Q[Q.length-1];if(!X)return process.stderr.write(`${R}No checkpoints found to roll back to.${W}
|
|
379
379
|
`),1;return process.stdout.write(`Rolling back to latest checkpoint: ${O}${X.id}${W}
|
|
380
|
-
`),
|
|
381
|
-
`),process.stderr.write(
|
|
380
|
+
`),r0(X.id)}default:return process.stderr.write(`Unknown subcommand: ${$}
|
|
381
|
+
`),process.stderr.write(s0),2}}function r0(K){let $;try{$=o0(K)}catch(Q){return process.stderr.write(`${R}Cannot plan rollback:${W} ${Q.message}
|
|
382
382
|
`),1}if($.restore.length===0)return process.stdout.write(`${E}Checkpoint ${K} has no restorable state files; nothing to do.${W}
|
|
383
|
-
`),0;let z=
|
|
383
|
+
`),0;let z=n0($);if(z.errors.length>0){for(let Q of z.errors)process.stderr.write(`${R}restore error:${W} ${Q}
|
|
384
384
|
`);return process.stderr.write(`${R}Partial rollback: ${z.restored}/${$.restore.length} files restored.${W}
|
|
385
385
|
`),1}return process.stdout.write(`${D}Rolled back ${z.restored}/${$.restore.length} state files from ${K}.${W}
|
|
386
|
-
`),process.stdout.write("Run `loki start` to resume from the restored state.\n"),0}var
|
|
386
|
+
`),process.stdout.write("Run `loki start` to resume from the restored state.\n"),0}var s0=`Usage: loki rollback <subcommand>
|
|
387
387
|
|
|
388
388
|
Subcommands:
|
|
389
389
|
list List checkpoints (newest first)
|
|
@@ -398,8 +398,8 @@ Restored files (matches autonomy/run.sh:7028 byte-for-byte):
|
|
|
398
398
|
Note: only state files are restored. Source code, git history, and the
|
|
399
399
|
session's autonomy-state.json are unchanged. Re-run \`loki start\` to
|
|
400
400
|
resume from the restored state.
|
|
401
|
-
`;var
|
|
402
|
-
`)}function
|
|
401
|
+
`;var i0=L(()=>{a0();n()});var d1={};b(d1,{renderFindingsForPrompt:()=>GK,loadPreviousFindings:()=>l1,findLatestReviewDir:()=>Q7,_parseReviewerOutputForTests:()=>BK});import{existsSync as K7,readFileSync as e0,readdirSync as $7,statSync as qK}from"fs";import{join as R1}from"path";function JK(K){let $=K.toLowerCase();if($==="critical")return"Critical";if($==="high")return"High";if($==="medium")return"Medium";return"Low"}function z7(K,$,z,Q){let X=[],H=K.split(/\r?\n/);for(let Z of H){let q=Z.trim();if(q.length===0)continue;let U=q.replace(/^[-*]\s*/,""),V=UK.exec(U);if(!V||!V[1]||!V[2])continue;let G=JK(V[1]),B=V[2].trim(),J=VK.exec(B),Y=J&&J[1]?J[1]:null,j=J&&J[2]?Number.parseInt(J[2],10):null;X.push({reviewId:z,iteration:Q,reviewer:$,severity:G,description:B,file:Y,line:Number.isFinite(j)?j:null,raw:q})}return X}function Q7(K,$){let z=R1(K,"quality","reviews");if(!K7(z))return null;let Q;try{Q=$7(z)}catch{return null}let X=$===void 0?Q.filter((q)=>q.startsWith("review-")):Q.filter((q)=>q.endsWith(`-${$}`)&&q.startsWith("review-"));if(X.length===0)return null;X.sort();let H=X[X.length-1];if(!H)return null;let Z=R1(z,H);try{if(!qK(Z).isDirectory())return null}catch{return null}return Z}function l1(K,$){let z=Q7(K,$);if(z===null)return{reviewDir:null,reviewId:null,iteration:null,findings:[]};let Q=null,X=null,H=R1(z,"aggregate.json");if(K7(H))try{let V=e0(H,"utf-8"),G=JSON.parse(V);if(typeof G.review_id==="string")Q=G.review_id;if(typeof G.iteration==="number")X=G.iteration}catch{}let Z;try{Z=$7(z)}catch{return{reviewDir:z,reviewId:Q,iteration:X,findings:[]}}let q=new Set(["diff.txt","files.txt","anti-sycophancy.txt"]),U=[];for(let V of Z){if(!V.endsWith(".txt"))continue;if(q.has(V))continue;if(V.endsWith("-prompt.txt"))continue;let G=V.replace(/\.txt$/,""),B;try{B=e0(R1(z,V),"utf-8")}catch{continue}U.push(...z7(B,G,Q??"",X??-1))}return{reviewDir:z,reviewId:Q,iteration:X,findings:U}}function GK(K){if(K.length===0)return"";let $=["Critical","High","Medium","Low"],z=new Map;for(let X of $)z.set(X,[]);for(let X of K){let H=z.get(X.severity);if(H)H.push(X)}let Q=[];Q.push("PREVIOUS REVIEWER FINDINGS (must address each, or supply counter-evidence in .loki/state/counter-evidence-<iter>.json):");for(let X of $){let H=z.get(X)??[];if(H.length===0)continue;Q.push(` [${X}] (${H.length}):`);for(let Z of H){let q=Z.file?` (${Z.file}${Z.line!==null?":"+Z.line:""})`:"";Q.push(` - ${Z.description}${q} -- via ${Z.reviewer}`)}}return Q.join(`
|
|
402
|
+
`)}function BK(K,$,z="review-test",Q=0){return z7(K,$,z,Q)}var UK,VK;var E1=L(()=>{UK=/\[(Critical|High|Medium|Low)\]\s*(.+)/i,VK=/([\w.\-/]+\.[a-zA-Z]+):(\d+)/});import{existsSync as MK}from"fs";import{join as YK}from"path";async function X7(K,$){let z=YK(K,"memory");if(!MK(z))return{stored:!1,reason:"memory dir not initialized"};let Q=Math.max(0,Math.floor($.durationSeconds??0)),X={_LOKI_PROJECT_DIR:u,_LOKI_TARGET_DIR:process.cwd(),_LOKI_TASK_ID:$.taskId,_LOKI_OUTCOME:$.outcome,_LOKI_PHASE:$.phase,_LOKI_GOAL:$.goal,_LOKI_DURATION:String(Q),_LOKI_LOKI_DIR:K},Z=await a(`
|
|
403
403
|
import os, sys
|
|
404
404
|
project = os.environ.get('_LOKI_PROJECT_DIR', '')
|
|
405
405
|
loki = os.environ.get('_LOKI_LOKI_DIR', '.loki')
|
|
@@ -426,23 +426,23 @@ try:
|
|
|
426
426
|
print('OK')
|
|
427
427
|
except Exception as e:
|
|
428
428
|
print('ERR:' + str(e))
|
|
429
|
-
`,{env:X,timeoutMs:15000});if(Z.exitCode===127)return{stored:!1,reason:"python3 not found"};let
|
|
430
|
-
`)}async function
|
|
431
|
-
`)}function
|
|
432
|
-
`),process.stderr.write(
|
|
433
|
-
`),2;let z=P();try{let X=(await Promise.resolve().then(() => (
|
|
434
|
-
`),0;let H=
|
|
429
|
+
`,{env:X,timeoutMs:15000});if(Z.exitCode===127)return{stored:!1,reason:"python3 not found"};let q=Z.stdout.trim();if(q==="OK")return{stored:!0,reason:"stored"};if(q.startsWith("ERR:"))return{stored:!1,reason:q.replace(/^ERR:/,"")};return{stored:!1,reason:Z.stderr.trim()||"unknown"}}var Z7=L(()=>{Q1();y()});var U7={};b(U7,{loadLearnings:()=>o1,appendLearning:()=>q1,appendFromGateFailure:()=>LK});import{existsSync as OK,readFileSync as TK}from"fs";import{join as H7}from"path";import{createHash as AK}from"crypto";function W7(K){return H7(K,jK)}function _K(K){if(K===null||typeof K!=="object")return!1;let $=K;return typeof $.id==="string"&&typeof $.timestamp==="string"&&typeof $.iteration==="number"&&typeof $.trigger==="string"&&typeof $.rootCause==="string"&&typeof $.fix==="string"&&typeof $.preventInFuture==="string"&&typeof $.evidence==="object"&&$.evidence!==null}function q7(K){if(!OK(K))return{version:1,learnings:[]};try{let $=TK(K,"utf-8"),z=JSON.parse($);if(z.version===1&&Array.isArray(z.learnings))return{version:1,learnings:z.learnings.filter(_K)}}catch{}return{version:1,learnings:[]}}function IK(K,$){return AK("sha256").update(`${K}\x00${$}`).digest("hex").slice(0,16)}async function q1(K,$,z={}){let Q=IK($.trigger,$.rootCause),X=new Date().toISOString(),H={id:Q,timestamp:X,...$},Z=W7(K);if(await p0(Z,()=>{let U=q7(Z),V=U.learnings.findIndex((G)=>G.id===Q);if(V>=0){let G=U.learnings[V];U.learnings[V]={...G,timestamp:X,iteration:H.iteration}}else U.learnings.push(H);W1(Z,U)}),z.episodeBridge!==null&&(z.episodeBridge!==void 0||process.env.LOKI_AUTO_LEARNINGS_EPISODE==="1")){let U=z.episodeBridge??X7,V=z.bridgeFailureLog??PK;try{let G=await U(K,{taskId:`learning-${Q}`,outcome:"failure",phase:"VERIFY",goal:`${$.trigger}: ${$.rootCause}`});if(G&&!G.stored){if(!new Set(["memory dir not initialized","stub"]).has(G.reason))V(`episode_bridge skipped: ${G.reason}`)}}catch(G){V(`episode_bridge threw: ${G.message}`)}}return H}function PK(K){process.stderr.write(`[learnings_writer] ${K}
|
|
430
|
+
`)}async function LK(K,$,z,Q={}){let X=`[${z.severity}] ${z.description}`;return q1(K,{iteration:$,trigger:"gate_failure",rootCause:X,fix:"pending: dev agent must address in next iteration or supply counter-evidence",preventInFuture:"if this finding recurs, lower its severity threshold or add a regression test",evidence:{reviewId:z.reviewId,file:z.file??void 0,line:z.line??void 0,severity:z.severity,reviewer:z.reviewer}},Q)}function o1(K){return q7(W7(K))}var jK;var x1=L(()=>{I1();Z7();jK=H7("state","relevant-learnings.json")});var J7={};b(J7,{runOverrideCouncil:()=>SK,recordOverrideOutcome:()=>NK,loadCounterEvidence:()=>wK,canonicalFindingId:()=>n1,DEFAULT_OVERRIDE_JUDGES:()=>V7});import{existsSync as RK,readFileSync as EK}from"fs";import{join as xK}from"path";function wK(K,$){let z=xK(K,"state",`counter-evidence-${$}.json`);if(!RK(z))return null;try{let Q=EK(z,"utf-8"),X=JSON.parse(Q);if(typeof X.iteration!=="number")return null;let H=Array.isArray(X.evidence)?X.evidence:[],Z=[];for(let q of H){if(typeof q!=="object"||q===null)continue;let U=q;if(typeof U.findingId!=="string")continue;if(typeof U.claim!=="string")continue;let V=U.proofType;if(typeof V!=="string"||!FK.has(V))continue;let G=V,B=Array.isArray(U.artifacts)?U.artifacts:[];Z.push({findingId:U.findingId,claim:U.claim,proofType:G,artifacts:B.filter((J)=>typeof J==="string")})}return{iteration:X.iteration,evidence:Z}}catch{return null}}async function SK(K,$,z,Q={}){let X=Q.judges??V7,H=new Set,Z=new Set,q={},U=new Map;for(let V of $.evidence)U.set(V.findingId,V);for(let V of K){let G=n1(V),B=U.get(G);if(!B){Z.add(G);continue}let J=await Promise.all(X.map((j)=>z({finding:V,evidence:B,judge:j})));if(q[G]=J,J.filter((j)=>j.verdict==="APPROVE_OVERRIDE").length>=2)H.add(G);else Z.add(G)}return{approvedFindingIds:H,rejectedFindingIds:Z,votes:q}}function n1(K){let $=K.raw.slice(0,80).replace(/\s+/g," ").trim();return`${K.reviewer}::${$}`}async function NK(K,$,z,Q,X={}){let H={episodeBridge:X.episodeBridge===void 0?null:X.episodeBridge};for(let Z of Q){let q=n1(Z);if(z.approvedFindingIds.has(q))await q1(K,{iteration:$,trigger:"override_approved",rootCause:`[${Z.severity}] ${Z.description}`,fix:"override council approved counter-evidence; finding lifted",preventInFuture:"if this reviewer/file pair recurs, narrow the reviewer's selector OR add a baseline doc",evidence:{findingId:q,reviewId:Z.reviewId,file:Z.file??void 0,line:Z.line??void 0,severity:Z.severity,reviewer:Z.reviewer}},H);else if(z.rejectedFindingIds.has(q))await q1(K,{iteration:$,trigger:"override_rejected",rootCause:`[${Z.severity}] ${Z.description}`,fix:"override council rejected -- dev agent must fix the finding",preventInFuture:"address this finding in the next iteration",evidence:{findingId:q,reviewId:Z.reviewId,file:Z.file??void 0,line:Z.line??void 0,severity:Z.severity,reviewer:Z.reviewer}},H)}}var FK,V7;var G7=L(()=>{x1();FK=new Set(["file-exists","test-passes","grep-miss","reviewer-misread","duplicate-code-path","out-of-scope"]);V7=["judge-primary","judge-secondary","judge-tertiary"]});var Y7={};b(Y7,{writeEscalationHandoff:()=>pK,renderHandoff:()=>B7,readLatestHandoff:()=>cK});import{existsSync as kK,mkdirSync as DK,readdirSync as CK,readFileSync as hK,renameSync as bK,writeFileSync as yK}from"fs";import{dirname as vK,join as F1}from"path";function gK(){return new Date().toISOString()}function mK(K){let $=K.file?` (${K.file}${K.line!==null?":"+K.line:""})`:"";return` - [${K.severity}] ${K.description}${$} -- ${K.reviewer}`}function fK(K){let $=K.evidence,z=$.file?` ${$.file}${$.line!==void 0?":"+$.line:""}`:"";return` - **${K.trigger}** (iter ${K.iteration})${z}: ${K.rootCause}`}function B7(K,$,z){let Q=[];if(Q.push(`# Loki escalation handoff -- ${gK()}`),Q.push(""),Q.push(`Gate **${K.gateName}** has failed ${K.consecutiveFailures} consecutive times at iteration ${K.iteration}.`),Q.push(""),Q.push(`Reason: ${K.detail}`),Q.push(""),$.length>0){Q.push(`## Outstanding findings (${$.length})`),Q.push("");for(let X of $)Q.push(mK(X));Q.push("")}else Q.push("## Outstanding findings"),Q.push(""),Q.push("(no per-finding records captured -- gate failed without populating reviewer outputs)"),Q.push("");if(z.length>0){Q.push(`## Recent learnings (${Math.min(z.length,10)})`),Q.push("");for(let X of z.slice(-10))Q.push(fK(X));Q.push("")}return Q.push("## What the human must decide"),Q.push(""),Q.push("- Approve override? Write `.loki/state/counter-evidence-<iter>.json` with one entry per finding to dispute, then `rm .loki/PAUSE` to resume."),Q.push("- Disable a gate? Set `LOKI_GATE_<NAME>=false` in env (see skills/quality-gates.md)."),Q.push("- Tweak escalation? Set `LOKI_GATE_PAUSE_LIMIT` or `LOKI_GATE_ESCALATE_LIMIT`."),Q.push("- Roll back? Switch to `LOKI_LEGACY_BASH=1` and re-run; the bash route does not consult this handoff doc."),Q.push(""),Q.push("To resume: address the findings (or supply counter-evidence) and `rm .loki/PAUSE`."),Q.join(`
|
|
431
|
+
`)}function uK(K,$){DK(vK(K),{recursive:!0});let z=`${K}.tmp.${process.pid}.${++M7}`;yK(z,$),bK(z,K)}function pK(K,$,z={}){let Q=z.findings??l1(K,$.iteration).findings,X=z.learnings??o1(K).learnings,H=B7($,Q,X),Z=(z.now?.()??new Date).toISOString().replace(/[-:.]/g,""),q=F1(K,"escalations"),U=++M7,V=F1(q,`handoff-${Z}-${process.pid}-${U}-${$.gateName}.md`);return uK(V,H),{path:V,bytes:H.length}}function cK(K){let $=F1(K,"escalations");if(!kK($))return null;let z;try{z=CK($).filter((H)=>H.endsWith(".md"))}catch{return null}if(z.length===0)return null;z.sort();let Q=z[z.length-1];if(!Q)return null;let X=F1($,Q);try{return{path:X,body:hK(X,"utf-8")}}catch{return null}}var M7=0;var O7=L(()=>{E1();x1()});var T7={};b(T7,{runInternalPhase1Hooks:()=>sK,_resolveForTests:()=>aK,_internalPhase1HooksHelp:()=>eK});import{existsSync as lK,mkdirSync as dK,readdirSync as oK,statSync as nK}from"fs";import{join as U1,resolve as aK}from"path";async function sK(K){let[$,...z]=K;switch($){case void 0:case"help":case"--help":case"-h":return process.stdout.write(a1),$===void 0?1:0;case"reflect":return rK(z);case"override":return tK(z);case"handoff":return iK(z);default:return process.stderr.write(`Unknown subcommand: ${$}
|
|
432
|
+
`),process.stderr.write(a1),2}}async function rK(K){let $=s1(K[0]);if($===null)return process.stderr.write(`reflect: missing or invalid <iter>
|
|
433
|
+
`),2;let z=P();try{let X=(await Promise.resolve().then(() => (E1(),d1))).loadPreviousFindings(z,$);if(X.findings.length===0)return process.stdout.write(`reflect: no findings for iter ${$} (nothing to do)
|
|
434
|
+
`),0;let H=U1(z,"state");dK(H,{recursive:!0}),W1(U1(H,`findings-${$}.json`),{review_id:X.reviewId,iteration:$,findings:X.findings});let Z=await Promise.resolve().then(() => (x1(),U7)),q=0;if(process.env.LOKI_AUTO_LEARNINGS!=="0"){for(let U of X.findings)if(U.severity==="Critical"||U.severity==="High")await Z.appendFromGateFailure(z,$,U,{episodeBridge:null}),q+=1}return process.stdout.write(`reflect: persisted ${X.findings.length} findings + ${q} learnings (iter ${$})
|
|
435
435
|
`),0}catch(Q){return process.stderr.write(`reflect: ${Q.message}
|
|
436
|
-
`),1}}async function
|
|
437
|
-
`),2;let z=P();try{let Q=await Promise.resolve().then(() => (
|
|
438
|
-
`),0;let Z=(await Promise.resolve().then(() => (
|
|
439
|
-
`),0;let
|
|
436
|
+
`),1}}async function tK(K){let $=s1(K[0]);if($===null)return process.stderr.write(`override: missing or invalid <iter>
|
|
437
|
+
`),2;let z=P();try{let Q=await Promise.resolve().then(() => (G7(),J7)),X=Q.loadCounterEvidence(z,$);if(X===null||X.evidence.length===0)return process.stdout.write(`override: no counter-evidence for iter ${$} (skip)
|
|
438
|
+
`),0;let Z=(await Promise.resolve().then(() => (E1(),d1))).loadPreviousFindings(z,$),q=Z.findings.filter((_)=>_.severity==="Critical"||_.severity==="High");if(q.length===0)return process.stdout.write(`override: no blocking findings for iter ${$} (skip)
|
|
439
|
+
`),0;let U=new Set(["duplicate-code-path","file-exists","test-passes","grep-miss","out-of-scope"]),V=async(_)=>{let C=U.has(_.evidence.proofType);return{judge:_.judge,verdict:C?"APPROVE_OVERRIDE":"REJECT_OVERRIDE",reasoning:C?`[stub] proofType=${_.evidence.proofType} trusted`:`[stub] proofType=${_.evidence.proofType} requires manual review`}},G=await Q.runOverrideCouncil(q,X,V);await Q.recordOverrideOutcome(z,$,G,q);let B=U1(z,"quality","reviews");if(lK(B))try{let _=oK(B).filter((g)=>g.startsWith("review-")).sort(),C=_[_.length-1];if(C&&nK(U1(B,C)).isDirectory())W1(U1(B,C,`override-${$}.json`),{review_id:Z.reviewId,iteration:$,approved_finding_ids:Array.from(G.approvedFindingIds),rejected_finding_ids:Array.from(G.rejectedFindingIds),votes:G.votes})}catch{}let J=G.approvedFindingIds.size,Y=G.rejectedFindingIds.size;if(Y===0&&J>0)process.stdout.write(`override: LIFTED -- ${J} approved, ${Y} rejected
|
|
440
440
|
`);else process.stdout.write(`override: BLOCKED -- ${J} approved, ${Y} rejected
|
|
441
441
|
`);return 0}catch(Q){return process.stderr.write(`override: ${Q.message}
|
|
442
|
-
`),1}}async function
|
|
443
|
-
`),2;let X=P();try{let Z=(await Promise.resolve().then(() => (
|
|
442
|
+
`),1}}async function iK(K){let $=K[0],z=Number.parseInt(K[1]??"0",10),Q=s1(K[2]);if(!$||!Number.isFinite(z)||Q===null)return process.stderr.write(`handoff: usage: handoff <gate> <consecutive-failures> <iter>
|
|
443
|
+
`),2;let X=P();try{let Z=(await Promise.resolve().then(() => (O7(),Y7))).writeEscalationHandoff(X,{gateName:$,iteration:Q,consecutiveFailures:z,detail:`${$} hit PAUSE_LIMIT (${z} consecutive failures)`});return process.stdout.write(`handoff: wrote ${Z.path} (${Z.bytes}B)
|
|
444
444
|
`),0}catch(H){return process.stderr.write(`handoff: ${H.message}
|
|
445
|
-
`),1}}function
|
|
445
|
+
`),1}}function s1(K){if(K===void 0)return null;let $=Number.parseInt(K,10);return Number.isFinite($)&&$>=0?$:null}var a1=`loki internal phase1-hooks <subcommand>
|
|
446
446
|
|
|
447
447
|
Subcommands:
|
|
448
448
|
reflect <iter> Persist structured findings + auto-learnings.
|
|
@@ -451,8 +451,8 @@ Subcommands:
|
|
|
451
451
|
|
|
452
452
|
This command is invoked by autonomy/run.sh between iterations. Users
|
|
453
453
|
should not run it directly -- run \`loki start\` instead.
|
|
454
|
-
`,
|
|
455
|
-
`),0}p();n();y();import{readFileSync as
|
|
454
|
+
`,eK;var A7=L(()=>{y();I1();eK=a1});D1();function K0(){return process.stdout.write(`Loki Mode v${G1()}
|
|
455
|
+
`),0}p();n();y();import{readFileSync as h7,existsSync as b7}from"fs";import{resolve as y7}from"path";var v7=["claude","codex","cline","aider"];function z0(){let K=y7(P(),"state","provider");if(!b7(K))return"";try{return h7(K,"utf-8").trim()}catch{return""}}function g7(K,$){return K||$||process.env.LOKI_PROVIDER||"claude"}function m7(K){let $=z0(),z=g7(K,$);switch(process.stdout.write(`${k}Current Provider${W}
|
|
456
456
|
`),process.stdout.write(`
|
|
457
457
|
`),process.stdout.write(`${O}Provider:${W} ${z}
|
|
458
458
|
`),z){case"claude":process.stdout.write(`${D}Status:${W} Full features (subagents, parallel, MCP)
|
|
@@ -463,12 +463,12 @@ should not run it directly -- run \`loki start\` instead.
|
|
|
463
463
|
`);return process.stdout.write(`
|
|
464
464
|
`),process.stdout.write(`Switch provider: ${O}loki provider set <name>${W}
|
|
465
465
|
`),process.stdout.write(`Available: ${O}loki provider list${W}
|
|
466
|
-
`),0}async function
|
|
466
|
+
`),0}async function f7(){let $=z0()||process.env.LOKI_PROVIDER||"claude";process.stdout.write(`${k}Available Providers${W}
|
|
467
467
|
`),process.stdout.write(`
|
|
468
|
-
`);let z=await Promise.all(
|
|
468
|
+
`);let z=await Promise.all(v7.map(async(H)=>[H,await h(H)!==null])),Q=new Map;for(let[H,Z]of z)Q.set(H,Z?`${D}installed${W}`:`${R}not installed${W}`);let X=[["claude","claude - Claude Code (Anthropic) "],["codex","codex - Codex CLI (OpenAI) "],["cline","cline - Cline (multi-provider) "],["aider","aider - Aider (terminal pair prog) "]];for(let[H,Z]of X){let q=$===H?` ${O}(current)${W}`:"";process.stdout.write(` ${Z} ${Q.get(H)}${q}
|
|
469
469
|
`)}return process.stdout.write(`
|
|
470
470
|
`),process.stdout.write(`Set provider: ${O}loki provider set <name>${W}
|
|
471
|
-
`),0}function
|
|
471
|
+
`),0}function u7(){return process.stdout.write(`${k}Loki Mode Provider Management${W}
|
|
472
472
|
`),process.stdout.write(`
|
|
473
473
|
`),process.stdout.write(`Usage: loki provider <command>
|
|
474
474
|
`),process.stdout.write(`
|
|
@@ -486,8 +486,8 @@ should not run it directly -- run \`loki start\` instead.
|
|
|
486
486
|
`),process.stdout.write(` loki provider list
|
|
487
487
|
`),process.stdout.write(` loki provider info codex
|
|
488
488
|
`),process.stdout.write(` loki provider models
|
|
489
|
-
`),0}async function
|
|
490
|
-
`))if(Q.includes('"description"'))z++;return z}catch{return 0}}async function
|
|
489
|
+
`),0}async function Q0(K){let $=K[0]??"show",z=K.slice(1);switch($){case"show":case"current":return m7(z[0]);case"list":return f7();case"set":case"info":case"models":return p7(["provider",$,...z]);default:return u7()}}async function p7(K){let{run:$}=await Promise.resolve().then(() => (p(),$0)),{resolve:z}=await import("path"),{REPO_ROOT:Q}=await Promise.resolve().then(() => (y(),e1)),X=z(Q,"autonomy","loki"),H=await $([X,...K],{env:{LOKI_LEGACY_BASH:"1"},timeoutMs:3600000});return process.stdout.write(H.stdout),process.stderr.write(H.stderr),H.exitCode}n();y();Q1();p();import{existsSync as X0,readFileSync as l7}from"fs";import{resolve as t}from"path";import{mkdir as d7}from"fs/promises";var X1=t(k1(),"learnings");function h1(K){if(!X0(K))return 0;try{let $=l7(K,"utf-8"),z=0;for(let Q of $.split(`
|
|
490
|
+
`))if(Q.includes('"description"'))z++;return z}catch{return 0}}async function o7(){await d7(X1,{recursive:!0});let K=h1(t(X1,"patterns.jsonl")),$=h1(t(X1,"mistakes.jsonl")),z=h1(t(X1,"successes.jsonl"));return process.stdout.write(`${k}Cross-Project Learnings${W}
|
|
491
491
|
`),process.stdout.write(`
|
|
492
492
|
`),process.stdout.write(` Patterns: ${D}${K}${W}
|
|
493
493
|
`),process.stdout.write(` Mistakes: ${E}${$}${W}
|
|
@@ -496,7 +496,7 @@ should not run it directly -- run \`loki start\` instead.
|
|
|
496
496
|
`),process.stdout.write(`Location: ${X1}
|
|
497
497
|
`),process.stdout.write(`
|
|
498
498
|
`),process.stdout.write(`Use 'loki memory show <type>' to view entries
|
|
499
|
-
`),0}async function
|
|
499
|
+
`),0}async function n7(K){if(K){let Q=`
|
|
500
500
|
try:
|
|
501
501
|
from memory.layers import IndexLayer
|
|
502
502
|
layer = IndexLayer('.loki/memory')
|
|
@@ -506,9 +506,9 @@ except ImportError:
|
|
|
506
506
|
print('Error: memory.layers module not found')
|
|
507
507
|
except Exception as e:
|
|
508
508
|
print(f'Error: {e}')
|
|
509
|
-
`.trim(),X=await a(Q,{cwd:u});return process.stdout.write(X.stdout),0}let $=t(P(),"memory","index.json");if(!
|
|
509
|
+
`.trim(),X=await a(Q,{cwd:u});return process.stdout.write(X.stdout),0}let $=t(P(),"memory","index.json");if(!X0($))return process.stdout.write(`No index found
|
|
510
510
|
`),0;let z=await a(`import json, sys; sys.stdout.write(json.dumps(json.load(open(${JSON.stringify($)})), indent=4) + "\\n")`);if(z.exitCode!==0)return process.stdout.write(`No index found
|
|
511
|
-
`),0;return process.stdout.write(z.stdout),0}async function
|
|
511
|
+
`),0;return process.stdout.write(z.stdout),0}async function Z0(K){switch(K[0]??"list"){case"list":case"ls":return o7();case"index":return n7(K[1]==="rebuild");default:{let z=t(u,"autonomy","loki"),Q=await w([z,"memory",...K],{env:{LOKI_LEGACY_BASH:"1"},timeoutMs:3600000});return process.stdout.write(Q.stdout),process.stderr.write(Q.stderr),Q.exitCode}}}var j7=`Loki Mode (TypeScript port, Phase 2 of bash->Bun migration)
|
|
512
512
|
|
|
513
513
|
Usage: loki <command> [args...]
|
|
514
514
|
|
|
@@ -526,12 +526,12 @@ Phase 2 ported (Bun-native, fast):
|
|
|
526
526
|
|
|
527
527
|
All other commands fall through to the bash CLI (autonomy/loki).
|
|
528
528
|
Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
529
|
-
`;function
|
|
530
|
-
`)}async function
|
|
529
|
+
`;function K6(){let K=process.env.LOKI_LEGACY_BASH;if(K===void 0)return;let $=K.trim().toLowerCase();if($!=="1"&&$!=="true"&&$!=="yes"&&$!=="on")return;if(process.env.LOKI_SUPPRESS_BUN_DIRECT_WARN==="1")return;process.stderr.write(`warning: LOKI_LEGACY_BASH is set, but you are running the Bun runtime directly (src/cli.ts). The env var only takes effect via the bin/loki shim, which dispatches between Bun and bash. Behavior is unchanged; this message is informational.
|
|
530
|
+
`)}async function $6(K){K6();let $=K[0],z=K.slice(1);switch($){case void 0:case"help":case"--help":case"-h":return process.stdout.write(j7),0;case"version":case"--version":case"-v":return K0();case"provider":return Q0(z);case"memory":return Z0(z);case"status":{let{runStatus:Q}=await Promise.resolve().then(() => (B0(),G0));return Q(z)}case"stats":{let{runStats:Q}=await Promise.resolve().then(() => (A0(),T0));return Q(z)}case"doctor":{let{runDoctor:Q}=await Promise.resolve().then(() => (w0(),F0));return Q(z)}case"kpis":{let{runKpis:Q}=await Promise.resolve().then(() => (u0(),f0));return Q(z)}case"rollback":{let{runRollback:Q}=await Promise.resolve().then(() => (i0(),t0));return Q(z)}case"internal":{let Q=z[0];if(!Q||Q==="--help"||Q==="-h"||Q==="help"){let H=["loki internal -- runtime hooks driven by autonomy/run.sh","","Subcommands:"," phase1-hooks Persist structured findings, run override council,"," append learnings, and write the escalation handoff"," doc once per iteration. Driven by run.sh; not"," intended for direct invocation.","","Phase 1 (RARV-C closure) env vars:"," LOKI_INJECT_FINDINGS=1 Persist structured reviewer findings to"," .loki/state/findings-<iter>.json so the"," next iteration can address them."," LOKI_OVERRIDE_COUNCIL=1 Allow a 3-LLM override panel to lift a"," BLOCK when counter-evidence is presented."," See LOKI_OVERRIDE_JUDGES (csv),"," LOKI_OVERRIDE_PANEL_SIZE,"," LOKI_OVERRIDE_REAL_JUDGE."," LOKI_AUTO_LEARNINGS=1 Append failure rootcauses to"," .loki/state/relevant-learnings.json via"," the episodic memory bridge."," LOKI_HANDOFF_MD=1 Write a structured human handoff doc to"," .loki/escalations/<ts>.md before PAUSE.","","All four are default-on as of v7.5.3. Set to 0 to disable.","Reference: CHANGELOG.md (search 'Phase 1') and skills/healing.md.","","These commands are wired into the autonomous loop and may change","without notice. Do not script against them.",""].join(`
|
|
531
531
|
`);return process.stdout.write(`${H}
|
|
532
|
-
`),0}if(Q==="phase1-hooks"){let{runInternalPhase1Hooks:H}=await Promise.resolve().then(() => (
|
|
532
|
+
`),0}if(Q==="phase1-hooks"){let{runInternalPhase1Hooks:H}=await Promise.resolve().then(() => (A7(),T7));return H(z.slice(1))}return process.stderr.write(`Unknown internal subcommand: ${Q}
|
|
533
533
|
`),process.stderr.write(`Run 'loki internal --help' for the supported list.
|
|
534
534
|
`),2}default:return process.stderr.write(`Unknown command: ${$}
|
|
535
|
-
`),process.stderr.write(
|
|
535
|
+
`),process.stderr.write(j7),2}}process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var z6=await $6(Bun.argv.slice(2));process.exit(z6);
|
|
536
536
|
|
|
537
|
-
//# debugId=
|
|
537
|
+
//# debugId=9A266FF314C7FA3664756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "7.6.
|
|
3
|
+
"version": "7.6.2",
|
|
4
4
|
"description": "Loki Mode by Autonomi. Multi-agent autonomous SDLC framework. Spec to deployed app: PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. 4 AI providers (Claude Code, OpenAI Codex, Cline, Aider). 11 quality gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|