loki-mode 9.53.0 → 9.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +18 -4
- package/autonomy/council-v2.sh +13 -7
- package/autonomy/lib/done-recognition.sh +24 -16
- package/autonomy/lib/proof-check.sh +11 -3
- package/autonomy/lib/voter-agents.sh +8 -3
- package/autonomy/lib/workspace_diff.py +106 -12
- package/autonomy/prd-checklist.sh +19 -11
- package/autonomy/run.sh +274 -26
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BACKLOG.md +41 -0
- package/docs/v10/METRICS.md +2 -1
- package/docs/v10/PROGRESS.md +17 -0
- package/loki-ts/dist/loki.js +323 -322
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v9.
|
|
6
|
+
# Loki Mode v9.54.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.
|
|
473
|
+
**v9.54.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.54.0
|
|
@@ -2116,6 +2116,16 @@ INCONCLUSIVE_EOF
|
|
|
2116
2116
|
local _diff_ok _tests_ok
|
|
2117
2117
|
if [ "$diff_fails" = "true" ]; then _diff_ok="false"; else _diff_ok="true"; fi
|
|
2118
2118
|
if [ "$test_fails" = "true" ]; then _tests_ok="false"; else _tests_ok="true"; fi
|
|
2119
|
+
# BACKLOG 55: tests.pass is true only for affirmative evidence (a real
|
|
2120
|
+
# runner recorded a boolean pass and nothing downgraded it). $test_pass
|
|
2121
|
+
# itself is "true" for every inconclusive outcome (no runner, no results,
|
|
2122
|
+
# zero tests, no pass recorded, tautological), so it is not written raw.
|
|
2123
|
+
local _tests_pass_json='"inconclusive"'
|
|
2124
|
+
if [ "$test_fails" = "true" ]; then
|
|
2125
|
+
_tests_pass_json="false"
|
|
2126
|
+
elif [ "$test_inconclusive" != "true" ] && [ "$test_runner" != "none" ] && [ "$test_pass" = "true" ]; then
|
|
2127
|
+
_tests_pass_json="true"
|
|
2128
|
+
fi
|
|
2119
2129
|
# Proof-of-Function axes (nomock/persistence/auth) are declared later in
|
|
2120
2130
|
# the gate; default to "true"/"" when this helper runs before they are
|
|
2121
2131
|
# set (it never does in practice, but keep the write robust).
|
|
@@ -2139,7 +2149,7 @@ INCONCLUSIVE_EOF
|
|
|
2139
2149
|
"tests": {
|
|
2140
2150
|
"ok": $_tests_ok,
|
|
2141
2151
|
"runner": "$test_runner",
|
|
2142
|
-
"pass": $
|
|
2152
|
+
"pass": $_tests_pass_json,
|
|
2143
2153
|
"inconclusive": $test_inconclusive,
|
|
2144
2154
|
"inconclusive_reason": "$test_inconclusive_reason"
|
|
2145
2155
|
},
|
|
@@ -4227,13 +4237,17 @@ except Exception:
|
|
|
4227
4237
|
_CC_VERDICTS_DIR="$verdicts_dir" \
|
|
4228
4238
|
_CC_LOKI_DIR="$loki_dir" \
|
|
4229
4239
|
LOKI_TARGET_DIR="${TARGET_DIR:-$(pwd)}" \
|
|
4240
|
+
PROJECT_DIR="${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)}" \
|
|
4230
4241
|
python3 -E - <<'PYEOF' 2>/dev/null || exit_code=$?
|
|
4231
4242
|
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
4232
4243
|
import json, os, sys, pathlib
|
|
4233
4244
|
|
|
4234
|
-
# Path setup:
|
|
4235
|
-
|
|
4236
|
-
|
|
4245
|
+
# Path setup: import providers.managed only from an explicit PROJECT_DIR. Never
|
|
4246
|
+
# fall back to the cwd (D7): it is the agent's repo, which could ship its own
|
|
4247
|
+
# providers/managed.py. Unset -> ImportError -> the Bash voting path.
|
|
4248
|
+
project_dir = os.environ.get("PROJECT_DIR", "")
|
|
4249
|
+
if project_dir:
|
|
4250
|
+
sys.path.insert(0, project_dir)
|
|
4237
4251
|
|
|
4238
4252
|
try:
|
|
4239
4253
|
from providers.managed import (
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
# - swarm/sycophancy.py (sycophancy detection)
|
|
20
20
|
# - swarm/calibration.py (reviewer calibration)
|
|
21
21
|
#
|
|
22
|
+
# Inline Python (D7): the council runs with the cwd inside the agent's repo, so
|
|
23
|
+
# every python3 here runs -E and drops '' and '.' from sys.path before any other
|
|
24
|
+
# import (the swarm dir is inserted after that); a committed json.py or
|
|
25
|
+
# sitecustomize.py cannot read the votes. Pinned by
|
|
26
|
+
# tests/test-council-no-fabricated-verdict.sh (D7 section).
|
|
27
|
+
#
|
|
22
28
|
#===============================================================================
|
|
23
29
|
|
|
24
30
|
COUNCIL_V2_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -97,7 +103,7 @@ council_v2_vote() {
|
|
|
97
103
|
votes_json="$votes_json$vote_content"
|
|
98
104
|
|
|
99
105
|
local verdict
|
|
100
|
-
verdict=$(echo "$vote_content" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "UNKNOWN")
|
|
106
|
+
verdict=$(echo "$vote_content" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "UNKNOWN")
|
|
101
107
|
# TRUST: a reviewer that could not be REACHED did not vote REJECT.
|
|
102
108
|
#
|
|
103
109
|
# Every provider arm below used to substitute a literal
|
|
@@ -135,7 +141,7 @@ council_v2_vote() {
|
|
|
135
141
|
|
|
136
142
|
# Step 4: Sycophancy detection
|
|
137
143
|
local sycophancy_score
|
|
138
|
-
sycophancy_score=$(python3 -c "
|
|
144
|
+
sycophancy_score=$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
139
145
|
import sys
|
|
140
146
|
sys.path.insert(0, '${COUNCIL_V2_DIR}/../swarm')
|
|
141
147
|
from sycophancy import detect_sycophancy
|
|
@@ -150,7 +156,7 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
|
|
|
150
156
|
if [ "$approve_count" -eq "$council_size" ]; then
|
|
151
157
|
local threshold="${LOKI_COUNCIL_SYCOPHANCY_THRESHOLD:-0.6}"
|
|
152
158
|
local should_challenge
|
|
153
|
-
should_challenge=$(python3 -c "print('yes' if float('$sycophancy_score') >= float('$threshold') else 'no')" 2>/dev/null || echo "no")
|
|
159
|
+
should_challenge=$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; print('yes' if float('$sycophancy_score') >= float('$threshold') else 'no')" 2>/dev/null || echo "no")
|
|
154
160
|
|
|
155
161
|
if [ "$should_challenge" = "yes" ]; then
|
|
156
162
|
log_warn "Sycophancy score $sycophancy_score >= $threshold -- adding devil's advocate"
|
|
@@ -167,7 +173,7 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
|
|
|
167
173
|
# Same trust rule as the main tally: an unparseable devil's
|
|
168
174
|
# advocate did not vote REJECT. Defaulting to REJECT here would
|
|
169
175
|
# silently overturn a unanimous APPROVE on a transient failure.
|
|
170
|
-
da_verdict=$(cat "$da_vote" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "INCONCLUSIVE")
|
|
176
|
+
da_verdict=$(cat "$da_vote" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "INCONCLUSIVE")
|
|
171
177
|
if [ "$da_verdict" = "INCONCLUSIVE" ]; then
|
|
172
178
|
log_warn "Devil's advocate produced no verdict -- unanimous approval left UNCHANGED (not overturned)"
|
|
173
179
|
elif [ "$da_verdict" = "REJECT" ]; then
|
|
@@ -201,7 +207,7 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
|
|
|
201
207
|
final_decision="reject"
|
|
202
208
|
fi
|
|
203
209
|
|
|
204
|
-
python3 -c "
|
|
210
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
205
211
|
import sys
|
|
206
212
|
sys.path.insert(0, '${COUNCIL_V2_DIR}/../swarm')
|
|
207
213
|
from calibration import CalibrationTracker
|
|
@@ -388,7 +394,7 @@ Respond ONLY with a valid JSON object. No markdown fencing."
|
|
|
388
394
|
--json-schema "$_c2_schema_content" --output-format json 2>/dev/null)" || _c2_json=""
|
|
389
395
|
if [ -n "$_c2_json" ]; then
|
|
390
396
|
# Pull the schema-validated object out of the envelope.
|
|
391
|
-
result="$(printf '%s' "$_c2_json" | python3 -c 'import sys,json
|
|
397
|
+
result="$(printf '%s' "$_c2_json" | python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; import json
|
|
392
398
|
try:
|
|
393
399
|
e=json.load(sys.stdin)
|
|
394
400
|
p=e.get("structured_output")
|
|
@@ -490,7 +496,7 @@ except Exception:
|
|
|
490
496
|
# or neither, stays INCONCLUSIVE -- never guessed.
|
|
491
497
|
if [ -z "$extracted" ] && [ -n "${result:-}" ]; then
|
|
492
498
|
local _recovered
|
|
493
|
-
_recovered="$(printf '%s' "$result" | _LOKI_RAW="$result" python3 -c '
|
|
499
|
+
_recovered="$(printf '%s' "$result" | _LOKI_RAW="$result" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
494
500
|
import os, re, sys, json
|
|
495
501
|
raw = os.environ.get("_LOKI_RAW", "")
|
|
496
502
|
# Standalone words only: "APPROVE" not "approved-by", and not inside a URL.
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
# the single injection seam, mirroring autonomy/lib/prd-enrich.sh.
|
|
37
37
|
#
|
|
38
38
|
# No emojis. No em dashes. bash 3.2 safe. Honors `set -uo pipefail`.
|
|
39
|
+
#
|
|
40
|
+
# Inline Python (D7): the gate runs with the cwd inside the agent's repo, so
|
|
41
|
+
# every python3 here runs -E and drops '' and '.' from sys.path before any other
|
|
42
|
+
# import; a committed json.py or sitecustomize.py cannot turn the verdict done.
|
|
43
|
+
# Pinned by tests/test-reuse-done-recognition.sh case (d7).
|
|
39
44
|
|
|
40
45
|
# Bound the single model call so a huge PRD or test log cannot run away.
|
|
41
46
|
: "${LOKI_DONE_RECOG_TIMEOUT:=180}" # seconds for the single model call
|
|
@@ -122,7 +127,7 @@ _loki_done_recog_invoke() {
|
|
|
122
127
|
--json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
|
|
123
128
|
fi
|
|
124
129
|
if [ -n "$_dr_json" ]; then
|
|
125
|
-
_dr_obj=$(printf '%s' "$_dr_json" | python3 -c 'import sys,json
|
|
130
|
+
_dr_obj=$(printf '%s' "$_dr_json" | python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; import json
|
|
126
131
|
try:
|
|
127
132
|
e=json.load(sys.stdin)
|
|
128
133
|
p=e.get("structured_output")
|
|
@@ -194,7 +199,7 @@ _loki_done_recog_prd_sha() {
|
|
|
194
199
|
sha256sum "$prd_file" 2>/dev/null | awk '{print $1}'
|
|
195
200
|
else
|
|
196
201
|
# Last-resort: a python hash, still deterministic over the same bytes.
|
|
197
|
-
LOKI_DR_PRD="$prd_file" python3 -c "
|
|
202
|
+
LOKI_DR_PRD="$prd_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
198
203
|
import hashlib, os
|
|
199
204
|
p = os.environ.get('LOKI_DR_PRD','')
|
|
200
205
|
try:
|
|
@@ -275,7 +280,8 @@ reuse_done_recognition_gate() {
|
|
|
275
280
|
LOKI_DR_CHECKLIST="$loki_dir/checklist/checklist.json" \
|
|
276
281
|
LOKI_DR_MAX_PRD="${LOKI_DONE_RECOG_MAX_PRD_CHARS}" \
|
|
277
282
|
LOKI_DR_MAX_TEST="${LOKI_DONE_RECOG_MAX_TEST_CHARS}" \
|
|
278
|
-
python3 << 'DR_PROMPT_EOF'
|
|
283
|
+
python3 -E << 'DR_PROMPT_EOF'
|
|
284
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
279
285
|
import json, os, sys
|
|
280
286
|
|
|
281
287
|
def read_capped(path, cap):
|
|
@@ -376,7 +382,8 @@ DR_PROMPT_EOF
|
|
|
376
382
|
LOKI_DR_TESTS="$_test_results" \
|
|
377
383
|
LOKI_DR_ACTION="$action" \
|
|
378
384
|
LOKI_DR_PRD="$prd_path" \
|
|
379
|
-
python3 << 'DR_PARSE_EOF'
|
|
385
|
+
python3 -E << 'DR_PARSE_EOF'
|
|
386
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
380
387
|
import json, os, re, sys
|
|
381
388
|
|
|
382
389
|
resp = os.environ.get("LOKI_DR_RESP", "")
|
|
@@ -674,7 +681,7 @@ DR_PARSE_EOF
|
|
|
674
681
|
fi
|
|
675
682
|
|
|
676
683
|
local verdict
|
|
677
|
-
verdict=$(printf '%s' "$parsed" | python3 -c "import
|
|
684
|
+
verdict=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('verdict',''))" 2>/dev/null)
|
|
678
685
|
|
|
679
686
|
case "$verdict" in
|
|
680
687
|
done)
|
|
@@ -687,7 +694,7 @@ DR_PARSE_EOF
|
|
|
687
694
|
;;
|
|
688
695
|
*)
|
|
689
696
|
local _reason
|
|
690
|
-
_reason=$(printf '%s' "$parsed" | python3 -c "import
|
|
697
|
+
_reason=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('reason','') or 'unverifiable')" 2>/dev/null)
|
|
691
698
|
log_info "Done-recognition: could not confirm the existing code already satisfies the reused spec (${_reason}). Proceeding to build to be safe."
|
|
692
699
|
return 1
|
|
693
700
|
;;
|
|
@@ -723,10 +730,10 @@ _loki_done_recog_finish() {
|
|
|
723
730
|
mkdir -p "$loki_dir/state" 2>/dev/null || true
|
|
724
731
|
|
|
725
732
|
local summary met total axis
|
|
726
|
-
summary=$(printf '%s' "$parsed" | python3 -c "import
|
|
727
|
-
met=$(printf '%s' "$parsed" | python3 -c "import
|
|
728
|
-
total=$(printf '%s' "$parsed" | python3 -c "import
|
|
729
|
-
axis=$(printf '%s' "$parsed" | python3 -c "import
|
|
733
|
+
summary=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('summary','') or 'Project already satisfies its spec.')" 2>/dev/null)
|
|
734
|
+
met=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('met_count',0))" 2>/dev/null)
|
|
735
|
+
total=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('total_count',0))" 2>/dev/null)
|
|
736
|
+
axis=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('tests_axis','unknown'))" 2>/dev/null)
|
|
730
737
|
[ -n "$met" ] || met=0
|
|
731
738
|
[ -n "$total" ] || total=0
|
|
732
739
|
[ -n "$axis" ] || axis="unknown"
|
|
@@ -766,7 +773,7 @@ _loki_done_recog_finish() {
|
|
|
766
773
|
echo ""
|
|
767
774
|
echo "## Per-requirement evidence"
|
|
768
775
|
echo ""
|
|
769
|
-
printf '%s' "$parsed" | python3 -c "
|
|
776
|
+
printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
770
777
|
import json, sys
|
|
771
778
|
try:
|
|
772
779
|
d = json.load(sys.stdin)
|
|
@@ -785,7 +792,7 @@ for t in d.get('satisfied', []):
|
|
|
785
792
|
LOKI_DR_MET="$met" \
|
|
786
793
|
LOKI_DR_TOTAL="$total" \
|
|
787
794
|
LOKI_DR_TS="$ts" \
|
|
788
|
-
python3 -c "
|
|
795
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
789
796
|
import json, os, tempfile
|
|
790
797
|
out = os.environ['LOKI_DR_OUT']
|
|
791
798
|
def i(v):
|
|
@@ -856,7 +863,7 @@ _loki_done_recog_write_manifest() {
|
|
|
856
863
|
LOKI_DR_PARSED="$parsed" \
|
|
857
864
|
LOKI_DR_SHA="$prd_sha" \
|
|
858
865
|
LOKI_DR_TS="$ts" \
|
|
859
|
-
python3 -c "
|
|
866
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
860
867
|
import json, os, sys, tempfile
|
|
861
868
|
out = os.environ['LOKI_DR_OUT']
|
|
862
869
|
try:
|
|
@@ -897,7 +904,8 @@ except Exception:
|
|
|
897
904
|
if [ -f "$loki_dir/queue/pending.json" ]; then
|
|
898
905
|
# Drop prior PRD-sourced tasks so the incremental pass is the source of
|
|
899
906
|
# truth; non-PRD tasks (if any) are preserved.
|
|
900
|
-
LOKI_DR_PENDING="$loki_dir/queue/pending.json" python3 - <<'RESET_EOF' 2>/dev/null || true
|
|
907
|
+
LOKI_DR_PENDING="$loki_dir/queue/pending.json" python3 -E - <<'RESET_EOF' 2>/dev/null || true
|
|
908
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
901
909
|
import json, os, tempfile
|
|
902
910
|
p = os.environ.get("LOKI_DR_PENDING", ".loki/queue/pending.json")
|
|
903
911
|
try:
|
|
@@ -921,8 +929,8 @@ RESET_EOF
|
|
|
921
929
|
fi
|
|
922
930
|
|
|
923
931
|
local met total
|
|
924
|
-
met=$(printf '%s' "$parsed" | python3 -c "import
|
|
925
|
-
total=$(printf '%s' "$parsed" | python3 -c "import
|
|
932
|
+
met=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('met_count',0))" 2>/dev/null)
|
|
933
|
+
total=$(printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json;print(json.load(sys.stdin).get('total_count',0))" 2>/dev/null)
|
|
926
934
|
local unmet=$(( ${total:-0} - ${met:-0} ))
|
|
927
935
|
log_info "Done-recognition: ${met:-0} of ${total:-0} requirements already satisfied; building only the ${unmet} unmet. Pass --fresh-prd to rebuild from scratch."
|
|
928
936
|
}
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
#
|
|
27
27
|
# This function is only ever called when the operator opted in (the call site
|
|
28
28
|
# guards on LOKI_PROVEN_PR_CHECK=1). It is also safe if called directly.
|
|
29
|
+
#
|
|
30
|
+
# Inline Python (D7): the call site's cwd is the agent's repo, so the proof
|
|
31
|
+
# readers run python3 -E and drop '' and '.' from sys.path first; a committed
|
|
32
|
+
# json.py or sitecustomize.py cannot change the headline. Pinned by
|
|
33
|
+
# tests/test-proven-pr-check.sh (D7 section).
|
|
29
34
|
|
|
30
35
|
# Double-source guard.
|
|
31
36
|
[ -n "${_PROOF_CHECK_SH:-}" ] && return 0
|
|
@@ -53,7 +58,8 @@ _proof_check_headline() {
|
|
|
53
58
|
command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
|
|
54
59
|
|
|
55
60
|
local headline=""
|
|
56
|
-
headline="$(python3 - "$proof_path" <<'PY' 2>/dev/null || true
|
|
61
|
+
headline="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
|
|
62
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
57
63
|
import json, sys
|
|
58
64
|
try:
|
|
59
65
|
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
@@ -84,7 +90,8 @@ _proof_check_proof_head_sha() {
|
|
|
84
90
|
command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
|
|
85
91
|
|
|
86
92
|
local sha=""
|
|
87
|
-
sha="$(python3 - "$proof_path" <<'PY' 2>/dev/null || true
|
|
93
|
+
sha="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
|
|
94
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
88
95
|
import json, sys
|
|
89
96
|
try:
|
|
90
97
|
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
@@ -118,7 +125,8 @@ _proof_check_run_id() {
|
|
|
118
125
|
command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
|
|
119
126
|
|
|
120
127
|
local rid=""
|
|
121
|
-
rid="$(python3 - "$proof_path" <<'PY' 2>/dev/null || true
|
|
128
|
+
rid="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
|
|
129
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
122
130
|
import json, sys
|
|
123
131
|
try:
|
|
124
132
|
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
# on success, 1 on any fallback
|
|
27
27
|
# condition (caller falls through
|
|
28
28
|
# to existing heuristic dispatch).
|
|
29
|
+
#
|
|
30
|
+
# Inline Python (D7): the dispatch runs with the cwd inside the agent's repo, so
|
|
31
|
+
# every python3 here runs -E and drops '' and '.' from sys.path before any other
|
|
32
|
+
# import; a committed json.py or sitecustomize.py cannot read the votes.
|
|
33
|
+
# Pinned by tests/test-voter-agents-json.sh (Case D).
|
|
29
34
|
|
|
30
35
|
# Guard against double-source.
|
|
31
36
|
if [ "${__LOKI_VOTER_AGENTS_SH_LOADED:-0}" = "1" ]; then
|
|
@@ -59,7 +64,7 @@ loki_voter_agents_json() {
|
|
|
59
64
|
_VA_PRD="$prd" \
|
|
60
65
|
_VA_TIER="$tier" \
|
|
61
66
|
_VA_COMPLEXITY="${LOKI_COMPLEXITY:-standard}" \
|
|
62
|
-
python3 -c '
|
|
67
|
+
python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
63
68
|
import json, os
|
|
64
69
|
iter_n = os.environ.get("_VA_ITER", "0")
|
|
65
70
|
prd = os.environ.get("_VA_PRD", "")
|
|
@@ -128,7 +133,7 @@ print(json.dumps(agents, separators=(",", ":")))
|
|
|
128
133
|
# Arg $1: terse base findings summary used to brief the DA prompt.
|
|
129
134
|
loki_devils_advocate_json() {
|
|
130
135
|
local summary="${1:-}"
|
|
131
|
-
_VA_SUMMARY="$summary" python3 -c '
|
|
136
|
+
_VA_SUMMARY="$summary" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
132
137
|
import json, os
|
|
133
138
|
summary = os.environ.get("_VA_SUMMARY", "")[:1000]
|
|
134
139
|
agents = {
|
|
@@ -349,7 +354,7 @@ loki_council_dispatch_agents() {
|
|
|
349
354
|
_VA_VDIR="$verdicts_dir" \
|
|
350
355
|
_VA_RFILE="$votes_dir/round-${iteration}.json" \
|
|
351
356
|
_VA_EXPECTED="${COUNCIL_SIZE:-3}" \
|
|
352
|
-
python3 -c '
|
|
357
|
+
python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
353
358
|
import json, os, sys
|
|
354
359
|
from datetime import datetime, timezone
|
|
355
360
|
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import hashlib
|
|
6
7
|
import os
|
|
8
|
+
import stat as _stat
|
|
7
9
|
import subprocess
|
|
10
|
+
import sys
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
_EMPTY = {"count": 0, "insertions": 0, "deletions": 0, "files": []}
|
|
@@ -29,19 +32,93 @@ def _excluded(path):
|
|
|
29
32
|
return path == ".loki" or path.startswith(".loki/")
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
# Written by run.sh setup_agent_branch (_loki_snapshot_preexisting): the paths
|
|
36
|
+
# git did not track when the session started, untracked and gitignored,
|
|
37
|
+
# NUL-delimited and relative to the repo top. An entry ending in "/" is an
|
|
38
|
+
# ignored directory and covers its subtree. The sibling holds "<sha256> <path>"
|
|
39
|
+
# records, one per file entry, written by write_snapshot_hashes below.
|
|
40
|
+
_SNAPSHOT = "preexisting-untracked.z"
|
|
41
|
+
_SNAPSHOT_HASHES = "preexisting-untracked.sha.z"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _content_hash(path):
|
|
45
|
+
"""sha256 of a regular file's bytes or a symlink's target, else None.
|
|
46
|
+
|
|
47
|
+
The snapshot writer and the receipt reader both call this, so the two sides
|
|
48
|
+
always agree (git hash-object varies with filters and object format).
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
st = os.lstat(path)
|
|
52
|
+
digest = hashlib.sha256()
|
|
53
|
+
if _stat.S_ISLNK(st.st_mode):
|
|
54
|
+
digest.update(b"link\0" + os.fsencode(os.readlink(path)))
|
|
55
|
+
elif _stat.S_ISREG(st.st_mode):
|
|
56
|
+
with open(path, "rb") as fh:
|
|
57
|
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
58
|
+
digest.update(chunk)
|
|
59
|
+
else:
|
|
60
|
+
return None
|
|
61
|
+
return digest.hexdigest()
|
|
62
|
+
except OSError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_snapshot_hashes(top, snapshot):
|
|
67
|
+
"""Record a content hash for every file entry of ``snapshot`` that exists
|
|
68
|
+
now (a directory entry or a missing path gets none), atomically, into the
|
|
69
|
+
sibling hashes file."""
|
|
70
|
+
with open(snapshot, "rb") as fh:
|
|
71
|
+
raw = fh.read()
|
|
72
|
+
top_b = os.fsencode(top)
|
|
73
|
+
records = []
|
|
74
|
+
for path in raw.split(b"\0"):
|
|
75
|
+
if not path or path.endswith(b"/"):
|
|
76
|
+
continue
|
|
77
|
+
digest = _content_hash(os.path.join(top_b, path))
|
|
78
|
+
if digest:
|
|
79
|
+
records.append(digest.encode("ascii") + b" " + path + b"\0")
|
|
80
|
+
dest = os.path.join(os.path.dirname(snapshot), _SNAPSHOT_HASHES)
|
|
81
|
+
with open(dest + ".tmp", "wb") as fh:
|
|
82
|
+
fh.write(b"".join(records))
|
|
83
|
+
os.replace(dest + ".tmp", dest)
|
|
84
|
+
|
|
85
|
+
|
|
32
86
|
def _preexisting_untracked(repo_dir):
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
87
|
+
"""The session-start snapshot: (prefix of repo_dir inside the repo, set of
|
|
88
|
+
repo-top-relative paths, {path: content hash}). Those paths are not this
|
|
89
|
+
run's work, so the receipt lists one only when its content changed;
|
|
90
|
+
tree_sha256 still binds their bytes. Empty when absent."""
|
|
91
|
+
state = os.path.join(repo_dir, ".loki", "state")
|
|
38
92
|
try:
|
|
39
|
-
with open(os.path.join(
|
|
93
|
+
with open(os.path.join(state, _SNAPSHOT), "rb") as fh:
|
|
40
94
|
raw = fh.read()
|
|
41
95
|
except OSError:
|
|
42
|
-
return "", frozenset()
|
|
96
|
+
return "", frozenset(), {}
|
|
43
97
|
paths = frozenset(p.decode("utf-8", "surrogateescape") for p in raw.split(b"\0") if p)
|
|
44
|
-
|
|
98
|
+
hashes = {}
|
|
99
|
+
try:
|
|
100
|
+
with open(os.path.join(state, _SNAPSHOT_HASHES), "rb") as fh:
|
|
101
|
+
raw = fh.read()
|
|
102
|
+
except OSError:
|
|
103
|
+
raw = b""
|
|
104
|
+
for record in raw.split(b"\0"):
|
|
105
|
+
digest, sep, path = record.partition(b" ")
|
|
106
|
+
if sep:
|
|
107
|
+
hashes[path.decode("utf-8", "surrogateescape")] = digest.decode("ascii", "replace")
|
|
108
|
+
return (_git(repo_dir, ["rev-parse", "--show-prefix"]) or "").strip(), paths, hashes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _covered(key, paths):
|
|
112
|
+
"""True when ``key`` or one of its parent directories ("a/", "a/b/") is in
|
|
113
|
+
``paths``: an ignored directory entry covers everything below it."""
|
|
114
|
+
if key in paths:
|
|
115
|
+
return True
|
|
116
|
+
cut = key.find("/")
|
|
117
|
+
while cut != -1:
|
|
118
|
+
if key[:cut + 1] in paths:
|
|
119
|
+
return True
|
|
120
|
+
cut = key.find("/", cut + 1)
|
|
121
|
+
return False
|
|
45
122
|
|
|
46
123
|
|
|
47
124
|
def _parse_numstat(raw):
|
|
@@ -122,9 +199,19 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
122
199
|
diffs.append({"path": path, "patch": chunk})
|
|
123
200
|
|
|
124
201
|
untracked = _git(repo_dir, ["ls-files", "--others", "--exclude-standard", "-z"])
|
|
125
|
-
prefix, preexisting = _preexisting_untracked(repo_dir)
|
|
126
|
-
for path in sorted(p for p in (untracked or "").split("\0")
|
|
127
|
-
|
|
202
|
+
prefix, preexisting, hashes = _preexisting_untracked(repo_dir)
|
|
203
|
+
for path in sorted(p for p in (untracked or "").split("\0") if p and not _excluded(p)):
|
|
204
|
+
key = prefix + path
|
|
205
|
+
if _covered(key, preexisting):
|
|
206
|
+
# The user's file, never committed. Listed only when the run changed
|
|
207
|
+
# it, with no counts and no patch: the old bytes were never stored,
|
|
208
|
+
# and its content (a config.local.json the agent un-ignored) must
|
|
209
|
+
# not land in the receipt.
|
|
210
|
+
recorded = hashes.get(key)
|
|
211
|
+
if recorded and _content_hash(os.path.join(repo_dir, path)) != recorded:
|
|
212
|
+
files.append({"path": path, "insertions": 0, "deletions": 0,
|
|
213
|
+
"status": "preexisting_modified"})
|
|
214
|
+
continue
|
|
128
215
|
stat = _git(
|
|
129
216
|
repo_dir,
|
|
130
217
|
["diff", "--no-index", "--no-renames", "--numstat", "-z", "--", "/dev/null", path],
|
|
@@ -155,4 +242,11 @@ def collect_workspace_diff(repo_dir, base, include_diffs=False):
|
|
|
155
242
|
}, diffs
|
|
156
243
|
|
|
157
244
|
|
|
158
|
-
__all__ = ["collect_workspace_diff"]
|
|
245
|
+
__all__ = ["collect_workspace_diff", "write_snapshot_hashes"]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
# run.sh: python3 -E workspace_diff.py hash-snapshot <repo top> <snapshot>
|
|
250
|
+
if len(sys.argv) != 4 or sys.argv[1] != "hash-snapshot":
|
|
251
|
+
sys.exit("usage: workspace_diff.py hash-snapshot <repo top> <snapshot>")
|
|
252
|
+
write_snapshot_hashes(sys.argv[2], sys.argv[3])
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
# LOKI_CHECKLIST_TIMEOUT - Timeout per check in seconds (default: 30)
|
|
19
19
|
# LOKI_CHECKLIST_ENABLED - Enable/disable checklist (default: true)
|
|
20
20
|
#
|
|
21
|
+
# Inline Python (D7): verification runs with the cwd inside the agent's repo,
|
|
22
|
+
# so every python3 here runs -E (an empty PYTHONPATH component would load a
|
|
23
|
+
# committed sitecustomize.py) and every -c / - body drops '' and '.' from
|
|
24
|
+
# sys.path before any other import (a committed json.py would print the
|
|
25
|
+
# verdict). Pinned by tests/moat/p2-honest-verdict.sh
|
|
26
|
+
# P2.checklist-verify-not-shadowed.
|
|
27
|
+
#
|
|
21
28
|
# Data:
|
|
22
29
|
# .loki/checklist/checklist.json - Full checklist with verification
|
|
23
30
|
# .loki/checklist/verification-results.json - Summary of last verification
|
|
@@ -198,7 +205,8 @@ checklist_oracle_triangulate() {
|
|
|
198
205
|
_ORACLE_OUT="$findings_file" \
|
|
199
206
|
_ORACLE_PROJECT="$project_dir" \
|
|
200
207
|
_ORACLE_INSTALL_DIR="$oracle_install_dir" \
|
|
201
|
-
python3 - > "$status_file" 2>/dev/null <<'ORACLE_PY'
|
|
208
|
+
python3 -E - > "$status_file" 2>/dev/null <<'ORACLE_PY'
|
|
209
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
202
210
|
import json, os, re, sys, tempfile, glob
|
|
203
211
|
|
|
204
212
|
spec_path = os.environ["_ORACLE_SPEC"]
|
|
@@ -693,7 +701,7 @@ checklist_oracle_evidence() {
|
|
|
693
701
|
if [ ! -f "$findings_file" ]; then
|
|
694
702
|
return 0
|
|
695
703
|
fi
|
|
696
|
-
_ORACLE_OUT="$findings_file" python3 -c '
|
|
704
|
+
_ORACLE_OUT="$findings_file" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
697
705
|
import json, os
|
|
698
706
|
try:
|
|
699
707
|
with open(os.environ["_ORACLE_OUT"]) as f:
|
|
@@ -797,7 +805,7 @@ checklist_select_heldout() {
|
|
|
797
805
|
# items the build loop already saw in earlier prompts (the hidden-from-loop
|
|
798
806
|
# guarantee is best-effort once the checklist ids change mid-run).
|
|
799
807
|
local status_token
|
|
800
|
-
status_token=$(_CHECKLIST_FILE="$CHECKLIST_FILE" _HELDOUT_FILE="$heldout_file" python3 -c "
|
|
808
|
+
status_token=$(_CHECKLIST_FILE="$CHECKLIST_FILE" _HELDOUT_FILE="$heldout_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
801
809
|
import json, os, sys, hashlib, tempfile
|
|
802
810
|
|
|
803
811
|
cl_path = os.environ['_CHECKLIST_FILE']
|
|
@@ -945,7 +953,7 @@ checklist_heldout_ids() {
|
|
|
945
953
|
if [ ! -f "$heldout_file" ]; then
|
|
946
954
|
return 0
|
|
947
955
|
fi
|
|
948
|
-
_HELDOUT_FILE="$heldout_file" python3 -c "
|
|
956
|
+
_HELDOUT_FILE="$heldout_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
949
957
|
import json, os
|
|
950
958
|
try:
|
|
951
959
|
with open(os.environ['_HELDOUT_FILE']) as f:
|
|
@@ -991,7 +999,7 @@ checklist_verify() {
|
|
|
991
999
|
|
|
992
1000
|
log_step "Running PRD checklist verification..."
|
|
993
1001
|
|
|
994
|
-
python3 "$verify_script" \
|
|
1002
|
+
python3 -E "$verify_script" \
|
|
995
1003
|
--checklist "$CHECKLIST_FILE" \
|
|
996
1004
|
--timeout "$CHECKLIST_TIMEOUT" 2>/dev/null || true
|
|
997
1005
|
|
|
@@ -1023,7 +1031,7 @@ checklist_summary() {
|
|
|
1023
1031
|
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" \
|
|
1024
1032
|
_CHECKLIST_WAIVERS="${CHECKLIST_DIR:-".loki/checklist"}/waivers.json" \
|
|
1025
1033
|
_CHECKLIST_HELDOUT="${CHECKLIST_DIR:-".loki/checklist"}/held-out.json" \
|
|
1026
|
-
python3 -c "
|
|
1034
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1027
1035
|
import json, sys, os
|
|
1028
1036
|
try:
|
|
1029
1037
|
fpath = os.environ.get('_CHECKLIST_RESULTS', '')
|
|
@@ -1126,7 +1134,7 @@ checklist_as_evidence() {
|
|
|
1126
1134
|
echo "## PRD Checklist Verification"
|
|
1127
1135
|
echo ""
|
|
1128
1136
|
|
|
1129
|
-
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" python3 -c "
|
|
1137
|
+
_CHECKLIST_RESULTS="$CHECKLIST_RESULTS_FILE" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1130
1138
|
import json, os
|
|
1131
1139
|
try:
|
|
1132
1140
|
data = json.load(open(os.environ['_CHECKLIST_RESULTS']))
|
|
@@ -1162,7 +1170,7 @@ checklist_waiver_load() {
|
|
|
1162
1170
|
if [ ! -f "$waivers_file" ]; then
|
|
1163
1171
|
return 0
|
|
1164
1172
|
fi
|
|
1165
|
-
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
1173
|
+
_WAIVERS_FILE="$waivers_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1166
1174
|
import json, sys, os
|
|
1167
1175
|
try:
|
|
1168
1176
|
waivers_file = os.environ['_WAIVERS_FILE']
|
|
@@ -1186,7 +1194,7 @@ checklist_waiver_add() {
|
|
|
1186
1194
|
local timestamp
|
|
1187
1195
|
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1188
1196
|
|
|
1189
|
-
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
1197
|
+
_WAIVERS_FILE="$waivers_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1190
1198
|
import json, os, sys
|
|
1191
1199
|
|
|
1192
1200
|
waivers_file = os.environ['_WAIVERS_FILE']
|
|
@@ -1239,7 +1247,7 @@ checklist_waiver_remove() {
|
|
|
1239
1247
|
return 1
|
|
1240
1248
|
fi
|
|
1241
1249
|
|
|
1242
|
-
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
1250
|
+
_WAIVERS_FILE="$waivers_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1243
1251
|
import json, os, sys
|
|
1244
1252
|
|
|
1245
1253
|
waivers_file = os.environ['_WAIVERS_FILE']
|
|
@@ -1275,7 +1283,7 @@ checklist_waiver_list() {
|
|
|
1275
1283
|
return 0
|
|
1276
1284
|
fi
|
|
1277
1285
|
|
|
1278
|
-
_WAIVERS_FILE="$waivers_file" python3 -c "
|
|
1286
|
+
_WAIVERS_FILE="$waivers_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
1279
1287
|
import json, os
|
|
1280
1288
|
waivers_file = os.environ['_WAIVERS_FILE']
|
|
1281
1289
|
with open(waivers_file) as f:
|