loki-mode 7.117.2 → 7.119.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.119.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.119.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.117.2
1
+ 7.119.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
 
@@ -250,7 +341,20 @@ def _collect_build(loki_dir):
250
341
  out["exit_code"] = _to_int(ec, None) if ec is not None else None
251
342
  dur = raw.get("duration_sec")
252
343
  out["duration_sec"] = _to_float(dur, None) if dur is not None else None
253
- if not ran:
344
+ # HONEST APPLICABILITY (build has no writer historically -> facts.build was
345
+ # permanently "not_run" for every project, counting a CLI with no build step
346
+ # as a gap and dragging the headline to WITH GAPS). The writer now records a
347
+ # POSITIVE applicability signal: applicable:false means "this stack genuinely
348
+ # has no build phase" (a CLI, a plain script). That is N/A, not a skipped
349
+ # gap, so it must NOT be a degraded item. CRITICAL anti-fake-green rule:
350
+ # not_applicable ONLY from an EXPLICIT applicable:false in the file. An absent
351
+ # file or a missing/true applicable flag stays not_run (an honest gap) -- an
352
+ # un-built project must never flip green just because nobody wrote the file.
353
+ applicable = raw.get("applicable", True)
354
+ out["applicable"] = bool(applicable)
355
+ if applicable is False:
356
+ out["status"] = "not_applicable"
357
+ elif not ran:
254
358
  out["status"] = "not_run"
255
359
  elif out["exit_code"] == 0:
256
360
  out["status"] = "verified"
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}"
package/autonomy/run.sh CHANGED
@@ -8354,6 +8354,163 @@ COMPOUND_SCRIPT
8354
8354
  # Results stored in .loki/quality/static-analysis.json
8355
8355
  # ============================================================================
8356
8356
 
8357
+ # ---------------------------------------------------------------------------
8358
+ # enforce_build_check -- run the stack's build (if it has one) and record an
8359
+ # HONEST build fact for the Evidence Receipt (#47). Historically nothing wrote
8360
+ # .loki/quality/build-results.json, so facts.build was PERMANENTLY "not_run" for
8361
+ # every project -- a CLI with no build step read identically to a skipped real
8362
+ # build, and both dragged the receipt to "WITH GAPS". This is the missing writer.
8363
+ #
8364
+ # The distinction the receipt needs (and the founder demanded, no fabrication) is
8365
+ # THREE-WAY, not two (a council review caught the two-way version laundering
8366
+ # "unrecognized build system" into a fake N/A):
8367
+ # - a build command WE RUN exists (npm "build" / go / cargo) -> RUN it, record
8368
+ # ran/exit_code (verified on 0, failed otherwise -- a real gap kept honest).
8369
+ # - a build system EXISTS but we do not auto-run it (Make/Maven/Gradle/.NET, a
8370
+ # non-"build" npm script, a pyproject build-system) -> not_run (an HONEST GAP,
8371
+ # matching verify.sh:291's "skipped"). NEVER N/A -- a real, un-run build must
8372
+ # never read VERIFIED.
8373
+ # - genuinely NO build phase at all (a plain CLI/script) -> applicable:false
8374
+ # (status not_applicable). A POSITIVE "no build phase" determination, NOT an
8375
+ # absent file -- proof-generator maps applicable:false -> N/A (not a gap) but a
8376
+ # MISSING file or an unrecognized-real build stays not_run. One vocabulary, no
8377
+ # fake-green. Pinned by tests/test-build-check-applicability.sh.
8378
+ # ---------------------------------------------------------------------------
8379
+ enforce_build_check() {
8380
+ local tree="${TARGET_DIR:-.}"
8381
+ local loki_dir="$tree/.loki"
8382
+ local quality_dir="$loki_dir/quality"
8383
+ mkdir -p "$quality_dir"
8384
+ local out_file="$quality_dir/build-results.json"
8385
+ local timeout_s="${LOKI_GATE_TIMEOUT:-300}"
8386
+ local ts; ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
8387
+
8388
+ # THREE-WAY classification (detection is always cheap). The anti-fake-green line
8389
+ # (council-caught): N/A is ONLY for a POSITIVE "this stack has no build phase"
8390
+ # determination. An unrecognized-but-real build system (Make, Maven, Gradle,
8391
+ # .NET, a non-"build" npm build script) must stay an HONEST GAP (not_run), NOT
8392
+ # be laundered into N/A -- otherwise a real, un-run build reads VERIFIED. This
8393
+ # mirrors verify.sh:291, which records the same fall-through as a "skipped" gap.
8394
+ # runnable -> a build command WE run (npm build / go / cargo) -> verified/failed
8395
+ # present -> a build system EXISTS but we don't auto-run it -> not_run (GAP)
8396
+ # none -> positively no build phase at all -> not_applicable (N/A)
8397
+ local cmd="" ran="false" applicable="false" exit_code="null" status="not_run"
8398
+ local build_class="present"
8399
+
8400
+ # INVERTED classification (three council rounds each found a fake-green hole in
8401
+ # an allowlist of build systems -> the allowlist is structurally leaky). The
8402
+ # two error directions are NOT equal: a REAL build read as N/A -> VERIFIED is
8403
+ # DISHONEST over-claim (the cardinal fake-green sin); a no-build CLI read as a
8404
+ # not_run gap is an HONEST under-claim (merely the founder's original UX nit).
8405
+ # So not_run is the SAFE CATCH-ALL and N/A requires a POSITIVE proof-of-no-build.
8406
+ # A forgotten stack (Bazel, meson, autotools, Zig, Nix...) now falls to not_run
8407
+ # (honest), never to fake N/A -- fixing the whole CLASS, not each instance.
8408
+ # 1. runnable: a build command WE run -> verified/failed
8409
+ # 2. none: POSITIVE proof of no build phase -> not_applicable (N/A)
8410
+ # 3. else: not_run (honest gap) -- the default, covers every unknown stack
8411
+
8412
+ if [ -f "$tree/package.json" ] && grep -q '"build"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8413
+ cmd="npm run build"; applicable="true"; build_class="runnable"
8414
+ elif [ -f "$tree/go.mod" ] && command -v go >/dev/null 2>&1; then
8415
+ cmd="go build ./..."; applicable="true"; build_class="runnable"
8416
+ elif [ -f "$tree/Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
8417
+ cmd="cargo build"; applicable="true"; build_class="runnable"
8418
+ else
8419
+ # POSITIVE proof-of-no-build (the ONLY path to N/A). Two narrow cases, and
8420
+ # anything short of a clear determination stays not_run:
8421
+ # (a) a package.json whose scripts exist but include NO build-ish script
8422
+ # AND there is no foreign build manifest -> a JS CLI/lib with no build.
8423
+ # (b) NO build manifest of ANY known kind at all -> a plain script dir.
8424
+ # The build-ish regex is used as an EXCLUSION so a build hidden under a
8425
+ # non-"build" script name (compile/dist/...) does NOT misfire N/A.
8426
+ local has_foreign_manifest="false"
8427
+ if [ -f "$tree/go.mod" ] || [ -f "$tree/Cargo.toml" ] \
8428
+ || [ -f "$tree/Makefile" ] || [ -f "$tree/makefile" ] || [ -f "$tree/GNUmakefile" ] \
8429
+ || [ -f "$tree/Makefile.in" ] || [ -f "$tree/configure" ] || [ -f "$tree/configure.ac" ] || [ -f "$tree/configure.in" ] \
8430
+ || [ -f "$tree/CMakeLists.txt" ] || [ -f "$tree/meson.build" ] \
8431
+ || [ -f "$tree/pom.xml" ] || [ -f "$tree/build.xml" ] \
8432
+ || [ -f "$tree/build.gradle" ] || [ -f "$tree/build.gradle.kts" ] \
8433
+ || [ -f "$tree/settings.gradle" ] || [ -f "$tree/settings.gradle.kts" ] || [ -f "$tree/gradlew" ] \
8434
+ || [ -f "$tree/build.sbt" ] \
8435
+ || [ -f "$tree/WORKSPACE" ] || [ -f "$tree/WORKSPACE.bazel" ] || [ -f "$tree/MODULE.bazel" ] \
8436
+ || [ -f "$tree/setup.py" ] || [ -f "$tree/pyproject.toml" ] \
8437
+ || compgen -G "$tree/*.csproj" >/dev/null 2>&1 \
8438
+ || compgen -G "$tree/*.sln" >/dev/null 2>&1 \
8439
+ || compgen -G "$tree/*.fsproj" >/dev/null 2>&1; then
8440
+ has_foreign_manifest="true"
8441
+ fi
8442
+ # N/A fires on POSITIVE proof-of-no-build, which we can only assert for a
8443
+ # package.json project: it EXISTS and its scripts are build-ish-free and no
8444
+ # foreign manifest / build-tool devDep is present -> a JS CLI/lib that
8445
+ # genuinely has no build step (the founder's invoice case). CRITICAL (the
8446
+ # council's convergence line): a project with NO package.json is "I don't
8447
+ # recognize a build here" = IGNORANCE, not proof-of-no-build. Ignorance must
8448
+ # map to not_run (honest gap), NEVER N/A -- else Zig/autotools/bare-C/meson/
8449
+ # bazel/anything-unborn silently reads VERIFIED. This makes the ENTIRE
8450
+ # no-package.json universe honest by CONSTRUCTION (list-free), which is why
8451
+ # this converges where four rounds of adding-to-a-list could not.
8452
+ if [ "$has_foreign_manifest" = "false" ] && [ -f "$tree/package.json" ]; then
8453
+ local has_build_devdep="false"
8454
+ # A build TOOL as a (dev)dependency means a build exists even if it runs
8455
+ # under a non-"build" script name -> stay not_run, do not claim N/A.
8456
+ if grep -qE '"(vite|webpack|rollup|esbuild|parcel|typescript|tsc|@swc/core|turbopack|rspack|tsup|microbundle)"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8457
+ has_build_devdep="true"
8458
+ fi
8459
+ if [ "$has_build_devdep" = "false" ] \
8460
+ && ! grep -qE '"(build|compile|dist|bundle|webpack|vite|tsc|rollup|esbuild|prepare|prepack)"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8461
+ applicable="false"; build_class="none"
8462
+ fi
8463
+ # else: a build-ish script or a build-tool devDep we don't auto-run ->
8464
+ # stays not_run (an honest gap). Known bounded edge: a polyglot repo with
8465
+ # a package.json (no build) co-present with an UNLISTED foreign build
8466
+ # manifest could read N/A; not common and not list-free-fixable, so it is
8467
+ # a documented CONCERN, not a blocker.
8468
+ fi
8469
+ # No package.json (foreign or not) -> build_class stays "present" -> not_run.
8470
+ # has_foreign_manifest=true -> also not_run. Only the narrow package.json-
8471
+ # no-build case above reaches N/A.
8472
+ fi
8473
+
8474
+ if [ "$build_class" = "present" ]; then
8475
+ # A build phase exists but we did not run it -> honest not_run gap. applicable
8476
+ # stays true (a build IS applicable), status not_run (we did not verify it).
8477
+ applicable="true"; ran="false"; status="not_run"
8478
+ log_info "Build check: a build system is present but not auto-run -> honest gap (not verified)"
8479
+ elif [ "$applicable" = "true" ]; then
8480
+ # EXECUTION is expensive (a real vite/webpack/go/cargo build, seconds to
8481
+ # minutes) so we run it AT MOST ONCE per build run, not every iteration --
8482
+ # honoring the "fastest / lowest cost" goal. A freshness marker (mirrors the
8483
+ # test-results.iter pattern) records that the build already ran this run; a
8484
+ # later iteration reuses the recorded verified/failed status instead of
8485
+ # re-building. LOKI_BUILD_CHECK=0 opts out entirely (records N/A-style skip
8486
+ # without running). If a prior result exists and is still valid, keep it.
8487
+ local marker="$quality_dir/.build-check.done"
8488
+ if [ "${LOKI_BUILD_CHECK:-1}" = "0" ]; then
8489
+ log_info "Build check: execution disabled (LOKI_BUILD_CHECK=0); recording applicability only"
8490
+ ran="false"; exit_code="null"; status="not_run"
8491
+ elif [ -f "$marker" ] && [ -f "$out_file" ]; then
8492
+ log_info "Build check: already ran this build; reusing recorded result"
8493
+ return 0
8494
+ else
8495
+ local rc=0
8496
+ ( cd "$tree" && timeout "$timeout_s" sh -c "$cmd" >/dev/null 2>&1 ) || rc=$?
8497
+ ran="true"; exit_code="$rc"
8498
+ if [ "$rc" -eq 0 ]; then status="verified"; else status="failed"; fi
8499
+ : > "$marker" 2>/dev/null || true
8500
+ log_info "Build check: $cmd -> exit $rc"
8501
+ fi
8502
+ else
8503
+ # build_class="none": a POSITIVE proof-of-no-build determination -> honest
8504
+ # N/A (the founder's CLI). applicable already false; status not_applicable.
8505
+ applicable="false"; ran="false"; status="not_applicable"
8506
+ log_info "Build check: no build step for this stack (N/A)"
8507
+ fi
8508
+
8509
+ printf '{"timestamp":"%s","command":"%s","ran":%s,"applicable":%s,"exit_code":%s,"duration_sec":null,"status":"%s"}\n' \
8510
+ "$ts" "$cmd" "$ran" "$applicable" "$exit_code" "$status" > "$out_file"
8511
+ return 0
8512
+ }
8513
+
8357
8514
  enforce_static_analysis() {
8358
8515
  local loki_dir="${TARGET_DIR:-.}/.loki"
8359
8516
  local quality_dir="$loki_dir/quality"
@@ -17574,6 +17731,14 @@ if __name__ == "__main__":
17574
17731
  fi
17575
17732
  emit_stage_complete "static_analysis" "$_stg_ok" "$_stg_t0"
17576
17733
  fi
17734
+ # Build check (#47). Records .loki/quality/build-results.json each
17735
+ # iteration so the Evidence Receipt tells the truth about the build:
17736
+ # a real build that ran (verified/failed) or an HONEST N/A when the
17737
+ # stack has no build step. Advisory (best-effort): a build failure
17738
+ # surfaces as a receipt gap via facts.build, never a hard loop block,
17739
+ # mirroring the security scan. Absent writer historically = facts.build
17740
+ # permanently not_run for every project (the founder-reported defect).
17741
+ enforce_build_check || true
17577
17742
  # Secure-by-default scan (v7.87.0). Advisory by default (never
17578
17743
  # blocks); records .loki/quality/security-findings.json each
17579
17744
  # iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
@@ -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.119.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -0,0 +1,79 @@
1
+ # Evidence Receipt: honest build applicability (done once, for all customers)
2
+
3
+ ## The defect (founder-reported, 2026-07-03)
4
+
5
+ A sophisticated user built a Node CLI (invoice tool) and saw the Evidence Receipt say
6
+ "Build: not run - build not run", "Live health check: not captured", "No screenshot",
7
+ headline "Working, with gaps". He concluded *nothing was validated* -- and that read is the
8
+ bug: it destroys trust in genuinely-validated work.
9
+
10
+ ## Ground truth (from the persisted proof of run-20260703034719)
11
+
12
+ - `facts.tests` = `verified`, exit 0 -- tests GENUINELY ran and passed.
13
+ - `facts.security` = `ran:true, clean` -- security GENUINELY ran, real clean scan.
14
+ - `facts.build` = `ran:false, status:not_run, command:""` -- and this is counted as a GAP
15
+ by `_compute_degraded` (proof-generator.py:903-908), which forces "WITH GAPS".
16
+ - `facts.quality_gates` = `[]` (EMPTY) -- verify.sh's deterministic build gate does NOT
17
+ reach the SaaS proof.
18
+
19
+ ## Root cause (systemic, every build, every customer)
20
+
21
+ `build-results.json` has **NO writer**. `_collect_build` (proof-generator.py:319) reads
22
+ `.loki/quality/build-results.json`, which nothing ever writes, so `facts.build` defaults to
23
+ `not_run` (line 333) for EVERY build. The sibling collectors DO have writers in run.sh
24
+ (`enforce_static_analysis` 8357, `run_secure_scan` 8720, `enforce_test_coverage` 9170 ->
25
+ test-results.json / security-findings.json / static-analysis.json). Build was never wired.
26
+
27
+ So "Build: not run" is not a per-project decision -- it is a permanent, structural
28
+ mislabel. A build that has no build step (a CLI) and a build that genuinely skipped a real
29
+ build both read identically as "not run" and both count as a gap.
30
+
31
+ ## The fix (honest, no fabrication, no mocking -- the founder's hard constraint)
32
+
33
+ Add the missing writer as a sibling collector, with a POSITIVE applicability determination:
34
+
35
+ 1. **run.sh `enforce_build_check()`** (new, sibling to enforce_static_analysis): detect the
36
+ stack's build command (npm "build" script / go build / cargo build -- same detectors as
37
+ verify.sh:250-291). Then:
38
+ - build command exists -> RUN it -> record `{ran:true, command, exit_code, status:
39
+ verified|failed}`.
40
+ - genuinely NO build step for this stack -> record `{ran:false, applicable:false,
41
+ status:not_applicable, command:""}`. This is a POSITIVE "no build phase" signal, not an
42
+ absent file.
43
+ Write `.loki/quality/build-results.json`. Call it where the sibling collectors are called.
44
+
45
+ 2. **proof-generator.py `_collect_build`**: pass through `applicable:false` -> status
46
+ `not_applicable`. CRITICAL: `not_applicable` ONLY from an explicit `applicable:false` in
47
+ the file. File ABSENT or unknown status -> `not_run` (honest gap), NEVER N/A. Absent must
48
+ never mean N/A (that would flip un-built projects green -- fake-green).
49
+
50
+ 3. **proof-generator.py `_compute_degraded`**: `not_applicable` is NOT a gap (excluded from
51
+ the weak set for build). A failed/skipped/not_run build stays a gap. Mirror-image guard:
52
+ a real build script that FAILS still emits `failed` -> gap -> never green.
53
+
54
+ 4. **proof-generator.py status normalizer (line 215)**: `not_applicable` passes through.
55
+
56
+ 5. **SPA adapter.ts**: `not_applicable` renders "N/A - no build step for this stack" (a
57
+ neutral/pass tier, NOT a warn/gap), distinct from "not run".
58
+
59
+ ## Anti-fake-green invariants (the trust moat)
60
+
61
+ - N/A ONLY on positive `applicable:false`. Absent file / unknown status -> honest gap.
62
+ - A build script that got skipped or failed STILL shows a gap and CANNOT go green.
63
+ - The directional regression test (write FIRST): Node project WITH a build script whose
64
+ build FAILS -> `facts.build=failed` -> degraded -> headline NOT green. And: build gate
65
+ MISSING -> still a gap, not N/A.
66
+ - N/A does not gate the seal any more than a gap does; it is honest applicability, not a pass.
67
+
68
+ ## Measurable before/after
69
+
70
+ Before: invoice CLI -> "Build: not run", headline "Working, with gaps".
71
+ After: fresh invoice CLI build -> "Build: N/A (no build step for this stack)", tests passed,
72
+ security scanned-clean, headline VERIFIED (no un-closed applicable gaps).
73
+
74
+ ## Constraints
75
+
76
+ Verdict-core: council 3/3 + local-ci + 14 version locations. Verify via
77
+ function-extraction/golden harness -- NEVER run the engine build from the worktree. Run the
78
+ EXISTING tests/test_proof_generator.py green before AND after (it has build-results.json
79
+ fixtures that this change touches). Deploy to live SaaS is `bun install -g loki-mode@<ver>`.
@@ -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.119.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.119.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=7B2014E7E7C6AC4464756E2164756E21
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.119.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.119.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.119.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
  {