loki-mode 9.74.0 → 9.76.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 (79) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/completion-council.sh +24 -6
  4. package/autonomy/council-v2.sh +60 -13
  5. package/autonomy/lib/done-recognition.sh +73 -21
  6. package/autonomy/lib/proof-check.sh +43 -8
  7. package/autonomy/lib/voter-agents.sh +42 -5
  8. package/autonomy/loki +3 -0
  9. package/autonomy/prd-checklist.sh +77 -16
  10. package/autonomy/run.sh +38 -3
  11. package/dashboard/__init__.py +1 -1
  12. package/docs/INSTALLATION.md +1 -1
  13. package/docs/R3-COST-OBSERVABILITY-DESIGN.md +7 -1
  14. package/docs/exit-codes.md +21 -0
  15. package/docs/v10/ARCHITECT-CUTS.md +397 -234
  16. package/docs/v10/BOARD.md +84 -44
  17. package/docs/v10/GUARDS.md +45 -28
  18. package/loki-ts/dist/loki.js +341 -338
  19. package/mcp/__init__.py +1 -1
  20. package/package.json +1 -1
  21. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  22. package/web-app/dist/assets/{AdminPage-DaeYgQSE.js → AdminPage-Bw8oPnII.js} +1 -1
  23. package/web-app/dist/assets/{Avatar-CgwQi58_.js → Avatar-BwEtc3RH.js} +1 -1
  24. package/web-app/dist/assets/{Badge-Bwq-lpRh.js → Badge-C_-lfbUl.js} +1 -1
  25. package/web-app/dist/assets/{Button-BEf4Nher.js → Button-BdDGKKeF.js} +1 -1
  26. package/web-app/dist/assets/CockpitPage-Dx9d4fEm.js +47 -0
  27. package/web-app/dist/assets/{ComparePage-tFbdo90G.js → ComparePage-BKtAhhIJ.js} +1 -1
  28. package/web-app/dist/assets/{ErrorBoundary-Brewvgm9.js → ErrorBoundary-BuG16GpG.js} +1 -1
  29. package/web-app/dist/assets/{EvidenceReceiptPanel-h_82QBVa.js → EvidenceReceiptPanel-NATnqxKs.js} +1 -1
  30. package/web-app/dist/assets/{GitHubIssuesPanel-Dcg5xDMm.js → GitHubIssuesPanel-BgOwdOpj.js} +1 -1
  31. package/web-app/dist/assets/{GitHubPRsPanel-DFKzVyA1.js → GitHubPRsPanel-CBJyHI1c.js} +1 -1
  32. package/web-app/dist/assets/{HomePage-D21cM1Je.js → HomePage-BHj17Epq.js} +2 -2
  33. package/web-app/dist/assets/{LoginPage-DJ9yqMn4.js → LoginPage-Dig4H-HO.js} +1 -1
  34. package/web-app/dist/assets/{MagicPage-mwkjJhBy.js → MagicPage-w6WJ35PH.js} +1 -1
  35. package/web-app/dist/assets/{MetricsPage-D2q-PD-s.js → MetricsPage-CvjSUOWn.js} +1 -1
  36. package/web-app/dist/assets/{NotFoundPage-BtusJvgS.js → NotFoundPage-PAcYmGkU.js} +1 -1
  37. package/web-app/dist/assets/{ProjectPage-CRPALtO8.js → ProjectPage-CBkT5JyW.js} +3 -3
  38. package/web-app/dist/assets/{ProjectsPage-DuzABpfc.js → ProjectsPage-Cs6yuWEU.js} +1 -1
  39. package/web-app/dist/assets/{SettingsPage-DhJPX-Rf.js → SettingsPage-BU9F40Yn.js} +1 -1
  40. package/web-app/dist/assets/{ShowcasePage-CvgS47nu.js → ShowcasePage-jCNABTeT.js} +1 -1
  41. package/web-app/dist/assets/{SystemSettingsPage-l3Cv9YjY.js → SystemSettingsPage-Crmc-ryL.js} +1 -1
  42. package/web-app/dist/assets/{TeamsPage-DridGcnR.js → TeamsPage-DN8W-9m5.js} +1 -1
  43. package/web-app/dist/assets/{TemplatesPage-CTNsfp03.js → TemplatesPage-WvaXwZIl.js} +1 -1
  44. package/web-app/dist/assets/{TerminalOutput-BuyksqLh.js → TerminalOutput-Bf_do2tO.js} +1 -1
  45. package/web-app/dist/assets/{activity-DM2TKaBi.js → activity-CMl9JvHl.js} +1 -1
  46. package/web-app/dist/assets/{bell-D2PMLbbX.js → bell-CnjqTAUX.js} +1 -1
  47. package/web-app/dist/assets/{bot-BnN32Jxp.js → bot-BBprDFVS.js} +1 -1
  48. package/web-app/dist/assets/{check-CU128nw_.js → check-DW9Hq4y4.js} +1 -1
  49. package/web-app/dist/assets/{chevron-left-ehNW1Do7.js → chevron-left-DC9pMI2E.js} +1 -1
  50. package/web-app/dist/assets/{circle-alert-Cpw2gtOC.js → circle-alert-BJoll9YV.js} +1 -1
  51. package/web-app/dist/assets/{clock-BOZHdPVS.js → clock-BdHhRTHO.js} +1 -1
  52. package/web-app/dist/assets/{cloud-BZ2L9Fzi.js → cloud-C54B7EO9.js} +1 -1
  53. package/web-app/dist/assets/{code-xml-sSo522gu.js → code-xml-DwQK2vpV.js} +1 -1
  54. package/web-app/dist/assets/{database-DrR1xHzS.js → database-DsWVFir7.js} +1 -1
  55. package/web-app/dist/assets/{dollar-sign-CHE3T_rh.js → dollar-sign-DuRPNc2M.js} +1 -1
  56. package/web-app/dist/assets/{file-code-corner-CgcssyRQ.js → file-code-corner-C4j2P44-.js} +1 -1
  57. package/web-app/dist/assets/{file-plus-DtwfrxYH.js → file-plus-rnI2RZjF.js} +1 -1
  58. package/web-app/dist/assets/{globe-Dzla77iL.js → globe-BbBhsaS5.js} +1 -1
  59. package/web-app/dist/assets/{hammer-BAMdG66t.js → hammer-BxcycH3E.js} +1 -1
  60. package/web-app/dist/assets/{index-mQOnSYoW.js → index-xGj3noKA.js} +2 -2
  61. package/web-app/dist/assets/{layers-DhuyLqmc.js → layers-CcAOhZeY.js} +1 -1
  62. package/web-app/dist/assets/{loader-circle-DclFyQ3B.js → loader-circle-DN_Xjd5e.js} +1 -1
  63. package/web-app/dist/assets/{lock-C8v_bXNl.js → lock-Dn8AMO-D.js} +1 -1
  64. package/web-app/dist/assets/{package-B4AnjM5D.js → package-uroTpCjn.js} +1 -1
  65. package/web-app/dist/assets/{plus-DM95CmZI.js → plus-CYJAEX02.js} +1 -1
  66. package/web-app/dist/assets/{refresh-cw-CWaauzqX.js → refresh-cw-BUGsMChj.js} +1 -1
  67. package/web-app/dist/assets/{rotate-ccw-DxcvylpJ.js → rotate-ccw-C_4D-XGl.js} +1 -1
  68. package/web-app/dist/assets/{scroll-text-Dx-V_BpP.js → scroll-text-DrAJG0H3.js} +1 -1
  69. package/web-app/dist/assets/{server-Cj2EfoGN.js → server-Di6lHsM9.js} +1 -1
  70. package/web-app/dist/assets/{shield-alert-OlmWv7sU.js → shield-alert-DH1bNjkD.js} +1 -1
  71. package/web-app/dist/assets/{thumbs-up-CIpJhyTd.js → thumbs-up-Enk3ppj_.js} +1 -1
  72. package/web-app/dist/assets/{trash-2-BIkoZkY6.js → trash-2-CezqQT3x.js} +1 -1
  73. package/web-app/dist/assets/{trending-down-CI-OcCGt.js → trending-down-B_ChI0U7.js} +1 -1
  74. package/web-app/dist/assets/{trending-up-B0dpgKi-.js → trending-up-BtsvHd_-.js} +1 -1
  75. package/web-app/dist/assets/{upload-NhOBAjKA.js → upload-XiWa_WBM.js} +1 -1
  76. package/web-app/dist/assets/{usePolling-_QRF6f7x.js → usePolling-B6Hxwbk1.js} +1 -1
  77. package/web-app/dist/assets/{user-kRNvOpun.js → user-oL_Oya-S.js} +1 -1
  78. package/web-app/dist/index.html +1 -1
  79. package/web-app/dist/assets/CockpitPage-Bj41Xtg9.js +0 -47
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.74.0
6
+ # Loki Mode v9.76.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.74.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.76.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.74.0
1
+ 9.76.0
@@ -4368,19 +4368,34 @@ council_managed_should_stop() {
4368
4368
  return 1
4369
4369
  fi
4370
4370
 
4371
+ # S-196: every reader and the session heredoc run on the resolved -I -S
4372
+ # interpreter (see _loki_snapshot_py_tool); -E still loads user-site .pth
4373
+ # files. None resolvable -> never start the managed session: Bash fallback.
4374
+ # Deliberate consequence: -S hides site-packages, so the anthropic SDK is
4375
+ # never importable and is_enabled() is always False. The managed council
4376
+ # therefore always falls back to Bash voting (warned below). Reviving it
4377
+ # needs a pinned interpreter plus an SDK path that loads no .pth file;
4378
+ # a PATH or venv python3 without -S would let the agent forge the verdict.
4379
+ local _ms_py
4380
+ _ms_py="$(_loki_snapshot_py_tool)" || {
4381
+ log_warn "[Council] Managed completion council unavailable: no isolated -I -S interpreter; falling back to Bash voting"
4382
+ return 1
4383
+ }
4384
+
4371
4385
  local loki_dir="${TARGET_DIR:-.}/.loki"
4372
- local project_dir="${PROJECT_DIR:-$(pwd)}"
4373
4386
  local round="${ITERATION_COUNT:-0}"
4374
4387
  local verdicts_dir="$COUNCIL_STATE_DIR/verdicts"
4375
4388
  mkdir -p "$verdicts_dir" 2>/dev/null || true
4376
4389
 
4377
4390
  # Build session context: diff_summary, test_summary, pending_tasks.
4378
4391
  # Kept deliberately small so we never choke the session budget.
4392
+ # S-196: diff the target project (same root as loki_dir and
4393
+ # LOKI_TARGET_DIR), never PROJECT_DIR, which is Loki's install tree.
4379
4394
  local diff_summary=""
4380
- diff_summary=$(cd "$project_dir" 2>/dev/null && git diff --stat 2>/dev/null | tail -20 | tr '\n' ' ' || echo "")
4395
+ diff_summary=$(cd "${TARGET_DIR:-.}" 2>/dev/null && git diff --stat 2>/dev/null | tail -20 | tr '\n' ' ' || echo "")
4381
4396
  local test_summary=""
4382
4397
  if [ -f "$loki_dir/quality/test-results.json" ]; then
4383
- test_summary=$(_TRF="$loki_dir/quality/test-results.json" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
4398
+ test_summary=$(_TRF="$loki_dir/quality/test-results.json" "$_ms_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
4384
4399
  import json, os
4385
4400
  try:
4386
4401
  d = json.load(open(os.environ['_TRF']))
@@ -4391,7 +4406,7 @@ except Exception:
4391
4406
  fi
4392
4407
  local pending_tasks="[]"
4393
4408
  if [ -f "$loki_dir/queue/pending.json" ]; then
4394
- pending_tasks=$(_QF="$loki_dir/queue/pending.json" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
4409
+ pending_tasks=$(_QF="$loki_dir/queue/pending.json" "$_ms_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
4395
4410
  import json, os
4396
4411
  try:
4397
4412
  d = json.load(open(os.environ['_QF']))
@@ -4415,7 +4430,7 @@ except Exception:
4415
4430
  _CC_LOKI_DIR="$loki_dir" \
4416
4431
  LOKI_TARGET_DIR="${TARGET_DIR:-$(pwd)}" \
4417
4432
  PROJECT_DIR="${PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)}" \
4418
- python3 -E - <<'PYEOF' 2>/dev/null || exit_code=$?
4433
+ "$_ms_py" -I -S - <<'PYEOF' 2>/dev/null || exit_code=$?
4419
4434
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
4420
4435
  import json, os, sys, pathlib
4421
4436
 
@@ -4553,7 +4568,10 @@ PYEOF
4553
4568
  return 0
4554
4569
  fi
4555
4570
 
4556
- log_warn "[Council] Managed completion council unavailable (exit=$exit_code); falling back to Bash voting"
4571
+ case "$exit_code" in
4572
+ 2|3) log_warn "[Council] Managed completion council unavailable (exit=$exit_code): SDK not importable under the isolated -I -S interpreter; falling back to Bash voting" ;;
4573
+ *) log_warn "[Council] Managed completion council unavailable (exit=$exit_code); falling back to Bash voting" ;;
4574
+ esac
4557
4575
  return 1
4558
4576
  }
4559
4577
 
@@ -20,15 +20,43 @@
20
20
  # - swarm/calibration.py (reviewer calibration)
21
21
  #
22
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).
23
+ # every python here drops '' and '.' from sys.path before any other import (the
24
+ # swarm dir is inserted after that); a committed json.py or sitecustomize.py
25
+ # cannot read the votes. Pinned by tests/test-council-no-fabricated-verdict.sh
26
+ # (D7 section). S-201: -E alone still loads user site-packages, whose .pth
27
+ # "import" lines run before that scrub and can forge json.load, so every site
28
+ # runs the _loki_snapshot_py_tool interpreter -I -S. Pinned by
29
+ # tests/test-council-v2-no-user-site-pth.sh.
27
30
  #
28
31
  #===============================================================================
29
32
 
30
33
  COUNCIL_V2_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
34
 
35
+ # Guarded copy for callers that source this file on its own (tests). In
36
+ # production completion-council.sh (or run.sh) has already defined it. run.sh
37
+ # is the source of truth: keep this copy byte-identical to it.
38
+ declare -F _loki_snapshot_py_tool >/dev/null 2>&1 || \
39
+ _loki_snapshot_py_tool() {
40
+ local c
41
+ for c in /usr/bin/python3 /bin/python3; do
42
+ [ -x "$c" ] && [ ! -d "$c" ] && "$c" -I -S -c '' >/dev/null 2>&1 && { printf '%s\n' "$c"; return 0; }
43
+ done
44
+ local dir
45
+ local IFS=:
46
+ for dir in $PATH; do
47
+ case "$dir" in
48
+ /*) ;;
49
+ *) continue ;;
50
+ esac
51
+ if [ -x "$dir/python3" ] && [ ! -d "$dir/python3" ] \
52
+ && "$dir/python3" -I -S -c '' >/dev/null 2>&1; then
53
+ printf '%s\n' "$dir/python3"
54
+ return 0
55
+ fi
56
+ done
57
+ return 1
58
+ }
59
+
32
60
  #===============================================================================
33
61
  # council_v2_vote() -- Main entry point for v2 voting
34
62
  #
@@ -48,6 +76,9 @@ council_v2_vote() {
48
76
  local iteration="${4:-0}"
49
77
 
50
78
  local council_size="${COUNCIL_SIZE:-3}"
79
+ # S-201: none resolvable -> every read below takes its fail-safe fallback.
80
+ local _c2_py
81
+ _c2_py="$(_loki_snapshot_py_tool)" || _c2_py=""
51
82
 
52
83
  log_header "COMPLETION COUNCIL v2 - Iteration $iteration"
53
84
  log_info "Convening ${council_size}-member blind review council..."
@@ -103,7 +134,7 @@ council_v2_vote() {
103
134
  votes_json="$votes_json$vote_content"
104
135
 
105
136
  local verdict
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")
137
+ verdict=$(echo "$vote_content" | "$_c2_py" -I -S -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")
107
138
  # TRUST: a reviewer that could not be REACHED did not vote REJECT.
108
139
  #
109
140
  # Every provider arm below used to substitute a literal
@@ -141,14 +172,21 @@ council_v2_vote() {
141
172
 
142
173
  # Step 4: Sycophancy detection
143
174
  local sycophancy_score
144
- sycophancy_score=$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
175
+ sycophancy_score=$("$_c2_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
145
176
  import sys
146
177
  sys.path.insert(0, '${COUNCIL_V2_DIR}/../swarm')
147
178
  from sycophancy import detect_sycophancy
148
179
  import json
149
180
  votes = json.loads(sys.argv[1])
150
181
  print('{:.3f}'.format(detect_sycophancy(votes)))
151
- " "$votes_json" 2>/dev/null || echo "0.000")
182
+ " "$votes_json" 2>/dev/null) || sycophancy_score=""
183
+ # S-201: a crashed or missing detector did not measure 0.000. Record the
184
+ # score as unmeasured (null keeps summary.json valid JSON) so Step 5
185
+ # challenges instead of reading "independent".
186
+ case "$sycophancy_score" in
187
+ [0-9]*.[0-9][0-9][0-9]) ;;
188
+ *) sycophancy_score="null" ;;
189
+ esac
152
190
 
153
191
  log_info "Sycophancy score: $sycophancy_score"
154
192
 
@@ -156,10 +194,17 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
156
194
  if [ "$approve_count" -eq "$council_size" ]; then
157
195
  local threshold="${LOKI_COUNCIL_SYCOPHANCY_THRESHOLD:-0.6}"
158
196
  local should_challenge
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")
197
+ # S-201: an unmeasured score, or a compare that cannot run, challenges.
198
+ # Only a measured score below the threshold skips the devil's advocate.
199
+ if [ "$sycophancy_score" = "null" ]; then
200
+ should_challenge="yes"
201
+ else
202
+ should_challenge=$("$_c2_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; print('no' if float(sys.argv[1]) < float(sys.argv[2]) else 'yes')" "$sycophancy_score" "$threshold" 2>/dev/null) || should_challenge="yes"
203
+ [ "$should_challenge" = "no" ] || should_challenge="yes"
204
+ fi
160
205
 
161
206
  if [ "$should_challenge" = "yes" ]; then
162
- log_warn "Sycophancy score $sycophancy_score >= $threshold -- adding devil's advocate"
207
+ log_warn "Sycophancy score $sycophancy_score (threshold $threshold, null = unmeasured) -- adding devil's advocate"
163
208
  # Run devil's advocate with fresh evidence (no visibility of other votes)
164
209
  local da_dir
165
210
  da_dir=$(mktemp -d)
@@ -173,7 +218,7 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
173
218
  # Same trust rule as the main tally: an unparseable devil's
174
219
  # advocate did not vote REJECT. Defaulting to REJECT here would
175
220
  # silently overturn a unanimous APPROVE on a transient failure.
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")
221
+ da_verdict=$(cat "$da_vote" | "$_c2_py" -I -S -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")
177
222
  if [ "$da_verdict" = "INCONCLUSIVE" ]; then
178
223
  log_warn "Devil's advocate produced no verdict -- unanimous approval left UNCHANGED (not overturned)"
179
224
  elif [ "$da_verdict" = "REJECT" ]; then
@@ -207,7 +252,7 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
207
252
  final_decision="reject"
208
253
  fi
209
254
 
210
- python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
255
+ "$_c2_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
211
256
  import sys
212
257
  sys.path.insert(0, '${COUNCIL_V2_DIR}/../swarm')
213
258
  from calibration import CalibrationTracker
@@ -271,6 +316,8 @@ council_v2_run_reviewer() {
271
316
  local role="$1"
272
317
  local review_dir="$2"
273
318
  local output_file="$3"
319
+ local _c2_py
320
+ _c2_py="$(_loki_snapshot_py_tool)" || _c2_py=""
274
321
 
275
322
  # Build review prompt based on role
276
323
  local prompt
@@ -394,7 +441,7 @@ Respond ONLY with a valid JSON object. No markdown fencing."
394
441
  --json-schema "$_c2_schema_content" --output-format json 2>/dev/null)" || _c2_json=""
395
442
  if [ -n "$_c2_json" ]; then
396
443
  # Pull the schema-validated object out of the envelope.
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
444
+ result="$(printf '%s' "$_c2_json" | "$_c2_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; import json
398
445
  try:
399
446
  e=json.load(sys.stdin)
400
447
  p=e.get("structured_output")
@@ -496,7 +543,7 @@ except Exception:
496
543
  # or neither, stays INCONCLUSIVE -- never guessed.
497
544
  if [ -z "$extracted" ] && [ -n "${result:-}" ]; then
498
545
  local _recovered
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 ("", ".")]
546
+ _recovered="$(printf '%s' "$result" | _LOKI_RAW="$result" "$_c2_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
500
547
  import os, re, sys, json
501
548
  raw = os.environ.get("_LOKI_RAW", "")
502
549
  # Standalone words only: "APPROVE" not "approved-by", and not inside a URL.
@@ -38,9 +38,42 @@
38
38
  # No emojis. No em dashes. bash 3.2 safe. Honors `set -uo pipefail`.
39
39
  #
40
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).
41
+ # every python3 here drops '' and '.' from sys.path before any other import; a
42
+ # committed json.py or sitecustomize.py cannot turn the verdict done. Pinned by
43
+ # tests/test-reuse-done-recognition.sh case (d7).
44
+ #
45
+ # S-200 (BACKLOG 54): -E alone still loads the user site-packages, whose .pth
46
+ # "import" lines run before that scrub and can forge json.load (a forged verdict
47
+ # read fast-stopped an incomplete project as done). Every call site resolves
48
+ # its interpreter through _loki_snapshot_py_tool and runs it -I -S; when none
49
+ # resolves, nothing reads as done or met (the gate builds, the writers write
50
+ # nothing). See that function's comment in run.sh for the rationale and accepted
51
+ # gaps. run.sh sources this file AFTER it defines _loki_snapshot_py_tool, so in
52
+ # production run.sh's definition is kept; the copy below only serves callers
53
+ # that source this file on its own (tests). run.sh is the source of truth: keep
54
+ # this copy byte-identical to it. Pinned by
55
+ # tests/test-done-recognition-no-user-site-pth.sh.
56
+ declare -F _loki_snapshot_py_tool >/dev/null 2>&1 || \
57
+ _loki_snapshot_py_tool() {
58
+ local c
59
+ for c in /usr/bin/python3 /bin/python3; do
60
+ [ -x "$c" ] && [ ! -d "$c" ] && "$c" -I -S -c '' >/dev/null 2>&1 && { printf '%s\n' "$c"; return 0; }
61
+ done
62
+ local dir
63
+ local IFS=:
64
+ for dir in $PATH; do
65
+ case "$dir" in
66
+ /*) ;;
67
+ *) continue ;;
68
+ esac
69
+ if [ -x "$dir/python3" ] && [ ! -d "$dir/python3" ] \
70
+ && "$dir/python3" -I -S -c '' >/dev/null 2>&1; then
71
+ printf '%s\n' "$dir/python3"
72
+ return 0
73
+ fi
74
+ done
75
+ return 1
76
+ }
44
77
 
45
78
  # Bound the single model call so a huge PRD or test log cannot run away.
46
79
  : "${LOKI_DONE_RECOG_TIMEOUT:=180}" # seconds for the single model call
@@ -133,8 +166,9 @@ _loki_done_recog_invoke() {
133
166
  claude --dangerously-skip-permissions -p "$prompt" \
134
167
  --json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
135
168
  fi
136
- if [ -n "$_dr_json" ]; then
137
- _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
169
+ local _dr_py
170
+ if [ -n "$_dr_json" ] && _dr_py="$(_loki_snapshot_py_tool)"; then
171
+ _dr_obj=$(printf '%s' "$_dr_json" | "$_dr_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; import json
138
172
  try:
139
173
  e=json.load(sys.stdin)
140
174
  p=e.get("structured_output")
@@ -206,7 +240,9 @@ _loki_done_recog_prd_sha() {
206
240
  sha256sum "$prd_file" 2>/dev/null | awk '{print $1}'
207
241
  else
208
242
  # Last-resort: a python hash, still deterministic over the same bytes.
209
- LOKI_DR_PRD="$prd_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
243
+ local _dr_py
244
+ _dr_py="$(_loki_snapshot_py_tool)" || { printf ''; return 0; }
245
+ LOKI_DR_PRD="$prd_file" "$_dr_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
210
246
  import hashlib, os
211
247
  p = os.environ.get('LOKI_DR_PRD','')
212
248
  try:
@@ -278,6 +314,13 @@ reuse_done_recognition_gate() {
278
314
  fi
279
315
  local _test_results="$loki_dir/quality/test-results.json"
280
316
 
317
+ # --- Resolve the -I -S interpreter (S-200); none -> inconclusive -> build.
318
+ local _dr_py
319
+ if ! _dr_py="$(_loki_snapshot_py_tool)"; then
320
+ log_info "Done-recognition: no isolated python3 interpreter resolved; proceeding to build."
321
+ return 1
322
+ fi
323
+
281
324
  # --- Build the model prompt payload (python: bounded, defensive) ---------
282
325
  local prompt
283
326
  prompt=$(LOKI_DR_PRD="$prd_path" \
@@ -287,7 +330,7 @@ reuse_done_recognition_gate() {
287
330
  LOKI_DR_CHECKLIST="$loki_dir/checklist/checklist.json" \
288
331
  LOKI_DR_MAX_PRD="${LOKI_DONE_RECOG_MAX_PRD_CHARS}" \
289
332
  LOKI_DR_MAX_TEST="${LOKI_DONE_RECOG_MAX_TEST_CHARS}" \
290
- python3 -E << 'DR_PROMPT_EOF'
333
+ "$_dr_py" -I -S << 'DR_PROMPT_EOF'
291
334
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
292
335
  import json, os, sys
293
336
 
@@ -389,7 +432,7 @@ DR_PROMPT_EOF
389
432
  LOKI_DR_TESTS="$_test_results" \
390
433
  LOKI_DR_ACTION="$action" \
391
434
  LOKI_DR_PRD="$prd_path" \
392
- python3 -E << 'DR_PARSE_EOF'
435
+ "$_dr_py" -I -S << 'DR_PARSE_EOF'
393
436
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
394
437
  import json, os, re, sys
395
438
 
@@ -700,11 +743,11 @@ DR_PARSE_EOF
700
743
  fi
701
744
 
702
745
  local verdict
703
- 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)
746
+ verdict=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
704
747
 
705
748
  case "$verdict" in
706
749
  done)
707
- _loki_done_recog_finish "$prd_path" "$parsed"
750
+ _loki_done_recog_finish "$prd_path" "$parsed" || return 1
708
751
  return 0
709
752
  ;;
710
753
  incomplete)
@@ -713,7 +756,7 @@ DR_PARSE_EOF
713
756
  ;;
714
757
  *)
715
758
  local _reason
716
- _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)
759
+ _reason=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
717
760
  log_info "Done-recognition: could not confirm the existing code already satisfies the reused spec (${_reason}). Proceeding to build to be safe."
718
761
  return 1
719
762
  ;;
@@ -729,6 +772,10 @@ _loki_done_recog_finish() {
729
772
  local parsed="$2"
730
773
  local loki_dir="${TARGET_DIR:-.}/.loki"
731
774
 
775
+ # S-200: no -I -S interpreter -> write no done record (the gate then builds).
776
+ local _dr_py
777
+ _dr_py="$(_loki_snapshot_py_tool)" || return 1
778
+
732
779
  # The gate runs EARLY in run_autonomous, before the run normally mints these
733
780
  # run-scoped ids/baselines (run.sh sets them just after this call site). The
734
781
  # council-parity finalizers below (and what they transitively call) expect
@@ -749,10 +796,10 @@ _loki_done_recog_finish() {
749
796
  mkdir -p "$loki_dir/state" 2>/dev/null || true
750
797
 
751
798
  local summary met total axis
752
- 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)
753
- 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)
754
- 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)
755
- 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)
799
+ summary=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
800
+ met=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
801
+ total=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
802
+ axis=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
756
803
  [ -n "$met" ] || met=0
757
804
  [ -n "$total" ] || total=0
758
805
  [ -n "$axis" ] || axis="unknown"
@@ -792,7 +839,7 @@ _loki_done_recog_finish() {
792
839
  echo ""
793
840
  echo "## Per-requirement evidence"
794
841
  echo ""
795
- printf '%s' "$parsed" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
842
+ printf '%s' "$parsed" | "$_dr_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
796
843
  import json, sys
797
844
  try:
798
845
  d = json.load(sys.stdin)
@@ -811,7 +858,7 @@ for t in d.get('satisfied', []):
811
858
  LOKI_DR_MET="$met" \
812
859
  LOKI_DR_TOTAL="$total" \
813
860
  LOKI_DR_TS="$ts" \
814
- python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
861
+ "$_dr_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
815
862
  import json, os, tempfile
816
863
  out = os.environ['LOKI_DR_OUT']
817
864
  def i(v):
@@ -861,6 +908,7 @@ except Exception:
861
908
  log_info "Verified ${met}/${total} requirements met by code inspection (no passing test run was available to confirm). ${summary}"
862
909
  fi
863
910
  log_info "To rebuild from scratch run 'loki start --fresh-prd'; to extend it, edit the spec or pass a new/changed PRD."
911
+ return 0
864
912
  }
865
913
 
866
914
  # incomplete path: write the satisfied-requirements manifest so
@@ -871,6 +919,10 @@ _loki_done_recog_write_manifest() {
871
919
  local parsed="$2"
872
920
  local loki_dir="${TARGET_DIR:-.}/.loki"
873
921
 
922
+ # S-200: no -I -S interpreter -> record nothing as met (full build).
923
+ local _dr_py
924
+ _dr_py="$(_loki_snapshot_py_tool)" || return 1
925
+
874
926
  mkdir -p "$loki_dir/state" 2>/dev/null || true
875
927
 
876
928
  local prd_sha
@@ -882,7 +934,7 @@ _loki_done_recog_write_manifest() {
882
934
  LOKI_DR_PARSED="$parsed" \
883
935
  LOKI_DR_SHA="$prd_sha" \
884
936
  LOKI_DR_TS="$ts" \
885
- python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
937
+ "$_dr_py" -I -S -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
886
938
  import json, os, sys, tempfile
887
939
  out = os.environ['LOKI_DR_OUT']
888
940
  try:
@@ -923,7 +975,7 @@ except Exception:
923
975
  if [ -f "$loki_dir/queue/pending.json" ]; then
924
976
  # Drop prior PRD-sourced tasks so the incremental pass is the source of
925
977
  # truth; non-PRD tasks (if any) are preserved.
926
- LOKI_DR_PENDING="$loki_dir/queue/pending.json" python3 -E - <<'RESET_EOF' 2>/dev/null || true
978
+ LOKI_DR_PENDING="$loki_dir/queue/pending.json" "$_dr_py" -I -S - <<'RESET_EOF' 2>/dev/null || true
927
979
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
928
980
  import json, os, tempfile
929
981
  p = os.environ.get("LOKI_DR_PENDING", ".loki/queue/pending.json")
@@ -948,8 +1000,8 @@ RESET_EOF
948
1000
  fi
949
1001
 
950
1002
  local met total
951
- 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)
952
- 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)
1003
+ met=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
1004
+ total=$(printf '%s' "$parsed" | "$_dr_py" -I -S -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)
953
1005
  local unmet=$(( ${total:-0} - ${met:-0} ))
954
1006
  log_info "Done-recognition: ${met:-0} of ${total:-0} requirements already satisfied; building only the ${unmet} unmet. Pass --fresh-prd to rebuild from scratch."
955
1007
  }
@@ -28,9 +28,41 @@
28
28
  # guards on LOKI_PROVEN_PR_CHECK=1). It is also safe if called directly.
29
29
  #
30
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
31
+ # readers drop '' and '.' from sys.path first; a committed json.py or
32
+ # sitecustomize.py cannot change the headline. Pinned by
33
33
  # tests/test-proven-pr-check.sh (D7 section).
34
+ #
35
+ # BACKLOG 54 / S-203: python3 -E still loaded the user site-packages, where a
36
+ # .pth "import" line runs before any sys.path scrub and could forge json.load
37
+ # (a NOT VERIFIED proof read as VERIFIED). The readers resolve their
38
+ # interpreter through _loki_snapshot_py_tool and run it -I -S; when none
39
+ # resolves they print empty, as they do when python3 is absent. run.sh sources
40
+ # this file before it defines _loki_snapshot_py_tool, and its later definition
41
+ # replaces this copy, so the copy below only serves callers that source this
42
+ # file on its own (tests). run.sh is the source of truth: keep this copy
43
+ # byte-identical to it. Pinned by tests/test-proof-check-no-user-site-pth.sh
44
+ # and tests/test-council-py-tool-identity.sh.
45
+ declare -F _loki_snapshot_py_tool >/dev/null 2>&1 || \
46
+ _loki_snapshot_py_tool() {
47
+ local c
48
+ for c in /usr/bin/python3 /bin/python3; do
49
+ [ -x "$c" ] && [ ! -d "$c" ] && "$c" -I -S -c '' >/dev/null 2>&1 && { printf '%s\n' "$c"; return 0; }
50
+ done
51
+ local dir
52
+ local IFS=:
53
+ for dir in $PATH; do
54
+ case "$dir" in
55
+ /*) ;;
56
+ *) continue ;;
57
+ esac
58
+ if [ -x "$dir/python3" ] && [ ! -d "$dir/python3" ] \
59
+ && "$dir/python3" -I -S -c '' >/dev/null 2>&1; then
60
+ printf '%s\n' "$dir/python3"
61
+ return 0
62
+ fi
63
+ done
64
+ return 1
65
+ }
34
66
 
35
67
  # Double-source guard.
36
68
  [ -n "${_PROOF_CHECK_SH:-}" ] && return 0
@@ -71,10 +103,11 @@ _proof_check_headline() {
71
103
  local proof_path="${1:-}"
72
104
  [ -n "$proof_path" ] || { printf '%s' ""; return 0; }
73
105
  [ -f "$proof_path" ] || { printf '%s' ""; return 0; }
74
- command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
106
+ local _pc_py=""
107
+ _pc_py="$(_loki_snapshot_py_tool)" || { printf '%s' ""; return 0; }
75
108
 
76
109
  local headline=""
77
- headline="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
110
+ headline="$("$_pc_py" -I -S - "$proof_path" <<'PY' 2>/dev/null || true
78
111
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
79
112
  import json, sys
80
113
  try:
@@ -103,10 +136,11 @@ _proof_check_proof_head_sha() {
103
136
  local proof_path="${1:-}"
104
137
  [ -n "$proof_path" ] || { printf '%s' ""; return 0; }
105
138
  [ -f "$proof_path" ] || { printf '%s' ""; return 0; }
106
- command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
139
+ local _pc_py=""
140
+ _pc_py="$(_loki_snapshot_py_tool)" || { printf '%s' ""; return 0; }
107
141
 
108
142
  local sha=""
109
- sha="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
143
+ sha="$("$_pc_py" -I -S - "$proof_path" <<'PY' 2>/dev/null || true
110
144
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
111
145
  import json, sys
112
146
  try:
@@ -138,10 +172,11 @@ _proof_check_run_id() {
138
172
  local proof_path="${1:-}"
139
173
  [ -n "$proof_path" ] || { printf '%s' ""; return 0; }
140
174
  [ -f "$proof_path" ] || { printf '%s' ""; return 0; }
141
- command -v python3 >/dev/null 2>&1 || { printf '%s' ""; return 0; }
175
+ local _pc_py=""
176
+ _pc_py="$(_loki_snapshot_py_tool)" || { printf '%s' ""; return 0; }
142
177
 
143
178
  local rid=""
144
- rid="$(python3 -E - "$proof_path" <<'PY' 2>/dev/null || true
179
+ rid="$("$_pc_py" -I -S - "$proof_path" <<'PY' 2>/dev/null || true
145
180
  import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
146
181
  import json, sys
147
182
  try:
@@ -28,9 +28,18 @@
28
28
  # to existing heuristic dispatch).
29
29
  #
30
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.
31
+ # every python3 here drops '' and '.' from sys.path before any other import; a
32
+ # committed json.py or sitecustomize.py cannot read the votes.
33
33
  # Pinned by tests/test-voter-agents-json.sh (Case D).
34
+ #
35
+ # S-202 (BACKLOG 54): -E alone still loads the user site-packages, whose .pth
36
+ # "import" lines run before that scrub and can forge json.loads (3 REJECT
37
+ # findings read as a COMPLETE round). Every call site resolves its interpreter
38
+ # through _loki_snapshot_py_tool and runs it -I -S; when none resolves, the
39
+ # dispatch returns 1 and completion-council's fail-closed path applies. run.sh
40
+ # is the source of truth for the helper; the guarded copy below only serves
41
+ # callers that source this file on its own. Keep it byte-identical to run.sh's.
42
+ # Pinned by tests/test-voter-agents-no-user-site-pth.sh.
34
43
 
35
44
  # Guard against double-source.
36
45
  if [ "${__LOKI_VOTER_AGENTS_SH_LOADED:-0}" = "1" ]; then
@@ -38,6 +47,28 @@ if [ "${__LOKI_VOTER_AGENTS_SH_LOADED:-0}" = "1" ]; then
38
47
  fi
39
48
  __LOKI_VOTER_AGENTS_SH_LOADED=1
40
49
 
50
+ declare -F _loki_snapshot_py_tool >/dev/null 2>&1 || \
51
+ _loki_snapshot_py_tool() {
52
+ local c
53
+ for c in /usr/bin/python3 /bin/python3; do
54
+ [ -x "$c" ] && [ ! -d "$c" ] && "$c" -I -S -c '' >/dev/null 2>&1 && { printf '%s\n' "$c"; return 0; }
55
+ done
56
+ local dir
57
+ local IFS=:
58
+ for dir in $PATH; do
59
+ case "$dir" in
60
+ /*) ;;
61
+ *) continue ;;
62
+ esac
63
+ if [ -x "$dir/python3" ] && [ ! -d "$dir/python3" ] \
64
+ && "$dir/python3" -I -S -c '' >/dev/null 2>&1; then
65
+ printf '%s\n' "$dir/python3"
66
+ return 0
67
+ fi
68
+ done
69
+ return 1
70
+ }
71
+
41
72
  # Resolve repo root once. This file lives at autonomy/lib/voter-agents.sh.
42
73
  __LOKI_VA_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
43
74
  __LOKI_VA_REPO_ROOT="$(cd "${__LOKI_VA_LIB_DIR}/../.." 2>/dev/null && pwd)"
@@ -59,12 +90,14 @@ loki_voter_agents_json() {
59
90
  fi
60
91
  local prd="${LOKI_PRD_PATH:-}"
61
92
  local tier="${LOKI_RARV_TIER:-development}"
93
+ local _va_py
94
+ _va_py="$(_loki_snapshot_py_tool)" || { printf '%s' '{}'; return 0; }
62
95
 
63
96
  _VA_ITER="$iter" \
64
97
  _VA_PRD="$prd" \
65
98
  _VA_TIER="$tier" \
66
99
  _VA_COMPLEXITY="${LOKI_COMPLEXITY:-standard}" \
67
- python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
100
+ "$_va_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
68
101
  import json, os
69
102
  iter_n = os.environ.get("_VA_ITER", "0")
70
103
  prd = os.environ.get("_VA_PRD", "")
@@ -133,7 +166,9 @@ print(json.dumps(agents, separators=(",", ":")))
133
166
  # Arg $1: terse base findings summary used to brief the DA prompt.
134
167
  loki_devils_advocate_json() {
135
168
  local summary="${1:-}"
136
- _VA_SUMMARY="$summary" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
169
+ local _va_py
170
+ _va_py="$(_loki_snapshot_py_tool)" || { printf '%s' '{}'; return 0; }
171
+ _VA_SUMMARY="$summary" "$_va_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
137
172
  import json, os
138
173
  summary = os.environ.get("_VA_SUMMARY", "")[:1000]
139
174
  agents = {
@@ -355,13 +390,15 @@ loki_council_dispatch_agents() {
355
390
  local verdicts_dir="$COUNCIL_STATE_DIR/verdicts"
356
391
  local votes_dir="$COUNCIL_STATE_DIR/votes"
357
392
  mkdir -p "$verdicts_dir" "$votes_dir" 2>/dev/null || return 1
393
+ local _va_py
394
+ _va_py="$(_loki_snapshot_py_tool)" || return 1
358
395
 
359
396
  _VA_RESP="$response" \
360
397
  _VA_ITER="$iteration" \
361
398
  _VA_VDIR="$verdicts_dir" \
362
399
  _VA_RFILE="$votes_dir/round-${iteration}.json" \
363
400
  _VA_EXPECTED="${COUNCIL_SIZE:-3}" \
364
- python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
401
+ "$_va_py" -I -S -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
365
402
  import json, os, sys
366
403
  from datetime import datetime, timezone
367
404