loki-mode 7.129.5 → 8.0.1

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.
Files changed (75) hide show
  1. package/README.md +121 -9
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +326 -92
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +169 -105
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +141 -0
  29. package/autonomy/loki +196 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4587 -671
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/providers.md +42 -0
  75. package/skills/quality-gates.md +22 -0
@@ -47,6 +47,25 @@
47
47
  #
48
48
  #===============================================================================
49
49
 
50
+ # RUN-25 iter 21 (Wave D #2): share the ONE secret matcher with the commit gate.
51
+ # The completion evidence gate uses _commit_scan_secret_file / _commit_path_looks_secret
52
+ # to add a secret-leak axis, so a credential can no longer ship in a "done"
53
+ # deliverable via the completion-promise / dirty-tree route. Resolve this file's
54
+ # dir robustly: BASH_SOURCE[0] can be EMPTY when the file is sourced at top level,
55
+ # so fall back to run.sh's exported SCRIPT_DIR and finally to the on-disk location.
56
+ _LOKI_CC_DIR="$(dirname "${BASH_SOURCE[0]:-}" 2>/dev/null)"
57
+ if [ -z "$_LOKI_CC_DIR" ] || [ "$_LOKI_CC_DIR" = "." ]; then
58
+ _LOKI_CC_DIR="${SCRIPT_DIR:-}"
59
+ fi
60
+ for _cc_cand in "$_LOKI_CC_DIR/lib/secret-scan.sh" "${SCRIPT_DIR:-}/lib/secret-scan.sh" "autonomy/lib/secret-scan.sh"; do
61
+ if [ -n "$_cc_cand" ] && [ -f "$_cc_cand" ]; then
62
+ # shellcheck source=lib/secret-scan.sh
63
+ source "$_cc_cand"
64
+ break
65
+ fi
66
+ done
67
+ unset _LOKI_CC_DIR _cc_cand
68
+
50
69
  # Council configuration
51
70
  COUNCIL_ENABLED=${LOKI_COUNCIL_ENABLED:-true}
52
71
  COUNCIL_SIZE=${LOKI_COUNCIL_SIZE:-3}
@@ -128,6 +147,31 @@ _council_effective_min_iter() {
128
147
  fi
129
148
  printf '%s' "${COUNCIL_MIN_ITERATIONS}"
130
149
  }
150
+
151
+ # _loki_auto_tune_interval: OPT-IN auto-tune of the council CHECK interval by
152
+ # complexity. Env: LOKI_AUTO_TUNE (default 0 = OFF; stock runs are unchanged).
153
+ # Pure function of $1 (complexity tier) so it is unit-testable in isolation.
154
+ # When LOKI_AUTO_TUNE!=1 -> echoes the current COUNCIL_CHECK_INTERVAL verbatim
155
+ # (no behavior change). When ON: simple -> 3 (converge faster on trivial builds,
156
+ # fewer idle iterations before the council MAY evaluate); standard/complex/unknown
157
+ # -> COUNCIL_CHECK_INTERVAL (unchanged, default 5).
158
+ #
159
+ # This only changes WHEN the council is allowed to evaluate, never WHETHER a gate
160
+ # passes: MIN_ITERATIONS floor, hard gate, evidence gate and the aggregate vote
161
+ # are all unchanged. Explicit LOKI_COUNCIL_CHECK_INTERVAL still wins because
162
+ # COUNCIL_CHECK_INTERVAL already carries it as the base value.
163
+ _loki_auto_tune_interval() {
164
+ local complexity="${1:-}"
165
+ local base="${COUNCIL_CHECK_INTERVAL:-5}"
166
+ if [ "${LOKI_AUTO_TUNE:-0}" != "1" ]; then
167
+ printf '%s' "$base"
168
+ return 0
169
+ fi
170
+ case "$complexity" in
171
+ simple) printf '%s' 3 ;;
172
+ *) printf '%s' "$base" ;;
173
+ esac
174
+ }
131
175
  COUNCIL_CONVERGENCE_WINDOW=${LOKI_COUNCIL_CONVERGENCE_WINDOW:-3}
132
176
  COUNCIL_STAGNATION_LIMIT=${LOKI_COUNCIL_STAGNATION_LIMIT:-5}
133
177
  COUNCIL_DONE_SIGNAL_LIMIT=${LOKI_COUNCIL_DONE_SIGNAL_LIMIT:-10}
@@ -263,8 +307,30 @@ council_track_iteration() {
263
307
  fi
264
308
  COUNCIL_LAST_DIFF_HASH="$combined_hash"
265
309
 
266
- # Track "done" signals from agent output
267
- if [ -n "$log_file" ] && [ -f "$log_file" ]; then
310
+ # Track "done" signals from agent output.
311
+ #
312
+ # A "done signal" is any claim by the agent that the work is complete this
313
+ # iteration. There are TWO ways it can claim: (1) the STRUCTURED path -- the
314
+ # loki_complete_task MCP tool writing .loki/signals/COMPLETION_REQUESTED (or
315
+ # the runner's TASK_COMPLETION_CLAIMED), which is the DEFAULT since v6.82.0;
316
+ # and (2) fuzzy completion-like language in the iteration log. Historically
317
+ # only (2) was counted -- so a build that claimed done every iteration via the
318
+ # structured signal (medium confidence, thin evidence, council correctly
319
+ # rejects) never incremented COUNCIL_TOTAL_DONE_SIGNALS, the done-signal safety
320
+ # valve (council_should_stop, "Safety valve 2") never armed, and the loop ran
321
+ # to the wall spending real dollars. Observed on real builds: 11 identical
322
+ # structured claims, 0 counted, 60-min timeout at ~$34. We now count a
323
+ # structured claim as a done signal too, PEEKING the signal files
324
+ # non-destructively (the council owns consumption; the same two paths the loop
325
+ # tests at the council_should_stop call site).
326
+ local _done_this_iter=0
327
+
328
+ if [ -f "${TARGET_DIR:-.}/.loki/signals/COMPLETION_REQUESTED" ] \
329
+ || [ -f "${TARGET_DIR:-.}/.loki/signals/TASK_COMPLETION_CLAIMED" ]; then
330
+ _done_this_iter=1
331
+ fi
332
+
333
+ if [ "$_done_this_iter" = "0" ] && [ -n "$log_file" ] && [ -f "$log_file" ]; then
268
334
  # Check last 200 lines for completion-like language
269
335
  local done_indicators
270
336
  done_indicators=$(tail -200 "$log_file" 2>/dev/null | grep -ciE \
@@ -273,14 +339,15 @@ council_track_iteration() {
273
339
  # Ensure we have a clean integer (strip any whitespace/newlines)
274
340
  done_indicators=$(echo "$done_indicators" | tr -dc '0-9')
275
341
  done_indicators="${done_indicators:-0}"
342
+ [ "$done_indicators" -gt 0 ] && _done_this_iter=1
343
+ fi
276
344
 
277
- if [ "$done_indicators" -gt 0 ]; then
278
- ((COUNCIL_DONE_SIGNALS++))
279
- ((COUNCIL_TOTAL_DONE_SIGNALS++))
280
- else
281
- # Reset if agent stopped claiming done
282
- COUNCIL_DONE_SIGNALS=0
283
- fi
345
+ if [ "$_done_this_iter" = "1" ]; then
346
+ ((COUNCIL_DONE_SIGNALS++))
347
+ ((COUNCIL_TOTAL_DONE_SIGNALS++))
348
+ else
349
+ # Reset consecutive count if agent stopped claiming done
350
+ COUNCIL_DONE_SIGNALS=0
284
351
  fi
285
352
 
286
353
  # Store convergence data point
@@ -1252,7 +1319,25 @@ except: print('Results unavailable')
1252
1319
  council_reverify_checklist() {
1253
1320
  if type checklist_verify &>/dev/null && [ -f ".loki/checklist/checklist.json" ]; then
1254
1321
  log_info "[Council] Re-verifying checklist before evaluation..."
1255
- checklist_verify 2>/dev/null || true
1322
+ # RUN-25 iter 15 (Wave B #10): capture the REAL exit status. The old
1323
+ # `checklist_verify 2>/dev/null || true` swallowed a failure, leaving LAST
1324
+ # iteration's (possibly-green) verification-results.json in place -- so a
1325
+ # build that regressed since the last checklist run would sail through the
1326
+ # gates on STALE green evidence. On a re-verify failure we cannot trust the
1327
+ # existing results, so we mark them unverifiable: overwrite
1328
+ # verification-results.json with an explicit critical-failure sentinel that
1329
+ # council_checklist_gate reads as BLOCK (fail-closed). The next successful
1330
+ # re-verify overwrites the sentinel with fresh data.
1331
+ local _rv_rc=0
1332
+ checklist_verify 2>/dev/null || _rv_rc=$?
1333
+ if [ "$_rv_rc" -ne 0 ]; then
1334
+ log_warn "[Council] Checklist re-verify FAILED (rc=$_rv_rc); invalidating stale results (fail-closed)."
1335
+ local _rf=".loki/checklist/verification-results.json"
1336
+ local _tmp="${_rf}.tmp.$$"
1337
+ if printf '%s\n' '{"categories":[{"items":[{"id":"reverify_failed","title":"checklist re-verify failed","priority":"critical","status":"failing"}]}]}' > "$_tmp" 2>/dev/null; then
1338
+ mv -f "$_tmp" "$_rf" 2>/dev/null || rm -f "$_tmp" 2>/dev/null
1339
+ fi
1340
+ fi
1256
1341
  fi
1257
1342
  }
1258
1343
 
@@ -1267,11 +1352,24 @@ council_checklist_gate() {
1267
1352
  local waivers_file=".loki/checklist/waivers.json"
1268
1353
  local heldout_file=".loki/checklist/held-out.json"
1269
1354
 
1270
- # No checklist = no gate (backwards compatible)
1355
+ # No checklist = no gate (backwards compatible). Absent is genuinely "nothing
1356
+ # to verify"; present-but-corrupt is handled fail-closed below.
1271
1357
  if [ ! -f "$results_file" ]; then
1272
1358
  return 0
1273
1359
  fi
1274
1360
 
1361
+ # RUN-25 iter 13 (Wave B #4): the results file EXISTS (a checklist was
1362
+ # generated, possibly with a failing critical item), so if we cannot read it
1363
+ # we must fail CLOSED -- a checklist we cannot parse is NOT proof of a clean
1364
+ # build. python3 absent -> we cannot evaluate the gate at all -> BLOCK rather
1365
+ # than the old `|| echo PASS`, which cleared the first hard gate on a broken
1366
+ # host. (Absent python is a real deployment problem; failing open here is
1367
+ # exactly the fake-green this gate exists to prevent.)
1368
+ if ! command -v python3 >/dev/null 2>&1; then
1369
+ log_warn "[Council] Hard gate BLOCKED: python3 unavailable, cannot verify checklist (fail-closed)."
1370
+ return 1
1371
+ fi
1372
+
1275
1373
  # Check for critical failures, excluding waived AND held-out items. Held-out
1276
1374
  # items (v7.28.0) must NOT block here: they are evaluated separately by
1277
1375
  # council_heldout_gate at the ship gate, and surfacing them in this gate's
@@ -1287,8 +1385,11 @@ heldout_file = os.environ.get('_HELDOUT_FILE', '')
1287
1385
  try:
1288
1386
  with open(results_file) as f:
1289
1387
  results = json.load(f)
1290
- except (json.JSONDecodeError, IOError, KeyError):
1291
- print('PASS')
1388
+ except (json.JSONDecodeError, IOError, KeyError, ValueError):
1389
+ # RUN-25 iter 13 (Wave B #4): the file EXISTS but is unreadable/corrupt. A
1390
+ # checklist we cannot parse is NOT proof of a clean build -- a failing
1391
+ # critical item could be hiding in the unparseable bytes. Fail CLOSED.
1392
+ print('BLOCK:unreadable_checklist_results')
1292
1393
  sys.exit(0)
1293
1394
 
1294
1395
  # Load waivers
@@ -1325,7 +1426,7 @@ if critical_failures:
1325
1426
  else:
1326
1427
  print('PASS')
1327
1428
  sys.exit(0)
1328
- " 2>/dev/null || echo "PASS")
1429
+ " 2>/dev/null || echo "BLOCK:checklist_gate_error")
1329
1430
 
1330
1431
  if [[ "$gate_result" == BLOCK:* ]]; then
1331
1432
  local failures="${gate_result#BLOCK:}"
@@ -1425,8 +1526,14 @@ try:
1425
1526
  results = json.load(f)
1426
1527
  with open(heldout_file) as f:
1427
1528
  heldout_ids = set(json.load(f).get('held_out', []))
1428
- except (json.JSONDecodeError, IOError, KeyError):
1429
- print('NONE 0 0')
1529
+ except (json.JSONDecodeError, IOError, KeyError, ValueError):
1530
+ # RUN-25 iter 13 (Wave B #4): BOTH files are known to EXIST (guarded above),
1531
+ # so an error here is a present-but-corrupt results/held-out file, NOT an
1532
+ # inert no-items-reserved state. A held-out gate we cannot evaluate must fail
1533
+ # CLOSED (BLOCK), never emit NONE (which the caller treats as nothing-to-check).
1534
+ # The caller reads verdict pass fail, so emit BLOCK as the first field with a
1535
+ # synthetic 1 fail. A hidden failing reserved item could ship otherwise.
1536
+ print('BLOCK 0 1')
1430
1537
  sys.exit(0)
1431
1538
 
1432
1539
  # No held-out items reserved (e.g. N<4): gate is inert. Emit NONE so the caller
@@ -1476,7 +1583,7 @@ if matched == 0:
1476
1583
 
1477
1584
  verdict = 'BLOCK' if failed > 0 else 'PASS'
1478
1585
  print('%s %d %d' % (verdict, passed, failed))
1479
- " 2>/dev/null || echo "NONE 0 0")
1586
+ " 2>/dev/null || echo "BLOCK 0 1")
1480
1587
 
1481
1588
  local verdict pass_count fail_count
1482
1589
  read -r verdict pass_count fail_count <<< "$gate_result"
@@ -1705,6 +1812,12 @@ council_evidence_gate() {
1705
1812
  # Knob first: opt-out is exact-as-today, before any file read or write.
1706
1813
  [ "${LOKI_EVIDENCE_GATE:-1}" = "0" ] && return 0
1707
1814
 
1815
+ # Every Python helper in this trust boundary inherits a sanitized import
1816
+ # environment. A project-controlled sitecustomize.py must never execute
1817
+ # while the engine decides whether completion evidence is credible.
1818
+ local PYTHONPATH="" PYTHONNOUSERSITE=1
1819
+ export PYTHONPATH PYTHONNOUSERSITE
1820
+
1708
1821
  # The gate may run even when the completion council is disabled
1709
1822
  # (LOKI_COUNCIL_ENABLED=false leaves COUNCIL_STATE_DIR unset by council_init),
1710
1823
  # because it now also guards the default completion-promise route. Default
@@ -1984,6 +2097,14 @@ INCONCLUSIVE_EOF
1984
2097
  local _diff_ok _tests_ok
1985
2098
  if [ "$diff_fails" = "true" ]; then _diff_ok="false"; else _diff_ok="true"; fi
1986
2099
  if [ "$test_fails" = "true" ]; then _tests_ok="false"; else _tests_ok="true"; fi
2100
+ # Proof-of-Function axes (nomock/persistence/auth) are declared later in
2101
+ # the gate; default to "true"/"" when this helper runs before they are
2102
+ # set (it never does in practice, but keep the write robust).
2103
+ local _nomock_ok _persist_ok _auth_ok _authz_ok
2104
+ if [ "${nomock_fails:-false}" = "true" ]; then _nomock_ok="false"; else _nomock_ok="true"; fi
2105
+ if [ "${persist_fails:-false}" = "true" ]; then _persist_ok="false"; else _persist_ok="true"; fi
2106
+ if [ "${auth_fails:-false}" = "true" ]; then _auth_ok="false"; else _auth_ok="true"; fi
2107
+ if [ "${authz_fails:-false}" = "true" ]; then _authz_ok="false"; else _authz_ok="true"; fi
1987
2108
  cat > "$_det_tmp" << DETAILS_EOF
1988
2109
  {
1989
2110
  "recorded_at": "$_det_ts",
@@ -2002,14 +2123,456 @@ INCONCLUSIVE_EOF
2002
2123
  "pass": $test_pass,
2003
2124
  "inconclusive": $test_inconclusive,
2004
2125
  "inconclusive_reason": "$test_inconclusive_reason"
2126
+ },
2127
+ "nomock": {
2128
+ "ok": $_nomock_ok,
2129
+ "inconclusive": ${nomock_inconclusive:-false},
2130
+ "reason": "${nomock_inconclusive_reason:-}"
2131
+ },
2132
+ "persistence": {
2133
+ "ok": $_persist_ok,
2134
+ "inconclusive": ${persist_inconclusive:-false},
2135
+ "reason": "${persist_inconclusive_reason:-}"
2136
+ },
2137
+ "auth": {
2138
+ "ok": $_auth_ok,
2139
+ "inconclusive": ${auth_inconclusive:-false},
2140
+ "reason": "${auth_inconclusive_reason:-}"
2141
+ },
2142
+ "authorization": {
2143
+ "ok": $_authz_ok,
2144
+ "inconclusive": ${authz_inconclusive:-false},
2145
+ "reason": "${authz_inconclusive_reason:-}"
2005
2146
  }
2006
2147
  }
2007
2148
  DETAILS_EOF
2008
2149
  mv "$_det_tmp" "$_det_file" 2>/dev/null || rm -f "$_det_tmp" 2>/dev/null || true
2009
2150
  }
2010
2151
 
2011
- # --- Block decision: block iff DIFF FAILS or TEST FAILS ---
2012
- if [ "$diff_fails" != "true" ] && [ "$test_fails" != "true" ]; then
2152
+ # --- Evidence check (c): RUNTIME BOOT -- does the built app actually run? ---
2153
+ # RUN-25 iter 20 (Wave D #1): the single most load-bearing claim of a
2154
+ # spec-to-product builder is "the app runs", and until now the evidence gate
2155
+ # NEVER checked it -- app_runner_health_check already writes
2156
+ # .loki/app-runner/health.json but nothing read it here. Mirror the test axis:
2157
+ # a SERVEABLE app confirmed unhealthy -> boot_fails (BLOCK); no serveable
2158
+ # runner / probe never attempted (a CLI tool or library has no server to boot)
2159
+ # -> boot_inconclusive, pass-through (must NOT deadlock a non-web project).
2160
+ # Opt out with LOKI_EVIDENCE_BOOT_GATE=0.
2161
+ local boot_fails="false"
2162
+ local boot_inconclusive="false"
2163
+ local boot_inconclusive_reason=""
2164
+ local _health_file=".loki/app-runner/health.json"
2165
+ local _state_file=".loki/app-runner/state.json"
2166
+ if [ "${LOKI_EVIDENCE_BOOT_GATE:-1}" = "0" ]; then
2167
+ boot_inconclusive="true"
2168
+ boot_inconclusive_reason="boot_gate_disabled"
2169
+ elif [ ! -f "$_health_file" ] && [ ! -f "$_state_file" ]; then
2170
+ # No app-runner artifacts at all -> no serveable app was ever launched
2171
+ # (CLI/library project, or the runner never ran). Inconclusive, not a block.
2172
+ boot_inconclusive="true"
2173
+ boot_inconclusive_reason="no_app_runner"
2174
+ elif command -v python3 >/dev/null 2>&1; then
2175
+ local _boot_status
2176
+ _boot_status=$(_HEALTH="$_health_file" _STATE="$_state_file" python3 <<'PYEOF' 2>/dev/null || echo "INCONCLUSIVE:parse_error"
2177
+ import json, os
2178
+ health_file = os.environ.get('_HEALTH', '')
2179
+ state_file = os.environ.get('_STATE', '')
2180
+
2181
+ def load(p):
2182
+ try:
2183
+ with open(p) as f:
2184
+ return json.load(f)
2185
+ except Exception:
2186
+ return None
2187
+
2188
+ state = load(state_file) or {}
2189
+ health = load(health_file) or {}
2190
+ # A "serveable" app is one the runner classified with a real service (status not
2191
+ # 'none'/'unknown' and a primary_service or url present). Only then is an
2192
+ # unhealthy probe a genuine BLOCK; otherwise there is nothing to boot.
2193
+ status = str(state.get('status', 'unknown')).lower()
2194
+ svc = state.get('primary_service') or ''
2195
+ url = state.get('url') or ''
2196
+ serveable = status not in ('none', 'unknown', '') and (bool(svc) or bool(url))
2197
+ # health 'ok' is the last health-check verdict (True healthy / False failing).
2198
+ ok = health.get('ok', None)
2199
+ if not serveable:
2200
+ print('INCONCLUSIVE:not_serveable')
2201
+ elif ok is True:
2202
+ print('PASS')
2203
+ elif ok is False:
2204
+ print('FAIL')
2205
+ else:
2206
+ # serveable but no health verdict recorded -> probe never completed.
2207
+ print('INCONCLUSIVE:no_health_probe')
2208
+ PYEOF
2209
+ )
2210
+ case "$_boot_status" in
2211
+ FAIL)
2212
+ boot_fails="true" ;;
2213
+ PASS)
2214
+ : ;; # affirmative: the app boots and serves.
2215
+ INCONCLUSIVE:*)
2216
+ boot_inconclusive="true"
2217
+ boot_inconclusive_reason="${_boot_status#INCONCLUSIVE:}" ;;
2218
+ *)
2219
+ boot_inconclusive="true"
2220
+ boot_inconclusive_reason="unknown" ;;
2221
+ esac
2222
+ else
2223
+ boot_inconclusive="true"
2224
+ boot_inconclusive_reason="no_python3"
2225
+ fi
2226
+
2227
+ # === Proof-of-Function (PoF) axes: NO-MOCK, PERSISTENCE, AUTH ==============
2228
+ # Three functionality-proving axes. Each is proven by an OBSERVED artifact
2229
+ # (static source scan for no-mock; a real browser drive + DB/API read-back
2230
+ # for persistence + auth), never a self-report. Each fails CLOSED on POSITIVE
2231
+ # disproof and passes through ONLY when genuinely inconclusive (non-web build,
2232
+ # browser unavailable, no create path, no auth signal). Web-app-only: the
2233
+ # dynamic axes reuse the SAME serveable gate as boot, so a CLI/API/library
2234
+ # build is NEVER blocked. Master knob LOKI_PROOF_GATE=0 disables all three;
2235
+ # per-axis LOKI_PROOF_NOMOCK / LOKI_PROOF_PERSIST / LOKI_PROOF_AUTH=0.
2236
+ local _proof_gate="${LOKI_PROOF_GATE:-1}"
2237
+ local nomock_fails="false" nomock_inconclusive="false" nomock_inconclusive_reason=""
2238
+ local persist_fails="false" persist_inconclusive="false" persist_inconclusive_reason=""
2239
+ local auth_fails="false" auth_inconclusive="false" auth_inconclusive_reason=""
2240
+ local authz_fails="false" authz_inconclusive="false" authz_inconclusive_reason=""
2241
+ local _proof_file=".loki/verification/functional-proof.json"
2242
+
2243
+ # --- Evidence check (e): NO-MOCK (static, cheap, runs first) ---
2244
+ # A list/table/dashboard whose backing data traces to an inline mock array /
2245
+ # faker / placeholder literal instead of a real fetch/query is a fake app.
2246
+ # This is the ONLY axis that can run without the app up. Consumer of
2247
+ # verify.sh writes nomock-scan.json as an audit artifact. Completion never
2248
+ # trusts that mutable cache, even when its base SHA matches: the run base is
2249
+ # constant across iterations while source bytes can keep changing. Re-run
2250
+ # the engine-owned scanner over the current changed-file union every time.
2251
+ if [ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_NOMOCK:-1}" = "0" ]; then
2252
+ nomock_inconclusive="true"
2253
+ nomock_inconclusive_reason="nomock_gate_disabled"
2254
+ elif command -v python3 >/dev/null 2>&1; then
2255
+ local _nm_files
2256
+ _nm_files=$(
2257
+ {
2258
+ [ -n "$base_sha" ] && git diff --name-only "$base_sha" 2>/dev/null
2259
+ git diff --name-only 2>/dev/null
2260
+ git diff --name-only --cached 2>/dev/null
2261
+ git ls-files --others --exclude-standard 2>/dev/null
2262
+ } | grep -v '^$' | grep -vE '^\.loki/' | sort -u
2263
+ )
2264
+ local _nm_status _nm_scanner _nm_cc_dir
2265
+ _nm_cc_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-}")" 2>/dev/null && pwd || true)"
2266
+ _nm_scanner="$_nm_cc_dir/lib/no_mock_scan.py"
2267
+ if [ -z "$_nm_cc_dir" ] || [ ! -f "$_nm_scanner" ]; then
2268
+ _nm_status="INCONCLUSIVE:scanner_unavailable"
2269
+ else
2270
+ _nm_status=$(
2271
+ _NM_FILES="$_nm_files" \
2272
+ _NM_TREE="." \
2273
+ python3 -I "$_nm_scanner" 2>/dev/null \
2274
+ || echo "INCONCLUSIVE:detector_error"
2275
+ )
2276
+ fi
2277
+ case "$_nm_status" in
2278
+ FAIL:*) nomock_fails="true" ;;
2279
+ PASS:*) : ;;
2280
+ SKIP:*) nomock_inconclusive="true"; nomock_inconclusive_reason="no_ui_files" ;;
2281
+ INCONCLUSIVE:*) nomock_inconclusive="true"; nomock_inconclusive_reason="${_nm_status#INCONCLUSIVE:}" ;;
2282
+ *) nomock_inconclusive="true"; nomock_inconclusive_reason="unknown" ;;
2283
+ esac
2284
+ else
2285
+ nomock_inconclusive="true"
2286
+ nomock_inconclusive_reason="no_python3"
2287
+ fi
2288
+
2289
+ # --- Evidence checks (f) PERSISTENCE + (g) AUTH (dynamic, driven) ---
2290
+ # Both are read from .loki/verification/functional-proof.json, produced by
2291
+ # playwright_prove_functional (run.sh interval-gated, serveable-only). The
2292
+ # gate reads ONLY this artifact -- never a screenshot, never an LLM opinion.
2293
+ # Tri-state per property: attempted / proven / reason. Fail-closed:
2294
+ # PERSISTENCE: proven==true -> PASS. attempted && !proven -> BLOCK (a
2295
+ # create path was exercised and the record did NOT survive reload; a
2296
+ # submit that errors is the #1 churn bug and must not green-wash).
2297
+ # attempted==false with reason no_create_path/not_serveable/driver_
2298
+ # unavailable -> logged pass-through. Missing/unparseable/STALE (a stamp
2299
+ # whose iteration != the current one) file -> treated as NOT PRESENT ->
2300
+ # inconclusive pass-through, NEVER a block. Only a FRESH attempted &&
2301
+ # !proven disproof blocks: absence and staleness must never false-block a
2302
+ # legitimate build (the fail-closed rule is disproven-blocks, not
2303
+ # absent-blocks).
2304
+ # AUTH: proven==true (observed 401/403/redirect) -> PASS. attempted &&
2305
+ # !proven (protected route served 200 logged-out, or only a login screen
2306
+ # rendered) -> BLOCK. no_auth/not_serveable -> pass-through. Auth
2307
+ # detected but unprovable under LOKI_PROOF_AUTH_STRICT=1 -> BLOCK
2308
+ # (security property never assumed).
2309
+ # Web-app-only: only evaluate the dynamic axes when the app is SERVEABLE
2310
+ # (same signal the boot axis uses). A non-web build never launches a browser,
2311
+ # writes no proof file, and passes through by construction.
2312
+ local _proof_persist_disabled="false" _proof_auth_disabled="false" _proof_authz_disabled="false"
2313
+ [ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_PERSIST:-1}" = "0" ] && _proof_persist_disabled="true"
2314
+ [ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_AUTH:-1}" = "0" ] && _proof_auth_disabled="true"
2315
+ [ "$_proof_gate" = "0" ] || [ "${LOKI_PROOF_AUTHZ:-1}" = "0" ] && _proof_authz_disabled="true"
2316
+
2317
+ # Serveable? Reuse the boot axis's own classification: boot_fails or an
2318
+ # affirmative boot pass both imply a serveable app was launched. Only a
2319
+ # not_serveable / no_app_runner boot-inconclusive means non-web.
2320
+ local _proof_serveable="false"
2321
+ if [ "$boot_fails" = "true" ]; then
2322
+ _proof_serveable="true"
2323
+ elif [ "$boot_inconclusive" != "true" ]; then
2324
+ _proof_serveable="true"
2325
+ fi
2326
+
2327
+ if [ "$_proof_persist_disabled" = "true" ]; then
2328
+ persist_inconclusive="true"; persist_inconclusive_reason="persist_gate_disabled"
2329
+ elif [ "$_proof_serveable" != "true" ]; then
2330
+ persist_inconclusive="true"; persist_inconclusive_reason="not_serveable"
2331
+ elif command -v python3 >/dev/null 2>&1; then
2332
+ local _p_status
2333
+ _p_status=$(_PF="$_proof_file" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
2334
+ import json, os
2335
+ p = os.environ['_PF']
2336
+ if not os.path.isfile(p):
2337
+ print("MISSING"); raise SystemExit
2338
+ try:
2339
+ d = json.load(open(p))
2340
+ except Exception:
2341
+ print("UNPARSEABLE"); raise SystemExit
2342
+ # BUG 1b: freshness. A proof stamped for a different iteration is a STALE artifact
2343
+ # (a timeout/hang left a prior iteration's file, or the producer did not run this
2344
+ # iteration). Treat it as NOT PRESENT -- never let a stale proven:true pass.
2345
+ stamp = d.get("stamp") or {}
2346
+ try:
2347
+ cur = int(os.environ.get("_ITER", "0"))
2348
+ except Exception:
2349
+ cur = 0
2350
+ if "iteration" in stamp:
2351
+ try:
2352
+ if int(stamp.get("iteration")) != cur:
2353
+ print("MISSING"); raise SystemExit
2354
+ except (TypeError, ValueError):
2355
+ print("MISSING"); raise SystemExit
2356
+ per = d.get("persistence") or {}
2357
+ attempted = per.get("attempted")
2358
+ proven = per.get("proven")
2359
+ reason = str(per.get("reason", "") or "")
2360
+ if attempted is True and proven is True:
2361
+ print("PASS")
2362
+ elif attempted is False and reason in ("no_create_path", "not_serveable", "driver_unavailable"):
2363
+ print("INCONCLUSIVE:" + reason)
2364
+ elif attempted is True and proven is False:
2365
+ # A create path WAS exercised and the record did not survive -> disproven.
2366
+ print("FAIL")
2367
+ else:
2368
+ # attempted True/proven unknown, or malformed record on a serveable app.
2369
+ # We cannot prove persistence, but "not proven" is not "disproven": pass
2370
+ # through inconclusive rather than block a legitimate build (BUG 2).
2371
+ print("INCONCLUSIVE:proof_indeterminate")
2372
+ PYEOF
2373
+ )
2374
+ case "$_p_status" in
2375
+ PASS) : ;;
2376
+ INCONCLUSIVE:*) persist_inconclusive="true"; persist_inconclusive_reason="${_p_status#INCONCLUSIVE:}" ;;
2377
+ # BUG 2: absence / staleness / unparseable is NOT a disproof. A proof
2378
+ # that was never freshly attempted this iteration (interval not fired,
2379
+ # driver unavailable, timeout left nothing) is INCONCLUSIVE pass-through,
2380
+ # never a hard block. ONLY a FRESHLY-attempted proven:false blocks.
2381
+ MISSING) persist_inconclusive="true"; persist_inconclusive_reason="proof_not_attempted" ;;
2382
+ UNPARSEABLE) persist_inconclusive="true"; persist_inconclusive_reason="proof_file_unparseable" ;;
2383
+ FAIL) persist_fails="true" ;;
2384
+ *) persist_inconclusive="true"; persist_inconclusive_reason="proof_indeterminate" ;;
2385
+ esac
2386
+ else
2387
+ persist_inconclusive="true"; persist_inconclusive_reason="no_python3"
2388
+ fi
2389
+
2390
+ if [ "$_proof_auth_disabled" = "true" ]; then
2391
+ auth_inconclusive="true"; auth_inconclusive_reason="auth_gate_disabled"
2392
+ elif [ "$_proof_serveable" != "true" ]; then
2393
+ auth_inconclusive="true"; auth_inconclusive_reason="not_serveable"
2394
+ elif command -v python3 >/dev/null 2>&1; then
2395
+ local _a_status
2396
+ _a_status=$(_PF="$_proof_file" _STRICT="${LOKI_PROOF_AUTH_STRICT:-1}" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
2397
+ import json, os
2398
+ p = os.environ['_PF']
2399
+ strict = os.environ.get('_STRICT', '1') != '0'
2400
+ if not os.path.isfile(p):
2401
+ # No proof file on a serveable app: cannot prove auth. If auth was never
2402
+ # detected we would expect no_auth; a missing file is ambiguous. Under
2403
+ # STRICT this is not a pass, but we cannot know auth was detected, so treat
2404
+ # missing as inconclusive (the boot/persist axes already catch a dead app).
2405
+ print("INCONCLUSIVE:proof_file_missing"); raise SystemExit
2406
+ try:
2407
+ d = json.load(open(p))
2408
+ except Exception:
2409
+ print("INCONCLUSIVE:proof_file_unparseable"); raise SystemExit
2410
+ # BUG 1b: freshness. A proof stamped for a different iteration is stale -> treat
2411
+ # as not present (inconclusive), never let a stale auth proven:true pass.
2412
+ stamp = d.get("stamp") or {}
2413
+ try:
2414
+ cur = int(os.environ.get("_ITER", "0"))
2415
+ except Exception:
2416
+ cur = 0
2417
+ if "iteration" in stamp:
2418
+ try:
2419
+ if int(stamp.get("iteration")) != cur:
2420
+ print("INCONCLUSIVE:proof_stale"); raise SystemExit
2421
+ except (TypeError, ValueError):
2422
+ print("INCONCLUSIVE:proof_stale"); raise SystemExit
2423
+ a = d.get("auth") or {}
2424
+ attempted = a.get("attempted")
2425
+ proven = a.get("proven")
2426
+ reason = str(a.get("reason", "") or "")
2427
+ if attempted is True and proven is True:
2428
+ print("PASS")
2429
+ elif attempted is True and proven is False:
2430
+ # observed a protected route served logged-out, or only a login screen
2431
+ # rendered -> auth NOT enforced -> BLOCK.
2432
+ print("FAIL")
2433
+ elif attempted is False and reason in ("no_auth", "not_serveable", "driver_unavailable"):
2434
+ print("INCONCLUSIVE:" + reason)
2435
+ elif reason in ("auth_detected_untestable", "auth_detected_timeout"):
2436
+ # auth signal detected but enforcement could not be proven. Security
2437
+ # property: STRICT fails closed to BLOCK; otherwise inconclusive-but-logged.
2438
+ print("FAIL" if strict else "INCONCLUSIVE:" + reason)
2439
+ else:
2440
+ print("INCONCLUSIVE:" + (reason or "unknown"))
2441
+ PYEOF
2442
+ )
2443
+ case "$_a_status" in
2444
+ PASS) : ;;
2445
+ INCONCLUSIVE:*) auth_inconclusive="true"; auth_inconclusive_reason="${_a_status#INCONCLUSIVE:}" ;;
2446
+ FAIL) auth_fails="true" ;;
2447
+ *) auth_inconclusive="true"; auth_inconclusive_reason="unknown" ;;
2448
+ esac
2449
+ else
2450
+ auth_inconclusive="true"; auth_inconclusive_reason="no_python3"
2451
+ fi
2452
+
2453
+ # --- Evidence check (h): AUTHORIZATION / TENANT ISOLATION (dynamic, driven) ---
2454
+ # The Lovable-breach class: two LOGGED-IN users where user A can read user B's
2455
+ # owned rows (ownership/RLS check inverted or missing). "auth" proves anon->401
2456
+ # (authentication); "authorization" proves user-A-data-denied-to-user-B (tenant
2457
+ # isolation). Read from the SAME functional-proof.json, key "authorization".
2458
+ #
2459
+ # CRITICAL ASYMMETRY vs persistence: a security property must NEVER be declared
2460
+ # VIOLATED by our own tooling's inability to observe. So attempted && !proven
2461
+ # blocks ONLY when the reason is the explicit positive-leak prefix
2462
+ # 'user_b_read_user_a_' (B's real session actually received A's sentinel on a
2463
+ # path). Every other outcome -- no multi-user auth, no owned data, could not
2464
+ # create a second user, no read path for B, a drive error, missing/stale --
2465
+ # is INCONCLUSIVE pass-through, never a block. Absence != insecure. This is the
2466
+ # discipline that prevents the false-block: ONLY a fresh positive disproof of
2467
+ # isolation blocks.
2468
+ if [ "$_proof_authz_disabled" = "true" ]; then
2469
+ authz_inconclusive="true"; authz_inconclusive_reason="authz_gate_disabled"
2470
+ elif [ "$_proof_serveable" != "true" ]; then
2471
+ authz_inconclusive="true"; authz_inconclusive_reason="not_serveable"
2472
+ elif command -v python3 >/dev/null 2>&1; then
2473
+ local _authz_status
2474
+ _authz_status=$(_PF="$_proof_file" _ITER="${ITERATION_COUNT:-0}" python3 <<'PYEOF' 2>/dev/null || echo "MISSING"
2475
+ import json, os
2476
+ p = os.environ['_PF']
2477
+ if not os.path.isfile(p):
2478
+ print("MISSING"); raise SystemExit
2479
+ try:
2480
+ d = json.load(open(p))
2481
+ except Exception:
2482
+ print("UNPARSEABLE"); raise SystemExit
2483
+ # Freshness: a proof stamped for a different iteration is stale -> treat as NOT
2484
+ # PRESENT. A stale proven:false leak reason must never linger-block; a stale
2485
+ # proven:true must never be a stale green.
2486
+ stamp = d.get("stamp") or {}
2487
+ try:
2488
+ cur = int(os.environ.get("_ITER", "0"))
2489
+ except Exception:
2490
+ cur = 0
2491
+ if "iteration" in stamp:
2492
+ try:
2493
+ if int(stamp.get("iteration")) != cur:
2494
+ print("STALE"); raise SystemExit
2495
+ except (TypeError, ValueError):
2496
+ print("STALE"); raise SystemExit
2497
+ az = d.get("authorization") or {}
2498
+ attempted = az.get("attempted")
2499
+ proven = az.get("proven")
2500
+ reason = str(az.get("reason", "") or "")
2501
+ if attempted is True and proven is True:
2502
+ # Isolation observed to HOLD on every path tried.
2503
+ print("PASS")
2504
+ elif attempted is True and proven is False and reason.startswith("user_b_read_user_a_"):
2505
+ # The ONLY blocking outcome: B's real session received A's sentinel on a path.
2506
+ print("FAIL")
2507
+ else:
2508
+ # EVERY other case is inconclusive pass-through (never a block):
2509
+ # attempted:false -> no_multiuser_auth / no_owned_data /
2510
+ # could_not_create_second_user / not_serveable / driver_unavailable
2511
+ # attempted:true/proven:false -> no_read_path_for_b / authz_drive_error:*
2512
+ # A generic "could not read as B" or a driver error is OUR failure to observe,
2513
+ # not a proven leak. Never block on it.
2514
+ print("INCONCLUSIVE:" + (reason or "unknown"))
2515
+ PYEOF
2516
+ )
2517
+ case "$_authz_status" in
2518
+ PASS) : ;;
2519
+ INCONCLUSIVE:*) authz_inconclusive="true"; authz_inconclusive_reason="${_authz_status#INCONCLUSIVE:}" ;;
2520
+ MISSING) authz_inconclusive="true"; authz_inconclusive_reason="proof_not_attempted" ;;
2521
+ UNPARSEABLE) authz_inconclusive="true"; authz_inconclusive_reason="proof_file_unparseable" ;;
2522
+ STALE) authz_inconclusive="true"; authz_inconclusive_reason="proof_stale" ;;
2523
+ FAIL) authz_fails="true" ;;
2524
+ *) authz_inconclusive="true"; authz_inconclusive_reason="unknown" ;;
2525
+ esac
2526
+ else
2527
+ authz_inconclusive="true"; authz_inconclusive_reason="no_python3"
2528
+ fi
2529
+
2530
+ # --- Evidence check (d): SECRET LEAK -- did the build ship a credential? ---
2531
+ # RUN-25 iter 21 (Wave D #2): the secret matcher ran ONLY on the auto-commit
2532
+ # path, so a completion via the promise / dirty-tree route could ship a leaked
2533
+ # credential in a "done" deliverable -- a hole in the exact trust moat. Run the
2534
+ # SAME matcher (shared lib) over the changed files. A high-confidence hit ->
2535
+ # secret_fails (BLOCK); no matcher available or no scannable files ->
2536
+ # inconclusive pass-through. Opt out LOKI_EVIDENCE_SECRET_GATE=0.
2537
+ local secret_fails="false"
2538
+ if [ "${LOKI_EVIDENCE_SECRET_GATE:-1}" != "0" ] \
2539
+ && type _commit_scan_secret_file >/dev/null 2>&1; then
2540
+ # Changed files vs run-start SHA (committed) + working-tree (staged/unstaged/
2541
+ # untracked), excluding .loki/ (Loki's own state is never project work).
2542
+ local _sec_files
2543
+ _sec_files=$(
2544
+ {
2545
+ [ -n "$base_sha" ] && git diff --name-only "$base_sha" 2>/dev/null
2546
+ git diff --name-only 2>/dev/null
2547
+ git diff --name-only --cached 2>/dev/null
2548
+ git ls-files --others --exclude-standard 2>/dev/null
2549
+ } | grep -v '^$' | grep -vE '^\.loki/' | sort -u
2550
+ )
2551
+ local _sf
2552
+ while IFS= read -r _sf; do
2553
+ [ -n "$_sf" ] || continue
2554
+ # CONTENT ONLY. The filename heuristic (_commit_path_looks_secret) is
2555
+ # correct for the AUTO-COMMIT path -- refusing to stage a file called
2556
+ # `.env` is cheap and right. It is WRONG as a completion gate, because
2557
+ # it blocks on names alone with no look at what is inside:
2558
+ # `src/auth/token.ts` is a routine file in any auth-enabled app, which
2559
+ # is exactly what this engine is asked to build, and v7.129.5 shipped
2560
+ # such builds without complaint (v7's council had no secret logic at
2561
+ # all). Blocking there tells a user their finished, correct app is a
2562
+ # security leak because of how they named a file.
2563
+ # A real leak is still caught: _commit_scan_secret_file reads the
2564
+ # bytes, and it carries the example/dummy/placeholder deny-filter that
2565
+ # keeps fixtures and .env.example from tripping it.
2566
+ if [ -f "$_sf" ] && _commit_scan_secret_file "$_sf"; then
2567
+ secret_fails="true"
2568
+ log_warn "[Council] Evidence gate: secret-leak match in changed file '${_sf}'"
2569
+ break
2570
+ fi
2571
+ done <<< "$_sec_files"
2572
+ fi
2573
+
2574
+ # --- Block decision: block iff any axis FAILS (diff/test/boot/secret/nomock/persist/auth/authz) ---
2575
+ if [ "$diff_fails" != "true" ] && [ "$test_fails" != "true" ] && [ "$boot_fails" != "true" ] && [ "$secret_fails" != "true" ] && [ "$nomock_fails" != "true" ] && [ "$persist_fails" != "true" ] && [ "$auth_fails" != "true" ] && [ "$authz_fails" != "true" ]; then
2013
2576
  # Gate passes: remove any stale block report.
2014
2577
  if [ -f "$COUNCIL_STATE_DIR/evidence-block.json" ]; then
2015
2578
  rm -f "$COUNCIL_STATE_DIR/evidence-block.json"
@@ -2022,6 +2585,26 @@ DETAILS_EOF
2022
2585
  if [ "$test_inconclusive" = "true" ]; then
2023
2586
  log_warn "[Council] Evidence gate: completion not backed by test evidence (${test_inconclusive_reason}). Pass-through; set LOKI_EVIDENCE_NO_TESTS_AFFIRMATIVE=1 to treat no-tests as affirmative."
2024
2587
  fi
2588
+ # Same honesty for the runtime-boot axis: a pass that could not confirm the
2589
+ # app boots (CLI/library, probe never ran, gate disabled) says so out loud
2590
+ # rather than implying the app was verified to run.
2591
+ if [ "$boot_inconclusive" = "true" ]; then
2592
+ log_warn "[Council] Evidence gate: app-boot not confirmed (${boot_inconclusive_reason}). Pass-through; set LOKI_EVIDENCE_BOOT_GATE=0 to silence, or run the app so its health probe records a verdict."
2593
+ fi
2594
+ # Proof-of-Function honesty: a pass-through that could not PROVE a
2595
+ # functionality property says so out loud (never imply it was proven).
2596
+ if [ "$nomock_inconclusive" = "true" ] && [ "$nomock_inconclusive_reason" != "nomock_gate_disabled" ]; then
2597
+ log_warn "[Council] Evidence gate: no-mock not confirmed (${nomock_inconclusive_reason}). Pass-through; set LOKI_PROOF_NOMOCK=0 to silence."
2598
+ fi
2599
+ if [ "$persist_inconclusive" = "true" ] && [ "$persist_inconclusive_reason" != "persist_gate_disabled" ]; then
2600
+ log_warn "[Council] Evidence gate: persistence not proven (${persist_inconclusive_reason}). Pass-through; set LOKI_PROOF_PERSIST=0 to silence, or expose a create form the driver can exercise (LOKI_PROOF_CREATE_SELECTOR)."
2601
+ fi
2602
+ if [ "$auth_inconclusive" = "true" ] && [ "$auth_inconclusive_reason" != "auth_gate_disabled" ]; then
2603
+ log_warn "[Council] Evidence gate: auth enforcement not proven (${auth_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTH=0 to silence, or set LOKI_PROOF_PROTECTED_PATH so the driver can test a logged-out request."
2604
+ fi
2605
+ if [ "$authz_inconclusive" = "true" ] && [ "$authz_inconclusive_reason" != "authz_gate_disabled" ]; then
2606
+ log_warn "[Council] Evidence gate: tenant isolation not proven (${authz_inconclusive_reason}). Pass-through; set LOKI_PROOF_AUTHZ=0 to silence, or configure LOKI_PROOF_AUTHZ_* selectors so the driver can drive two distinct sessions."
2607
+ fi
2025
2608
  _write_evidence_details "pass"
2026
2609
  return 0
2027
2610
  fi
@@ -2034,6 +2617,18 @@ DETAILS_EOF
2034
2617
  reason="empty_diff"
2035
2618
  elif [ "$test_fails" = "true" ]; then
2036
2619
  reason="tests_red"
2620
+ elif [ "$boot_fails" = "true" ]; then
2621
+ reason="app_boot_failed"
2622
+ elif [ "$secret_fails" = "true" ]; then
2623
+ reason="secret_leak_in_changed_files"
2624
+ elif [ "$nomock_fails" = "true" ]; then
2625
+ reason="mock_backed_data"
2626
+ elif [ "$persist_fails" = "true" ]; then
2627
+ reason="persistence_unproven"
2628
+ elif [ "$auth_fails" = "true" ]; then
2629
+ reason="auth_not_enforced"
2630
+ elif [ "$authz_fails" = "true" ]; then
2631
+ reason="tenant_isolation_broken"
2037
2632
  fi
2038
2633
 
2039
2634
  local failures=""
@@ -2049,6 +2644,75 @@ DETAILS_EOF
2049
2644
  fi
2050
2645
  log_warn "[Council] Evidence gate BLOCKED: test runner '${test_runner}' was red"
2051
2646
  fi
2647
+ if [ "$boot_fails" = "true" ]; then
2648
+ # Wave D #1: the built app was launched and its health probe reported
2649
+ # unhealthy -- a spec-to-product build cannot be "done" if its app does not
2650
+ # run. Pass-through for CLI/library projects (boot_inconclusive above).
2651
+ if [ -n "$failures" ]; then
2652
+ failures="${failures}|app runner started but the app is not healthy (does not serve)"
2653
+ else
2654
+ failures="app runner started but the app is not healthy (does not serve)"
2655
+ fi
2656
+ log_warn "[Council] Evidence gate BLOCKED: the built app is not running/serving (app-runner health FAIL)"
2657
+ fi
2658
+ if [ "$secret_fails" = "true" ]; then
2659
+ # Wave D #2: a credential-shaped secret is present in the changed files. A
2660
+ # trust builder cannot call a build "done" while it would leak a secret.
2661
+ if [ -n "$failures" ]; then
2662
+ failures="${failures}|a secret/credential was detected in the changed files"
2663
+ else
2664
+ failures="a secret/credential was detected in the changed files"
2665
+ fi
2666
+ log_warn "[Council] Evidence gate BLOCKED: a secret/credential was detected in the changed files"
2667
+ fi
2668
+ if [ "$nomock_fails" = "true" ]; then
2669
+ # PoF #1: a list, table, or dashboard resolves to an inline mock array,
2670
+ # faker value, or placeholder literal. Opt out: LOKI_PROOF_NOMOCK=0.
2671
+ if [ -n "$failures" ]; then
2672
+ failures="${failures}|a rendered operational collection resolves to inline, faker, or placeholder-backed data (set LOKI_PROOF_NOMOCK=0 to opt out)"
2673
+ else
2674
+ failures="a rendered operational collection resolves to inline, faker, or placeholder-backed data (set LOKI_PROOF_NOMOCK=0 to opt out)"
2675
+ fi
2676
+ log_warn "[Council] Evidence gate BLOCKED: a rendered operational collection resolves to inline, faker, or placeholder-backed data. Opt out: LOKI_PROOF_NOMOCK=0"
2677
+ fi
2678
+ if [ "$persist_fails" = "true" ]; then
2679
+ # PoF #2: a create path was FRESHLY driven this iteration and the record
2680
+ # did NOT survive a fresh-context read-back (or the submit errored).
2681
+ # "Submit does nothing" is the #1 churn bug. Absence/staleness of a proof
2682
+ # is inconclusive pass-through, not this block. Opt out: LOKI_PROOF_PERSIST=0.
2683
+ if [ -n "$failures" ]; then
2684
+ failures="${failures}|a create/submit was exercised but the record did not persist across reload (set LOKI_PROOF_PERSIST=0 to opt out)"
2685
+ else
2686
+ failures="a create/submit was exercised but the record did not persist across reload (set LOKI_PROOF_PERSIST=0 to opt out)"
2687
+ fi
2688
+ log_warn "[Council] Evidence gate BLOCKED: persistence not proven -- a submit did not survive reload (${persist_inconclusive_reason:-not_persisted}). Opt out: LOKI_PROOF_PERSIST=0"
2689
+ fi
2690
+ if [ "$auth_fails" = "true" ]; then
2691
+ # PoF #3: a protected route served logged-out (200), only a login screen
2692
+ # rendered, or auth was detected but enforcement could not be proven
2693
+ # under STRICT. A rendered login screen is not "auth done". Opt out:
2694
+ # LOKI_PROOF_AUTH=0 (or LOKI_PROOF_AUTH_STRICT=0 to relax the detected-but-
2695
+ # untestable case, or LOKI_PROOF_PROTECTED_PATH to point the driver).
2696
+ if [ -n "$failures" ]; then
2697
+ failures="${failures}|auth enforcement was not proven -- a protected route was reachable logged-out or could not be tested (set LOKI_PROOF_AUTH=0 to opt out)"
2698
+ else
2699
+ failures="auth enforcement was not proven -- a protected route was reachable logged-out or could not be tested (set LOKI_PROOF_AUTH=0 to opt out)"
2700
+ fi
2701
+ log_warn "[Council] Evidence gate BLOCKED: auth enforcement not proven (${auth_inconclusive_reason:-not_enforced}). Opt out: LOKI_PROOF_AUTH=0 (or LOKI_PROOF_PROTECTED_PATH / LOKI_PROOF_AUTH_STRICT=0)"
2702
+ fi
2703
+ if [ "$authz_fails" = "true" ]; then
2704
+ # PoF #4 (the Lovable-breach class): user B's real second session READ user
2705
+ # A's owned sentinel via a list/detail/API path -- tenant isolation is
2706
+ # BROKEN (ownership/RLS check inverted or missing). This is the ONLY authz
2707
+ # block outcome: absence/undetectable/driver-error all pass through. Opt
2708
+ # out: LOKI_PROOF_AUTHZ=0.
2709
+ if [ -n "$failures" ]; then
2710
+ failures="${failures}|tenant isolation is broken -- a second user could read another user's owned data (set LOKI_PROOF_AUTHZ=0 to opt out)"
2711
+ else
2712
+ failures="tenant isolation is broken -- a second user could read another user's owned data (set LOKI_PROOF_AUTHZ=0 to opt out)"
2713
+ fi
2714
+ log_warn "[Council] Evidence gate BLOCKED: tenant isolation broken -- user B read user A's owned data (${authz_inconclusive_reason:-user_b_read_user_a}). Opt out: LOKI_PROOF_AUTHZ=0"
2715
+ fi
2052
2716
 
2053
2717
  # Rail 3 (one-step self-rescue): the terminal user (no dashboard open) must
2054
2718
  # be told, right at the block site, how to opt out of the gate. A false
@@ -2069,8 +2733,23 @@ import json, os
2069
2733
  items = [s for s in os.environ['_FAILURES'].split('|') if s]
2070
2734
  print(json.dumps(items[:5]))
2071
2735
  " 2>/dev/null || echo '[]')
2736
+ local boot_ok secret_ok boot_reason_json
2737
+ local nomock_ok persist_ok auth_ok authz_ok nomock_reason_json persist_reason_json auth_reason_json authz_reason_json
2072
2738
  if [ "$diff_fails" = "true" ]; then diff_ok="false"; else diff_ok="true"; fi
2073
2739
  if [ "$test_fails" = "true" ]; then tests_ok="false"; else tests_ok="true"; fi
2740
+ if [ "$boot_fails" = "true" ]; then boot_ok="false"; else boot_ok="true"; fi
2741
+ if [ "$secret_fails" = "true" ]; then secret_ok="false"; else secret_ok="true"; fi
2742
+ if [ "$nomock_fails" = "true" ]; then nomock_ok="false"; else nomock_ok="true"; fi
2743
+ if [ "$persist_fails" = "true" ]; then persist_ok="false"; else persist_ok="true"; fi
2744
+ if [ "$auth_fails" = "true" ]; then auth_ok="false"; else auth_ok="true"; fi
2745
+ if [ "$authz_fails" = "true" ]; then authz_ok="false"; else authz_ok="true"; fi
2746
+ # Record WHY boot was inconclusive (no_app_runner / not_serveable / etc.) so a
2747
+ # consumer of the block report can tell a genuine boot pass from a pass-through.
2748
+ boot_reason_json=$(_R="${boot_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
2749
+ nomock_reason_json=$(_R="${nomock_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
2750
+ persist_reason_json=$(_R="${persist_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
2751
+ auth_reason_json=$(_R="${auth_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
2752
+ authz_reason_json=$(_R="${authz_inconclusive_reason:-}" python3 -c "import json,os; print(json.dumps(os.environ['_R']))" 2>/dev/null || echo '""')
2074
2753
  base_for_json="${base_sha:-}"
2075
2754
  cat > "$ev_tmp" << EVIDENCE_EOF
2076
2755
  {
@@ -2081,7 +2760,13 @@ print(json.dumps(items[:5]))
2081
2760
  "reason": "$reason",
2082
2761
  "checks": {
2083
2762
  "diff": {"ok": $diff_ok, "base_sha": "$base_for_json", "files_changed": $diff_files, "sources": "committed|unstaged|staged|untracked union"},
2084
- "tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $test_pass}
2763
+ "tests": {"ok": $tests_ok, "runner": "$test_runner", "pass": $test_pass},
2764
+ "boot": {"ok": $boot_ok, "inconclusive": $boot_inconclusive, "reason": $boot_reason_json},
2765
+ "secret": {"ok": $secret_ok},
2766
+ "nomock": {"ok": $nomock_ok, "inconclusive": $nomock_inconclusive, "reason": $nomock_reason_json},
2767
+ "persistence": {"ok": $persist_ok, "inconclusive": $persist_inconclusive, "reason": $persist_reason_json},
2768
+ "auth": {"ok": $auth_ok, "inconclusive": $auth_inconclusive, "reason": $auth_reason_json},
2769
+ "authorization": {"ok": $authz_ok, "inconclusive": $authz_inconclusive, "reason": $authz_reason_json}
2085
2770
  },
2086
2771
  "failures": $failures_json
2087
2772
  }
@@ -2093,10 +2778,19 @@ EVIDENCE_EOF
2093
2778
  # be the cross-run corpus for the block rate. Append an event here, where a
2094
2779
  # block is definitely happening. Additive, best-effort, stdout-silent.
2095
2780
  if type record_trust_event_bash &>/dev/null; then
2781
+ # proof_axis attributes a Proof-of-Function block to its axis (nomock|
2782
+ # persistence|auth) so the TS trust metrics can attribute it without a
2783
+ # schema-breaking change; empty for the diff/tests/boot/secret axes.
2784
+ local _proof_axis=""
2785
+ if [ "$nomock_fails" = "true" ]; then _proof_axis="nomock"
2786
+ elif [ "$persist_fails" = "true" ]; then _proof_axis="persistence"
2787
+ elif [ "$auth_fails" = "true" ]; then _proof_axis="auth"
2788
+ elif [ "$authz_fails" = "true" ]; then _proof_axis="authorization"; fi
2096
2789
  record_trust_event_bash "evidence_block" \
2097
2790
  "reason=$reason" \
2098
2791
  "diff_ok=$diff_ok" \
2099
2792
  "tests_ok=$tests_ok" \
2793
+ "proof_axis=$_proof_axis" \
2100
2794
  >/dev/null 2>&1 || true
2101
2795
  fi
2102
2796
 
@@ -2223,7 +2917,19 @@ council_member_review() {
2223
2917
 
2224
2918
  # Validate provider CLI is available
2225
2919
  case "${PROVIDER_NAME:-claude}" in
2226
- claude) command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; } ;;
2920
+ claude)
2921
+ # v8: the raw-SDK vote path (LOKI_SDK_COUNCIL_VOTE=1) needs no claude
2922
+ # binary, so the precondition passes when that path is viable (bridge
2923
+ # + bun). The vote body still falls closed to claude on an SDK miss.
2924
+ if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ] \
2925
+ && { [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/bin/loki" ] \
2926
+ || [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../bin/loki" ]; } \
2927
+ && command -v bun >/dev/null 2>&1; then
2928
+ :
2929
+ else
2930
+ command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; }
2931
+ fi
2932
+ ;;
2227
2933
  codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
2228
2934
  gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
2229
2935
  cline) command -v cline >/dev/null 2>&1 || { log_error "Cline CLI not found"; return 1; } ;;
@@ -2306,7 +3012,59 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
2306
3012
  # Use the configured provider for review
2307
3013
  case "${PROVIDER_NAME:-claude}" in
2308
3014
  claude)
2309
- if command -v claude &>/dev/null; then
3015
+ # v8 RAW-SDK VOTE PATH (opt-in LOKI_SDK_COUNCIL_VOTE=1). Run the member
3016
+ # completion vote via the pure-HTTPS @anthropic-ai/sdk text bridge (no
3017
+ # claude binary) through `internal sdk-text`. The reviewer emits the
3018
+ # SAME free-form VOTE/REASON/ISSUES text the claude path does, so the
3019
+ # elaborate downstream parser (grep VOTE:/REASON:/ISSUES:) is UNCHANGED
3020
+ # -- text bridge, not a schema change, to keep the trust-core parse
3021
+ # byte-identical. Runs BEFORE the claude-binary guard so the no-binary
3022
+ # deploy win holds. Fail-closed: on ANY miss (flag off, no key, bun/
3023
+ # entrypoint absent, non-zero, empty) fall through to the claude path;
3024
+ # a truly empty verdict then hits the conservative REJECT default. The
3025
+ # captured text is written to $verdict (same var the claude arm sets)
3026
+ # and _provider_rc is set so a timeout still routes conservatively.
3027
+ if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ]; then
3028
+ local _cv_root _cv_loki _cv_pf _cv_out _cv_rc
3029
+ _cv_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3030
+ _cv_loki="${_cv_root}/bin/loki"
3031
+ [ -x "$_cv_loki" ] || _cv_loki="${_cv_root}/../bin/loki"
3032
+ if [ -x "$_cv_loki" ] && command -v bun >/dev/null 2>&1; then
3033
+ _cv_pf="$(mktemp 2>/dev/null)" || _cv_pf=""
3034
+ if [ -n "$_cv_pf" ]; then
3035
+ printf '%s' "$prompt" > "$_cv_pf"
3036
+ _cv_rc=0
3037
+ local _cv_to_s="${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}"
3038
+ local _cv_wrap
3039
+ if command -v timeout >/dev/null 2>&1; then _cv_wrap="timeout $(( _cv_to_s + 15 ))"
3040
+ elif command -v gtimeout >/dev/null 2>&1; then _cv_wrap="gtimeout $(( _cv_to_s + 15 ))"
3041
+ else _cv_wrap=""; fi
3042
+ _cv_out="$($_cv_wrap "$_cv_loki" internal sdk-text \
3043
+ --prompt-file "$_cv_pf" \
3044
+ --model "${LOKI_SDK_COUNCIL_MODEL:-claude-haiku-4-5}" --effort medium \
3045
+ --timeout-ms "$(( _cv_to_s * 1000 ))" 2>/dev/null)" || _cv_rc=$?
3046
+ rm -f "$_cv_pf" 2>/dev/null || true
3047
+ if [ "$_cv_rc" -eq 0 ] && [ -n "$_cv_out" ]; then
3048
+ verdict="$_cv_out"
3049
+ _provider_rc=0
3050
+ elif [ "$_cv_rc" -eq 124 ] || [ "$_cv_rc" -eq 137 ] || [ "$_cv_rc" -eq 143 ]; then
3051
+ # TRUST-CORE SAFE DEFAULT (council review, bash-F4 parity):
3052
+ # an SDK-subprocess TIMEOUT must propagate to _provider_rc
3053
+ # so the post-case guard forces a conservative REJECT.
3054
+ # Without this, a no-claude-binary deploy would fall to
3055
+ # council_heuristic_review (which can APPROVE on benign
3056
+ # evidence) on a hung/rate-limited endpoint -- correlated
3057
+ # member timeouts could then fake-APPROVE the council.
3058
+ # Only set on a genuine timeout kill; a normal SDK miss
3059
+ # (rc 1: no key / transport / refusal) still falls through
3060
+ # to the claude arm exactly as before.
3061
+ _provider_rc=$_cv_rc
3062
+ fi
3063
+ fi
3064
+ fi
3065
+ # if verdict is still unset, fall through to the claude arm below
3066
+ fi
3067
+ if [ -z "${verdict:-}" ] && command -v claude &>/dev/null; then
2310
3068
  local council_model="${PROVIDER_MODEL_FAST:-haiku}"
2311
3069
  # EMBED 2 + 3 (v7.33.0). Council member completion vote. The
2312
3070
  # $prompt is fully self-contained (evidence + instructions +
@@ -2415,7 +3173,19 @@ council_devils_advocate() {
2415
3173
 
2416
3174
  # Validate provider CLI is available
2417
3175
  case "${PROVIDER_NAME:-claude}" in
2418
- claude) command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; } ;;
3176
+ claude)
3177
+ # v8: the raw-SDK vote path (LOKI_SDK_COUNCIL_VOTE=1) needs no claude
3178
+ # binary, so the precondition passes when that path is viable (bridge
3179
+ # + bun). The vote body still falls closed to claude on an SDK miss.
3180
+ if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ] \
3181
+ && { [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/bin/loki" ] \
3182
+ || [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../bin/loki" ]; } \
3183
+ && command -v bun >/dev/null 2>&1; then
3184
+ :
3185
+ else
3186
+ command -v claude >/dev/null 2>&1 || { log_error "Claude CLI not found"; return 1; }
3187
+ fi
3188
+ ;;
2419
3189
  codex) command -v codex >/dev/null 2>&1 || { log_error "Codex CLI not found"; return 1; } ;;
2420
3190
  gemini) command -v gemini >/dev/null 2>&1 || { log_error "Gemini CLI not found"; return 1; } ;;
2421
3191
  cline) command -v cline >/dev/null 2>&1 || { log_error "Cline CLI not found"; return 1; } ;;
@@ -2452,7 +3222,42 @@ REASON: your reasoning"
2452
3222
  local verdict=""
2453
3223
  case "${PROVIDER_NAME:-claude}" in
2454
3224
  claude)
2455
- if command -v claude &>/dev/null; then
3225
+ # v8 RAW-SDK CONTRARIAN VOTE (opt-in LOKI_SDK_COUNCIL_VOTE=1). Same
3226
+ # text-bridge pattern as the member vote: emit the SAME free-form
3227
+ # VOTE/REASON text via the pure-HTTPS SDK (no claude binary), leaving
3228
+ # the downstream VOTE: parser untouched. Runs BEFORE the claude guard.
3229
+ # Fail-closed: on ANY miss fall through to the claude arm; a truly
3230
+ # empty verdict then hits the conservative REJECT default. (The
3231
+ # contrarian path tracks no _provider_rc -- an empty verdict already
3232
+ # routes conservatively -- so we only set $verdict on success.)
3233
+ if [ "${LOKI_SDK_COUNCIL_VOTE:-0}" = "1" ]; then
3234
+ local _dv_root _dv_loki _dv_pf _dv_out _dv_rc
3235
+ _dv_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
3236
+ _dv_loki="${_dv_root}/bin/loki"
3237
+ [ -x "$_dv_loki" ] || _dv_loki="${_dv_root}/../bin/loki"
3238
+ if [ -x "$_dv_loki" ] && command -v bun >/dev/null 2>&1; then
3239
+ _dv_pf="$(mktemp 2>/dev/null)" || _dv_pf=""
3240
+ if [ -n "$_dv_pf" ]; then
3241
+ printf '%s' "$prompt" > "$_dv_pf"
3242
+ _dv_rc=0
3243
+ local _dv_to_s="${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}"
3244
+ local _dv_wrap
3245
+ if command -v timeout >/dev/null 2>&1; then _dv_wrap="timeout $(( _dv_to_s + 15 ))"
3246
+ elif command -v gtimeout >/dev/null 2>&1; then _dv_wrap="gtimeout $(( _dv_to_s + 15 ))"
3247
+ else _dv_wrap=""; fi
3248
+ _dv_out="$($_dv_wrap "$_dv_loki" internal sdk-text \
3249
+ --prompt-file "$_dv_pf" \
3250
+ --model "${LOKI_SDK_COUNCIL_MODEL:-claude-haiku-4-5}" --effort medium \
3251
+ --timeout-ms "$(( _dv_to_s * 1000 ))" 2>/dev/null)" || _dv_rc=$?
3252
+ rm -f "$_dv_pf" 2>/dev/null || true
3253
+ if [ "$_dv_rc" -eq 0 ] && [ -n "$_dv_out" ]; then
3254
+ verdict="$_dv_out"
3255
+ fi
3256
+ fi
3257
+ fi
3258
+ # if verdict is still unset, fall through to the claude arm below
3259
+ fi
3260
+ if [ -z "${verdict:-}" ] && command -v claude &>/dev/null; then
2456
3261
  local council_model="${PROVIDER_MODEL_FAST:-haiku}"
2457
3262
  # EMBED 2 + 3 (v7.33.0). Contrarian (devil's-advocate) vote --
2458
3263
  # an adversarial reviewer. Self-contained $prompt via stdin,
@@ -2536,11 +3341,35 @@ council_heuristic_review() {
2536
3341
  fi
2537
3342
  ;;
2538
3343
  test_auditor)
2539
- # Check for test files
2540
- if ! echo "$evidence" | grep -qiE "(test|spec)"; then
3344
+ # RUN-25 iter 14 (Wave B #9): require AFFIRMATIVE evidence that tests
3345
+ # actually ran AND passed before approving -- the old logic approved on
3346
+ # the ABSENCE of a negative signal (evidence merely MENTIONS "test" and
3347
+ # lacks "fail"), which is a fake-green: a build where the suite never
3348
+ # ran has no failure text either. Mirror council_evaluate_member: a real
3349
+ # .loki/quality/test-results.json with a runner (not "none") AND pass
3350
+ # true is the only thing that clears this auditor. No such affirmative
3351
+ # evidence -> REJECT (keep iterating), never a heuristic APPROVE.
3352
+ local _tr_file=".loki/quality/test-results.json"
3353
+ local _tests_ok=0
3354
+ if [ -f "$_tr_file" ] && command -v python3 >/dev/null 2>&1; then
3355
+ _tests_ok=$(_TR="$_tr_file" python3 -c "
3356
+ import json, os
3357
+ try:
3358
+ with open(os.environ['_TR']) as f:
3359
+ d = json.load(f)
3360
+ except Exception:
3361
+ print(0); raise SystemExit
3362
+ runner = str(d.get('runner', 'none'))
3363
+ p = d.get('pass', None)
3364
+ # affirmative: a real runner ran AND reported pass true (not inconclusive/false).
3365
+ print(1 if runner not in ('none', '', 'None') and p is True else 0)
3366
+ " 2>/dev/null || echo 0)
3367
+ fi
3368
+ if [ "${_tests_ok:-0}" != "1" ]; then
3369
+ # No affirmative pass evidence -> this auditor cannot approve.
2541
3370
  ((issues++))
2542
3371
  fi
2543
- # Check for passing indicators
3372
+ # A negative signal in the evidence is still an independent issue.
2544
3373
  if echo "$evidence" | grep -qiE "(fail|error|FAIL)"; then
2545
3374
  ((issues++))
2546
3375
  fi
@@ -3530,7 +4359,12 @@ _council_should_check_now() {
3530
4359
  local circuit_triggered="${1:-false}"
3531
4360
  local completion_claimed="${2:-false}"
3532
4361
  local iter="${ITERATION_COUNT:-0}"
3533
- local interval="${COUNCIL_CHECK_INTERVAL:-5}"
4362
+ # LOKI_AUTO_TUNE (default 0): when on, converge faster on simple builds by
4363
+ # lowering the check interval (simple -> 3). No-op when off. Resolved here (not
4364
+ # at source time) because DETECTED_COMPLEXITY is populated after this file is
4365
+ # sourced -- same reason _council_effective_min_iter is resolved at run time.
4366
+ local interval
4367
+ interval="$(_loki_auto_tune_interval "${DETECTED_COMPLEXITY:-}")"
3534
4368
  # SaaS #122: tier-aware effective floor (simple->1) so the no-claim early
3535
4369
  # check can also fire at iteration 1 on a genuinely-done simple app.
3536
4370
  local min_iter