loki-mode 7.117.2 → 7.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.117.2
6
+ # Loki Mode v7.118.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
408
408
 
409
409
  ---
410
410
 
411
- **v7.117.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.118.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.117.2
1
+ 7.118.0
@@ -92,6 +92,42 @@ COUNCIL_MIN_ITERATIONS=${LOKI_COUNCIL_MIN_ITERATIONS:-3}
92
92
  if [ "$COUNCIL_MIN_ITERATIONS" -lt 1 ] 2>/dev/null; then
93
93
  COUNCIL_MIN_ITERATIONS=1
94
94
  fi
95
+
96
+ # _council_effective_min_iter (SaaS #122): resolve the effective MIN_ITERATIONS
97
+ # floor as a function of DETECTED_COMPLEXITY, computed at council-run time (NOT at
98
+ # source time -- DETECTED_COMPLEXITY is populated by run.sh:detect_complexity
99
+ # before the loop, but AFTER this file is sourced). The floor is a HARD gate in
100
+ # council_should_stop: while ITERATION_COUNT < floor the council is not allowed to
101
+ # even evaluate, so a genuinely-complete SIMPLE app (e.g. a small invoice app) that
102
+ # claims done at iteration 1 was still FORCED through ~2 extra idle iterations
103
+ # (~15min) before the council could approve. Lowering the floor to 1 for the
104
+ # `simple` tier removes only those FORCED idle iterations; it does NOT skip any
105
+ # verification. At floor=1, `ITERATION_COUNT=1` makes `1 -lt 1` false, so the
106
+ # council is ALLOWED to convene -- the evidence gate, checklist gate, aggregate
107
+ # vote, provenance, boot smoke and devil's advocate all still run and can still
108
+ # reject. Standard/complex keep floor 3.
109
+ #
110
+ # Precedence (the explicit-override case must win so a user who sets
111
+ # LOKI_COUNCIL_MIN_ITERATIONS=3 on a simple app is honored, not silently lowered):
112
+ # 1. explicit LOKI_COUNCIL_MIN_ITERATIONS env set (non-empty) -> that value
113
+ # (already clamped into COUNCIL_MIN_ITERATIONS above), verbatim.
114
+ # 2. DETECTED_COMPLEXITY == simple (default floor, no override) -> 1.
115
+ # 3. otherwise -> COUNCIL_MIN_ITERATIONS (the standard/complex default of 3).
116
+ # When DETECTED_COMPLEXITY is empty (unset) this returns COUNCIL_MIN_ITERATIONS --
117
+ # a no-op that preserves the historical standard-path behavior.
118
+ _council_effective_min_iter() {
119
+ # Explicit user override wins: read the RAW env var (COUNCIL_MIN_ITERATIONS
120
+ # collapses "user set 3" and "default 3" into one value and can't distinguish).
121
+ if [ -n "${LOKI_COUNCIL_MIN_ITERATIONS:-}" ]; then
122
+ printf '%s' "${COUNCIL_MIN_ITERATIONS}"
123
+ return 0
124
+ fi
125
+ if [ "${DETECTED_COMPLEXITY:-}" = "simple" ]; then
126
+ printf '%s' 1
127
+ return 0
128
+ fi
129
+ printf '%s' "${COUNCIL_MIN_ITERATIONS}"
130
+ }
95
131
  COUNCIL_CONVERGENCE_WINDOW=${LOKI_COUNCIL_CONVERGENCE_WINDOW:-3}
96
132
  COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}
97
133
  COUNCIL_DONE_SIGNAL_LIMIT=${LOKI_COUNCIL_DONE_SIGNAL_LIMIT:-10}
@@ -3470,7 +3506,10 @@ _council_should_check_now() {
3470
3506
  local completion_claimed="${2:-false}"
3471
3507
  local iter="${ITERATION_COUNT:-0}"
3472
3508
  local interval="${COUNCIL_CHECK_INTERVAL:-5}"
3473
- local min_iter="${COUNCIL_MIN_ITERATIONS:-3}"
3509
+ # SaaS #122: tier-aware effective floor (simple->1) so the no-claim early
3510
+ # check can also fire at iteration 1 on a genuinely-done simple app.
3511
+ local min_iter
3512
+ min_iter="$(_council_effective_min_iter)"
3474
3513
 
3475
3514
  # Circuit breaker and explicit-claim paths are UNCHANGED (they already bypass
3476
3515
  # the interval); return check-now for them without touching the early logic.
@@ -3505,8 +3544,16 @@ council_should_stop() {
3505
3544
  return 1 # Council disabled, don't stop
3506
3545
  fi
3507
3546
 
3508
- # Don't check before minimum iterations
3509
- if [ "$ITERATION_COUNT" -lt "$COUNCIL_MIN_ITERATIONS" ]; then
3547
+ # Don't check before minimum iterations. SaaS #122: the floor is tier-aware
3548
+ # (simple->1) via _council_effective_min_iter so a genuinely-complete simple
3549
+ # app that claims done at iteration 1 is not FORCED through ~2 extra idle
3550
+ # iterations before the council may evaluate. This only changes WHETHER the
3551
+ # council is allowed to convene early; every verification gate inside
3552
+ # council_evaluate still runs and can still reject. Standard/complex keep 3,
3553
+ # and an explicit LOKI_COUNCIL_MIN_ITERATIONS override is always honored.
3554
+ local _min_iter
3555
+ _min_iter="$(_council_effective_min_iter)"
3556
+ if [ "$ITERATION_COUNT" -lt "$_min_iter" ]; then
3510
3557
  return 1
3511
3558
  fi
3512
3559
 
@@ -652,6 +652,288 @@ with open(sys.argv[1], 'w') as f:
652
652
  return 0
653
653
  }
654
654
 
655
+ #===============================================================================
656
+ # RANK 4: Golden-master boundary-equivalence for heal/migrate
657
+ #
658
+ # The whole-suite exit-code check in hook_healing_phase_gate is necessary but not
659
+ # sufficient: a transform can keep unit tests green while ALTERING an observable
660
+ # boundary (CLI stdout, HTTP response, file/DB delta). Golden-master capture
661
+ # records the OBSERVED output of each detected boundary during archaeology; the
662
+ # modernize->validate gate re-runs each boundary and compares to that baseline
663
+ # PER-BOUNDARY. A changed boundary is BLOCKED unless a documented-intentional-
664
+ # change record exists for exactly that boundary.
665
+ #
666
+ # These are deterministic shell/python functions (NOT LLM calls), consistent with
667
+ # the file contract at the top: they run whether the agent cooperates or not.
668
+ #===============================================================================
669
+
670
+ # Enumerate the boundaries to golden-master. Sources of truth (all merged, first
671
+ # occurrence of a boundary id wins):
672
+ # 1. .loki/healing/boundaries.json -- canonical heal boundary manifest that
673
+ # archaeology is instructed to write (see the heal prompt). Array of
674
+ # {id, boundary_type, command} (or `characterization_test`).
675
+ # 2. .loki/healing/features.json -- the migrate-flow feature list; each
676
+ # entry's `characterization_test` is a reproducible boundary invocation.
677
+ # 3. .loki/healing/characterization-tests/*.json -- per-boundary characterization
678
+ # files (heal archaeology's tool output); any object carrying a runnable
679
+ # `command`/`characterization_test`/`cmd` field is a boundary.
680
+ # heal writes (1) and/or (3); migrate writes (2). Reading all three keeps the
681
+ # golden-master populated in BOTH real flows, not just the synthetic fixture.
682
+ # Emits one TSV line per boundary: <boundary_id>\t<invocation_command>
683
+ # Prints nothing (returns 0) when no boundaries are detectable -- callers must
684
+ # treat "no boundaries" as an honest degrade, never a green verdict.
685
+ _heal_enumerate_boundaries() {
686
+ local heal_dir="$1"
687
+ HEAL_DIR="$heal_dir" python3 -c "
688
+ import json, os, sys, glob
689
+
690
+ heal_dir = os.environ['HEAL_DIR']
691
+ seen = set()
692
+ out = []
693
+
694
+ def emit(bid, cmd):
695
+ if not cmd or not str(cmd).strip():
696
+ return
697
+ bid = str(bid).replace('\t', ' ').replace('\n', ' ').strip()
698
+ if not bid or bid in seen:
699
+ return
700
+ seen.add(bid)
701
+ out.append((bid, str(cmd).replace('\n', ' ').replace('\r', ' ')))
702
+
703
+ def cmd_of(it):
704
+ for k in ('command', 'characterization_test', 'cmd'):
705
+ v = it.get(k)
706
+ if v and str(v).strip():
707
+ return v
708
+ return None
709
+
710
+ def id_of(it, default='boundary'):
711
+ fid = it.get('id') or it.get('name') or it.get('description') or default
712
+ btype = it.get('boundary_type') or it.get('category') or it.get('type') or 'cli'
713
+ return '%s:%s' % (btype, fid)
714
+
715
+ def load(path):
716
+ try:
717
+ with open(path) as f:
718
+ return json.load(f)
719
+ except Exception:
720
+ return None
721
+
722
+ # (1) canonical heal boundary manifest
723
+ data = load(os.path.join(heal_dir, 'boundaries.json'))
724
+ if data is not None:
725
+ items = data if isinstance(data, list) else data.get('boundaries', data.get('features', []))
726
+ for it in items if isinstance(items, list) else []:
727
+ if isinstance(it, dict):
728
+ emit(id_of(it), cmd_of(it))
729
+
730
+ # (2) migrate feature list
731
+ data = load(os.path.join(heal_dir, 'features.json'))
732
+ if data is not None:
733
+ items = data if isinstance(data, list) else data.get('features', [])
734
+ for it in items if isinstance(items, list) else []:
735
+ if isinstance(it, dict):
736
+ emit(id_of(it), cmd_of(it))
737
+
738
+ # (3) per-boundary characterization files
739
+ for path in sorted(glob.glob(os.path.join(heal_dir, 'characterization-tests', '*.json'))):
740
+ data = load(path)
741
+ if data is None:
742
+ continue
743
+ items = data if isinstance(data, list) else [data]
744
+ for it in items:
745
+ if isinstance(it, dict) and cmd_of(it):
746
+ base = os.path.splitext(os.path.basename(path))[0]
747
+ emit(id_of(it, default=base), cmd_of(it))
748
+
749
+ for bid, cmd in out:
750
+ print('%s\t%s' % (bid, cmd))
751
+ " 2>/dev/null || true
752
+ }
753
+
754
+ # Stable, filesystem-safe filename for a boundary id's golden-master artifact.
755
+ _heal_boundary_artifact_name() {
756
+ local bid="$1"
757
+ # sha of the id keeps names stable + collision-free across weird chars.
758
+ local h
759
+ h=$(printf '%s' "$bid" | shasum 2>/dev/null | awk '{print $1}')
760
+ [[ -z "$h" ]] && h=$(printf '%s' "$bid" | cksum | awk '{print $1}')
761
+ printf 'boundary-%s.json' "$h"
762
+ }
763
+
764
+ # Run a single boundary invocation and capture OBSERVED output as a golden-master
765
+ # artifact. Captures stdout (the compared channel) + exit code + the reproduction
766
+ # command (so validate can re-run it). stderr is captured into a separate field and
767
+ # is NOT compared: on a real Py2->Py3 boundary stderr carries DeprecationWarnings
768
+ # and tracebacks that vary run-to-run and would false-block equivalence.
769
+ # Deterministic-by-contract: the boundary command's STDOUT must not emit
770
+ # nondeterministic tokens (timestamps/pids); normalization of such is a documented
771
+ # limitation for the real integration, out of scope for the deterministic core.
772
+ # Returns 0 on capture, 1 on failure to write.
773
+ hook_capture_behavioral_baseline() {
774
+ local codebase_path="${1:-${LOKI_CODEBASE_PATH:-.}}"
775
+ local heal_dir="${codebase_path}/.loki/healing"
776
+ local baseline_dir="${heal_dir}/behavioral-baseline"
777
+
778
+ [[ "${LOKI_HEAL_MODE:-false}" != "true" ]] && return 0
779
+
780
+ mkdir -p "$baseline_dir" 2>/dev/null || return 1
781
+
782
+ local captured=0
783
+ local any=0
784
+ while IFS=$'\t' read -r bid cmd; do
785
+ [[ -z "$bid" || -z "$cmd" ]] && continue
786
+ any=1
787
+ local out rc art errf
788
+ errf=$(mktemp)
789
+ # stdout is the compared channel; stderr is diverted to $errf (recorded
790
+ # separately, never compared).
791
+ out=$(cd "$codebase_path" && eval "$cmd" 2>"$errf")
792
+ rc=$?
793
+ local err
794
+ err=$(cat "$errf" 2>/dev/null); rm -f "$errf"
795
+ art="$baseline_dir/$(_heal_boundary_artifact_name "$bid")"
796
+ # Write a self-describing artifact: id + reproduction command + observed
797
+ # stdout (compared) + stderr (recorded, not compared) + observed exit.
798
+ # validate re-runs `command` and compares stdout+exit only.
799
+ if BID="$bid" CMD="$cmd" OUT="$out" ERR="$err" RC="$rc" ART="$art" python3 -c "
800
+ import json, os
801
+ art = {
802
+ 'boundary': os.environ['BID'],
803
+ 'boundary_type': os.environ['BID'].split(':', 1)[0],
804
+ 'command': os.environ['CMD'],
805
+ 'stdout': os.environ['OUT'],
806
+ 'stderr': os.environ.get('ERR', ''),
807
+ 'exit_code': int(os.environ['RC']),
808
+ 'captured_by': 'hook_capture_behavioral_baseline',
809
+ 'note': 'stdout+exit_code are the compared channels; stderr is informational only.',
810
+ }
811
+ with open(os.environ['ART'], 'w') as f:
812
+ json.dump(art, f, indent=2, sort_keys=True)
813
+ " 2>/dev/null; then
814
+ captured=$((captured + 1))
815
+ fi
816
+ done < <(_heal_enumerate_boundaries "$heal_dir")
817
+
818
+ if [[ "$any" -eq 0 ]]; then
819
+ # Honest degrade: no boundaries detected. Do NOT fabricate artifacts.
820
+ echo "BASELINE: no boundaries detected (no runnable boundary command in boundaries.json / features.json / characterization-tests/); nothing to golden-master."
821
+ return 0
822
+ fi
823
+ echo "BASELINE: captured ${captured} golden-master artifact(s) into behavioral-baseline/"
824
+ return 0
825
+ }
826
+
827
+ # Compare current boundary outputs to the golden-master baseline PER-BOUNDARY.
828
+ # Emits a diff report enumerating EVERY changed boundary. A boundary is "changed"
829
+ # when its current stdout OR exit code differs from the baseline artifact, UNLESS
830
+ # a documented-intentional-change record exists for exactly that boundary id in
831
+ # behavioral-baseline/intentional-changes.json.
832
+ # Echoes a human-readable report on stdout.
833
+ # Returns:
834
+ # 0 - no baseline / no boundaries (honest degrade; caller falls through), OR
835
+ # all boundaries equivalent (or every change documented-intentional).
836
+ # 1 - at least one undocumented boundary change -> caller must BLOCK.
837
+ hook_compare_behavioral_baseline() {
838
+ local codebase_path="${1:-${LOKI_CODEBASE_PATH:-.}}"
839
+ local heal_dir="${codebase_path}/.loki/healing"
840
+ local baseline_dir="${heal_dir}/behavioral-baseline"
841
+ local intentional="${baseline_dir}/intentional-changes.json"
842
+
843
+ # Honest degrade: off/greenfield/no-baseline -> byte-identical no-op. The
844
+ # caller falls through to its existing whole-suite check unchanged.
845
+ [[ "${LOKI_HEAL_MODE:-false}" != "true" ]] && return 0
846
+ [[ -d "$baseline_dir" ]] || return 0
847
+ # Any golden-master artifacts at all?
848
+ local art_count
849
+ art_count=$(find "$baseline_dir" -type f -name 'boundary-*.json' 2>/dev/null | wc -l | tr -d ' ')
850
+ if [[ "${art_count:-0}" -eq 0 ]]; then
851
+ echo "BOUNDARY-CHECK: no golden-master baseline present; skipping per-boundary comparison (honest degrade)."
852
+ return 0
853
+ fi
854
+
855
+ local changed=0
856
+ local report=""
857
+ local art
858
+ while IFS= read -r art; do
859
+ [[ -z "$art" ]] && continue
860
+ # Read stored boundary id + reproduction command in one python call. A
861
+ # trailing sentinel is appended to each so command-substitution's trailing
862
+ # -newline strip does not corrupt a value (bid/cmd are single-line by
863
+ # construction, but this keeps the read exact and robust).
864
+ local bid cmd base_rc
865
+ bid=$(ART="$art" python3 -c "import json,os;print(json.load(open(os.environ['ART']))['boundary'])" 2>/dev/null) || continue
866
+ cmd=$(ART="$art" python3 -c "import json,os;print(json.load(open(os.environ['ART']))['command'])" 2>/dev/null) || continue
867
+ base_rc=$(ART="$art" python3 -c "import json,os;print(json.load(open(os.environ['ART']))['exit_code'])" 2>/dev/null)
868
+
869
+ # Re-run the boundary NOW. stderr diverted (not compared), matching the
870
+ # capture channel; only stdout + exit are compared.
871
+ local cur_out cur_rc
872
+ cur_out=$(cd "$codebase_path" && eval "$cmd" 2>/dev/null)
873
+ cur_rc=$?
874
+
875
+ # Byte-exact stdout comparison via python (handles trailing newlines /
876
+ # binary-ish content that shell string compare would mangle).
877
+ if ART="$art" CUR="$cur_out" python3 -c "
878
+ import json, os, sys
879
+ base = json.load(open(os.environ['ART']))['stdout']
880
+ sys.exit(0 if base == os.environ['CUR'] else 1)
881
+ " 2>/dev/null && [[ "$cur_rc" == "$base_rc" ]]; then
882
+ continue
883
+ fi
884
+ # Recover baseline stdout for the report (may be multi-line).
885
+ local base_out
886
+ base_out=$(ART="$art" python3 -c "import json,os,sys;sys.stdout.write(json.load(open(os.environ['ART']))['stdout'])" 2>/dev/null)
887
+
888
+ # Boundary changed. Is it documented-intentional for THIS boundary id?
889
+ local is_doc="false"
890
+ if [[ -f "$intentional" ]]; then
891
+ is_doc=$(INT="$intentional" BID="$bid" python3 -c "
892
+ import json, os
893
+ try:
894
+ data = json.load(open(os.environ['INT']))
895
+ except Exception:
896
+ print('false'); raise SystemExit
897
+ changes = data.get('changes', []) if isinstance(data, dict) else []
898
+ bid = os.environ['BID']
899
+ for c in changes:
900
+ if isinstance(c, dict) and c.get('boundary') == bid and c.get('is_intentional') is True:
901
+ print('true'); break
902
+ else:
903
+ print('false')
904
+ " 2>/dev/null || echo false)
905
+ fi
906
+
907
+ if [[ "$is_doc" == "true" ]]; then
908
+ report+=" [documented-intentional] ${bid}: change accepted (intentional-changes.json)"$'\n'
909
+ continue
910
+ fi
911
+
912
+ changed=$((changed + 1))
913
+ report+=" [CHANGED] ${bid}"$'\n'
914
+ report+=" command: ${cmd}"$'\n'
915
+ report+=" baseline: exit=${base_rc} stdout=$(printf '%q' "$base_out")"$'\n'
916
+ report+=" current: exit=${cur_rc} stdout=$(printf '%q' "$cur_out")"$'\n'
917
+ done < <(find "$baseline_dir" -type f -name 'boundary-*.json' 2>/dev/null)
918
+
919
+ if [[ "$changed" -gt 0 ]]; then
920
+ echo "BOUNDARY-DIFF REPORT (${changed} undocumented boundary change(s)):"
921
+ printf '%s' "$report"
922
+ echo " To accept a change intentionally, add a record to:"
923
+ echo " ${intentional}"
924
+ echo " {\"changes\":[{\"boundary\":\"<id>\",\"is_intentional\":true,\"reason\":\"...\"}]}"
925
+ return 1
926
+ fi
927
+
928
+ if [[ -n "$report" ]]; then
929
+ echo "BOUNDARY-CHECK: all changed boundaries are documented-intentional:"
930
+ printf '%s' "$report"
931
+ else
932
+ echo "BOUNDARY-CHECK: all ${art_count} boundary(ies) equivalent to golden-master baseline."
933
+ fi
934
+ return 0
935
+ }
936
+
655
937
  # Hook: healing_phase_gate - mechanical verification before healing phase transition
656
938
  hook_healing_phase_gate() {
657
939
  local from_phase="${1:-}"
@@ -748,6 +1030,20 @@ except: print(0)
748
1030
  echo "GATE_BLOCKED: Tests do not pass after modernization"
749
1031
  return 1
750
1032
  fi
1033
+ # RANK 4: whole-suite exit code is necessary but NOT sufficient. A
1034
+ # transform can keep unit tests green while altering an observable
1035
+ # boundary. Compare each boundary to its golden-master baseline; block
1036
+ # any undocumented boundary change. Honest degrade (no baseline / no
1037
+ # boundaries) falls through as a no-op -- never a false-green.
1038
+ local boundary_report boundary_rc
1039
+ boundary_report=$(hook_compare_behavioral_baseline "$codebase_path")
1040
+ boundary_rc=$?
1041
+ if [[ "$boundary_rc" -ne 0 ]]; then
1042
+ echo "GATE_BLOCKED: boundary equivalence violated after modernization (unit tests green but an observable boundary changed)."
1043
+ echo "$boundary_report"
1044
+ return 1
1045
+ fi
1046
+ [[ -n "$boundary_report" ]] && echo "$boundary_report"
751
1047
  ;;
752
1048
  esac
753
1049
 
@@ -130,6 +130,46 @@ def _collect_council(loki_dir):
130
130
  rec = _read_json(os.path.join(votes_dir, vf), default=None)
131
131
  if not isinstance(rec, dict):
132
132
  continue
133
+ # completion-council.sh writes ROUND-SUMMARY files (round-N.json,
134
+ # devils-advocate-round-N.json) shaped
135
+ # {round, verdict, votes:[{member, role, vote, reason}]} -- NOT a flat
136
+ # per-reviewer record. When we see that shape, expand the nested votes[]
137
+ # into per-reviewer rows (the real trust signal) and adopt the round
138
+ # verdict; otherwise fall back to the flat single-record shape. Without
139
+ # this, rec.get("role") is empty and the proof's council section renders
140
+ # blank reviewers even though the council genuinely ran and voted (#125).
141
+ nested = rec.get("votes")
142
+ if isinstance(nested, list) and nested:
143
+ rv = str(rec.get("verdict") or rec.get("result") or "")
144
+ if rv and not final_verdict:
145
+ final_verdict = rv
146
+ if threshold is None and rec.get("threshold") is not None:
147
+ tm = rec.get("total_members")
148
+ threshold = (
149
+ "%s/%s" % (rec.get("threshold"), tm)
150
+ if tm is not None else rec.get("threshold")
151
+ )
152
+ round_tag = str(rec.get("round") or "")
153
+ da = "devil" in vf.lower()
154
+ for v in nested:
155
+ if not isinstance(v, dict):
156
+ continue
157
+ role = str(v.get("role") or v.get("member") or "")
158
+ vote = str(v.get("vote") or v.get("decision") or "")
159
+ # Skip empty vote rows (a blank role AND blank vote is noise, not
160
+ # a reviewer) so the proof never shows a phantom "-> " reviewer.
161
+ if not role and not vote:
162
+ continue
163
+ if da and role:
164
+ role = "%s (devil's advocate)" % role
165
+ reviewers.append({
166
+ "role": role,
167
+ "vote": vote,
168
+ "summary": str(
169
+ v.get("reason") or v.get("summary") or v.get("rationale") or ""
170
+ ) + (" [round %s]" % round_tag if round_tag else ""),
171
+ })
172
+ continue
133
173
  reviewers.append({
134
174
  "role": str(rec.get("role") or rec.get("reviewer") or ""),
135
175
  "vote": str(rec.get("vote") or rec.get("decision") or ""),
@@ -222,6 +262,57 @@ def _collect_quality_gates(loki_dir):
222
262
  total += 1
223
263
  if status == "passed":
224
264
  passed += 1
265
+
266
+ # #125: run.sh does NOT write state/quality-gates.json; it writes the gate
267
+ # outcomes under .loki/quality/ (a "<gate>.pass" marker on pass, plus
268
+ # per-gate result JSONs). When the aggregate above is empty/absent, read the
269
+ # real per-gate artifacts so a build that genuinely ran+passed its gates is
270
+ # not reported as quality_gates:{passed:0,total:0} (a receipt understatement
271
+ # that reads as "no verification ran"). Deterministic FACT from disk markers,
272
+ # never an LLM opinion. Only fills gates the aggregate did not already cover.
273
+ if not gates:
274
+ quality_dir = os.path.join(loki_dir, "quality")
275
+ seen = set()
276
+ # (gate name in proof, marker/result filename stem under quality/)
277
+ markers = [
278
+ ("static_analysis", "static-analysis"),
279
+ ("unit_tests", "unit-tests"),
280
+ ]
281
+ for gate_name, stem in markers:
282
+ pass_marker = os.path.join(quality_dir, stem + ".pass")
283
+ result_json = os.path.join(quality_dir, stem + ".json")
284
+ status = None
285
+ if os.path.exists(pass_marker):
286
+ status = "passed"
287
+ elif os.path.exists(result_json):
288
+ rj = _read_json(result_json, default=None)
289
+ if isinstance(rj, dict):
290
+ status = _norm_gate_status(
291
+ rj.get("passed", rj.get("status", "not_run"))
292
+ )
293
+ if status is not None:
294
+ gates.append({"name": gate_name, "status": status})
295
+ seen.add(gate_name)
296
+ total += 1
297
+ if status == "passed":
298
+ passed += 1
299
+ # test-results.json carries the suite outcome even when no .pass marker
300
+ # (e.g. {status:"verified"} = tests ran and passed).
301
+ if "unit_tests" not in seen:
302
+ tr = _read_json(
303
+ os.path.join(quality_dir, "test-results.json"), default=None
304
+ )
305
+ if isinstance(tr, dict):
306
+ st = str(tr.get("status") or "")
307
+ if st in ("verified", "pass", "passed"):
308
+ gates.append({"name": "unit_tests", "status": "passed"})
309
+ total += 1
310
+ passed += 1
311
+ elif st:
312
+ gates.append(
313
+ {"name": "unit_tests", "status": _norm_gate_status(st)}
314
+ )
315
+ total += 1
225
316
  return {"passed": passed, "total": total, "gates": gates}
226
317
 
227
318
 
package/autonomy/loki CHANGED
@@ -14784,7 +14784,7 @@ HEALING RULES:
14784
14784
  5. INCREMENTAL ONLY: Change ONE component at a time. Verify ALL characterization tests pass after each change.
14785
14785
 
14786
14786
  Phase-specific instructions:
14787
- - archaeology: Map dependencies, catalog friction, extract knowledge, write characterization tests. Do NOT modify source code.
14787
+ - archaeology: Map dependencies, catalog friction, extract knowledge, write characterization tests. Do NOT modify source code. ALSO write .loki/healing/boundaries.json: a JSON array of the OBSERVABLE system boundaries (CLI commands, HTTP endpoints, file/DB-producing entrypoints). Each element: {\"id\": string, \"boundary_type\": \"cli\"|\"http\"|\"file\"|\"db\", \"command\": string}. The \"command\" MUST be a deterministic shell invocation that exercises that boundary and prints its observable output to stdout (avoid timestamps/PIDs). These commands are golden-mastered into .loki/healing/behavioral-baseline/ and re-run at the modernize->validate gate to block any undocumented boundary change. If no observable boundary exists, write an empty array [] (do NOT invent one).
14788
14788
  - stabilize: Add logging and observability. Extract config. Add type hints. Do NOT change behavior.
14789
14789
  - isolate: Create adapter interfaces at component boundaries. Add integration tests.
14790
14790
  - modernize: Replace components behind adapters. Verify characterization tests pass.
@@ -14845,6 +14845,16 @@ except Exception: pass
14845
14845
 
14846
14846
  emit_event healing cli complete "phase=$phase" "exit=$heal_exit" 2>/dev/null || true
14847
14847
 
14848
+ # RANK 4: populate .loki/healing/behavioral-baseline/ during archaeology.
14849
+ # The dir is mkdir'd above but was never populated; the modernize->validate
14850
+ # gate needs a golden-master per detected boundary to compare against.
14851
+ # Deterministic capture (NOT an LLM call): re-runs each boundary's
14852
+ # characterization_test and records observed stdout+exit. Honest degrade when
14853
+ # no boundaries are detected. Byte-identical no-op when the hook is absent.
14854
+ if [ "$phase" = "archaeology" ] && type hook_capture_behavioral_baseline &>/dev/null; then
14855
+ hook_capture_behavioral_baseline "$codebase_path" 2>/dev/null || true
14856
+ fi
14857
+
14848
14858
  if [ "$heal_exit" -eq 0 ]; then
14849
14859
  echo ""
14850
14860
  echo -e "${GREEN}Healing phase '${phase}' complete.${NC}"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.117.2"
10
+ __version__ = "7.118.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.117.2
5
+ **Version:** v7.118.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.117.2";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
2
+ var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.118.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
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)
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
814
814
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
815
815
  `),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
816
816
 
817
- //# debugId=01CA3D14238C4AE464756E2164756E21
817
+ //# debugId=01D367D3C33A5E7664756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.117.2'
60
+ __version__ = '7.118.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.117.2",
4
+ "version": "7.118.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.117.2",
5
+ "version": "7.118.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
package/skills/healing.md CHANGED
@@ -411,10 +411,28 @@ When `loki heal` is active, the code review specialist pool includes:
411
411
  failure-modes.json # Cataloged failure modes
412
412
  institutional-knowledge.md # Extracted tribal knowledge
413
413
  healing-progress.json # Component-by-component healing status
414
- behavioral-baseline/ # Pre-healing system-boundary outputs
414
+ boundaries.json # Observable system boundaries (archaeology writes)
415
+ behavioral-baseline/ # Golden-master per-boundary outputs (archaeology captures)
415
416
  characterization-tests/ # Tests that capture current behavior
416
417
  ```
417
418
 
419
+ ### Golden-master boundary equivalence (modernize -> validate gate)
420
+
421
+ Whole-suite exit code is necessary but NOT sufficient: a transform can keep unit
422
+ tests green while altering an observable boundary (CLI stdout/exit, HTTP, file/DB
423
+ delta). Archaeology writes `.loki/healing/boundaries.json` -- a JSON array of
424
+ `{"id","boundary_type":"cli"|"http"|"file"|"db","command"}` -- where `command` is a
425
+ deterministic invocation that prints the boundary's observable output. During
426
+ archaeology each boundary is golden-mastered into `behavioral-baseline/`
427
+ (stdout + exit are the compared channels; stderr is recorded but never compared,
428
+ so Py2->Py3 warnings do not false-block). At the `modernize -> validate` gate every
429
+ boundary is re-run and compared PER-BOUNDARY; any change is BLOCKED with a diff
430
+ report unless a per-boundary record exists in
431
+ `behavioral-baseline/intentional-changes.json`
432
+ (`{"changes":[{"boundary":"<id>","is_intentional":true,"reason":"..."}]}`).
433
+ Honest degrade: no boundaries / no baseline / heal mode off is a byte-identical
434
+ no-op, never a false-green. (Deterministic shell hooks, not LLM calls.)
435
+
418
436
  **Progress Tracking:**
419
437
  ```json
420
438
  {