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 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.13
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.13 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.5.15 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.5.13
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
- output=$(cd "${TARGET_DIR:-.}" && pytest --tb=short 2>&1) || test_passed=false
6042
- details="pytest: $(echo "$output" | tail -5 | tr '\n' ' ')"
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" \
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.5.13"
10
+ __version__ = "7.5.15"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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)."""