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.
- package/README.md +1 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +113 -0
- package/autonomy/crash.sh +47 -21
- package/autonomy/loki +50 -27
- package/autonomy/run.sh +468 -5
- package/autonomy/spec-interrogation.sh +550 -0
- package/autonomy/telemetry.sh +28 -8
- package/bin/postinstall.js +22 -10
- package/dashboard/__init__.py +1 -1
- package/dashboard/auth.py +117 -2
- package/dashboard/telemetry.py +34 -6
- package/docs/ACKNOWLEDGEMENTS.md +1 -1
- package/docs/COMPETITIVE-ANALYSIS.md +1 -1
- package/docs/INSTALLATION.md +10 -3
- package/docs/OPEN-CORE-BOUNDARY.md +6 -5
- package/docs/P2-SPEC-ROBUSTNESS-PLAN.md +192 -0
- package/docs/PRIVACY.md +82 -24
- package/docs/R9-OPEN-CORE-HOOKS-PLAN.md +2 -2
- package/docs/auto-claude-comparison.md +2 -2
- package/docs/certification/README.md +1 -1
- package/docs/competitive/bolt-new-analysis.md +1 -1
- package/docs/competitive/emergence-others-analysis.md +6 -6
- package/docs/competitive/replit-lovable-analysis.md +4 -4
- package/docs/enterprise/security.md +43 -3
- package/docs/show-hn-post.md +1 -1
- package/loki-ts/dist/loki.js +30 -30
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-CKUOsWZW.js → AdminPage-CcCJ0Sjt.js} +1 -1
- package/web-app/dist/assets/{Avatar-CL9Id9Hi.js → Avatar-DK8kmayw.js} +1 -1
- package/web-app/dist/assets/{Badge-B12zwlD7.js → Badge-4uAWnemi.js} +1 -1
- package/web-app/dist/assets/{Button-CFLVoduT.js → Button-BBMk33tk.js} +1 -1
- package/web-app/dist/assets/ComparePage-bt9rwvST.js +1 -0
- package/web-app/dist/assets/{GitHubIssuesPanel-CSitxtAX.js → GitHubIssuesPanel-WDbH47UM.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BIT06FRo.js → GitHubPRsPanel-C2CiYtTx.js} +1 -1
- package/web-app/dist/assets/{HomePage-pU_0fGny.js → HomePage-BQk-MUjn.js} +4 -4
- package/web-app/dist/assets/{LoginPage-DTZtt2Yb.js → LoginPage-DMOZVGGL.js} +1 -1
- package/web-app/dist/assets/{MagicPage-10zfra8o.js → MagicPage-Bzp2Nt1z.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-C-wiKUkv.js → MetricsPage-C39JVdsw.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BDkcmhYe.js → NotFoundPage-6vT_U9UL.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CiCavQ8n.js → ProjectPage-BfFcZp-E.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BLCXQwwC.js → ProjectsPage-CPMBf8Wt.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-PkxtaMyg.js → SettingsPage-BnNN6ETl.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-iECp8Tha.js → ShowcasePage-WDrMf-cx.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DS6Anno1.js → SystemSettingsPage-DX4jb2e8.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-ls6h6bNL.js → TeamsPage-BCfqcXzu.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-Bk0QzlPt.js → TemplatesPage-CZvmimDj.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-4-1hWCtZ.js → TerminalOutput-BlRqFwWV.js} +1 -1
- package/web-app/dist/assets/{activity-DH3ih2nS.js → activity-CacZsUyr.js} +1 -1
- package/web-app/dist/assets/{bell-Gn17S6uv.js → bell-DK2qtHnk.js} +1 -1
- package/web-app/dist/assets/{bot-Cbycc3VE.js → bot-CkcUtHad.js} +1 -1
- package/web-app/dist/assets/{check-nIAqa-kf.js → check-CbCPjX3M.js} +1 -1
- package/web-app/dist/assets/{chevron-left-D2jcWDll.js → chevron-left-5NUKWw3i.js} +1 -1
- package/web-app/dist/assets/{circle-alert-CpL4Bhvt.js → circle-alert-S7uFoxC2.js} +1 -1
- package/web-app/dist/assets/{clock-IW4Wq86N.js → clock-CaQRrIrs.js} +1 -1
- package/web-app/dist/assets/{cloud-Cn8nNuH2.js → cloud-DBAX6c0r.js} +1 -1
- package/web-app/dist/assets/{code-xml-BiJBteXf.js → code-xml-De5-EXv3.js} +1 -1
- package/web-app/dist/assets/{copy-CnqkyNsi.js → copy-CUkT6k1v.js} +1 -1
- package/web-app/dist/assets/{database-CKSReqa5.js → database-BAWf1Gwt.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CDzDY64R.js → dollar-sign-Ji8zk86R.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Box4IwG1.js → file-code-corner-ChtXoBwS.js} +1 -1
- package/web-app/dist/assets/{file-plus-DpGqlXF8.js → file-plus-bFa37P76.js} +1 -1
- package/web-app/dist/assets/{folder-open-B57dAoBv.js → folder-open-DhXpXscO.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-BVbucmO5.js → git-commit-horizontal-DVPeDQ3j.js} +1 -1
- package/web-app/dist/assets/{globe-BkOnKl4x.js → globe-BPZgPeeu.js} +1 -1
- package/web-app/dist/assets/{hammer-DRbIQ4QU.js → hammer-jLCaujYH.js} +1 -1
- package/web-app/dist/assets/{index-CM_b_EhP.js → index-B-0iHBPO.js} +2 -2
- package/web-app/dist/assets/{layers-B78BiFiU.js → layers-B1vsrsFW.js} +1 -1
- package/web-app/dist/assets/{lightbulb-B-Itbm9g.js → lightbulb-C-uLoq9Y.js} +1 -1
- package/web-app/dist/assets/{loader-circle-Oq6NQhW2.js → loader-circle-JTfD-ZuM.js} +1 -1
- package/web-app/dist/assets/{lock-DbJ9zxbw.js → lock-G9rxD4gZ.js} +1 -1
- package/web-app/dist/assets/{mail-CzMRod6m.js → mail-BJ0PTN_V.js} +1 -1
- package/web-app/dist/assets/{package-WZ5osvej.js → package-CXClfLOO.js} +1 -1
- package/web-app/dist/assets/{plus-j08lFR-K.js → plus-EoL5OCB7.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CIr7E-g2.js → refresh-cw-BjREUnVq.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-gwoXxDeE.js → rotate-ccw-DahWX07H.js} +1 -1
- package/web-app/dist/assets/{save-B8fV_ZpE.js → save-Dek3gCn1.js} +1 -1
- package/web-app/dist/assets/{server-D5dO1paz.js → server-D6V1BAia.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Du08zhdg.js → shield-alert-BtTK5Sxb.js} +1 -1
- package/web-app/dist/assets/{trash-2-DEKSVae5.js → trash-2-BT5o_g0r.js} +1 -1
- package/web-app/dist/assets/{trending-down-DBiXUtxJ.js → trending-down-D4Jk7KF3.js} +1 -1
- package/web-app/dist/assets/{trending-up-BgmK_tHq.js → trending-up-EQFTzhEo.js} +1 -1
- package/web-app/dist/assets/{upload-IaViyeVD.js → upload-JfI5lCSE.js} +1 -1
- package/web-app/dist/assets/{usePolling-PiRLqNu6.js → usePolling-BnhPUuGd.js} +1 -1
- package/web-app/dist/assets/{user-BB5J8wAF.js → user-DSUiUYtj.js} +1 -1
- package/web-app/dist/index.html +1 -1
- 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
|
-
|
|
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
|