loki-mode 7.5.13 → 7.5.15
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/lib/sentrux-gate.sh +145 -0
- package/autonomy/loki +231 -0
- package/autonomy/run.sh +110 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +62 -0
- package/dashboard/static/index.html +199 -109
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +144 -142
- package/mcp/__init__.py +1 -1
- package/memory/storage.py +35 -7
- 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.5.
|
|
6
|
+
# Loki Mode v7.5.15
|
|
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.5.
|
|
384
|
+
**v7.5.15 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.5.
|
|
1
|
+
7.5.15
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Loki Mode -- sentrux architectural-drift helper (v7.5.14).
|
|
3
|
+
#
|
|
4
|
+
# Why this exists:
|
|
5
|
+
# Loki's existing 11 quality gates and 3-reviewer council catch correctness
|
|
6
|
+
# and behavioral regressions, but no current gate emits a deterministic,
|
|
7
|
+
# per-iteration architecture-drift signal. sentrux (https://github.com/sentrux/sentrux)
|
|
8
|
+
# is a Rust CLI that scores codebase structure (modularity, acyclicity,
|
|
9
|
+
# depth, equality, redundancy) into a single 0-1 number, with a
|
|
10
|
+
# `gate --save` baseline plus `gate` compare workflow that catches when
|
|
11
|
+
# an iteration silently degrades architecture.
|
|
12
|
+
#
|
|
13
|
+
# Why opt-in only:
|
|
14
|
+
# sentrux is an external Rust binary (v0.5.x as of this release) that users
|
|
15
|
+
# install themselves via brew or curl. We do NOT bundle it, do NOT auto-install,
|
|
16
|
+
# and do NOT touch the iteration hot path by default. Every entry point in
|
|
17
|
+
# this file no-ops gracefully when sentrux is not on PATH.
|
|
18
|
+
#
|
|
19
|
+
# Verified facts (v7.5.14, 2026-05-03):
|
|
20
|
+
# - sentrux v0.5.7 binary works on darwin-arm64.
|
|
21
|
+
# - `sentrux gate --save <path>` writes <path>/.sentrux/baseline.json with
|
|
22
|
+
# real JSON: {timestamp, quality_signal (0..1), coupling_score, cycle_count,
|
|
23
|
+
# god_file_count, hotspot_count, complex_fn_count, max_depth,
|
|
24
|
+
# total_import_edges, cross_module_edges}.
|
|
25
|
+
# - `sentrux gate <path>` prints a "Quality: <before> -> <after>"
|
|
26
|
+
# line and either "DEGRADED" or "No degradation detected".
|
|
27
|
+
# - Defect: `sentrux gate` exits 0 even when output reports DEGRADED. This
|
|
28
|
+
# helper parses stdout and the JSON file rather than relying on exit code.
|
|
29
|
+
#
|
|
30
|
+
# Public API:
|
|
31
|
+
# sentrux_available -> 0 if binary on PATH, 1 otherwise
|
|
32
|
+
# sentrux_version -> prints "X.Y.Z" or empty on fail
|
|
33
|
+
# sentrux_baseline_save <path> -> writes <path>/.sentrux/baseline.json
|
|
34
|
+
# sentrux_baseline_quality <path> -> prints quality_signal*10000 as int
|
|
35
|
+
# (or empty on missing/malformed)
|
|
36
|
+
# sentrux_gate_diff <path> -> prints "<before>|<after>|<verdict>"
|
|
37
|
+
# where verdict is OK|DEGRADED|UNKNOWN
|
|
38
|
+
#
|
|
39
|
+
# All functions are pure helpers: no global state mutations, no side effects
|
|
40
|
+
# beyond what sentrux itself writes inside <path>/.sentrux/.
|
|
41
|
+
|
|
42
|
+
# Guard against double-source.
|
|
43
|
+
if [ "${__LOKI_SENTRUX_GATE_SH_LOADED:-0}" = "1" ]; then
|
|
44
|
+
return 0 2>/dev/null || true
|
|
45
|
+
fi
|
|
46
|
+
__LOKI_SENTRUX_GATE_SH_LOADED=1
|
|
47
|
+
|
|
48
|
+
sentrux_available() {
|
|
49
|
+
command -v sentrux >/dev/null 2>&1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sentrux_version() {
|
|
53
|
+
if ! sentrux_available; then
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
sentrux --version 2>/dev/null | head -1 | awk '{print $NF}' | tr -d 'v'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Run sentrux gate --save against <path>. Returns 0 on success, 1 on failure
|
|
60
|
+
# or if sentrux is unavailable. stderr from sentrux is preserved for debugging
|
|
61
|
+
# but stdout is suppressed.
|
|
62
|
+
sentrux_baseline_save() {
|
|
63
|
+
local path="${1:-.}"
|
|
64
|
+
if ! sentrux_available; then
|
|
65
|
+
return 1
|
|
66
|
+
fi
|
|
67
|
+
if [ ! -d "$path" ]; then
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
sentrux gate --save "$path" >/dev/null 2>&1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Read quality_signal from <path>/.sentrux/baseline.json and print it as an
|
|
74
|
+
# integer in the 0-10000 range (matching sentrux's stdout convention). Prints
|
|
75
|
+
# empty string and returns 1 on missing file, malformed JSON, or missing field.
|
|
76
|
+
sentrux_baseline_quality() {
|
|
77
|
+
local path="${1:-.}"
|
|
78
|
+
local baseline="$path/.sentrux/baseline.json"
|
|
79
|
+
if [ ! -f "$baseline" ]; then
|
|
80
|
+
return 1
|
|
81
|
+
fi
|
|
82
|
+
# Use python3 for JSON parsing -- jq is not always installed and python3
|
|
83
|
+
# is already a hard requirement in cmd_doctor. Pin float math to int via
|
|
84
|
+
# round() so callers get a stable, comparable integer.
|
|
85
|
+
local q
|
|
86
|
+
q=$(python3 -c "
|
|
87
|
+
import json, sys
|
|
88
|
+
try:
|
|
89
|
+
with open(sys.argv[1]) as f:
|
|
90
|
+
d = json.load(f)
|
|
91
|
+
v = d.get('quality_signal')
|
|
92
|
+
if v is None:
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
print(int(round(float(v) * 10000)))
|
|
95
|
+
except Exception:
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
" "$baseline" 2>/dev/null)
|
|
98
|
+
if [ -z "$q" ]; then
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
printf '%s' "$q"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Run sentrux gate against <path> and print "<before>|<after>|<verdict>".
|
|
105
|
+
# verdict is OK | DEGRADED | UNKNOWN. Returns 0 if a valid verdict was parsed,
|
|
106
|
+
# 1 only when sentrux is unavailable, the path is missing, or no Quality line
|
|
107
|
+
# could be parsed from output. before/after are integers (0-10000) or empty.
|
|
108
|
+
#
|
|
109
|
+
# Important: sentrux gate's exit code is inconsistent in v0.5.7 -- it has been
|
|
110
|
+
# observed to exit 0 on DEGRADED in some shapes and 1 in others. This helper
|
|
111
|
+
# captures stdout regardless of exit code and relies on text parsing as the
|
|
112
|
+
# source of truth.
|
|
113
|
+
sentrux_gate_diff() {
|
|
114
|
+
local path="${1:-.}"
|
|
115
|
+
if ! sentrux_available; then
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
if [ ! -d "$path" ]; then
|
|
119
|
+
return 1
|
|
120
|
+
fi
|
|
121
|
+
local out
|
|
122
|
+
# Deliberately do NOT gate on sentrux's exit code -- capture output either
|
|
123
|
+
# way. The `|| true` on the substitution keeps a nonzero gate exit from
|
|
124
|
+
# tripping callers running under `set -e`.
|
|
125
|
+
out=$(sentrux gate "$path" 2>/dev/null || true)
|
|
126
|
+
if [ -z "$out" ]; then
|
|
127
|
+
printf '%s' "||UNKNOWN"
|
|
128
|
+
return 1
|
|
129
|
+
fi
|
|
130
|
+
local quality_line before after verdict
|
|
131
|
+
# Match "Quality: 4333 -> 4321" or with arrow variants.
|
|
132
|
+
quality_line=$(printf '%s\n' "$out" | grep -E '^Quality:' | head -1 || true)
|
|
133
|
+
if [ -n "$quality_line" ]; then
|
|
134
|
+
before=$(printf '%s' "$quality_line" | grep -oE '[0-9]+' | sed -n '1p')
|
|
135
|
+
after=$(printf '%s' "$quality_line" | grep -oE '[0-9]+' | sed -n '2p')
|
|
136
|
+
fi
|
|
137
|
+
if printf '%s' "$out" | grep -q 'DEGRADED'; then
|
|
138
|
+
verdict="DEGRADED"
|
|
139
|
+
elif printf '%s' "$out" | grep -q 'No degradation detected'; then
|
|
140
|
+
verdict="OK"
|
|
141
|
+
else
|
|
142
|
+
verdict="UNKNOWN"
|
|
143
|
+
fi
|
|
144
|
+
printf '%s|%s|%s' "${before:-}" "${after:-}" "$verdict"
|
|
145
|
+
}
|
package/autonomy/loki
CHANGED
|
@@ -3096,6 +3096,11 @@ cmd_dashboard_help() {
|
|
|
3096
3096
|
echo ""
|
|
3097
3097
|
echo "Usage: loki dashboard <command> [options]"
|
|
3098
3098
|
echo ""
|
|
3099
|
+
echo "Note: 'loki dashboard' is the operations/observability UI (port ${DASHBOARD_DEFAULT_PORT})."
|
|
3100
|
+
echo " It is NOT the same as 'loki web' (Purple Lab, port ${PURPLE_LAB_DEFAULT_PORT}, where you input PRDs)."
|
|
3101
|
+
echo " Use 'loki dashboard' to monitor agents, tasks, costs, council, escalations."
|
|
3102
|
+
echo " Use 'loki web' to submit a PRD and watch agents build."
|
|
3103
|
+
echo ""
|
|
3099
3104
|
echo "Commands:"
|
|
3100
3105
|
echo " start Start the dashboard server"
|
|
3101
3106
|
echo " stop Stop the dashboard server"
|
|
@@ -3644,6 +3649,11 @@ cmd_web_help() {
|
|
|
3644
3649
|
echo ""
|
|
3645
3650
|
echo "Usage: loki web [command] [options]"
|
|
3646
3651
|
echo ""
|
|
3652
|
+
echo "Note: 'loki web' is Purple Lab (the PRD-input/build-watch UI, port ${PURPLE_LAB_DEFAULT_PORT})."
|
|
3653
|
+
echo " It is NOT the same as 'loki dashboard' (operations UI, port ${DASHBOARD_DEFAULT_PORT})."
|
|
3654
|
+
echo " Use 'loki web' to submit a PRD and watch agents build it."
|
|
3655
|
+
echo " Use 'loki dashboard' to monitor running agents, tasks, costs, council, escalations."
|
|
3656
|
+
echo ""
|
|
3647
3657
|
echo "Commands:"
|
|
3648
3658
|
echo " start Start Purple Lab (default)"
|
|
3649
3659
|
echo " stop Stop Purple Lab server"
|
|
@@ -6795,6 +6805,16 @@ cmd_doctor() {
|
|
|
6795
6805
|
echo -e " ${YELLOW}WARN${NC} OTEL - not configured (set LOKI_OTEL_ENDPOINT)"
|
|
6796
6806
|
warn_count=$((warn_count + 1))
|
|
6797
6807
|
fi
|
|
6808
|
+
# sentrux check (v7.5.14, optional architectural-drift gate)
|
|
6809
|
+
if command -v sentrux &>/dev/null; then
|
|
6810
|
+
local _sentrux_ver
|
|
6811
|
+
_sentrux_ver=$(sentrux --version 2>/dev/null | head -1 | awk '{print $NF}')
|
|
6812
|
+
echo -e " ${GREEN}PASS${NC} sentrux ${_sentrux_ver:-unknown} (architectural drift gate: loki sentrux help)"
|
|
6813
|
+
pass_count=$((pass_count + 1))
|
|
6814
|
+
else
|
|
6815
|
+
echo -e " ${YELLOW}WARN${NC} sentrux - not installed (optional, brew install sentrux/tap/sentrux)"
|
|
6816
|
+
warn_count=$((warn_count + 1))
|
|
6817
|
+
fi
|
|
6798
6818
|
echo ""
|
|
6799
6819
|
|
|
6800
6820
|
echo -e "${CYAN}System:${NC}"
|
|
@@ -6951,6 +6971,20 @@ if disk_gb is not None:
|
|
|
6951
6971
|
elif disk_gb < 5:
|
|
6952
6972
|
disk_status = 'warn'
|
|
6953
6973
|
|
|
6974
|
+
# v7.5.15: expose the sentrux architectural-drift gate state in --json so
|
|
6975
|
+
# dashboards/automation can surface it the same way the text-mode block does
|
|
6976
|
+
# (added in v7.5.14). Sibling of checks/disk -- intentionally not counted in
|
|
6977
|
+
# the summary tally to keep summary numbers backwards-compatible.
|
|
6978
|
+
sentrux_found = shutil.which('sentrux') is not None
|
|
6979
|
+
sentrux_version = get_version('sentrux') if sentrux_found else None
|
|
6980
|
+
sentrux_status = 'pass' if sentrux_found else 'warn'
|
|
6981
|
+
sentrux = {
|
|
6982
|
+
'found': sentrux_found,
|
|
6983
|
+
'version': sentrux_version,
|
|
6984
|
+
'status': sentrux_status,
|
|
6985
|
+
'required': 'optional'
|
|
6986
|
+
}
|
|
6987
|
+
|
|
6954
6988
|
pass_count = sum(1 for c in checks if c['status'] == 'pass')
|
|
6955
6989
|
fail_count = sum(1 for c in checks if c['status'] == 'fail')
|
|
6956
6990
|
warn_count = sum(1 for c in checks if c['status'] == 'warn')
|
|
@@ -6965,6 +6999,7 @@ result = {
|
|
|
6965
6999
|
'available_gb': disk_gb,
|
|
6966
7000
|
'status': disk_status
|
|
6967
7001
|
},
|
|
7002
|
+
'sentrux': sentrux,
|
|
6968
7003
|
'summary': {
|
|
6969
7004
|
'passed': pass_count,
|
|
6970
7005
|
'failed': fail_count,
|
|
@@ -6977,6 +7012,199 @@ print(json.dumps(result, indent=2))
|
|
|
6977
7012
|
"
|
|
6978
7013
|
}
|
|
6979
7014
|
|
|
7015
|
+
# Architectural-drift gate (v7.5.14, opt-in).
|
|
7016
|
+
#
|
|
7017
|
+
# Wraps the external sentrux Rust binary -- https://github.com/sentrux/sentrux --
|
|
7018
|
+
# to give users a deterministic per-iteration architecture-drift signal that
|
|
7019
|
+
# complements the existing 11 quality gates and 3-reviewer council. This is a
|
|
7020
|
+
# manual subcommand only in v7.5.14; iteration-loop integration is deferred
|
|
7021
|
+
# pending a real-PRD smoke test (tracked in CHANGELOG).
|
|
7022
|
+
#
|
|
7023
|
+
# Subcommands:
|
|
7024
|
+
# loki sentrux baseline [<path>] -- save .sentrux/baseline.json
|
|
7025
|
+
# loki sentrux gate [<path>] -- compare current vs baseline
|
|
7026
|
+
# loki sentrux status [<path>] -- print current baseline + verdict
|
|
7027
|
+
#
|
|
7028
|
+
# Default path is the current working directory.
|
|
7029
|
+
cmd_sentrux() {
|
|
7030
|
+
# shellcheck source=autonomy/lib/sentrux-gate.sh
|
|
7031
|
+
if ! source "$_LOKI_SCRIPT_DIR/lib/sentrux-gate.sh" 2>/dev/null \
|
|
7032
|
+
&& ! source "$(dirname "$0")/lib/sentrux-gate.sh" 2>/dev/null; then
|
|
7033
|
+
echo -e "${RED}sentrux helper missing -- expected at autonomy/lib/sentrux-gate.sh${NC}" >&2
|
|
7034
|
+
return 1
|
|
7035
|
+
fi
|
|
7036
|
+
|
|
7037
|
+
local sub="${1:-help}"
|
|
7038
|
+
if [ "$#" -gt 0 ]; then shift; fi
|
|
7039
|
+
|
|
7040
|
+
# Parse --force flag (used by init-rules); collect remaining args into target.
|
|
7041
|
+
local force=0
|
|
7042
|
+
local positional=()
|
|
7043
|
+
while [ "$#" -gt 0 ]; do
|
|
7044
|
+
case "$1" in
|
|
7045
|
+
--force|-f) force=1; shift ;;
|
|
7046
|
+
--) shift; while [ "$#" -gt 0 ]; do positional+=("$1"); shift; done ;;
|
|
7047
|
+
*) positional+=("$1"); shift ;;
|
|
7048
|
+
esac
|
|
7049
|
+
done
|
|
7050
|
+
local target="${positional[0]:-.}"
|
|
7051
|
+
|
|
7052
|
+
case "$sub" in
|
|
7053
|
+
baseline)
|
|
7054
|
+
if ! sentrux_available; then
|
|
7055
|
+
echo -e "${YELLOW}sentrux not installed.${NC} Install via:" >&2
|
|
7056
|
+
echo " brew install sentrux/tap/sentrux" >&2
|
|
7057
|
+
echo " or download from https://github.com/sentrux/sentrux/releases" >&2
|
|
7058
|
+
return 2
|
|
7059
|
+
fi
|
|
7060
|
+
if sentrux_baseline_save "$target"; then
|
|
7061
|
+
local q
|
|
7062
|
+
q=$(sentrux_baseline_quality "$target" || echo "?")
|
|
7063
|
+
echo -e "${GREEN}Baseline saved.${NC} Quality: $q (path: $target)"
|
|
7064
|
+
return 0
|
|
7065
|
+
fi
|
|
7066
|
+
echo -e "${RED}Failed to save baseline.${NC}" >&2
|
|
7067
|
+
return 1
|
|
7068
|
+
;;
|
|
7069
|
+
gate)
|
|
7070
|
+
if ! sentrux_available; then
|
|
7071
|
+
echo -e "${YELLOW}sentrux not installed.${NC} Run 'loki sentrux baseline' for setup hints." >&2
|
|
7072
|
+
return 2
|
|
7073
|
+
fi
|
|
7074
|
+
local diff verdict before after
|
|
7075
|
+
diff=$(sentrux_gate_diff "$target")
|
|
7076
|
+
verdict=$(printf '%s' "$diff" | awk -F'|' '{print $3}')
|
|
7077
|
+
before=$(printf '%s' "$diff" | awk -F'|' '{print $1}')
|
|
7078
|
+
after=$(printf '%s' "$diff" | awk -F'|' '{print $2}')
|
|
7079
|
+
case "$verdict" in
|
|
7080
|
+
OK)
|
|
7081
|
+
echo -e "${GREEN}OK${NC} Quality: $before -> $after (no degradation)"
|
|
7082
|
+
return 0
|
|
7083
|
+
;;
|
|
7084
|
+
DEGRADED)
|
|
7085
|
+
echo -e "${RED}DEGRADED${NC} Quality: $before -> $after"
|
|
7086
|
+
echo " Architecture regressed since the saved baseline."
|
|
7087
|
+
echo " Inspect with: sentrux scan $target"
|
|
7088
|
+
return 1
|
|
7089
|
+
;;
|
|
7090
|
+
*)
|
|
7091
|
+
echo -e "${YELLOW}UNKNOWN${NC} Could not parse sentrux output."
|
|
7092
|
+
echo " Try: sentrux gate --save $target (to refresh baseline)"
|
|
7093
|
+
return 2
|
|
7094
|
+
;;
|
|
7095
|
+
esac
|
|
7096
|
+
;;
|
|
7097
|
+
status)
|
|
7098
|
+
if ! sentrux_available; then
|
|
7099
|
+
echo "sentrux: not installed (optional)"
|
|
7100
|
+
echo "install: brew install sentrux/tap/sentrux"
|
|
7101
|
+
return 0
|
|
7102
|
+
fi
|
|
7103
|
+
local v
|
|
7104
|
+
v=$(sentrux_version || echo "unknown")
|
|
7105
|
+
echo "sentrux: v$v"
|
|
7106
|
+
local q
|
|
7107
|
+
if q=$(sentrux_baseline_quality "$target"); then
|
|
7108
|
+
echo "baseline ($target): quality=$q"
|
|
7109
|
+
else
|
|
7110
|
+
echo "baseline ($target): not saved (run: loki sentrux baseline $target)"
|
|
7111
|
+
fi
|
|
7112
|
+
return 0
|
|
7113
|
+
;;
|
|
7114
|
+
init-rules)
|
|
7115
|
+
# Scaffold a conservative default .sentrux/rules.toml in <target>/.sentrux/.
|
|
7116
|
+
# Does not require sentrux binary -- the file is plain text.
|
|
7117
|
+
local rules_dir="$target/.sentrux"
|
|
7118
|
+
local rules_file="$rules_dir/rules.toml"
|
|
7119
|
+
local abs_path
|
|
7120
|
+
if [ -e "$rules_file" ] && [ "$force" -ne 1 ]; then
|
|
7121
|
+
echo -e "${YELLOW}Refusing to overwrite existing $rules_file${NC}" >&2
|
|
7122
|
+
echo " Re-run with --force to replace it." >&2
|
|
7123
|
+
return 1
|
|
7124
|
+
fi
|
|
7125
|
+
if ! mkdir -p "$rules_dir" 2>/dev/null; then
|
|
7126
|
+
echo -e "${RED}Failed to create $rules_dir${NC}" >&2
|
|
7127
|
+
return 2
|
|
7128
|
+
fi
|
|
7129
|
+
if ! cat > "$rules_file" <<'SENTRUX_RULES_EOF'
|
|
7130
|
+
# Sentrux architectural rules scaffolded by `loki sentrux init-rules` (Loki Mode v7.5.15).
|
|
7131
|
+
# Conservative defaults -- tighten per-project as needed.
|
|
7132
|
+
# See https://github.com/sentrux/sentrux for full spec.
|
|
7133
|
+
|
|
7134
|
+
[constraints]
|
|
7135
|
+
# Block any iteration that introduces an import cycle.
|
|
7136
|
+
max_cycles = 0
|
|
7137
|
+
# Block files that grow into "god files" (very high churn + many dependents).
|
|
7138
|
+
no_god_files = true
|
|
7139
|
+
# Cap cyclomatic complexity per function (sentrux default is liberal).
|
|
7140
|
+
max_cc = 30
|
|
7141
|
+
|
|
7142
|
+
# Layer enforcement is project-specific. Uncomment + edit when you know the
|
|
7143
|
+
# layout you want enforced. Example for a typical app:
|
|
7144
|
+
#
|
|
7145
|
+
# [[layers]]
|
|
7146
|
+
# name = "core"
|
|
7147
|
+
# paths = ["src/core/*"]
|
|
7148
|
+
# order = 0
|
|
7149
|
+
#
|
|
7150
|
+
# [[layers]]
|
|
7151
|
+
# name = "app"
|
|
7152
|
+
# paths = ["src/app/*"]
|
|
7153
|
+
# order = 2
|
|
7154
|
+
#
|
|
7155
|
+
# [[boundaries]]
|
|
7156
|
+
# from = "src/app/*"
|
|
7157
|
+
# to = "src/core/internal/*"
|
|
7158
|
+
# reason = "app must not depend on core internals"
|
|
7159
|
+
SENTRUX_RULES_EOF
|
|
7160
|
+
then
|
|
7161
|
+
echo -e "${RED}Failed to write $rules_file${NC}" >&2
|
|
7162
|
+
return 2
|
|
7163
|
+
fi
|
|
7164
|
+
# Resolve absolute path for friendly output (portable across macOS/Linux).
|
|
7165
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
7166
|
+
abs_path=$(python3 -c "import os,sys; print(os.path.abspath(sys.argv[1]))" "$rules_file" 2>/dev/null || echo "$rules_file")
|
|
7167
|
+
else
|
|
7168
|
+
abs_path=$(cd "$(dirname "$rules_file")" 2>/dev/null && pwd)/$(basename "$rules_file")
|
|
7169
|
+
fi
|
|
7170
|
+
echo -e "${GREEN}Wrote $abs_path${NC}"
|
|
7171
|
+
echo "Edit it to add layer/boundary rules, then run: loki sentrux baseline $target"
|
|
7172
|
+
return 0
|
|
7173
|
+
;;
|
|
7174
|
+
help|--help|-h|"")
|
|
7175
|
+
echo -e "${BOLD}loki sentrux${NC} - Architectural drift gate (opt-in, requires sentrux binary)"
|
|
7176
|
+
echo ""
|
|
7177
|
+
echo "Usage:"
|
|
7178
|
+
echo " loki sentrux baseline [<path>] Save current architecture as baseline"
|
|
7179
|
+
echo " loki sentrux gate [<path>] Compare current vs baseline (exit 1 on DEGRADED)"
|
|
7180
|
+
echo " loki sentrux status [<path>] Show binary version + saved baseline quality"
|
|
7181
|
+
echo " loki sentrux init-rules [<path>] [--force]"
|
|
7182
|
+
echo " Scaffold a default .sentrux/rules.toml"
|
|
7183
|
+
echo ""
|
|
7184
|
+
echo "Default path is the current directory."
|
|
7185
|
+
echo ""
|
|
7186
|
+
echo "About:"
|
|
7187
|
+
echo " Wraps the sentrux CLI (https://github.com/sentrux/sentrux), a Rust tool that"
|
|
7188
|
+
echo " scores codebase structure into a single 0-10000 quality signal. Useful as a"
|
|
7189
|
+
echo " supplement to Loki's existing 11 quality gates when you want an objective"
|
|
7190
|
+
echo " per-iteration architectural-drift number."
|
|
7191
|
+
echo ""
|
|
7192
|
+
echo " Iteration-loop auto-gating is planned for a future release; for now this"
|
|
7193
|
+
echo " subcommand is the manual integration surface."
|
|
7194
|
+
echo ""
|
|
7195
|
+
echo "Install sentrux:"
|
|
7196
|
+
echo " brew install sentrux/tap/sentrux"
|
|
7197
|
+
echo " curl -fsSL https://raw.githubusercontent.com/sentrux/sentrux/main/install.sh | sh"
|
|
7198
|
+
return 0
|
|
7199
|
+
;;
|
|
7200
|
+
*)
|
|
7201
|
+
echo -e "${RED}Unknown subcommand: $sub${NC}" >&2
|
|
7202
|
+
echo "Run 'loki sentrux help' for usage." >&2
|
|
7203
|
+
return 1
|
|
7204
|
+
;;
|
|
7205
|
+
esac
|
|
7206
|
+
}
|
|
7207
|
+
|
|
6980
7208
|
# Show version
|
|
6981
7209
|
cmd_version() {
|
|
6982
7210
|
echo "Loki Mode v$(get_version)"
|
|
@@ -11974,6 +12202,9 @@ main() {
|
|
|
11974
12202
|
doctor)
|
|
11975
12203
|
cmd_doctor "$@"
|
|
11976
12204
|
;;
|
|
12205
|
+
sentrux)
|
|
12206
|
+
cmd_sentrux "$@"
|
|
12207
|
+
;;
|
|
11977
12208
|
setup-skill)
|
|
11978
12209
|
cmd_setup_skill "$@"
|
|
11979
12210
|
;;
|
package/autonomy/run.sh
CHANGED
|
@@ -5916,6 +5916,29 @@ except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
|
5916
5916
|
# Results stored in .loki/quality/test-results.json
|
|
5917
5917
|
# ============================================================================
|
|
5918
5918
|
|
|
5919
|
+
# v7.5.15 (Triage #14): wrap pytest with a configurable timeout so a
|
|
5920
|
+
# deadlocked or infinite-loop test under /test cannot hang the gate
|
|
5921
|
+
# indefinitely. Uses `timeout` on Linux, `gtimeout` (coreutils) on macOS,
|
|
5922
|
+
# and degrades gracefully if neither is available (logs a warning, runs
|
|
5923
|
+
# unbounded). Configurable via LOKI_PYTEST_TIMEOUT (default 300s).
|
|
5924
|
+
#
|
|
5925
|
+
# Usage: _loki_run_pytest_with_timeout <target_dir> [pytest_args...]
|
|
5926
|
+
# Stdout: combined pytest output
|
|
5927
|
+
# Exit: 0 on pass, non-zero on fail. Exit 124 indicates the timeout fired.
|
|
5928
|
+
_loki_run_pytest_with_timeout() {
|
|
5929
|
+
local target_dir="$1"; shift
|
|
5930
|
+
local pytest_timeout="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
|
|
5931
|
+
local _to_cmd=()
|
|
5932
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
5933
|
+
_to_cmd=(gtimeout "${pytest_timeout}s")
|
|
5934
|
+
elif command -v timeout >/dev/null 2>&1; then
|
|
5935
|
+
_to_cmd=(timeout "${pytest_timeout}s")
|
|
5936
|
+
else
|
|
5937
|
+
log_warn "Neither gtimeout nor timeout available; pytest gate will run unbounded (install coreutils on macOS)"
|
|
5938
|
+
fi
|
|
5939
|
+
(cd "$target_dir" && "${_to_cmd[@]}" pytest "$@" 2>&1)
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5919
5942
|
enforce_test_coverage() {
|
|
5920
5943
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
5921
5944
|
local quality_dir="$loki_dir/quality"
|
|
@@ -6037,9 +6060,19 @@ enforce_test_coverage() {
|
|
|
6037
6060
|
fi
|
|
6038
6061
|
if [ "$has_python_project" = "true" ] && command -v pytest &>/dev/null; then
|
|
6039
6062
|
test_runner="pytest"
|
|
6040
|
-
local output
|
|
6041
|
-
|
|
6042
|
-
|
|
6063
|
+
local output pytest_exit
|
|
6064
|
+
# v7.5.15 (Triage #14): wrapped with configurable timeout via helper.
|
|
6065
|
+
output=$(_loki_run_pytest_with_timeout "${TARGET_DIR:-.}" --tb=short)
|
|
6066
|
+
pytest_exit=$?
|
|
6067
|
+
if [ "$pytest_exit" -eq 124 ]; then
|
|
6068
|
+
local _pt_to="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
|
|
6069
|
+
test_passed=false
|
|
6070
|
+
log_warn "pytest gate timed out after ${_pt_to}s (exit 124)"
|
|
6071
|
+
details="pytest: TIMED OUT after ${_pt_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
6072
|
+
else
|
|
6073
|
+
[ "$pytest_exit" -ne 0 ] && test_passed=false
|
|
6074
|
+
details="pytest: $(echo "$output" | tail -5 | tr '\n' ' ')"
|
|
6075
|
+
fi
|
|
6043
6076
|
fi
|
|
6044
6077
|
fi
|
|
6045
6078
|
|
|
@@ -10487,11 +10520,79 @@ PRD_PARSE_EOF
|
|
|
10487
10520
|
# Main Autonomous Loop
|
|
10488
10521
|
#===============================================================================
|
|
10489
10522
|
|
|
10523
|
+
#-------------------------------------------------------------------------------
|
|
10524
|
+
# Sentrux architectural-drift gate hooks (v7.5.15).
|
|
10525
|
+
#
|
|
10526
|
+
# Opt-in via LOKI_SENTRUX_GATE=1. Default OFF -- zero behavior change for users
|
|
10527
|
+
# who don't opt in. The helper at autonomy/lib/sentrux-gate.sh is sourced inside
|
|
10528
|
+
# run_autonomous() under the same guard. Both hook functions no-op silently if
|
|
10529
|
+
# the helper is not loaded or the sentrux binary is not on PATH.
|
|
10530
|
+
#-------------------------------------------------------------------------------
|
|
10531
|
+
_loki_sentrux_iteration_start() {
|
|
10532
|
+
local target="${1:-${TARGET_DIR:-.}}"
|
|
10533
|
+
if [ "${LOKI_SENTRUX_GATE:-0}" != "1" ]; then
|
|
10534
|
+
return 0
|
|
10535
|
+
fi
|
|
10536
|
+
if ! type sentrux_available >/dev/null 2>&1 || ! sentrux_available; then
|
|
10537
|
+
return 0
|
|
10538
|
+
fi
|
|
10539
|
+
sentrux_baseline_save "$target" >/dev/null 2>&1 || true
|
|
10540
|
+
return 0
|
|
10541
|
+
}
|
|
10542
|
+
|
|
10543
|
+
_loki_sentrux_iteration_end() {
|
|
10544
|
+
local iter="${1:-0}"
|
|
10545
|
+
local target="${2:-${TARGET_DIR:-.}}"
|
|
10546
|
+
if [ "${LOKI_SENTRUX_GATE:-0}" != "1" ]; then
|
|
10547
|
+
return 0
|
|
10548
|
+
fi
|
|
10549
|
+
if ! type sentrux_available >/dev/null 2>&1 || ! sentrux_available; then
|
|
10550
|
+
return 0
|
|
10551
|
+
fi
|
|
10552
|
+
local diff before after verdict
|
|
10553
|
+
diff=$(sentrux_gate_diff "$target" 2>/dev/null || true)
|
|
10554
|
+
if [ -z "$diff" ]; then
|
|
10555
|
+
return 0
|
|
10556
|
+
fi
|
|
10557
|
+
before="${diff%%|*}"
|
|
10558
|
+
local rest="${diff#*|}"
|
|
10559
|
+
after="${rest%%|*}"
|
|
10560
|
+
verdict="${rest#*|}"
|
|
10561
|
+
if type log_info >/dev/null 2>&1; then
|
|
10562
|
+
log_info "sentrux gate iter=$iter verdict=$verdict before=${before:-?} after=${after:-?}"
|
|
10563
|
+
fi
|
|
10564
|
+
if [ "$verdict" = "DEGRADED" ]; then
|
|
10565
|
+
local state_dir="$target/.loki/state"
|
|
10566
|
+
mkdir -p "$state_dir" 2>/dev/null || true
|
|
10567
|
+
local finding_path="$state_dir/findings-sentrux-${iter}.json"
|
|
10568
|
+
local ts
|
|
10569
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
10570
|
+
local before_json="${before:-0}"
|
|
10571
|
+
local after_json="${after:-0}"
|
|
10572
|
+
# Guard against non-numeric values when serializing to JSON.
|
|
10573
|
+
if ! [[ "$before_json" =~ ^[0-9]+$ ]]; then before_json=0; fi
|
|
10574
|
+
if ! [[ "$after_json" =~ ^[0-9]+$ ]]; then after_json=0; fi
|
|
10575
|
+
printf '{"type":"architectural-drift","iteration":%s,"before":%s,"after":%s,"verdict":"DEGRADED","timestamp":"%s","source":"sentrux"}\n' \
|
|
10576
|
+
"$iter" "$before_json" "$after_json" "$ts" \
|
|
10577
|
+
> "$finding_path" 2>/dev/null || true
|
|
10578
|
+
fi
|
|
10579
|
+
return 0
|
|
10580
|
+
}
|
|
10581
|
+
|
|
10490
10582
|
run_autonomous() {
|
|
10491
10583
|
local prd_path="$1"
|
|
10492
10584
|
|
|
10493
10585
|
log_header "Starting Autonomous Execution"
|
|
10494
10586
|
|
|
10587
|
+
# Sentrux architectural-drift gate (opt-in via LOKI_SENTRUX_GATE=1, v7.5.15).
|
|
10588
|
+
# Source the helper only when the gate is enabled to avoid hot-path overhead
|
|
10589
|
+
# for the default-off case. Failure to source is non-fatal -- the wrapper
|
|
10590
|
+
# functions degrade to no-ops via type checks.
|
|
10591
|
+
if [ "${LOKI_SENTRUX_GATE:-0}" = "1" ]; then
|
|
10592
|
+
# shellcheck disable=SC1090,SC1091
|
|
10593
|
+
source "${SCRIPT_DIR}/lib/sentrux-gate.sh" 2>/dev/null || true
|
|
10594
|
+
fi
|
|
10595
|
+
|
|
10495
10596
|
# Auto-detect PRD if not provided
|
|
10496
10597
|
if [ -z "$prd_path" ]; then
|
|
10497
10598
|
log_step "No PRD provided, searching for existing PRD files..."
|
|
@@ -10670,6 +10771,9 @@ except Exception as exc:
|
|
|
10670
10771
|
# Auto-track iteration start (for dashboard task queue)
|
|
10671
10772
|
track_iteration_start "$ITERATION_COUNT" "$prd_path"
|
|
10672
10773
|
|
|
10774
|
+
# Sentrux architectural-drift baseline snapshot (opt-in, v7.5.15).
|
|
10775
|
+
_loki_sentrux_iteration_start "${TARGET_DIR:-.}"
|
|
10776
|
+
|
|
10673
10777
|
local prompt
|
|
10674
10778
|
prompt=$(build_prompt "$retry" "$prd_path" "$ITERATION_COUNT")
|
|
10675
10779
|
|
|
@@ -11149,6 +11253,9 @@ if __name__ == "__main__":
|
|
|
11149
11253
|
# Auto-track iteration completion (for dashboard task queue)
|
|
11150
11254
|
track_iteration_complete "$ITERATION_COUNT" "$exit_code"
|
|
11151
11255
|
|
|
11256
|
+
# Sentrux architectural-drift gate diff + finding emission (opt-in, v7.5.15).
|
|
11257
|
+
_loki_sentrux_iteration_end "$ITERATION_COUNT" "${TARGET_DIR:-.}"
|
|
11258
|
+
|
|
11152
11259
|
# End OTEL phase span (if OTEL is enabled)
|
|
11153
11260
|
if [ -n "${LOKI_OTEL_ENDPOINT:-}" ]; then
|
|
11154
11261
|
emit_event_pending "otel_span_end" \
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -5955,6 +5955,68 @@ async def get_findings(iteration: int):
|
|
|
5955
5955
|
detail=f"No findings for iteration {iteration}")
|
|
5956
5956
|
|
|
5957
5957
|
|
|
5958
|
+
@app.get("/api/quality/architecture")
|
|
5959
|
+
async def get_quality_architecture():
|
|
5960
|
+
"""Return the sentrux architectural-drift series.
|
|
5961
|
+
|
|
5962
|
+
Globs `.loki/state/findings-sentrux-*.json` (written by the iteration
|
|
5963
|
+
loop when LOKI_SENTRUX_GATE=1), sorts by iteration ascending, and
|
|
5964
|
+
returns a series suitable for plotting drift over time.
|
|
5965
|
+
|
|
5966
|
+
Per-file JSON parse errors are logged and skipped; the endpoint stays
|
|
5967
|
+
200 OK even when no files exist or every file is corrupt.
|
|
5968
|
+
"""
|
|
5969
|
+
base = _get_loki_dir()
|
|
5970
|
+
state_dir = base / "state"
|
|
5971
|
+
series: list[dict[str, Any]] = []
|
|
5972
|
+
if state_dir.exists():
|
|
5973
|
+
try:
|
|
5974
|
+
paths = list(state_dir.glob("findings-sentrux-*.json"))
|
|
5975
|
+
except OSError as exc:
|
|
5976
|
+
logger.warning("sentrux: failed to glob %s: %s", state_dir, exc)
|
|
5977
|
+
paths = []
|
|
5978
|
+
for path in paths:
|
|
5979
|
+
try:
|
|
5980
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
5981
|
+
data = json.loads(text)
|
|
5982
|
+
except (OSError, IOError) as exc:
|
|
5983
|
+
logger.warning("sentrux: skipping unreadable %s: %s",
|
|
5984
|
+
path.name, exc)
|
|
5985
|
+
continue
|
|
5986
|
+
except json.JSONDecodeError as exc:
|
|
5987
|
+
logger.warning("sentrux: skipping corrupt JSON %s: %s",
|
|
5988
|
+
path.name, exc)
|
|
5989
|
+
continue
|
|
5990
|
+
if not isinstance(data, dict):
|
|
5991
|
+
logger.warning("sentrux: skipping non-object payload in %s",
|
|
5992
|
+
path.name)
|
|
5993
|
+
continue
|
|
5994
|
+
try:
|
|
5995
|
+
iteration = int(data.get("iteration"))
|
|
5996
|
+
before = int(data.get("before"))
|
|
5997
|
+
after = int(data.get("after"))
|
|
5998
|
+
except (TypeError, ValueError) as exc:
|
|
5999
|
+
logger.warning("sentrux: skipping %s, bad ints: %s",
|
|
6000
|
+
path.name, exc)
|
|
6001
|
+
continue
|
|
6002
|
+
verdict = data.get("verdict")
|
|
6003
|
+
if verdict not in ("DEGRADED", "OK", "UNKNOWN"):
|
|
6004
|
+
verdict = "UNKNOWN"
|
|
6005
|
+
timestamp = data.get("timestamp")
|
|
6006
|
+
if not isinstance(timestamp, str):
|
|
6007
|
+
timestamp = ""
|
|
6008
|
+
series.append({
|
|
6009
|
+
"iteration": iteration,
|
|
6010
|
+
"before": before,
|
|
6011
|
+
"after": after,
|
|
6012
|
+
"verdict": verdict,
|
|
6013
|
+
"timestamp": timestamp,
|
|
6014
|
+
})
|
|
6015
|
+
series.sort(key=lambda e: e["iteration"])
|
|
6016
|
+
current = series[-1]["after"] if series else None
|
|
6017
|
+
return {"series": series, "current": current, "samples": len(series)}
|
|
6018
|
+
|
|
6019
|
+
|
|
5958
6020
|
@app.get("/api/learnings")
|
|
5959
6021
|
async def get_learnings(limit: int = 50):
|
|
5960
6022
|
"""Read recent learnings (newest first)."""
|