loki-mode 7.46.0 → 7.48.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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/completion-council.sh +113 -0
  5. package/autonomy/crash.sh +47 -21
  6. package/autonomy/loki +50 -27
  7. package/autonomy/run.sh +468 -5
  8. package/autonomy/spec-interrogation.sh +550 -0
  9. package/autonomy/telemetry.sh +28 -8
  10. package/bin/postinstall.js +22 -10
  11. package/dashboard/__init__.py +1 -1
  12. package/dashboard/auth.py +117 -2
  13. package/dashboard/telemetry.py +34 -6
  14. package/docs/ACKNOWLEDGEMENTS.md +1 -1
  15. package/docs/COMPETITIVE-ANALYSIS.md +1 -1
  16. package/docs/INSTALLATION.md +10 -3
  17. package/docs/OPEN-CORE-BOUNDARY.md +6 -5
  18. package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +192 -0
  19. package/docs/PRIVACY.md +82 -24
  20. package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +2 -2
  21. package/docs/auto-claude-comparison.md +2 -2
  22. package/docs/certification/README.md +1 -1
  23. package/docs/competitive/bolt-new-analysis.md +1 -1
  24. package/docs/competitive/emergence-others-analysis.md +6 -6
  25. package/docs/competitive/replit-lovable-analysis.md +4 -4
  26. package/docs/enterprise/security.md +43 -3
  27. package/docs/show-hn-post.md +1 -1
  28. package/loki-ts/dist/loki.js +30 -30
  29. package/mcp/__init__.py +1 -1
  30. package/package.json +1 -1
  31. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  32. package/web-app/dist/assets/{AdminPage-CKUOsWZW.js → AdminPage-CcCJ0Sjt.js} +1 -1
  33. package/web-app/dist/assets/{Avatar-CL9Id9Hi.js → Avatar-DK8kmayw.js} +1 -1
  34. package/web-app/dist/assets/{Badge-B12zwlD7.js → Badge-4uAWnemi.js} +1 -1
  35. package/web-app/dist/assets/{Button-CFLVoduT.js → Button-BBMk33tk.js} +1 -1
  36. package/web-app/dist/assets/ComparePage-bt9rwvST.js +1 -0
  37. package/web-app/dist/assets/{GitHubIssuesPanel-CSitxtAX.js → GitHubIssuesPanel-WDbH47UM.js} +1 -1
  38. package/web-app/dist/assets/{GitHubPRsPanel-BIT06FRo.js → GitHubPRsPanel-C2CiYtTx.js} +1 -1
  39. package/web-app/dist/assets/{HomePage-pU_0fGny.js → HomePage-BQk-MUjn.js} +4 -4
  40. package/web-app/dist/assets/{LoginPage-DTZtt2Yb.js → LoginPage-DMOZVGGL.js} +1 -1
  41. package/web-app/dist/assets/{MagicPage-10zfra8o.js → MagicPage-Bzp2Nt1z.js} +1 -1
  42. package/web-app/dist/assets/{MetricsPage-C-wiKUkv.js → MetricsPage-C39JVdsw.js} +1 -1
  43. package/web-app/dist/assets/{NotFoundPage-BDkcmhYe.js → NotFoundPage-6vT_U9UL.js} +1 -1
  44. package/web-app/dist/assets/{ProjectPage-CiCavQ8n.js → ProjectPage-BfFcZp-E.js} +3 -3
  45. package/web-app/dist/assets/{ProjectsPage-BLCXQwwC.js → ProjectsPage-CPMBf8Wt.js} +1 -1
  46. package/web-app/dist/assets/{SettingsPage-PkxtaMyg.js → SettingsPage-BnNN6ETl.js} +1 -1
  47. package/web-app/dist/assets/{ShowcasePage-iECp8Tha.js → ShowcasePage-WDrMf-cx.js} +1 -1
  48. package/web-app/dist/assets/{SystemSettingsPage-DS6Anno1.js → SystemSettingsPage-DX4jb2e8.js} +1 -1
  49. package/web-app/dist/assets/{TeamsPage-ls6h6bNL.js → TeamsPage-BCfqcXzu.js} +1 -1
  50. package/web-app/dist/assets/{TemplatesPage-Bk0QzlPt.js → TemplatesPage-CZvmimDj.js} +1 -1
  51. package/web-app/dist/assets/{TerminalOutput-4-1hWCtZ.js → TerminalOutput-BlRqFwWV.js} +1 -1
  52. package/web-app/dist/assets/{activity-DH3ih2nS.js → activity-CacZsUyr.js} +1 -1
  53. package/web-app/dist/assets/{bell-Gn17S6uv.js → bell-DK2qtHnk.js} +1 -1
  54. package/web-app/dist/assets/{bot-Cbycc3VE.js → bot-CkcUtHad.js} +1 -1
  55. package/web-app/dist/assets/{check-nIAqa-kf.js → check-CbCPjX3M.js} +1 -1
  56. package/web-app/dist/assets/{chevron-left-D2jcWDll.js → chevron-left-5NUKWw3i.js} +1 -1
  57. package/web-app/dist/assets/{circle-alert-CpL4Bhvt.js → circle-alert-S7uFoxC2.js} +1 -1
  58. package/web-app/dist/assets/{clock-IW4Wq86N.js → clock-CaQRrIrs.js} +1 -1
  59. package/web-app/dist/assets/{cloud-Cn8nNuH2.js → cloud-DBAX6c0r.js} +1 -1
  60. package/web-app/dist/assets/{code-xml-BiJBteXf.js → code-xml-De5-EXv3.js} +1 -1
  61. package/web-app/dist/assets/{copy-CnqkyNsi.js → copy-CUkT6k1v.js} +1 -1
  62. package/web-app/dist/assets/{database-CKSReqa5.js → database-BAWf1Gwt.js} +1 -1
  63. package/web-app/dist/assets/{dollar-sign-CDzDY64R.js → dollar-sign-Ji8zk86R.js} +1 -1
  64. package/web-app/dist/assets/{file-code-corner-Box4IwG1.js → file-code-corner-ChtXoBwS.js} +1 -1
  65. package/web-app/dist/assets/{file-plus-DpGqlXF8.js → file-plus-bFa37P76.js} +1 -1
  66. package/web-app/dist/assets/{folder-open-B57dAoBv.js → folder-open-DhXpXscO.js} +1 -1
  67. package/web-app/dist/assets/{git-commit-horizontal-BVbucmO5.js → git-commit-horizontal-DVPeDQ3j.js} +1 -1
  68. package/web-app/dist/assets/{globe-BkOnKl4x.js → globe-BPZgPeeu.js} +1 -1
  69. package/web-app/dist/assets/{hammer-DRbIQ4QU.js → hammer-jLCaujYH.js} +1 -1
  70. package/web-app/dist/assets/{index-CM_b_EhP.js → index-B-0iHBPO.js} +2 -2
  71. package/web-app/dist/assets/{layers-B78BiFiU.js → layers-B1vsrsFW.js} +1 -1
  72. package/web-app/dist/assets/{lightbulb-B-Itbm9g.js → lightbulb-C-uLoq9Y.js} +1 -1
  73. package/web-app/dist/assets/{loader-circle-Oq6NQhW2.js → loader-circle-JTfD-ZuM.js} +1 -1
  74. package/web-app/dist/assets/{lock-DbJ9zxbw.js → lock-G9rxD4gZ.js} +1 -1
  75. package/web-app/dist/assets/{mail-CzMRod6m.js → mail-BJ0PTN_V.js} +1 -1
  76. package/web-app/dist/assets/{package-WZ5osvej.js → package-CXClfLOO.js} +1 -1
  77. package/web-app/dist/assets/{plus-j08lFR-K.js → plus-EoL5OCB7.js} +1 -1
  78. package/web-app/dist/assets/{refresh-cw-CIr7E-g2.js → refresh-cw-BjREUnVq.js} +1 -1
  79. package/web-app/dist/assets/{rotate-ccw-gwoXxDeE.js → rotate-ccw-DahWX07H.js} +1 -1
  80. package/web-app/dist/assets/{save-B8fV_ZpE.js → save-Dek3gCn1.js} +1 -1
  81. package/web-app/dist/assets/{server-D5dO1paz.js → server-D6V1BAia.js} +1 -1
  82. package/web-app/dist/assets/{shield-alert-Du08zhdg.js → shield-alert-BtTK5Sxb.js} +1 -1
  83. package/web-app/dist/assets/{trash-2-DEKSVae5.js → trash-2-BT5o_g0r.js} +1 -1
  84. package/web-app/dist/assets/{trending-down-DBiXUtxJ.js → trending-down-D4Jk7KF3.js} +1 -1
  85. package/web-app/dist/assets/{trending-up-BgmK_tHq.js → trending-up-EQFTzhEo.js} +1 -1
  86. package/web-app/dist/assets/{upload-IaViyeVD.js → upload-JfI5lCSE.js} +1 -1
  87. package/web-app/dist/assets/{usePolling-PiRLqNu6.js → usePolling-BnhPUuGd.js} +1 -1
  88. package/web-app/dist/assets/{user-BB5J8wAF.js → user-DSUiUYtj.js} +1 -1
  89. package/web-app/dist/index.html +1 -1
  90. package/web-app/dist/assets/ComparePage-Dg0UdZAk.js +0 -1
package/autonomy/run.sh CHANGED
@@ -2634,6 +2634,27 @@ except Exception:
2634
2634
  fi
2635
2635
  fi
2636
2636
 
2637
+ # P2-2: assumption-ledger summary for proof-of-done. "done" means "done, plus
2638
+ # here are the N places your spec was ambiguous and what Loki assumed."
2639
+ # spec_ledger_counts echoes "<total> <high>"; defined when spec-interrogation.sh
2640
+ # is sourced (run.sh sources it in DISCOVERY). Guarded for safety.
2641
+ local assumptions_total=0 assumptions_high=0 assumption_lines=""
2642
+ if type spec_ledger_counts &>/dev/null; then
2643
+ local _ac
2644
+ _ac="$(spec_ledger_counts 2>/dev/null || echo '0 0')"
2645
+ assumptions_total="${_ac%% *}"
2646
+ assumptions_high="${_ac##* }"
2647
+ case "$assumptions_total" in ''|*[!0-9]*) assumptions_total=0 ;; esac
2648
+ case "$assumptions_high" in ''|*[!0-9]*) assumptions_high=0 ;; esac
2649
+ if [ "$assumptions_total" != "0" ]; then
2650
+ local _ledger_md="$loki_dir/assumptions/ledger.md"
2651
+ if [ -f "$_ledger_md" ]; then
2652
+ # Pull just the "## <id> ..." headings as a terse one-per-line list.
2653
+ assumption_lines="$(grep '^## ' "$_ledger_md" 2>/dev/null | sed 's/^## / - /' || true)"
2654
+ fi
2655
+ fi
2656
+ fi
2657
+
2637
2658
  # ---- Durable human-readable file: .loki/COMPLETION.txt --------------------
2638
2659
  {
2639
2660
  echo "Loki Mode run summary"
@@ -2668,6 +2689,14 @@ except Exception:
2668
2689
  echo "$evidence_inconclusive_line"
2669
2690
  echo ""
2670
2691
  fi
2692
+ if [ "$assumptions_total" != "0" ]; then
2693
+ echo "Spec assumptions recorded: $assumptions_total ($assumptions_high high-severity)"
2694
+ echo " These are places your spec was ambiguous and what Loki assumed. See .loki/assumptions/ledger.md"
2695
+ if [ -n "$assumption_lines" ]; then
2696
+ echo "$assumption_lines"
2697
+ fi
2698
+ echo ""
2699
+ fi
2671
2700
  echo "Review the work:"
2672
2701
  echo " $review_cmd"
2673
2702
  echo ""
@@ -2691,6 +2720,8 @@ except Exception:
2691
2720
  _LOKI_CS_DELEGATE_BRANCH="$delegate_branch" \
2692
2721
  _LOKI_CS_PR_URL="$pr_url" \
2693
2722
  _LOKI_CS_TS="$ts" \
2723
+ _LOKI_CS_ASSUMPTIONS_TOTAL="$assumptions_total" \
2724
+ _LOKI_CS_ASSUMPTIONS_HIGH="$assumptions_high" \
2694
2725
  _LOKI_CS_OUT_FILE="$loki_dir/state/completion.json" \
2695
2726
  python3 -c "
2696
2727
  import json, os, tempfile
@@ -2710,6 +2741,8 @@ rec = {
2710
2741
  'delegate_branch': os.environ.get('_LOKI_CS_DELEGATE_BRANCH', ''),
2711
2742
  'pr_url': os.environ.get('_LOKI_CS_PR_URL', ''),
2712
2743
  'timestamp': os.environ.get('_LOKI_CS_TS', ''),
2744
+ 'assumptions_total': i(os.environ.get('_LOKI_CS_ASSUMPTIONS_TOTAL')),
2745
+ 'assumptions_high': i(os.environ.get('_LOKI_CS_ASSUMPTIONS_HIGH')),
2713
2746
  }
2714
2747
  d = os.path.dirname(out)
2715
2748
  fd, tmp = tempfile.mkstemp(dir=d, suffix='.json')
@@ -2718,6 +2751,113 @@ with os.fdopen(fd, 'w') as f:
2718
2751
  os.replace(tmp, out)
2719
2752
  " 2>/dev/null || true
2720
2753
 
2754
+ # ---- P3-5: run manifest / bill-of-materials: .loki/loki-run.json ----------
2755
+ # Best-effort, auditable + reproducible record of this run. Emitted from
2756
+ # build_completion_summary because that fires on EVERY terminal path
2757
+ # (complete / max_iterations / stopped / intervention / failed), including the
2758
+ # intervention/stopped calls that bypass emit_completion_summary. The whole
2759
+ # block is wrapped so a failure can NEVER abort the run.
2760
+ {
2761
+ # Portable sha256 (macOS shasum, Linux sha256sum). Echoes empty on miss.
2762
+ _loki_sha256() {
2763
+ local _f="$1"
2764
+ [ -f "$_f" ] || { printf ''; return 0; }
2765
+ if command -v shasum >/dev/null 2>&1; then
2766
+ shasum -a 256 "$_f" 2>/dev/null | awk '{print $1}'
2767
+ elif command -v sha256sum >/dev/null 2>&1; then
2768
+ sha256sum "$_f" 2>/dev/null | awk '{print $1}'
2769
+ else
2770
+ printf ''
2771
+ fi
2772
+ }
2773
+
2774
+ local _loki_ver
2775
+ _loki_ver="$(cat "$PROJECT_DIR/VERSION" 2>/dev/null || echo "unknown")"
2776
+ local _spec_path="${PRD_PATH:-}"
2777
+ [ -z "$_spec_path" ] && _spec_path="none"
2778
+ local _spec_hash=""
2779
+ [ "$_spec_path" != "none" ] && _spec_hash="$(_loki_sha256 "$_spec_path")"
2780
+
2781
+ # Evidence files we reference + hash (existing artifacts, not new ones).
2782
+ local _ev_tests="$loki_dir/quality/test-results.json"
2783
+ local _ev_cov="$loki_dir/quality/coverage.json"
2784
+ local _ev_completion="$loki_dir/state/completion.json"
2785
+
2786
+ _LOKI_RM_OUT="$loki_dir/loki-run.json" \
2787
+ _LOKI_RM_VERSION="$_loki_ver" \
2788
+ _LOKI_RM_SPEC_PATH="$_spec_path" \
2789
+ _LOKI_RM_SPEC_HASH="$_spec_hash" \
2790
+ _LOKI_RM_PROVIDER="${PROVIDER_NAME:-${LOKI_PROVIDER:-claude}}" \
2791
+ _LOKI_RM_TIER="${CURRENT_TIER:-unknown}" \
2792
+ _LOKI_RM_OUTCOME="$outcome" \
2793
+ _LOKI_RM_BRANCH="$branch" \
2794
+ _LOKI_RM_START_SHA="$start_sha" \
2795
+ _LOKI_RM_HEAD_SHA="$head_sha" \
2796
+ _LOKI_RM_ITERS="${ITERATION_COUNT:-0}" \
2797
+ _LOKI_RM_TS="$ts" \
2798
+ _LOKI_RM_EV_TESTS="$_ev_tests" \
2799
+ _LOKI_RM_EV_TESTS_HASH="$(_loki_sha256 "$_ev_tests")" \
2800
+ _LOKI_RM_EV_COV="$_ev_cov" \
2801
+ _LOKI_RM_EV_COV_HASH="$(_loki_sha256 "$_ev_cov")" \
2802
+ _LOKI_RM_EV_COMPLETION="$_ev_completion" \
2803
+ _LOKI_RM_NODE="$(node --version 2>/dev/null || echo '')" \
2804
+ _LOKI_RM_PYTHON="$(python3 --version 2>&1 | awk '{print $2}' || echo '')" \
2805
+ _LOKI_RM_GIT="$(git --version 2>/dev/null | awk '{print $3}' || echo '')" \
2806
+ _LOKI_RM_BUN="$(bun --version 2>/dev/null || echo '')" \
2807
+ python3 -c "
2808
+ import json, os, tempfile
2809
+ out=os.environ['_LOKI_RM_OUT']
2810
+ def s(k): return os.environ.get(k,'')
2811
+ def i(k):
2812
+ try: return int(os.environ.get(k,'0'))
2813
+ except (TypeError, ValueError): return 0
2814
+ def ev(path_k, hash_k=None):
2815
+ p=s(path_k)
2816
+ rec={'path': p, 'exists': bool(p) and os.path.isfile(p)}
2817
+ if hash_k:
2818
+ h=s(hash_k)
2819
+ if h: rec['sha256']=h
2820
+ return rec
2821
+ manifest={
2822
+ 'schema': 'loki-run-manifest/v1',
2823
+ 'loki_version': s('_LOKI_RM_VERSION'),
2824
+ 'timestamp': s('_LOKI_RM_TS'),
2825
+ 'outcome': s('_LOKI_RM_OUTCOME'),
2826
+ 'iterations': i('_LOKI_RM_ITERS'),
2827
+ 'provider': s('_LOKI_RM_PROVIDER'),
2828
+ # Tier cycles per RARV iteration (R/A/R/V); this is the LAST tier set, not
2829
+ # the only tier used. Recorded honestly as last_tier, not a single 'model'.
2830
+ 'last_tier': s('_LOKI_RM_TIER'),
2831
+ 'spec': {
2832
+ 'path': s('_LOKI_RM_SPEC_PATH'),
2833
+ 'sha256': s('_LOKI_RM_SPEC_HASH') or None,
2834
+ },
2835
+ 'git': {
2836
+ 'branch': s('_LOKI_RM_BRANCH'),
2837
+ 'start_sha': s('_LOKI_RM_START_SHA') or None,
2838
+ 'head_sha': s('_LOKI_RM_HEAD_SHA') or None,
2839
+ },
2840
+ 'tool_versions': {
2841
+ 'node': s('_LOKI_RM_NODE') or None,
2842
+ 'python': s('_LOKI_RM_PYTHON') or None,
2843
+ 'git': s('_LOKI_RM_GIT') or None,
2844
+ 'bun': s('_LOKI_RM_BUN') or None,
2845
+ },
2846
+ 'evidence': {
2847
+ 'test_results': ev('_LOKI_RM_EV_TESTS','_LOKI_RM_EV_TESTS_HASH'),
2848
+ 'coverage': ev('_LOKI_RM_EV_COV','_LOKI_RM_EV_COV_HASH'),
2849
+ 'completion': ev('_LOKI_RM_EV_COMPLETION'),
2850
+ },
2851
+ }
2852
+ d=os.path.dirname(out)
2853
+ fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
2854
+ with os.fdopen(fd,'w') as f:
2855
+ json.dump(manifest, f, indent=2)
2856
+ os.replace(tmp, out)
2857
+ " 2>/dev/null || true
2858
+ unset -f _loki_sha256 2>/dev/null || true
2859
+ } || true
2860
+
2721
2861
  # ---- Short strings for the desktop notification --------------------------
2722
2862
  # Desktop body stays terse; full detail lives in COMPLETION.txt.
2723
2863
  _LOKI_SUMMARY_TITLE="$notify_title"
@@ -7028,6 +7168,150 @@ _loki_run_pytest_with_timeout() {
7028
7168
  (cd "$target_dir" && "${_to_cmd[@]}" pytest "$@" 2>&1)
7029
7169
  }
7030
7170
 
7171
+ # ============================================================================
7172
+ # P0-1 Fix A: real test-coverage MEASUREMENT (v7.47.0)
7173
+ #
7174
+ # enforce_test_coverage() runs the project's suite for PASS/FAIL only -- it must
7175
+ # NOT add --coverage to that run, because a missing coverage provider
7176
+ # (@vitest/coverage-v8, the `coverage` pkg, pytest-cov, cargo-llvm-cov) makes the
7177
+ # instrumented command exit nonzero for a TOOLING reason, which would flip
7178
+ # test_passed=false and BLOCK a project whose tests actually pass. That would
7179
+ # destroy the honest pass/fail pass-through. So measurement is a SEPARATE,
7180
+ # best-effort second pass that can NEVER change test_passed.
7181
+ #
7182
+ # Contract:
7183
+ # - Sets COVERAGE_MEASURED (true|false), COVERAGE_PCT (number or empty),
7184
+ # COVERAGE_TOOL (string), COVERAGE_REASON (why not measured).
7185
+ # - Tool absent / unsupported language -> measured=false, no number, NEVER block.
7186
+ # - Tests run a SECOND time here when instrumented; LOKI_COVERAGE_GATE=0 skips
7187
+ # this whole measurement pass (saves the double-run).
7188
+ #
7189
+ # Usage: measure_test_coverage <target_dir> <test_runner>
7190
+ # ============================================================================
7191
+ measure_test_coverage() {
7192
+ local target_dir="$1"
7193
+ local runner="$2"
7194
+ COVERAGE_MEASURED=false
7195
+ COVERAGE_PCT=""
7196
+ COVERAGE_TOOL="none"
7197
+ COVERAGE_REASON=""
7198
+
7199
+ local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
7200
+ local cov_dir="$target_dir/.loki/quality"
7201
+ mkdir -p "$cov_dir" 2>/dev/null || true
7202
+ # Native tool reports land on a tool-specific path so they never collide
7203
+ # with our normalized coverage.json.
7204
+ local pyc_json="$cov_dir/coverage-pytest.json"
7205
+
7206
+ case "$runner" in
7207
+ vitest|monorepo-vitest)
7208
+ COVERAGE_TOOL="vitest"
7209
+ (cd "$target_dir" && timeout "$gate_timeout" npx vitest run --coverage \
7210
+ --coverage.reporter=json-summary \
7211
+ --coverage.reportsDirectory=.loki/quality/vitest-cov >/dev/null 2>&1) || true
7212
+ local f="$target_dir/.loki/quality/vitest-cov/coverage-summary.json"
7213
+ if [ -f "$f" ]; then
7214
+ COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
7215
+ import json, os, sys
7216
+ try:
7217
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7218
+ print(d['total']['lines']['pct'])
7219
+ except Exception:
7220
+ sys.exit(1)
7221
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="vitest coverage-summary.json unparsable"
7222
+ else
7223
+ COVERAGE_REASON="vitest coverage provider absent (install @vitest/coverage-v8)"
7224
+ fi
7225
+ ;;
7226
+ jest)
7227
+ COVERAGE_TOOL="jest"
7228
+ (cd "$target_dir" && timeout "$gate_timeout" npx jest --coverage \
7229
+ --coverageReporters=json-summary \
7230
+ --coverageDirectory=.loki/quality/jest-cov --passWithNoTests >/dev/null 2>&1) || true
7231
+ local f="$target_dir/.loki/quality/jest-cov/coverage-summary.json"
7232
+ if [ -f "$f" ]; then
7233
+ COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
7234
+ import json, os, sys
7235
+ try:
7236
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7237
+ print(d['total']['lines']['pct'])
7238
+ except Exception:
7239
+ sys.exit(1)
7240
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="jest coverage-summary.json unparsable"
7241
+ else
7242
+ COVERAGE_REASON="jest coverage report absent"
7243
+ fi
7244
+ ;;
7245
+ pytest)
7246
+ COVERAGE_TOOL="pytest-cov"
7247
+ # pytest-cov is optional; only measure when the plugin is importable.
7248
+ if python3 -c "import pytest_cov" >/dev/null 2>&1; then
7249
+ rm -f "$pyc_json" 2>/dev/null || true
7250
+ _loki_run_pytest_with_timeout "$target_dir" \
7251
+ --cov --cov-report="json:$pyc_json" -q >/dev/null 2>&1 || true
7252
+ if [ -f "$pyc_json" ]; then
7253
+ COVERAGE_PCT=$(_LOKI_COV_F="$pyc_json" python3 -c "
7254
+ import json, os, sys
7255
+ try:
7256
+ d=json.load(open(os.environ['_LOKI_COV_F']))
7257
+ print(d['totals']['percent_covered'])
7258
+ except Exception:
7259
+ sys.exit(1)
7260
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="pytest coverage.json unparsable"
7261
+ else
7262
+ COVERAGE_REASON="pytest produced no coverage.json"
7263
+ fi
7264
+ else
7265
+ COVERAGE_REASON="pytest-cov not installed"
7266
+ fi
7267
+ ;;
7268
+ go-test)
7269
+ COVERAGE_TOOL="go-cover"
7270
+ local prof="$cov_dir/go-coverage.out"
7271
+ rm -f "$prof" 2>/dev/null || true
7272
+ (cd "$target_dir" && timeout "$gate_timeout" go test -coverprofile="$prof" ./... >/dev/null 2>&1) || true
7273
+ if [ -f "$prof" ]; then
7274
+ local total_line
7275
+ total_line=$(cd "$target_dir" && go tool cover -func="$prof" 2>/dev/null | tail -1)
7276
+ # "total: (statements) 87.5%"
7277
+ COVERAGE_PCT=$(printf '%s\n' "$total_line" | grep -oE '[0-9]+(\.[0-9]+)?%' | tail -1 | tr -d '%')
7278
+ if [ -n "$COVERAGE_PCT" ]; then
7279
+ COVERAGE_MEASURED=true
7280
+ else
7281
+ COVERAGE_REASON="go tool cover produced no total"
7282
+ fi
7283
+ else
7284
+ COVERAGE_REASON="go test produced no coverage profile"
7285
+ fi
7286
+ ;;
7287
+ cargo-test)
7288
+ COVERAGE_TOOL="cargo-llvm-cov"
7289
+ if cargo llvm-cov --version >/dev/null 2>&1; then
7290
+ local out
7291
+ out=$(cd "$target_dir" && timeout "$gate_timeout" cargo llvm-cov --json 2>/dev/null) || true
7292
+ if [ -n "$out" ]; then
7293
+ COVERAGE_PCT=$(_LOKI_COV_JSON="$out" python3 -c "
7294
+ import json, os, sys
7295
+ try:
7296
+ d=json.loads(os.environ['_LOKI_COV_JSON'])
7297
+ print(d['data'][0]['totals']['lines']['percent'])
7298
+ except Exception:
7299
+ sys.exit(1)
7300
+ " 2>/dev/null) && COVERAGE_MEASURED=true || COVERAGE_REASON="cargo llvm-cov json unparsable"
7301
+ else
7302
+ COVERAGE_REASON="cargo llvm-cov produced no output"
7303
+ fi
7304
+ else
7305
+ COVERAGE_REASON="cargo-llvm-cov not installed"
7306
+ fi
7307
+ ;;
7308
+ *)
7309
+ COVERAGE_REASON="coverage not supported for runner '$runner'"
7310
+ ;;
7311
+ esac
7312
+ return 0
7313
+ }
7314
+
7031
7315
  enforce_test_coverage() {
7032
7316
  local loki_dir="${TARGET_DIR:-.}/.loki"
7033
7317
  local quality_dir="$loki_dir/quality"
@@ -7258,10 +7542,109 @@ TREOF
7258
7542
  # Finding #598: stamp the per-iteration freshness marker (see above).
7259
7543
  printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
7260
7544
 
7545
+ # ---- P0-1 Fix A: best-effort coverage MEASUREMENT (v7.47.0) --------------
7546
+ # Runs AFTER test_passed is decided. NEVER mutates test_passed (a coverage
7547
+ # tooling failure must not flip a green suite to red). Writes a normalized
7548
+ # .loki/quality/coverage.json with honest measured/pct/reason. Blocks the
7549
+ # gate (coverage_block=true) ONLY when measurable AND below threshold AND
7550
+ # LOKI_ENFORCE_COVERAGE=1. A coverage block is distinct from a tests-red
7551
+ # block: it does NOT set TESTS_FAILED and does NOT remove unit-tests.pass.
7552
+ #
7553
+ # Knob semantics (measurement is OPT-IN: it re-runs the suite instrumented,
7554
+ # so for an autonomous loop iterating many times it is off unless requested):
7555
+ # default (unset) -> skip measurement entirely (no double-run).
7556
+ # LOKI_COVERAGE_GATE=1 -> measure + record + warn, never block.
7557
+ # LOKI_ENFORCE_COVERAGE=1 -> implies measurement; measurable + below
7558
+ # LOKI_MIN_COVERAGE -> BLOCK.
7559
+ # tool absent / unsupported -> record measured:false, never block.
7560
+ local coverage_block=false
7561
+ if [ "${LOKI_COVERAGE_GATE:-0}" != "0" ] || [ "${LOKI_ENFORCE_COVERAGE:-0}" = "1" ]; then
7562
+ COVERAGE_MEASURED=false; COVERAGE_PCT=""; COVERAGE_TOOL="none"; COVERAGE_REASON=""
7563
+ measure_test_coverage "${TARGET_DIR:-.}" "$test_runner" || true
7564
+
7565
+ local cov_enforced="${LOKI_ENFORCE_COVERAGE:-0}"
7566
+ local cov_below=false
7567
+ if [ "$COVERAGE_MEASURED" = "true" ] && [ -n "$COVERAGE_PCT" ]; then
7568
+ # Float-safe compare via python3 (pct may be e.g. 87.5).
7569
+ if _LOKI_COV_PCT="$COVERAGE_PCT" _LOKI_COV_MIN="$min_coverage" python3 -c "
7570
+ import os, sys
7571
+ try:
7572
+ pct=float(os.environ['_LOKI_COV_PCT']); mn=float(os.environ['_LOKI_COV_MIN'])
7573
+ except Exception:
7574
+ sys.exit(2)
7575
+ sys.exit(0 if pct < mn else 1)
7576
+ " 2>/dev/null; then
7577
+ cov_below=true
7578
+ fi
7579
+ fi
7580
+ if [ "$COVERAGE_MEASURED" = "true" ] && [ "$cov_below" = "true" ] && [ "$cov_enforced" = "1" ]; then
7581
+ coverage_block=true
7582
+ fi
7583
+
7584
+ # Normalized coverage.json (single source of truth for coverage facts).
7585
+ _LOKI_COV_MEASURED="$COVERAGE_MEASURED" \
7586
+ _LOKI_COV_PCT="$COVERAGE_PCT" \
7587
+ _LOKI_COV_TOOL="$COVERAGE_TOOL" \
7588
+ _LOKI_COV_REASON="$COVERAGE_REASON" \
7589
+ _LOKI_COV_MIN="$min_coverage" \
7590
+ _LOKI_COV_ENFORCED="$cov_enforced" \
7591
+ _LOKI_COV_BLOCKED="$coverage_block" \
7592
+ _LOKI_COV_RUNNER="$test_runner" \
7593
+ _LOKI_COV_OUT="$quality_dir/coverage.json" \
7594
+ python3 -c "
7595
+ import json, os, tempfile
7596
+ out=os.environ['_LOKI_COV_OUT']
7597
+ measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
7598
+ pct_raw = os.environ.get('_LOKI_COV_PCT','')
7599
+ try:
7600
+ pct = float(pct_raw) if (measured and pct_raw != '') else None
7601
+ except ValueError:
7602
+ pct = None
7603
+ def b(v): return os.environ.get(v,'false') == 'true'
7604
+ def i(v):
7605
+ try: return int(float(os.environ.get(v,'0')))
7606
+ except (TypeError, ValueError): return 0
7607
+ rec = {
7608
+ 'measured': measured,
7609
+ 'pct': pct,
7610
+ 'tool': os.environ.get('_LOKI_COV_TOOL','none'),
7611
+ 'runner': os.environ.get('_LOKI_COV_RUNNER','none'),
7612
+ 'threshold': i('_LOKI_COV_MIN'),
7613
+ 'enforced': os.environ.get('_LOKI_COV_ENFORCED','0') == '1',
7614
+ 'blocked': b('_LOKI_COV_BLOCKED'),
7615
+ 'reason': os.environ.get('_LOKI_COV_REASON','') if not measured else '',
7616
+ 'timestamp': __import__('datetime').datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
7617
+ }
7618
+ d=os.path.dirname(out)
7619
+ fd, tmp=tempfile.mkstemp(dir=d, suffix='.json')
7620
+ with os.fdopen(fd,'w') as f:
7621
+ json.dump(rec, f, indent=2)
7622
+ os.replace(tmp, out)
7623
+ " 2>/dev/null || true
7624
+
7625
+ if [ "$COVERAGE_MEASURED" = "true" ]; then
7626
+ if [ "$coverage_block" = "true" ]; then
7627
+ log_warn "Coverage gate: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% < ${min_coverage}% (LOKI_ENFORCE_COVERAGE=1) -- BLOCK"
7628
+ elif [ "$cov_below" = "true" ]; then
7629
+ log_warn "Coverage: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% < ${min_coverage}% (warn only; set LOKI_ENFORCE_COVERAGE=1 to block)"
7630
+ else
7631
+ log_info "Coverage: ${COVERAGE_TOOL} measured ${COVERAGE_PCT}% (threshold ${min_coverage}%)"
7632
+ fi
7633
+ else
7634
+ log_info "Coverage: not measured (${COVERAGE_REASON:-unknown}); pass-through, not blocking"
7635
+ fi
7636
+ fi
7637
+
7261
7638
  if [ "$test_passed" = "true" ]; then
7262
7639
  touch "$quality_dir/unit-tests.pass"
7263
7640
  rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
7264
7641
  log_info "Test suite gate: $test_runner passed"
7642
+ # Coverage block is distinct from tests-red: tests passed, but enforced
7643
+ # coverage is below threshold. Return nonzero to gate WITHOUT writing the
7644
+ # TESTS_FAILED signal or removing unit-tests.pass.
7645
+ if [ "$coverage_block" = "true" ]; then
7646
+ return 1
7647
+ fi
7265
7648
  return 0
7266
7649
  else
7267
7650
  rm -f "$quality_dir/unit-tests.pass"
@@ -11578,9 +11961,39 @@ build_prompt() {
11578
11961
  test_summary=$(python3 -c "import json; d=json.load(open('${TARGET_DIR:-.}/.loki/quality/test-results.json')); print(d.get('summary',''))" 2>/dev/null || echo "")
11579
11962
  [ -n "$test_summary" ] && gate_failure_context="${gate_failure_context}Tests: ${test_summary}. "
11580
11963
  fi
11964
+ # P0-1 Fix A: when a coverage block fired (LOKI_ENFORCE_COVERAGE=1 +
11965
+ # measurable + below threshold), give the agent the ACCURATE reason. The
11966
+ # generic test_coverage token plus a passing test summary would otherwise
11967
+ # read as a contradictory "fix the tests" when the tests actually passed
11968
+ # and it is coverage that is low. Surface coverage.json so the next
11969
+ # iteration writes MORE TESTS rather than chasing a phantom red suite.
11970
+ if [ -f "${TARGET_DIR:-.}/.loki/quality/coverage.json" ]; then
11971
+ local cov_summary
11972
+ cov_summary=$(_LOKI_GFC="${TARGET_DIR:-.}/.loki/quality/coverage.json" python3 -c "
11973
+ import json, os
11974
+ try:
11975
+ d=json.load(open(os.environ['_LOKI_GFC']))
11976
+ except Exception:
11977
+ raise SystemExit
11978
+ if d.get('blocked'):
11979
+ print('Coverage %s%% is below the %s%% threshold (tests PASS; add tests to raise line coverage, do not change passing assertions).' % (d.get('pct'), d.get('threshold')))
11980
+ " 2>/dev/null || echo "")
11981
+ [ -n "$cov_summary" ] && gate_failure_context="${gate_failure_context}${cov_summary} "
11982
+ fi
11581
11983
  gate_failure_context="${gate_failure_context}FIX THESE ISSUES BEFORE PROCEEDING WITH NEW WORK."
11582
11984
  fi
11583
11985
 
11986
+ # P2-2: high-severity spec-assumption context. When DISCOVERY recorded any
11987
+ # high-severity assumption (the spec was ambiguous in a high-impact place),
11988
+ # surface it to the build agent so it implements with the gap in view (or
11989
+ # fixes the spec) instead of obliviously coding past it. spec_ledger_prompt_block
11990
+ # is defined when spec-interrogation.sh is sourced (run.sh sources it in
11991
+ # DISCOVERY); guarded so build_prompt is safe when the module is absent.
11992
+ local assumption_context=""
11993
+ if type spec_ledger_prompt_block &>/dev/null; then
11994
+ assumption_context="$(spec_ledger_prompt_block 2>/dev/null || true)"
11995
+ fi
11996
+
11584
11997
  # Human directive injection (from HUMAN_INPUT.md)
11585
11998
  # NOTE: Do NOT unset LOKI_HUMAN_INPUT here - build_prompt runs in a subshell
11586
11999
  # (command substitution) so unset would not affect the parent shell.
@@ -11833,15 +12246,15 @@ except Exception:
11833
12246
  else
11834
12247
  if [ $retry -eq 0 ]; then
11835
12248
  if [ -n "$prd" ]; then
11836
- echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12249
+ echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11837
12250
  else
11838
- echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12251
+ echo "Loki Mode. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11839
12252
  fi
11840
12253
  else
11841
12254
  if [ -n "$prd" ]; then
11842
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12255
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11843
12256
  else
11844
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12257
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
11845
12258
  fi
11846
12259
  fi
11847
12260
  fi
@@ -11946,6 +12359,7 @@ except Exception:
11946
12359
  fi
11947
12360
  [ -n "$human_directive" ] && printf '%s\n' "$human_directive"
11948
12361
  [ -n "$gate_failure_context" ] && printf '%s\n' "$gate_failure_context"
12362
+ [ -n "$assumption_context" ] && printf '%s\n' "$assumption_context"
11949
12363
  [ -n "$queue_tasks" ] && printf '%s\n' "$queue_tasks"
11950
12364
  [ -n "$bmad_context" ] && printf '%s\n' "$bmad_context"
11951
12365
  [ -n "$openspec_context" ] && printf '%s\n' "$openspec_context"
@@ -13066,6 +13480,22 @@ except Exception:
13066
13480
  fi
13067
13481
  fi
13068
13482
 
13483
+ # P2-1: Spec interrogation (DISCOVERY phase, BEFORE iteration 1 begins
13484
+ # coding). Auto-detects spec ambiguities/contradictions/underspecification
13485
+ # via the Devil's-Advocate grill + prd-analyzer, classifies them with a
13486
+ # deterministic severity, and records every gap as a first-class assumption
13487
+ # under .loki/assumptions/. Default-on; LOKI_SPEC_GRILL=0 opts out.
13488
+ # Provider-aware and degrades cleanly (no provider -> prd-analyzer
13489
+ # assumptions only, no fabricated questions). Best-effort: never blocks the
13490
+ # run. The completion-side teeth are council_assumption_ledger_gate.
13491
+ if [ -f "${SCRIPT_DIR}/spec-interrogation.sh" ]; then
13492
+ # shellcheck disable=SC1090
13493
+ . "${SCRIPT_DIR}/spec-interrogation.sh" 2>/dev/null || true
13494
+ if type spec_interrogation_run &>/dev/null; then
13495
+ spec_interrogation_run "$prd_path" || true
13496
+ fi
13497
+ fi
13498
+
13069
13499
  # Auto-derive completion promise from PRD (v6.10.0)
13070
13500
  # When PRD exists but no explicit promise, auto-derive one and switch to checkpoint mode
13071
13501
  if [ -n "$prd_path" ] && [ -f "$prd_path" ] && [ -z "$COMPLETION_PROMISE" ]; then
@@ -13227,6 +13657,18 @@ except Exception as exc:
13227
13657
  local prompt
13228
13658
  prompt=$(build_prompt "$retry" "$prd_path" "$ITERATION_COUNT")
13229
13659
 
13660
+ # P2-2 auto-acknowledgment lifecycle: build_prompt just injected the
13661
+ # high-severity spec assumptions into the prompt (assumption_context), so
13662
+ # the agent has now SEEN them. Mark them acknowledged so the completion
13663
+ # gate is not a permanent dead-end in autonomous (non-TTY) mode where no
13664
+ # human can ever set confirmed=true. This is the opposite of silent
13665
+ # autocorrect: the gap was recorded, prompt-injected, and is surfaced in
13666
+ # proof-of-done. LOKI_ASSUMPTIONS_REQUIRE_CONFIRM=1 disables auto-ack so
13667
+ # only a human confirmation clears the block (the helper checks the knob).
13668
+ if type spec_ledger_acknowledge_all &>/dev/null; then
13669
+ spec_ledger_acknowledge_all 2>/dev/null || true
13670
+ fi
13671
+
13230
13672
  # BUG #5 fix: Clear LOKI_HUMAN_INPUT in the parent shell after build_prompt
13231
13673
  # consumed it. build_prompt runs in a subshell (command substitution), so
13232
13674
  # any unset inside it does not affect the parent. Clear here to prevent
@@ -14209,7 +14651,15 @@ if __name__ == "__main__":
14209
14651
  local tc_count
14210
14652
  tc_count=$(track_gate_failure "test_coverage")
14211
14653
  gate_failures="${gate_failures}test_coverage,"
14212
- log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
14654
+ # P0-1 Fix A: distinguish a coverage-only block (tests passed,
14655
+ # enforced coverage below threshold) from a genuine tests-red
14656
+ # block in the log so the operator is not misled.
14657
+ if [ -f "${TARGET_DIR:-.}/.loki/quality/coverage.json" ] && \
14658
+ python3 -c "import json,sys; sys.exit(0 if json.load(open('${TARGET_DIR:-.}/.loki/quality/coverage.json')).get('blocked') else 1)" 2>/dev/null; then
14659
+ log_warn "Test coverage gate BLOCKED ($tc_count consecutive) - tests pass but coverage below threshold (LOKI_ENFORCE_COVERAGE=1)"
14660
+ else
14661
+ log_warn "Test suite gate FAILED ($tc_count consecutive) - must pass next iteration"
14662
+ fi
14213
14663
  fi
14214
14664
  fi
14215
14665
  # BUG-ST-002: Check pause signal between quality gates (after test coverage)
@@ -14539,6 +14989,17 @@ if __name__ == "__main__":
14539
14989
  log_warn "Completion claim rejected: held-out spec-eval gate found failing held-out acceptance check(s)."
14540
14990
  log_warn " Details under .loki/council/heldout-block.json ; opt out with LOKI_HELDOUT_GATE=0"
14541
14991
  # Fall through; keep iterating until the held-out checks pass.
14992
+ # P2-2: the assumption ledger gate must also guard the DEFAULT
14993
+ # completion-promise route, not only the interval-gated council path.
14994
+ # Otherwise an agent can self-assert "done" while a high-severity spec
14995
+ # assumption is still unresolved, bypassing the spec-robustness gate.
14996
+ # Mirrors the evidence/held-out gate arms above. Opt-out: the gate's
14997
+ # own LOKI_ASSUMPTION_GATE=0 (returns 0 immediately when disabled, so
14998
+ # this branch never fires). Gate output is printed by the gate itself.
14999
+ elif [ "$_completion_claimed" = 1 ] && type council_assumption_ledger_gate &>/dev/null && ! council_assumption_ledger_gate; then
15000
+ log_warn "Completion claim rejected: assumption ledger gate found unresolved high-severity spec assumption(s)."
15001
+ log_warn " Details under .loki/council/assumption-block.json ; opt out with LOKI_ASSUMPTION_GATE=0"
15002
+ # Fall through; keep iterating until high-sev assumptions resolve.
14542
15003
  elif [ "$_completion_claimed" = 1 ]; then
14543
15004
  echo ""
14544
15005
  if [ -n "$COMPLETION_PROMISE" ]; then
@@ -15014,6 +15475,8 @@ check_human_intervention() {
15014
15475
  log_info "Council force-review: blocked by evidence hard gate"
15015
15476
  elif type council_heldout_gate &>/dev/null && ! council_heldout_gate; then
15016
15477
  log_info "Council force-review: blocked by held-out spec-eval hard gate"
15478
+ elif type council_assumption_ledger_gate &>/dev/null && ! council_assumption_ledger_gate; then
15479
+ log_info "Council force-review: blocked by assumption ledger hard gate"
15017
15480
  elif type council_vote &>/dev/null && council_vote; then
15018
15481
  log_header "COMPLETION COUNCIL: FORCE REVIEW - PROJECT COMPLETE"
15019
15482
  # BUG #17 fix: Write COMPLETED marker, generate council report, and