loki-mode 7.19.1 → 7.19.3
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 +282 -0
- package/autonomy/config.example.yaml +26 -0
- package/autonomy/lib/proof-generator.py +20 -1
- package/autonomy/lib/proof-template.html +211 -16
- package/autonomy/run.sh +55 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/SHAREABLE-PROOF-PLAN.md +194 -0
- package/docs/UNCERTAINTY-ESCALATION-PLAN.md +396 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/skills/quality-gates.md +85 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.19.
|
|
6
|
+
# Loki Mode v7.19.3
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
383
383
|
|
|
384
384
|
---
|
|
385
385
|
|
|
386
|
-
**v7.19.
|
|
386
|
+
**v7.19.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.19.
|
|
1
|
+
7.19.3
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
# LOKI_COUNCIL_CONVERGENCE_WINDOW - Iterations to track for convergence (default: 3)
|
|
29
29
|
# LOKI_COUNCIL_STAGNATION_LIMIT - Max iterations with no git changes (default: 5)
|
|
30
30
|
# LOKI_COUNCIL_DONE_SIGNAL_LIMIT - Max total done signals before force stop (default: 10)
|
|
31
|
+
# LOKI_UNCERTAINTY_ESCALATION - Proactive stuck-escalation decision (default: 1; set 0 to disable, byte-identical)
|
|
32
|
+
# LOKI_UNCERTAINTY_ROUNDS - Consecutive co-occurrence rounds before escalate (default: 2)
|
|
33
|
+
# LOKI_UNCERTAINTY_NOCHANGE_MIN - Proxy 1 threshold on consecutive_no_change (default: COUNCIL_STAGNATION_LIMIT - 1)
|
|
34
|
+
# LOKI_UNCERTAINTY_SPLIT_ROUNDS - Proxy 3 trailing split-round run length (default: 2)
|
|
31
35
|
#
|
|
32
36
|
# Usage:
|
|
33
37
|
# source autonomy/completion-council.sh
|
|
@@ -221,6 +225,7 @@ council_track_iteration() {
|
|
|
221
225
|
_COUNCIL_TOTAL_DONE_SIGNALS="$COUNCIL_TOTAL_DONE_SIGNALS" \
|
|
222
226
|
_COUNCIL_ITERATION="${ITERATION_COUNT:-0}" \
|
|
223
227
|
_COUNCIL_FILES_CHANGED="$files_changed" \
|
|
228
|
+
_COUNCIL_DIFF_HASH="$combined_hash" \
|
|
224
229
|
python3 -c "
|
|
225
230
|
import json, os
|
|
226
231
|
state_file = os.environ['_COUNCIL_STATE_FILE']
|
|
@@ -234,6 +239,7 @@ state['done_signals'] = int(os.environ['_COUNCIL_DONE_SIGNALS'])
|
|
|
234
239
|
state['total_done_signals'] = int(os.environ['_COUNCIL_TOTAL_DONE_SIGNALS'])
|
|
235
240
|
state['last_track_iteration'] = int(os.environ['_COUNCIL_ITERATION'])
|
|
236
241
|
state['files_changed'] = int(os.environ['_COUNCIL_FILES_CHANGED'])
|
|
242
|
+
state['last_diff_hash'] = os.environ['_COUNCIL_DIFF_HASH']
|
|
237
243
|
with open(state_file, 'w') as f:
|
|
238
244
|
json.dump(state, f, indent=2)
|
|
239
245
|
" || log_warn "Failed to update council tracking state"
|
|
@@ -263,6 +269,282 @@ council_circuit_breaker_triggered() {
|
|
|
263
269
|
return 1
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
#===============================================================================
|
|
273
|
+
# Uncertainty-Gated Escalation - pure stuck-detection DECISION function
|
|
274
|
+
#
|
|
275
|
+
# Returns 0 = escalate now, 1 = do not escalate. Reads ONLY persisted state
|
|
276
|
+
# (the council state.json for the three proxies, plus its own uncertainty.json
|
|
277
|
+
# for the ring buffer + co-occurrence streak + debounce flag). Mutates only its
|
|
278
|
+
# own uncertainty.json (atomic temp+mv). Fires NO notifications and touches NO
|
|
279
|
+
# PAUSE file: the run.sh action site interprets the return code and performs the
|
|
280
|
+
# side effects. This keeps the function sourceable and testable in isolation.
|
|
281
|
+
#
|
|
282
|
+
# Three proxies (all read from state, no live shell vars, no git calls):
|
|
283
|
+
# P1 (no-change) : state.json consecutive_no_change >= NOCHANGE_MIN
|
|
284
|
+
# (default COUNCIL_STAGNATION_LIMIT - 1, i.e. approaching
|
|
285
|
+
# the circuit-breaker limit).
|
|
286
|
+
# P2 (oscillation) : current state.json last_diff_hash recurs at distance >= 2
|
|
287
|
+
# in the ring buffer (A -> B -> A). Immediate repeat (A -> A)
|
|
288
|
+
# is P1's territory and is excluded.
|
|
289
|
+
# P3 (council split): the trailing SPLIT_ROUNDS entries of state.json verdicts
|
|
290
|
+
# are all result == "REJECTED" with approve >= 1.
|
|
291
|
+
# Escalate iff >= 2 proxies are hot AND that has held for ROUNDS consecutive
|
|
292
|
+
# rounds AND we have not already escalated this episode (debounce). Re-arm when
|
|
293
|
+
# co-occurrence drops below 2 in any later round.
|
|
294
|
+
#===============================================================================
|
|
295
|
+
|
|
296
|
+
# Resolve the uncertainty.json path co-located with the council state root so a
|
|
297
|
+
# sourced test (which sets COUNCIL_STATE_DIR to a throwaway dir) reads and writes
|
|
298
|
+
# in that same throwaway dir, never the developer's real cwd. In production
|
|
299
|
+
# COUNCIL_STATE_DIR is "${TARGET_DIR}/.loki/council", so its parent is the right
|
|
300
|
+
# ".loki" and this lands at ".loki/state/uncertainty.json".
|
|
301
|
+
_uncertainty_state_path() {
|
|
302
|
+
local base_dir="${COUNCIL_STATE_DIR:-${TARGET_DIR:-.}/.loki/council}"
|
|
303
|
+
local loki_root
|
|
304
|
+
loki_root="$(dirname "$base_dir")"
|
|
305
|
+
echo "$loki_root/state/uncertainty.json"
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Read uncertainty.json (or emit a default object if missing/corrupt) to stdout.
|
|
309
|
+
_uncertainty_read_state() {
|
|
310
|
+
local file="$1"
|
|
311
|
+
_UNC_FILE="$file" python3 -c "
|
|
312
|
+
import json, os
|
|
313
|
+
f = os.environ['_UNC_FILE']
|
|
314
|
+
default = {
|
|
315
|
+
'schema_version': '1.0.0',
|
|
316
|
+
'consecutive_co_occur': 0,
|
|
317
|
+
'escalated_episode': False,
|
|
318
|
+
'escalated_at_iteration': 0,
|
|
319
|
+
'diff_hash_ring': [],
|
|
320
|
+
'last_round_iteration': -1,
|
|
321
|
+
'last_proxies': {'p1': False, 'p2': False, 'p3': False},
|
|
322
|
+
}
|
|
323
|
+
try:
|
|
324
|
+
with open(f) as fh:
|
|
325
|
+
state = json.load(fh)
|
|
326
|
+
if not isinstance(state, dict):
|
|
327
|
+
state = {}
|
|
328
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
329
|
+
state = {}
|
|
330
|
+
for k, v in default.items():
|
|
331
|
+
state.setdefault(k, v)
|
|
332
|
+
print(json.dumps(state))
|
|
333
|
+
" 2>/dev/null || echo '{}'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# Write a JSON string (read from _UNC_PAYLOAD) to uncertainty.json atomically
|
|
337
|
+
# (temp + mv), mirroring evidence-block.json.
|
|
338
|
+
_uncertainty_write_state() {
|
|
339
|
+
local file="$1"
|
|
340
|
+
local payload="$2"
|
|
341
|
+
local dir tmp
|
|
342
|
+
dir="$(dirname "$file")"
|
|
343
|
+
mkdir -p "$dir" 2>/dev/null || true
|
|
344
|
+
tmp="${file}.tmp.$$"
|
|
345
|
+
if _UNC_PAYLOAD="$payload" _UNC_TMP="$tmp" python3 -c "
|
|
346
|
+
import json, os
|
|
347
|
+
payload = os.environ['_UNC_PAYLOAD']
|
|
348
|
+
tmp = os.environ['_UNC_TMP']
|
|
349
|
+
state = json.loads(payload)
|
|
350
|
+
with open(tmp, 'w') as fh:
|
|
351
|
+
json.dump(state, fh, indent=2)
|
|
352
|
+
" 2>/dev/null; then
|
|
353
|
+
mv "$tmp" "$file" 2>/dev/null || { rm -f "$tmp" 2>/dev/null; return 1; }
|
|
354
|
+
return 0
|
|
355
|
+
fi
|
|
356
|
+
rm -f "$tmp" 2>/dev/null || true
|
|
357
|
+
return 1
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
uncertainty_should_escalate() {
|
|
361
|
+
# Knob first: opt-out is byte-identical to prior behavior. No read, no write,
|
|
362
|
+
# no state-file creation when disabled.
|
|
363
|
+
[ "${LOKI_UNCERTAINTY_ESCALATION:-1}" = "0" ] && return 1
|
|
364
|
+
|
|
365
|
+
# Tunable knobs (read inline; defaults documented in the env-var block).
|
|
366
|
+
local rounds_needed="${LOKI_UNCERTAINTY_ROUNDS:-2}"
|
|
367
|
+
local split_rounds="${LOKI_UNCERTAINTY_SPLIT_ROUNDS:-2}"
|
|
368
|
+
local nochange_min="${LOKI_UNCERTAINTY_NOCHANGE_MIN:-}"
|
|
369
|
+
if [ -z "$nochange_min" ]; then
|
|
370
|
+
nochange_min=$(( ${COUNCIL_STAGNATION_LIMIT:-5} - 1 ))
|
|
371
|
+
[ "$nochange_min" -lt 1 ] && nochange_min=1
|
|
372
|
+
fi
|
|
373
|
+
# Bounded constants.
|
|
374
|
+
local ring_size=6
|
|
375
|
+
|
|
376
|
+
# Resolve state file locations (council state root co-located).
|
|
377
|
+
local council_dir="${COUNCIL_STATE_DIR:-${TARGET_DIR:-.}/.loki/council}"
|
|
378
|
+
local state_json="$council_dir/state.json"
|
|
379
|
+
local unc_file
|
|
380
|
+
unc_file="$(_uncertainty_state_path)"
|
|
381
|
+
local iteration="${ITERATION_COUNT:-0}"
|
|
382
|
+
|
|
383
|
+
# Load prior uncertainty state.
|
|
384
|
+
local prior
|
|
385
|
+
prior="$(_uncertainty_read_state "$unc_file")"
|
|
386
|
+
|
|
387
|
+
# Compute the new state and decision entirely in python from persisted inputs.
|
|
388
|
+
# Echoes one line: "<rc> <new_json>" where rc is 0 (escalate) or 1 (no).
|
|
389
|
+
local result
|
|
390
|
+
result=$(_UNC_PRIOR="$prior" \
|
|
391
|
+
_UNC_STATE_JSON="$state_json" \
|
|
392
|
+
_UNC_ITERATION="$iteration" \
|
|
393
|
+
_UNC_ROUNDS="$rounds_needed" \
|
|
394
|
+
_UNC_SPLIT_ROUNDS="$split_rounds" \
|
|
395
|
+
_UNC_NOCHANGE_MIN="$nochange_min" \
|
|
396
|
+
_UNC_RING_SIZE="$ring_size" \
|
|
397
|
+
python3 -c "
|
|
398
|
+
import json, os
|
|
399
|
+
|
|
400
|
+
prior = json.loads(os.environ['_UNC_PRIOR'])
|
|
401
|
+
iteration = int(os.environ['_UNC_ITERATION'])
|
|
402
|
+
rounds_needed = int(os.environ['_UNC_ROUNDS'])
|
|
403
|
+
split_rounds = int(os.environ['_UNC_SPLIT_ROUNDS'])
|
|
404
|
+
nochange_min = int(os.environ['_UNC_NOCHANGE_MIN'])
|
|
405
|
+
ring_size = int(os.environ['_UNC_RING_SIZE'])
|
|
406
|
+
|
|
407
|
+
# Load council state.json (proxies). Missing/corrupt -> proxies cold.
|
|
408
|
+
try:
|
|
409
|
+
with open(os.environ['_UNC_STATE_JSON']) as fh:
|
|
410
|
+
cstate = json.load(fh)
|
|
411
|
+
if not isinstance(cstate, dict):
|
|
412
|
+
cstate = {}
|
|
413
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
414
|
+
cstate = {}
|
|
415
|
+
|
|
416
|
+
ring = prior.get('diff_hash_ring', [])
|
|
417
|
+
if not isinstance(ring, list):
|
|
418
|
+
ring = []
|
|
419
|
+
last_round = prior.get('last_round_iteration', -1)
|
|
420
|
+
try:
|
|
421
|
+
last_round = int(last_round)
|
|
422
|
+
except (TypeError, ValueError):
|
|
423
|
+
last_round = -1
|
|
424
|
+
|
|
425
|
+
# Idempotency: a repeated call at the same iteration must not double-mutate.
|
|
426
|
+
# Recompute proxies and re-emit the prior decision without pushing the ring or
|
|
427
|
+
# advancing the streak again.
|
|
428
|
+
same_round = (iteration == last_round)
|
|
429
|
+
|
|
430
|
+
# --- Proxy 1: no-change approaching circuit breaker ---
|
|
431
|
+
try:
|
|
432
|
+
no_change = int(cstate.get('consecutive_no_change', 0))
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
no_change = 0
|
|
435
|
+
p1 = no_change >= nochange_min
|
|
436
|
+
|
|
437
|
+
# --- Proxy 2: diff-hash recurrence at distance >= 2 (genuine oscillation) ---
|
|
438
|
+
cur_hash = cstate.get('last_diff_hash', '')
|
|
439
|
+
p2 = False
|
|
440
|
+
if cur_hash:
|
|
441
|
+
# Genuine oscillation (A -> B -> A) requires TWO things:
|
|
442
|
+
# 1. cur_hash recurs in the ring excluding the most-recent entry
|
|
443
|
+
# (distance >= 2; distance 1 immediate-repeat is P1's territory), AND
|
|
444
|
+
# 2. the most-recent ring entry (the previous round's hash) is DIFFERENT
|
|
445
|
+
# from cur_hash, i.e. there is an intervening distinct hash.
|
|
446
|
+
# Without (2), pure stagnation (A, A, A, ...) fills the ring with the same
|
|
447
|
+
# hash and would falsely fire P2 from the SAME root condition as P1, letting
|
|
448
|
+
# a single condition (no-change) light two proxies and escalate alone. That
|
|
449
|
+
# contradicts the 2-of-3 independent-proxy safety guarantee. Requiring an
|
|
450
|
+
# intervening distinct hash keeps A,B,A hot and A,A,A cold.
|
|
451
|
+
prev_hash = ring[-1] if ring else ''
|
|
452
|
+
if prev_hash != cur_hash:
|
|
453
|
+
for h in ring[:-1]:
|
|
454
|
+
if h == cur_hash:
|
|
455
|
+
p2 = True
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
# --- Proxy 3: persistent council split (trailing REJECTED with approve>=1) ---
|
|
459
|
+
verdicts = cstate.get('verdicts', [])
|
|
460
|
+
if not isinstance(verdicts, list):
|
|
461
|
+
verdicts = []
|
|
462
|
+
split_run = 0
|
|
463
|
+
for v in reversed(verdicts):
|
|
464
|
+
if not isinstance(v, dict):
|
|
465
|
+
break
|
|
466
|
+
try:
|
|
467
|
+
approve = int(v.get('approve', 0))
|
|
468
|
+
except (TypeError, ValueError):
|
|
469
|
+
approve = 0
|
|
470
|
+
if v.get('result') == 'REJECTED' and approve >= 1:
|
|
471
|
+
split_run += 1
|
|
472
|
+
else:
|
|
473
|
+
break
|
|
474
|
+
p3 = split_run >= split_rounds
|
|
475
|
+
|
|
476
|
+
hot_count = (1 if p1 else 0) + (1 if p2 else 0) + (1 if p3 else 0)
|
|
477
|
+
co_occur = hot_count >= 2
|
|
478
|
+
|
|
479
|
+
streak = prior.get('consecutive_co_occur', 0)
|
|
480
|
+
try:
|
|
481
|
+
streak = int(streak)
|
|
482
|
+
except (TypeError, ValueError):
|
|
483
|
+
streak = 0
|
|
484
|
+
escalated_episode = bool(prior.get('escalated_episode', False))
|
|
485
|
+
escalated_at = prior.get('escalated_at_iteration', 0)
|
|
486
|
+
try:
|
|
487
|
+
escalated_at = int(escalated_at)
|
|
488
|
+
except (TypeError, ValueError):
|
|
489
|
+
escalated_at = 0
|
|
490
|
+
|
|
491
|
+
new_state = dict(prior)
|
|
492
|
+
new_state['schema_version'] = prior.get('schema_version', '1.0.0')
|
|
493
|
+
new_state['last_proxies'] = {'p1': p1, 'p2': p2, 'p3': p3}
|
|
494
|
+
|
|
495
|
+
if same_round:
|
|
496
|
+
# No mutation of ring/streak; report no-escalate on the repeat call so we
|
|
497
|
+
# never fire twice for one round. Proxy snapshot is refreshed (harmless).
|
|
498
|
+
new_state['diff_hash_ring'] = ring
|
|
499
|
+
new_state['consecutive_co_occur'] = streak
|
|
500
|
+
new_state['escalated_episode'] = escalated_episode
|
|
501
|
+
new_state['escalated_at_iteration'] = escalated_at
|
|
502
|
+
new_state['last_round_iteration'] = last_round
|
|
503
|
+
rc = 1
|
|
504
|
+
else:
|
|
505
|
+
# Advance the ring with this round's hash (bounded).
|
|
506
|
+
if cur_hash:
|
|
507
|
+
ring = ring + [cur_hash]
|
|
508
|
+
if len(ring) > ring_size:
|
|
509
|
+
ring = ring[-ring_size:]
|
|
510
|
+
|
|
511
|
+
if co_occur:
|
|
512
|
+
streak += 1
|
|
513
|
+
else:
|
|
514
|
+
# Re-arm on clear: a resolved episode may legitimately re-escalate later.
|
|
515
|
+
streak = 0
|
|
516
|
+
escalated_episode = False
|
|
517
|
+
|
|
518
|
+
rc = 1
|
|
519
|
+
if co_occur and streak >= rounds_needed and not escalated_episode:
|
|
520
|
+
rc = 0
|
|
521
|
+
escalated_episode = True
|
|
522
|
+
escalated_at = iteration
|
|
523
|
+
|
|
524
|
+
new_state['diff_hash_ring'] = ring
|
|
525
|
+
new_state['consecutive_co_occur'] = streak
|
|
526
|
+
new_state['escalated_episode'] = escalated_episode
|
|
527
|
+
new_state['escalated_at_iteration'] = escalated_at
|
|
528
|
+
new_state['last_round_iteration'] = iteration
|
|
529
|
+
|
|
530
|
+
print(str(rc) + ' ' + json.dumps(new_state))
|
|
531
|
+
" 2>/dev/null) || return 1
|
|
532
|
+
|
|
533
|
+
[ -z "$result" ] && return 1
|
|
534
|
+
|
|
535
|
+
local rc new_json
|
|
536
|
+
rc="${result%% *}"
|
|
537
|
+
new_json="${result#* }"
|
|
538
|
+
|
|
539
|
+
# Persist the new state atomically (failure to persist must not escalate).
|
|
540
|
+
_uncertainty_write_state "$unc_file" "$new_json" || return 1
|
|
541
|
+
|
|
542
|
+
case "$rc" in
|
|
543
|
+
0) return 0 ;;
|
|
544
|
+
*) return 1 ;;
|
|
545
|
+
esac
|
|
546
|
+
}
|
|
547
|
+
|
|
266
548
|
#===============================================================================
|
|
267
549
|
# Council Voting - 3 independent reviewers check completion
|
|
268
550
|
#===============================================================================
|
|
@@ -80,6 +80,32 @@ completion:
|
|
|
80
80
|
# Ignore ALL completion signals (runs forever)
|
|
81
81
|
perpetual_mode: false
|
|
82
82
|
|
|
83
|
+
# Uncertainty-gated escalation (v7.19.2, default-on).
|
|
84
|
+
# When >=2 of 3 stuck-proxies (no-change counter, diff-hash oscillation,
|
|
85
|
+
# persistent council split) co-occur for `uncertainty.rounds` consecutive
|
|
86
|
+
# rounds, Loki escalates proactively via PAUSE + notification + handoff
|
|
87
|
+
# instead of silently burning iterations.
|
|
88
|
+
# IMPORTANT: when autonomy_mode is "perpetual" (the default), PAUSE is
|
|
89
|
+
# auto-cleared by the consumer so escalation degrades to notify-only; it
|
|
90
|
+
# does NOT halt the run. These are heuristics, not true metacognition.
|
|
91
|
+
#
|
|
92
|
+
# uncertainty:
|
|
93
|
+
# # Master toggle. Set to 0 to disable (byte-identical when off).
|
|
94
|
+
# escalation: 1
|
|
95
|
+
#
|
|
96
|
+
# # Consecutive rounds where >=2 of 3 proxies must co-occur before
|
|
97
|
+
# # escalating. Recommended range 2-3. Higher = less noise, later warning.
|
|
98
|
+
# rounds: 2
|
|
99
|
+
#
|
|
100
|
+
# # Proxy 1 threshold: consecutive_no_change must reach this value to mark
|
|
101
|
+
# # the no-change proxy hot. Default is COUNCIL_STAGNATION_LIMIT - 1
|
|
102
|
+
# # (one below the circuit-breaker limit). Leave unset to use the default.
|
|
103
|
+
# # nochange_min: 4
|
|
104
|
+
#
|
|
105
|
+
# # Proxy 3 threshold: trailing council verdicts that must be
|
|
106
|
+
# # REJECTED-with-at-least-one-approver (split) to mark the split proxy hot.
|
|
107
|
+
# split_rounds: 2
|
|
108
|
+
|
|
83
109
|
#===============================================================================
|
|
84
110
|
# Model & Routing Settings
|
|
85
111
|
#===============================================================================
|
|
@@ -382,6 +382,13 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
382
382
|
|
|
383
383
|
deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
|
|
384
384
|
|
|
385
|
+
# public_url is the publish-time injection slot: None at generate time so
|
|
386
|
+
# the default proof.json bytes + integrity hash are byte-identical to today.
|
|
387
|
+
# Optional LOKI_PROOF_PUBLIC_URL threads a value in HERE, inside the dict
|
|
388
|
+
# built before the redaction chokepoint (generate() at the redact_tree call),
|
|
389
|
+
# so the URL is redacted like every other field and folded into the hash.
|
|
390
|
+
public_url = os.environ.get("LOKI_PROOF_PUBLIC_URL") or None
|
|
391
|
+
|
|
385
392
|
# Assemble WITHOUT redaction / verification fields (advisor ordering).
|
|
386
393
|
proof = {
|
|
387
394
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -398,7 +405,7 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
|
|
|
398
405
|
"council": council,
|
|
399
406
|
"quality_gates": quality_gates,
|
|
400
407
|
"cost": cost,
|
|
401
|
-
"deployment": {"deployed_url": deployed_url, "public_url":
|
|
408
|
+
"deployment": {"deployed_url": deployed_url, "public_url": public_url},
|
|
402
409
|
}
|
|
403
410
|
return proof, run_id
|
|
404
411
|
|
|
@@ -571,6 +578,18 @@ def _render_html(proof, repo_root):
|
|
|
571
578
|
# (cost-free variant when uncollected) for the viral punch.
|
|
572
579
|
hook = _build_social_hook(proof)
|
|
573
580
|
tpl = tpl.replace("__PROOF_OG_DESCRIPTION__", _attr_esc(hook))
|
|
581
|
+
# Expose the share-buttons toggle into the page as an HTML-only token so
|
|
582
|
+
# the template JS can honor it. LOKI_PROOF_SHARE_BUTTONS defaults ON
|
|
583
|
+
# ("1"); set "0" to opt out. This is a PURE text substitution on the
|
|
584
|
+
# rendered template and is deliberately NOT placed in the proof dict, so
|
|
585
|
+
# proof.json bytes + the integrity hash stay byte-identical to today.
|
|
586
|
+
# The template carries <body data-share-buttons="__PROOF_SHARE_BUTTONS__">
|
|
587
|
+
# and renderHero reads that attribute, omitting the share row when it is
|
|
588
|
+
# "0". This substitution is LOAD-BEARING (not a no-op): do not remove it.
|
|
589
|
+
# Zero new network calls either way (the buttons are inert client-side
|
|
590
|
+
# markup; intent URLs are assembled only on click).
|
|
591
|
+
share_buttons = "0" if os.environ.get("LOKI_PROOF_SHARE_BUTTONS") == "0" else "1"
|
|
592
|
+
tpl = tpl.replace("__PROOF_SHARE_BUTTONS__", _attr_esc(share_buttons))
|
|
574
593
|
# Template renders client-side from an inlined JSON blob. Per the
|
|
575
594
|
# template GENERATOR CONTRACT, escape "<" so a value containing
|
|
576
595
|
# "</script>" or "<!--" cannot break out of the script block.
|