loki-mode 7.5.14 → 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/loki +102 -4
- 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 +80 -80
- 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
|
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"
|
|
@@ -6961,6 +6971,20 @@ if disk_gb is not None:
|
|
|
6961
6971
|
elif disk_gb < 5:
|
|
6962
6972
|
disk_status = 'warn'
|
|
6963
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
|
+
|
|
6964
6988
|
pass_count = sum(1 for c in checks if c['status'] == 'pass')
|
|
6965
6989
|
fail_count = sum(1 for c in checks if c['status'] == 'fail')
|
|
6966
6990
|
warn_count = sum(1 for c in checks if c['status'] == 'warn')
|
|
@@ -6975,6 +6999,7 @@ result = {
|
|
|
6975
6999
|
'available_gb': disk_gb,
|
|
6976
7000
|
'status': disk_status
|
|
6977
7001
|
},
|
|
7002
|
+
'sentrux': sentrux,
|
|
6978
7003
|
'summary': {
|
|
6979
7004
|
'passed': pass_count,
|
|
6980
7005
|
'failed': fail_count,
|
|
@@ -7011,7 +7036,18 @@ cmd_sentrux() {
|
|
|
7011
7036
|
|
|
7012
7037
|
local sub="${1:-help}"
|
|
7013
7038
|
if [ "$#" -gt 0 ]; then shift; fi
|
|
7014
|
-
|
|
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]:-.}"
|
|
7015
7051
|
|
|
7016
7052
|
case "$sub" in
|
|
7017
7053
|
baseline)
|
|
@@ -7075,13 +7111,75 @@ cmd_sentrux() {
|
|
|
7075
7111
|
fi
|
|
7076
7112
|
return 0
|
|
7077
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
|
+
;;
|
|
7078
7174
|
help|--help|-h|"")
|
|
7079
7175
|
echo -e "${BOLD}loki sentrux${NC} - Architectural drift gate (opt-in, requires sentrux binary)"
|
|
7080
7176
|
echo ""
|
|
7081
7177
|
echo "Usage:"
|
|
7082
|
-
echo " loki sentrux baseline
|
|
7083
|
-
echo " loki sentrux gate
|
|
7084
|
-
echo " loki sentrux status
|
|
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"
|
|
7085
7183
|
echo ""
|
|
7086
7184
|
echo "Default path is the current directory."
|
|
7087
7185
|
echo ""
|
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)."""
|