loki-mode 9.17.0 → 9.18.2
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/issue-providers.sh +0 -24
- package/autonomy/lib/agent_readiness.py +1 -79
- package/autonomy/lib/outcome_ledger.py +0 -122
- package/autonomy/lib/proof-generator.py +4 -71
- package/autonomy/lib/verdict.py +4 -25
- package/autonomy/loki +444 -264
- package/autonomy/notify.sh +1 -70
- package/autonomy/queue-consumer.sh +18 -290
- package/autonomy/run.sh +280 -189
- package/autonomy/trigger-server.py +518 -17
- package/autonomy/verify.sh +100 -12
- package/bin/loki +7 -1
- package/completions/_loki +0 -3
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +1 -168
- package/dashboard/static/index.html +222 -249
- package/docs/COMPETITIVE-SCORECARD.md +38 -0
- package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +475 -0
- package/docs/DEPLOYMENT.md +542 -0
- package/docs/STALE-STATE-AUDIT.md +174 -0
- package/docs/VERIFICATION-COST.md +46 -166
- package/loki-ts/dist/loki.js +311 -305
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +0 -25
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/autonomy/lib/gate_policy.py +0 -166
- package/docs/QUEUE-OPERATIONS.md +0 -107
package/autonomy/loki
CHANGED
|
@@ -556,6 +556,338 @@ require_jq() {
|
|
|
556
556
|
fi
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
# --- remote submit (loki start --remote <url>) --------------------------------
|
|
560
|
+
#
|
|
561
|
+
# Submits a spec to a deployed trigger-server's POST /jobs instead of building
|
|
562
|
+
# locally, then polls GET /jobs/<id> until the job reaches a terminal state.
|
|
563
|
+
#
|
|
564
|
+
# The bearer token is read from LOKI_REMOTE_TOKEN or LOKI_REMOTE_TOKEN_FILE and
|
|
565
|
+
# is NEVER placed in argv (the process list is world-readable) and never logged.
|
|
566
|
+
# It reaches curl through `--config -` on stdin.
|
|
567
|
+
|
|
568
|
+
# Extract the host from a URL: strip scheme, userinfo, then cut at the first
|
|
569
|
+
# '/', ':' or '?'. Compared EXACTLY, never by prefix -- "http://localhost*"
|
|
570
|
+
# would happily accept http://localhost.evil.com and leak a bearer token.
|
|
571
|
+
loki_remote_host() {
|
|
572
|
+
local url="$1"
|
|
573
|
+
local rest="${url#*://}"
|
|
574
|
+
rest="${rest##*@}"
|
|
575
|
+
rest="${rest%%/*}"
|
|
576
|
+
rest="${rest%%\?*}"
|
|
577
|
+
# Strip a :port, but keep an [::1] literal intact.
|
|
578
|
+
case "$rest" in
|
|
579
|
+
\[*\]*) rest="${rest%%\]*}]" ;;
|
|
580
|
+
*) rest="${rest%%:*}" ;;
|
|
581
|
+
esac
|
|
582
|
+
printf '%s' "$rest"
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
loki_remote_is_local_host() {
|
|
586
|
+
case "$(loki_remote_host "$1")" in
|
|
587
|
+
localhost|127.0.0.1|\[::1\]|::1) return 0 ;;
|
|
588
|
+
*) return 1 ;;
|
|
589
|
+
esac
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
# Read the token from env or a file. Never echoed, never passed as an argument.
|
|
593
|
+
loki_remote_token() {
|
|
594
|
+
if [ -n "${LOKI_REMOTE_TOKEN:-}" ]; then
|
|
595
|
+
printf '%s' "$LOKI_REMOTE_TOKEN"
|
|
596
|
+
return 0
|
|
597
|
+
fi
|
|
598
|
+
if [ -n "${LOKI_REMOTE_TOKEN_FILE:-}" ] && [ -f "${LOKI_REMOTE_TOKEN_FILE}" ]; then
|
|
599
|
+
# Mounted secrets end with a newline; strip it or every compare fails.
|
|
600
|
+
tr -d '\r\n' < "${LOKI_REMOTE_TOKEN_FILE}"
|
|
601
|
+
return 0
|
|
602
|
+
fi
|
|
603
|
+
return 1
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# curl with the Authorization header fed on stdin via --config, so the token
|
|
607
|
+
# never appears in argv. Usage: loki_remote_curl <token> <curl-args...>
|
|
608
|
+
loki_remote_curl() {
|
|
609
|
+
local _tok="$1"; shift
|
|
610
|
+
printf 'header = "Authorization: Bearer %s"\n' "$_tok" \
|
|
611
|
+
| curl --config - "$@"
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
loki_remote_submit() {
|
|
615
|
+
local url="$1"
|
|
616
|
+
local spec="$2"
|
|
617
|
+
|
|
618
|
+
url="${url%/}"
|
|
619
|
+
|
|
620
|
+
if [ -z "$spec" ]; then
|
|
621
|
+
echo -e "${RED}loki: --remote needs a spec to submit${NC}" >&2
|
|
622
|
+
echo " e.g. loki start ./prd.md --remote $url" >&2
|
|
623
|
+
return 1
|
|
624
|
+
fi
|
|
625
|
+
|
|
626
|
+
# A bearer token over plaintext to a remote host is a credential leak.
|
|
627
|
+
# Loopback is exempt (it never leaves the machine); anything else needs an
|
|
628
|
+
# explicit opt-in so the leak is a decision, not an accident.
|
|
629
|
+
case "$url" in
|
|
630
|
+
http://*)
|
|
631
|
+
if ! loki_remote_is_local_host "$url" && [ "${LOKI_REMOTE_INSECURE:-}" != "1" ]; then
|
|
632
|
+
echo -e "${RED}loki: refusing to send a bearer token over plaintext http to $(loki_remote_host "$url")${NC}" >&2
|
|
633
|
+
echo " Use https://, or set LOKI_REMOTE_INSECURE=1 to override." >&2
|
|
634
|
+
return 1
|
|
635
|
+
fi
|
|
636
|
+
;;
|
|
637
|
+
esac
|
|
638
|
+
|
|
639
|
+
local token
|
|
640
|
+
if ! token="$(loki_remote_token)" || [ -z "$token" ]; then
|
|
641
|
+
echo -e "${RED}loki: no remote token configured${NC}" >&2
|
|
642
|
+
echo " Set LOKI_REMOTE_TOKEN or LOKI_REMOTE_TOKEN_FILE (never pass it as an argument)." >&2
|
|
643
|
+
return 1
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
require_jq || return 1
|
|
647
|
+
|
|
648
|
+
local body resp
|
|
649
|
+
body="$(jq -nc --arg s "$spec" '{spec:$s}')"
|
|
650
|
+
|
|
651
|
+
echo "Submitting to $url ..."
|
|
652
|
+
if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
|
|
653
|
+
-H "Content-Type: application/json" \
|
|
654
|
+
-X POST --data "$body" "$url/jobs" 2>&1)"; then
|
|
655
|
+
# NEVER fall back to a local run: a user who asked for remote and
|
|
656
|
+
# silently got a local build would pay local spend they did not ask for.
|
|
657
|
+
echo -e "${RED}loki: remote submit failed: $url/jobs${NC}" >&2
|
|
658
|
+
echo " Is the server reachable, and is LOKI_REMOTE_TOKEN correct?" >&2
|
|
659
|
+
echo " Not falling back to a local build (you asked for --remote)." >&2
|
|
660
|
+
return 1
|
|
661
|
+
fi
|
|
662
|
+
|
|
663
|
+
local job_id
|
|
664
|
+
job_id="$(printf '%s' "$resp" | jq -r '.id // empty' 2>/dev/null)"
|
|
665
|
+
if [ -z "$job_id" ]; then
|
|
666
|
+
echo -e "${RED}loki: remote accepted the request but returned no job id${NC}" >&2
|
|
667
|
+
return 1
|
|
668
|
+
fi
|
|
669
|
+
echo "Job $job_id queued on $url"
|
|
670
|
+
|
|
671
|
+
local poll_sec="${LOKI_REMOTE_POLL_SEC:-5}"
|
|
672
|
+
local max_polls="${LOKI_REMOTE_MAX_POLLS:-720}"
|
|
673
|
+
local status="" last="" i=0
|
|
674
|
+
while [ "$i" -lt "$max_polls" ]; do
|
|
675
|
+
i=$((i+1))
|
|
676
|
+
if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
|
|
677
|
+
"$url/jobs/$job_id" 2>&1)"; then
|
|
678
|
+
echo -e "${RED}loki: lost contact with $url while polling job $job_id${NC}" >&2
|
|
679
|
+
return 1
|
|
680
|
+
fi
|
|
681
|
+
status="$(printf '%s' "$resp" | jq -r '.status // empty' 2>/dev/null)"
|
|
682
|
+
if [ "$status" != "$last" ]; then
|
|
683
|
+
echo " [$job_id] $status"
|
|
684
|
+
last="$status"
|
|
685
|
+
fi
|
|
686
|
+
case "$status" in
|
|
687
|
+
fired|error|rejected*)
|
|
688
|
+
break
|
|
689
|
+
;;
|
|
690
|
+
esac
|
|
691
|
+
sleep "$poll_sec"
|
|
692
|
+
done
|
|
693
|
+
|
|
694
|
+
# A TAMPERED receipt fails the whole submit regardless of the job status:
|
|
695
|
+
# reporting a build as successful while holding proof that its own receipt
|
|
696
|
+
# was altered would make the receipt decorative.
|
|
697
|
+
local _receipt_rc=0
|
|
698
|
+
loki_remote_fetch_receipt "$url" "$token" "$job_id" || _receipt_rc=$?
|
|
699
|
+
if [ "$_receipt_rc" -ne 0 ]; then
|
|
700
|
+
echo -e "${RED}loki: remote job $job_id returned a receipt that failed verification${NC}" >&2
|
|
701
|
+
return 1
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
# ponytail: the server reports DISPATCH success ("fired" = the pod launched
|
|
705
|
+
# the build), not the build's own exit code -- run_loki_command returns true
|
|
706
|
+
# for a --detach launch and for the detach-timeout path. So a non-"fired"
|
|
707
|
+
# status is a real failure, but "fired" is not yet proof the build passed.
|
|
708
|
+
# Upgrade path: have trigger-server record the build's exit code and expose
|
|
709
|
+
# a terminal status here, then gate CI on that instead.
|
|
710
|
+
case "$status" in
|
|
711
|
+
fired) return 0 ;;
|
|
712
|
+
"") echo -e "${RED}loki: job $job_id did not reach a terminal status${NC}" >&2; return 1 ;;
|
|
713
|
+
*) echo -e "${RED}loki: remote job $job_id failed: $status${NC}" >&2; return 1 ;;
|
|
714
|
+
esac
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
# Fetch the Evidence Receipt and write it where a local run puts it, so
|
|
718
|
+
# `loki proof` works on a remote build, then VERIFY it here -- on the machine
|
|
719
|
+
# that did NOT produce it. That is the entire point: a receipt you can only
|
|
720
|
+
# check by asking the builder whether it is good proves nothing.
|
|
721
|
+
#
|
|
722
|
+
# Returns 0 when the receipt verified or is honestly reported as unverifiable,
|
|
723
|
+
# and NON-ZERO when the receipt is TAMPERED. Absence of a receipt is not a
|
|
724
|
+
# failure (a build may legitimately produce none); a receipt that fails its
|
|
725
|
+
# own integrity check is.
|
|
726
|
+
loki_remote_fetch_receipt() {
|
|
727
|
+
local url="$1" token="$2" job_id="$3"
|
|
728
|
+
local dir="${LOKI_DIR:-.loki}/proofs/$job_id"
|
|
729
|
+
local tmp resp
|
|
730
|
+
|
|
731
|
+
if ! resp="$(loki_remote_curl "$token" -fsS --max-time 30 \
|
|
732
|
+
"$url/jobs/$job_id/proof" 2>/dev/null)"; then
|
|
733
|
+
echo " (no evidence receipt available from $url for $job_id)" >&2
|
|
734
|
+
return 0
|
|
735
|
+
fi
|
|
736
|
+
# Only write something that parses as JSON: a proxy error page must not
|
|
737
|
+
# land in .loki/proofs/ looking like a receipt.
|
|
738
|
+
if ! printf '%s' "$resp" | jq -e . >/dev/null 2>&1; then
|
|
739
|
+
echo " (remote returned a non-JSON receipt for $job_id; not written)" >&2
|
|
740
|
+
return 0
|
|
741
|
+
fi
|
|
742
|
+
mkdir -p "$dir"
|
|
743
|
+
tmp="$dir/.proof.json.$$"
|
|
744
|
+
printf '%s' "$resp" > "$tmp" && mv "$tmp" "$dir/proof.json"
|
|
745
|
+
echo " Evidence Receipt: $dir/proof.json"
|
|
746
|
+
|
|
747
|
+
loki_remote_verify_receipt "$dir/proof.json"
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
# Verify a fetched receipt locally and print exactly what was and was not
|
|
751
|
+
# proven. Exit 1 ONLY on tamper.
|
|
752
|
+
#
|
|
753
|
+
# THREE OUTCOMES, NEVER COLLAPSED INTO TWO:
|
|
754
|
+
# VERIFIED a gpg signature is present and valid -> integrity AND provenance
|
|
755
|
+
# UNSIGNED the hash matches, no signature -> integrity ONLY
|
|
756
|
+
# TAMPERED hash mismatch or bad signature -> exit 1
|
|
757
|
+
#
|
|
758
|
+
# UNSIGNED is not a weaker VERIFIED, it is a different claim. docs/VERIFICATION-COST.md
|
|
759
|
+
# records that the unsigned path is FORGEABLE: whoever rewrites the facts and
|
|
760
|
+
# the headline into a mutually consistent lie and recomputes the hash passes
|
|
761
|
+
# every check here. A user who trusts a "verified" label meaning only "these
|
|
762
|
+
# bytes are self-consistent" is worse off than one who knows they hold no proof,
|
|
763
|
+
# because they will stop looking. So the word "verified" is never printed for an
|
|
764
|
+
# unsigned receipt.
|
|
765
|
+
#
|
|
766
|
+
# There is a FOURTH state and it must not be folded into UNSIGNED: a signature
|
|
767
|
+
# is present but gpg is not installed here, so it could not be checked. Saying
|
|
768
|
+
# UNSIGNED there would be a false statement about the receipt (it IS signed),
|
|
769
|
+
# and saying VERIFIED would be a false statement about this machine (it checked
|
|
770
|
+
# nothing). It gets its own line and never claims verification.
|
|
771
|
+
# Ask gpg WHY a signature failed. Echoes "nopubkey" when the signing key is
|
|
772
|
+
# absent (uncheckable), "" otherwise (a genuinely bad signature, or we could
|
|
773
|
+
# not tell). Fails toward "" so an unclear answer keeps the loud TAMPERED
|
|
774
|
+
# verdict rather than silently downgrading a real forgery to a soft warning.
|
|
775
|
+
loki_remote_gpg_status() {
|
|
776
|
+
local pj="$1"
|
|
777
|
+
local d="${TMPDIR:-/tmp}/loki-sigcheck.$$"
|
|
778
|
+
mkdir -p "$d" || return 0
|
|
779
|
+
python3 - "$pj" "$d" <<'PY' 2>/dev/null || { rm -rf "$d"; return 0; }
|
|
780
|
+
import json, sys, os
|
|
781
|
+
proof = json.load(open(sys.argv[1]))
|
|
782
|
+
d = sys.argv[2]
|
|
783
|
+
sig = (proof.get("verification") or {}).get("gpg_signature") or ""
|
|
784
|
+
proof.pop("verification", None)
|
|
785
|
+
open(os.path.join(d, "canon.bin"), "wb").write(
|
|
786
|
+
json.dumps(proof, sort_keys=True, separators=(",", ":")).encode())
|
|
787
|
+
open(os.path.join(d, "sig.asc"), "w").write(sig)
|
|
788
|
+
PY
|
|
789
|
+
local st
|
|
790
|
+
st="$(gpg --status-fd 1 --verify "$d/sig.asc" "$d/canon.bin" 2>/dev/null)"
|
|
791
|
+
rm -rf "$d"
|
|
792
|
+
case "$st" in
|
|
793
|
+
*NO_PUBKEY*) printf 'nopubkey' ;;
|
|
794
|
+
*) printf '' ;;
|
|
795
|
+
esac
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
loki_remote_verify_receipt() {
|
|
799
|
+
local pj="$1"
|
|
800
|
+
local verifier="${_LOKI_SCRIPT_DIR}/lib/proof-verify.py"
|
|
801
|
+
|
|
802
|
+
if [ ! -f "$verifier" ] || ! command -v python3 >/dev/null 2>&1; then
|
|
803
|
+
echo " NOT VERIFIED: no local verifier available (need python3 and lib/proof-verify.py)" >&2
|
|
804
|
+
return 0
|
|
805
|
+
fi
|
|
806
|
+
|
|
807
|
+
# Whether the receipt CARRIES a signature is read from the receipt itself,
|
|
808
|
+
# not inferred from the verifier's gpg verdict. proof-verify.py reports
|
|
809
|
+
# gpg_ok="n/a" for two distinct situations -- no signature present, and gpg
|
|
810
|
+
# unavailable on this machine -- so gpg_ok alone cannot tell them apart, and
|
|
811
|
+
# collapsing them is exactly how a signed receipt gets mislabelled UNSIGNED.
|
|
812
|
+
local signed=0
|
|
813
|
+
if jq -e '.verification.gpg_signature // empty' "$pj" >/dev/null 2>&1; then
|
|
814
|
+
signed=1
|
|
815
|
+
fi
|
|
816
|
+
|
|
817
|
+
# Deliberately NOT gated on the verifier's exit code or its `ok` field.
|
|
818
|
+
# `ok` includes diff_drift, re-derived against the LOCAL repo -- which is
|
|
819
|
+
# not the machine that produced this build, so an honest remote receipt
|
|
820
|
+
# drifts by construction and would report TAMPERED on every single run.
|
|
821
|
+
# A false BLOCK is the more damaging direction (docs/VERIFICATION-COST.md):
|
|
822
|
+
# a gate that cries wolf every time trains users to ignore the verdict.
|
|
823
|
+
# Only hash_ok and gpg_ok are load-bearing here; drift is not our verdict.
|
|
824
|
+
local out hash_ok gpg_ok
|
|
825
|
+
out="$(python3 "$verifier" "$pj" "${TARGET_DIR:-.}" 2>/dev/null)"
|
|
826
|
+
hash_ok="$(printf '%s' "$out" | jq -r '.hash_ok // false' 2>/dev/null)"
|
|
827
|
+
gpg_ok="$(printf '%s' "$out" | jq -r '.gpg_ok | tostring' 2>/dev/null)"
|
|
828
|
+
|
|
829
|
+
if [ "$hash_ok" != "true" ]; then
|
|
830
|
+
echo -e " ${RED}TAMPERED: the receipt's integrity hash does not match its contents.${NC}" >&2
|
|
831
|
+
echo " proof.json was edited after it was written. Do not trust this build." >&2
|
|
832
|
+
return 1
|
|
833
|
+
fi
|
|
834
|
+
|
|
835
|
+
# gpg_ok=false covers TWO facts that must never be conflated: the signature
|
|
836
|
+
# is cryptographically bad over this content (the receipt was ALTERED), and
|
|
837
|
+
# gpg could not evaluate it at all (we do not hold the public key). Only the
|
|
838
|
+
# first is an accusation. Reporting a missing key as TAMPERED would send a
|
|
839
|
+
# user hunting a forgery that never happened, and would train them to shrug
|
|
840
|
+
# at a real TAMPERED -- the same error as calling an unsigned receipt
|
|
841
|
+
# verified, just pointed the other way.
|
|
842
|
+
#
|
|
843
|
+
# proof-verify.py returns a bare boolean, so the distinction is drawn here
|
|
844
|
+
# from gpg's own status codes: BADSIG = bad over this content, NO_PUBKEY /
|
|
845
|
+
# ERRSIG = could not be checked.
|
|
846
|
+
if [ "$signed" -eq 1 ] && [ "$gpg_ok" = "false" ]; then
|
|
847
|
+
local _why=""
|
|
848
|
+
if command -v gpg >/dev/null 2>&1; then
|
|
849
|
+
_why="$(loki_remote_gpg_status "$pj")"
|
|
850
|
+
fi
|
|
851
|
+
if [ "$_why" = "nopubkey" ]; then
|
|
852
|
+
echo -e " ${YELLOW}NOT CHECKED${NC}: the receipt is signed, but the signing key is not in your keyring." >&2
|
|
853
|
+
echo " Nothing is wrong with the contents: the integrity hash matches. The" >&2
|
|
854
|
+
echo " signature could not be evaluated, so PROVENANCE is unproven here." >&2
|
|
855
|
+
echo " Import the publisher's public key and re-run: loki proof verify $(basename "$(dirname "$pj")")" >&2
|
|
856
|
+
# Non-zero: a caller who asked for a signed receipt did not get
|
|
857
|
+
# proof, and in CI that must not pass silently. The message states
|
|
858
|
+
# plainly that no fault was found in the content, so this is a
|
|
859
|
+
# missing-proof exit, not an accusation of tampering.
|
|
860
|
+
return 1
|
|
861
|
+
fi
|
|
862
|
+
echo -e " ${RED}TAMPERED: the receipt carries a gpg signature that does not verify.${NC}" >&2
|
|
863
|
+
echo " Either the contents or the signature was altered. Do not trust this build." >&2
|
|
864
|
+
return 1
|
|
865
|
+
fi
|
|
866
|
+
|
|
867
|
+
if [ "$signed" -eq 1 ] && [ "$gpg_ok" = "true" ]; then
|
|
868
|
+
echo -e " ${GREEN}VERIFIED${NC}: integrity hash matches and the gpg signature is valid."
|
|
869
|
+
echo " Proven: these receipt bytes are unaltered, and they were signed by the"
|
|
870
|
+
echo " holder of the signing key. Verify that key is one you trust."
|
|
871
|
+
echo " NOT proven: that the code is correct. A receipt records which checks ran."
|
|
872
|
+
return 0
|
|
873
|
+
fi
|
|
874
|
+
|
|
875
|
+
if [ "$signed" -eq 1 ]; then
|
|
876
|
+
# gpg_ok is "n/a" with a signature present: gpg is missing here.
|
|
877
|
+
echo -e " ${YELLOW}NOT CHECKED${NC}: the receipt is signed, but gpg is unavailable on this machine."
|
|
878
|
+
echo " Its integrity hash matches. The signature was NOT checked, so provenance"
|
|
879
|
+
echo " is unproven here. Install gpg and re-run: loki proof verify $(basename "$(dirname "$pj")")"
|
|
880
|
+
return 0
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
echo -e " ${YELLOW}UNSIGNED${NC}: integrity hash matches. This is NOT proof of provenance."
|
|
884
|
+
echo " Proven: the receipt is internally consistent with its own hash."
|
|
885
|
+
echo " NOT proven: who produced it. An unsigned receipt is forgeable -- whoever"
|
|
886
|
+
echo " controls the builder can rewrite the facts and recompute the hash."
|
|
887
|
+
echo " For provenance, the build must set LOKI_PROOF_GPG_KEY (docs/SIGNED-RECEIPTS.md)."
|
|
888
|
+
return 0
|
|
889
|
+
}
|
|
890
|
+
|
|
559
891
|
# Emit event (non-blocking)
|
|
560
892
|
# Usage: emit_event <type> <source> <action> [key=value ...]
|
|
561
893
|
emit_event() {
|
|
@@ -1040,15 +1372,15 @@ show_help() {
|
|
|
1040
1372
|
echo " agent analyze api assets audit bench checkpoint (cp) ci cleanup"
|
|
1041
1373
|
echo " cluster cockpit code compliance completions compound config context (ctx)"
|
|
1042
1374
|
echo " cost council crash dashboard demo deploy docker docs doctor dogfood"
|
|
1043
|
-
echo " enterprise estimate explain export failover
|
|
1375
|
+
echo " enterprise estimate explain export failover github grill heal help"
|
|
1044
1376
|
echo " import init intent"
|
|
1045
1377
|
echo " issue kpis logs magic mcp memory metrics migrate modernize monitor"
|
|
1046
1378
|
echo " next notify onboard open optimize otel outcomes own (handoff) pause plan preview"
|
|
1047
|
-
echo " projects proof (receipt) provider quick quickstart rc
|
|
1379
|
+
echo " projects proof (receipt) provider quick quickstart rc remote report reset"
|
|
1048
1380
|
echo " resume review rollback run sandbox secrets secure self-update sentrux"
|
|
1049
1381
|
echo " serve setup-skill share ship spec start state stats status steer stop"
|
|
1050
1382
|
echo " syslog telemetry template test tour trigger trust trust-metrics"
|
|
1051
|
-
echo " ultracode update
|
|
1383
|
+
echo " ultracode update verify version voice watch watchdog web welcome why"
|
|
1052
1384
|
echo " wiki worktree (wt)"
|
|
1053
1385
|
echo ""
|
|
1054
1386
|
echo "Any command: loki <command> --help"
|
|
@@ -1495,6 +1827,7 @@ cmd_start() {
|
|
|
1495
1827
|
local mirofish_bg=false
|
|
1496
1828
|
local mirofish_disabled=false
|
|
1497
1829
|
local no_plan=false # v6.81.1: --no-plan opts out of auto-plan display
|
|
1830
|
+
local remote_url="${LOKI_REMOTE_URL:-}" # --remote: submit to a deployed cluster
|
|
1498
1831
|
|
|
1499
1832
|
# v6.84.0: unified entry point -- explicit mode overrides + issue-mode args
|
|
1500
1833
|
local explicit_mode="" # "prd" | "issue" | "" (auto-detect)
|
|
@@ -1532,6 +1865,10 @@ cmd_start() {
|
|
|
1532
1865
|
echo " --brief \"TEXT\" Force zero-config brief mode (fast first run)"
|
|
1533
1866
|
echo ""
|
|
1534
1867
|
echo "Options:"
|
|
1868
|
+
echo " --remote URL Submit to a deployed Loki (POST /jobs) instead of"
|
|
1869
|
+
echo " building locally; polls until the job finishes."
|
|
1870
|
+
echo " Token from LOKI_REMOTE_TOKEN / LOKI_REMOTE_TOKEN_FILE"
|
|
1871
|
+
echo " (never an argument). Env: LOKI_REMOTE_URL."
|
|
1535
1872
|
echo " --provider NAME AI provider: claude (default), codex, cline, aider"
|
|
1536
1873
|
echo " --parallel Enable parallel mode with git worktrees"
|
|
1537
1874
|
echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
|
|
@@ -2058,6 +2395,19 @@ cmd_start() {
|
|
|
2058
2395
|
--config=*|--vars=*|--env-file=*)
|
|
2059
2396
|
shift
|
|
2060
2397
|
;;
|
|
2398
|
+
--remote)
|
|
2399
|
+
if [ "$#" -ge 2 ]; then
|
|
2400
|
+
remote_url="$2"
|
|
2401
|
+
shift 2
|
|
2402
|
+
else
|
|
2403
|
+
echo "loki: --remote requires a URL (e.g. --remote https://loki.example.com)" >&2
|
|
2404
|
+
exit 1
|
|
2405
|
+
fi
|
|
2406
|
+
;;
|
|
2407
|
+
--remote=*)
|
|
2408
|
+
remote_url="${1#--remote=}"
|
|
2409
|
+
shift
|
|
2410
|
+
;;
|
|
2061
2411
|
--)
|
|
2062
2412
|
# End-of-options: everything after is positional. Lets a caller
|
|
2063
2413
|
# pass an untrusted spec that might start with '-' safely, e.g.
|
|
@@ -2084,6 +2434,18 @@ cmd_start() {
|
|
|
2084
2434
|
esac
|
|
2085
2435
|
done
|
|
2086
2436
|
|
|
2437
|
+
# --remote: submit to a deployed cluster instead of building locally.
|
|
2438
|
+
#
|
|
2439
|
+
# Branches HERE, after arg parsing but BEFORE the provider pre-flight and
|
|
2440
|
+
# provider_offer_gate below: those require a provider CLI on PATH, and the
|
|
2441
|
+
# whole point of a remote submit is that this machine is not doing the
|
|
2442
|
+
# work. Gating a remote submit on a local provider install would defeat it.
|
|
2443
|
+
if [ -n "$remote_url" ]; then
|
|
2444
|
+
local _remote_spec="${brief_text:-${prd_file:-$positional_arg}}"
|
|
2445
|
+
loki_remote_submit "$remote_url" "$_remote_spec"
|
|
2446
|
+
return $?
|
|
2447
|
+
fi
|
|
2448
|
+
|
|
2087
2449
|
# Explicit-provider pre-flight. Runs BEFORE provider_offer_gate below, and
|
|
2088
2450
|
# is deliberately narrower than it.
|
|
2089
2451
|
#
|
|
@@ -11364,6 +11726,10 @@ cmd_doctor() {
|
|
|
11364
11726
|
|
|
11365
11727
|
local pass_count=0
|
|
11366
11728
|
local fail_count=0
|
|
11729
|
+
# Set to 1 when the bundled SDK is the ONLY usable provider (no CLI on PATH).
|
|
11730
|
+
# Declared here, not inside the branch that sets it, so the "Next:" block at
|
|
11731
|
+
# the end of this function always reads an initialized value.
|
|
11732
|
+
local _doctor_sdk_only=0
|
|
11367
11733
|
# FUNNEL FIX (v8.2.x): collect the NAME + FIX of every hard failure, not just
|
|
11368
11734
|
# a count. A first-time user on a bare machine sees 15 warnings (sentrux, GPG
|
|
11369
11735
|
# signing, bash 4, Bun, python3.12, truecolor, inline-image probe -- all
|
|
@@ -11528,6 +11894,12 @@ cmd_doctor() {
|
|
|
11528
11894
|
echo -e " ${GREEN}PASS${NC} Bundled Claude Agent SDK is usable -- 'loki start' needs no separate CLI"
|
|
11529
11895
|
echo -e " ${YELLOW}Note: loki demo/quick/quickstart still need a provider CLI on PATH${NC}"
|
|
11530
11896
|
echo -e " ${YELLOW} Install: npm install -g @anthropic-ai/claude-code${NC}"
|
|
11897
|
+
# The note above was only half the fix. The final "Next:" line at the
|
|
11898
|
+
# bottom of this function recommended quickstart unconditionally, so a
|
|
11899
|
+
# user reading a GREEN doctor followed its LAST line straight into
|
|
11900
|
+
# quickstart.sh:476 exit 2. Record the state here (where the predicate
|
|
11901
|
+
# is already evaluated) and let the recommendation read it.
|
|
11902
|
+
_doctor_sdk_only=1
|
|
11531
11903
|
pass_count=$((pass_count + 1))
|
|
11532
11904
|
else
|
|
11533
11905
|
echo -e " ${RED}FAIL${NC} No AI provider CLI installed -- at least one is required"
|
|
@@ -11905,14 +12277,7 @@ STALE_DAYS = 90
|
|
|
11905
12277
|
p = os.environ['LOKI_CATALOG_PATH']
|
|
11906
12278
|
try:
|
|
11907
12279
|
updated = json.load(open(p))['updated']
|
|
11908
|
-
|
|
11909
|
-
# (doctor.ts:545). This is the TEXT-mode twin of that computation, and it was
|
|
11910
|
-
# missed when the JSON one was fixed: the parity gate compares BOTH surfaces,
|
|
11911
|
-
# so fixing only --json left doctor text-mode still diverging 5 vs 6 and the
|
|
11912
|
-
# gate still red. Two copies of one calculation is the actual defect here;
|
|
11913
|
-
# they are left as two only because the text path prints and the JSON path
|
|
11914
|
-
# returns a dict.
|
|
11915
|
-
age = (datetime.datetime.now(datetime.timezone.utc).date() - datetime.date.fromisoformat(updated)).days
|
|
12280
|
+
age = (datetime.date.today() - datetime.date.fromisoformat(updated)).days
|
|
11916
12281
|
except Exception:
|
|
11917
12282
|
print('warn|Catalog unreadable or missing an ISO \"updated\" date -- cannot determine age')
|
|
11918
12283
|
else:
|
|
@@ -12036,17 +12401,6 @@ else:
|
|
|
12036
12401
|
local _blk_key="other"
|
|
12037
12402
|
case "$_doctor_blockers" in
|
|
12038
12403
|
*"No AI provider CLI"*) _blk_key="no_provider" ;;
|
|
12039
|
-
# This doctor DETECTS the logged-out / expired wall above
|
|
12040
|
-
# (:11610, :11615) but had no arm for it, so the single most
|
|
12041
|
-
# common post-install failure was reported as `other` -- the one
|
|
12042
|
-
# bucket that cannot be acted on. not_logged_in is already a
|
|
12043
|
-
# first-class enum value (telemetry.sh:204) and the Bun doctor
|
|
12044
|
-
# already reports it, so without this arm the SAME host answered
|
|
12045
|
-
# `other` on bash and `not_logged_in` on Bun and the two routes'
|
|
12046
|
-
# counts could not be added together. Ordered directly after
|
|
12047
|
-
# no_provider because install-vs-authenticate need opposite fixes
|
|
12048
|
-
# and a missing provider is the earlier wall.
|
|
12049
|
-
*"not logged in"*|*"login has expired"*) _blk_key="not_logged_in" ;;
|
|
12050
12404
|
*"Node.js is not installed"*|*"Node.js must be"*) _blk_key="node" ;;
|
|
12051
12405
|
*"Python 3 is not installed"*|*"Python 3 must be"*) _blk_key="python3" ;;
|
|
12052
12406
|
*"jq is not installed"*) _blk_key="jq" ;;
|
|
@@ -12068,8 +12422,17 @@ else:
|
|
|
12068
12422
|
# on fail_count==0 above, so a failing setup is never told to build. The
|
|
12069
12423
|
# --json path returns long before this code, so machine output is untouched.
|
|
12070
12424
|
echo ""
|
|
12071
|
-
|
|
12072
|
-
|
|
12425
|
+
if [ "$_doctor_sdk_only" = "1" ]; then
|
|
12426
|
+
# SDK-only host: quickstart and demo stay on the bash route and need a
|
|
12427
|
+
# binary on PATH, so recommending them here is a green doctor pointing at
|
|
12428
|
+
# an exit 2. Recommend only what actually runs, plus the one command that
|
|
12429
|
+
# unlocks the rest.
|
|
12430
|
+
echo "Next: loki start ./prd.md (runs on the bundled SDK, no CLI install needed)"
|
|
12431
|
+
echo " For loki quickstart/demo: npm install -g @anthropic-ai/claude-code"
|
|
12432
|
+
else
|
|
12433
|
+
echo "Next: loki quickstart (guided first build from your idea, no PRD needed)"
|
|
12434
|
+
echo " or loki demo (builds a sample todo app end to end) or loki start ./prd.md"
|
|
12435
|
+
fi
|
|
12073
12436
|
# Best-effort stale-install nudge (stderr only; never blocks; off on
|
|
12074
12437
|
# non-TTY/CI). Reached only on the human-readable path (--json returns above).
|
|
12075
12438
|
maybe_print_update_hint
|
|
@@ -12330,18 +12693,7 @@ _cat_path = os.environ['LOKI_CATALOG_PATH']
|
|
|
12330
12693
|
try:
|
|
12331
12694
|
import datetime as _dt
|
|
12332
12695
|
_cat_updated = json.load(open(_cat_path))['updated']
|
|
12333
|
-
|
|
12334
|
-
# both sides at UTC midnight (doctor.ts:545 says so, to keep the day count
|
|
12335
|
-
# from shifting with the host timezone). Anywhere west of UTC the two
|
|
12336
|
-
# disagree for the hours between local midnight and UTC midnight, and
|
|
12337
|
-
# doctor --json is compared BYTE FOR BYTE between routes by bun-parity.
|
|
12338
|
-
#
|
|
12339
|
-
# Measured on this host at 02:45 UTC / 22:45 local: bash reported age_days 5
|
|
12340
|
-
# and Bun reported 6, from the same file at the same instant. It reproduced
|
|
12341
|
-
# across two full gate runs hours apart, so it is not a midnight-rollover
|
|
12342
|
-
# flake. It is a real parity defect that stays invisible while the local date
|
|
12343
|
-
# and the UTC date agree, which is most of the day.
|
|
12344
|
-
_cat_age = (_dt.datetime.now(_dt.timezone.utc).date() - _dt.date.fromisoformat(_cat_updated)).days
|
|
12696
|
+
_cat_age = (_dt.date.today() - _dt.date.fromisoformat(_cat_updated)).days
|
|
12345
12697
|
if _cat_age > CATALOG_STALE_DAYS:
|
|
12346
12698
|
# Deliberately NOT counted. The block comment above states catalog age is
|
|
12347
12699
|
# excluded from pass/fail/warn and from 'ok' so a stale catalog can never
|
|
@@ -13803,90 +14155,12 @@ set_ttfv_lightweight_profile() {
|
|
|
13803
14155
|
# never pollutes the v7.8.1 generated-PRD-reuse signature logic. The brief text
|
|
13804
14156
|
# is the project intent; the rest is a minimal scaffold the agent fills in.
|
|
13805
14157
|
# Usage: synthesize_brief_prd <output_file> <brief_text>
|
|
13806
|
-
# _brief_acceptance_criteria <brief_text>: acceptance criteria derived from what
|
|
13807
|
-
# the user ACTUALLY asked for, not constants.
|
|
13808
|
-
#
|
|
13809
|
-
# WHY. Every one-liner used to get byte-identical Requirements and Success
|
|
13810
|
-
# Criteria: "build a todo app" and "build a Stripe billing dashboard" produced
|
|
13811
|
-
# the same acceptance criteria, and the user's own words appeared exactly once,
|
|
13812
|
-
# under Overview. So the completion council, the checklist and the evidence gate
|
|
13813
|
-
# were all checking generic prose rather than the request. That is the weakest
|
|
13814
|
-
# input shape getting the least specific help, which is backwards -- a cheap
|
|
13815
|
-
# model's output quality depends more on how precisely the target is stated than
|
|
13816
|
-
# on the model.
|
|
13817
|
-
#
|
|
13818
|
-
# DETERMINISTIC ON PURPOSE. No model call: this runs before a provider is even
|
|
13819
|
-
# selected, must work with no API key, and must not add latency or cost to the
|
|
13820
|
-
# first thing a new user does. It is keyword-to-obligation mapping, which is
|
|
13821
|
-
# honest about being shallow -- it turns stated nouns into checkable lines and
|
|
13822
|
-
# claims nothing about intent it cannot see. Anything cleverer belongs in the
|
|
13823
|
-
# spec-interrogation grill, which already runs after this and does call a model.
|
|
13824
|
-
#
|
|
13825
|
-
# STABLE IDs. Each criterion is emitted as "AC-<AXIS>-NNN: <text>" rather than a
|
|
13826
|
-
# bare bullet. An anonymous bullet cannot be referred to: a receipt can say "3 of
|
|
13827
|
-
# 8 gates passed" but never "AC-PERSIST-001 is satisfied by this test", drift
|
|
13828
|
-
# cannot be tracked per criterion, and two runs of the same spec produce lists
|
|
13829
|
-
# nothing can diff. The ID is what turns a criterion into a citable claim, which
|
|
13830
|
-
# is the whole point of shipping a receipt someone can check.
|
|
13831
|
-
#
|
|
13832
|
-
# The axis is derived from WHICH obligation fired, not from the criterion's
|
|
13833
|
-
# position, so IDs are stable across runs: adding a payment criterion never
|
|
13834
|
-
# renumbers the persistence one. Same reason we do not use a running counter.
|
|
13835
|
-
_brief_acceptance_criteria() {
|
|
13836
|
-
local t
|
|
13837
|
-
t="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
|
|
13838
|
-
local out=""
|
|
13839
|
-
# _bac <AXIS> <text>. The sequence is per-axis and always 001 today because
|
|
13840
|
-
# each axis fires at most once; the NNN slot exists so a second criterion on
|
|
13841
|
-
# the same axis can be added later without renumbering the first.
|
|
13842
|
-
_bac() { out="${out}- AC-${1}-001: ${2}"$'\n'; }
|
|
13843
|
-
|
|
13844
|
-
# Persistence. The single most common churn report is "I submitted the form
|
|
13845
|
-
# and nothing happened", so a stated store or form becomes an explicit
|
|
13846
|
-
# survives-a-reload obligation rather than an implied one.
|
|
13847
|
-
case "$t" in
|
|
13848
|
-
*save*|*persist*|*store*|*databas*|*crud*|*todo*|*note*|*task*|*record*)
|
|
13849
|
-
_bac PERSIST "Data the user creates survives a page reload and a server restart (it is written to a real store, not held in memory)." ;;
|
|
13850
|
-
esac
|
|
13851
|
-
case "$t" in
|
|
13852
|
-
*form*|*submit*|*signup*|*"sign up"*|*contact*|*upload*|*checkout*)
|
|
13853
|
-
_bac FORM "Every form actually submits: the happy path writes real data and the user sees a confirmation, and a validation failure shows an inline error." ;;
|
|
13854
|
-
esac
|
|
13855
|
-
case "$t" in
|
|
13856
|
-
*auth*|*login*|*"log in"*|*"sign in"*|*account*|*user*|*password*|*session*)
|
|
13857
|
-
_bac AUTH "Authentication works end to end: a real signup, a real login, and a protected route that returns 401 when logged out." ;;
|
|
13858
|
-
esac
|
|
13859
|
-
case "$t" in
|
|
13860
|
-
*api*|*endpoint*|*rest*|*graphql*|*backend*|*server*)
|
|
13861
|
-
_bac API "Each endpoint returns real data with correct status codes, and is callable with curl without a browser." ;;
|
|
13862
|
-
esac
|
|
13863
|
-
case "$t" in
|
|
13864
|
-
*payment*|*stripe*|*billing*|*subscription*|*checkout*|*invoice*)
|
|
13865
|
-
_bac PAY "The payment path is wired to the provider's test mode and a test transaction completes; no mocked charge stands in for the integration." ;;
|
|
13866
|
-
esac
|
|
13867
|
-
case "$t" in
|
|
13868
|
-
*search*|*filter*|*sort*)
|
|
13869
|
-
_bac SEARCH "Search or filtering queries the real dataset and returns different results for different inputs." ;;
|
|
13870
|
-
esac
|
|
13871
|
-
case "$t" in
|
|
13872
|
-
*dashboard*|*chart*|*graph*|*analytic*|*report*|*metric*)
|
|
13873
|
-
_bac DATA "Every figure shown traces to a real query. No hardcoded sample numbers." ;;
|
|
13874
|
-
esac
|
|
13875
|
-
case "$t" in
|
|
13876
|
-
*page*|*landing*|*site*|*website*|*ui*|*app*|*frontend*)
|
|
13877
|
-
_bac UI "The page renders with real content, no lorem ipsum, and no placeholder image where a real asset belongs." ;;
|
|
13878
|
-
esac
|
|
13879
|
-
printf '%s' "$out"
|
|
13880
|
-
}
|
|
13881
|
-
|
|
13882
14158
|
synthesize_brief_prd() {
|
|
13883
14159
|
local out_file="$1"
|
|
13884
14160
|
local brief_text="$2"
|
|
13885
14161
|
local out_dir
|
|
13886
14162
|
out_dir="$(dirname "$out_file")"
|
|
13887
14163
|
mkdir -p "$out_dir" 2>/dev/null || true
|
|
13888
|
-
local _derived
|
|
13889
|
-
_derived="$(_brief_acceptance_criteria "$brief_text")"
|
|
13890
14164
|
cat > "$out_file" << BRIEFEOF
|
|
13891
14165
|
# Project Brief
|
|
13892
14166
|
|
|
@@ -13902,7 +14176,6 @@ $brief_text
|
|
|
13902
14176
|
## Success Criteria
|
|
13903
14177
|
- A user can run the result and observe the core behavior described above.
|
|
13904
14178
|
- No errors on a clean start; the happy path works end to end.
|
|
13905
|
-
${_derived}
|
|
13906
14179
|
|
|
13907
14180
|
## Constraints
|
|
13908
14181
|
- This is a fast first pass (zero-config first run). Keep scope tight.
|
|
@@ -14004,10 +14277,6 @@ cmd_quick() {
|
|
|
14004
14277
|
find "$LOKI_DIR" -maxdepth 1 -name 'quick-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
14005
14278
|
find "$LOKI_DIR" -maxdepth 1 -name 'brief-prd-*.md' -mtime +1 -delete 2>/dev/null || true
|
|
14006
14279
|
local quick_prd="$LOKI_DIR/quick-prd-$$.md"
|
|
14007
|
-
# Same spec-derived criteria as the brief path: a quick task was getting the
|
|
14008
|
-
# identical generic Success Criteria regardless of what was asked.
|
|
14009
|
-
local _derived
|
|
14010
|
-
_derived="$(_brief_acceptance_criteria "$task_desc")"
|
|
14011
14280
|
cat > "$quick_prd" << QPRDEOF
|
|
14012
14281
|
# Quick Task
|
|
14013
14282
|
|
|
@@ -14024,7 +14293,6 @@ $task_desc
|
|
|
14024
14293
|
- Task is completed as described
|
|
14025
14294
|
- No errors or regressions introduced
|
|
14026
14295
|
- Code follows project conventions
|
|
14027
|
-
${_derived}
|
|
14028
14296
|
|
|
14029
14297
|
## Constraints
|
|
14030
14298
|
- This is a quick single-task execution
|
|
@@ -19489,20 +19757,6 @@ main() {
|
|
|
19489
19757
|
# happened to it afterwards -- reverted, reworked, or survived.
|
|
19490
19758
|
cmd_outcomes "$@"
|
|
19491
19759
|
;;
|
|
19492
|
-
verdict)
|
|
19493
|
-
# All five trust signals in one block. The individual measurements
|
|
19494
|
-
# existed and were unreachable; this is the surface a reviewer reads.
|
|
19495
|
-
cmd_verdict "$@"
|
|
19496
|
-
;;
|
|
19497
|
-
readiness)
|
|
19498
|
-
# Can an agent verify its own work here? Measured, not LLM-scored.
|
|
19499
|
-
cmd_readiness "$@"
|
|
19500
|
-
;;
|
|
19501
|
-
gates)
|
|
19502
|
-
# What blocks here vs only advises, and what promoting a gate would
|
|
19503
|
-
# have cost. Reports only; never promotes.
|
|
19504
|
-
cmd_gates "$@"
|
|
19505
|
-
;;
|
|
19506
19760
|
secure)
|
|
19507
19761
|
# Secure-by-default gate surface: inspect findings + manage waivers.
|
|
19508
19762
|
cmd_secure "$@"
|
|
@@ -25167,6 +25421,60 @@ TELEM_DISABLE_PY
|
|
|
25167
25421
|
echo " OTEL tracing can be configured with 'loki telemetry enable [endpoint]'."
|
|
25168
25422
|
;;
|
|
25169
25423
|
|
|
25424
|
+
analytics)
|
|
25425
|
+
# Opt-in gate for build-outcome analytics (telemetry.sh
|
|
25426
|
+
# _loki_analytics_enabled). That gate reads ANALYTICS_ENABLED=true from
|
|
25427
|
+
# ~/.loki/config, and until now NOTHING in this repo ever wrote that
|
|
25428
|
+
# key -- docs/PRIVACY.md told users to hand-edit the file. So the
|
|
25429
|
+
# first-run funnel (first_start_attempted / first_run_blocked /
|
|
25430
|
+
# build_verified) could not fire for anyone, and drop-off was
|
|
25431
|
+
# structurally unmeasurable.
|
|
25432
|
+
#
|
|
25433
|
+
# Stays STRICTLY OPT-IN and default OFF: this only gives the existing
|
|
25434
|
+
# consent a way to be expressed. Same filter-then-append shape as
|
|
25435
|
+
# start|on above, so it is idempotent and never clobbers other keys.
|
|
25436
|
+
local global_config="${HOME}/.loki/config"
|
|
25437
|
+
local _an_action="${1:-status}"
|
|
25438
|
+
case "$_an_action" in
|
|
25439
|
+
on)
|
|
25440
|
+
mkdir -p "${HOME}/.loki"
|
|
25441
|
+
if [ -f "$global_config" ]; then
|
|
25442
|
+
grep -v "^ANALYTICS_ENABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
|
|
25443
|
+
mv "${global_config}.tmp" "$global_config"
|
|
25444
|
+
fi
|
|
25445
|
+
echo "ANALYTICS_ENABLED=true" >> "$global_config"
|
|
25446
|
+
echo -e "${BOLD}Build analytics enabled${NC}"
|
|
25447
|
+
echo ""
|
|
25448
|
+
echo " Opt-in saved to: $global_config"
|
|
25449
|
+
echo " Only already-computed scalars are sent -- never code, spec text, or paths."
|
|
25450
|
+
echo " Requires telemetry to be on as well (loki telemetry status)."
|
|
25451
|
+
echo " Run 'loki telemetry analytics off' to opt back out."
|
|
25452
|
+
;;
|
|
25453
|
+
off)
|
|
25454
|
+
if [ -f "$global_config" ]; then
|
|
25455
|
+
grep -v "^ANALYTICS_ENABLED=" "$global_config" > "${global_config}.tmp" 2>/dev/null || true
|
|
25456
|
+
mv "${global_config}.tmp" "$global_config"
|
|
25457
|
+
fi
|
|
25458
|
+
echo -e "${BOLD}Build analytics disabled${NC}"
|
|
25459
|
+
echo ""
|
|
25460
|
+
echo " ANALYTICS_ENABLED removed from: $global_config"
|
|
25461
|
+
echo " Nothing build-related is sent. This is the default state."
|
|
25462
|
+
;;
|
|
25463
|
+
status)
|
|
25464
|
+
if [ -f "$global_config" ] && grep -q "^ANALYTICS_ENABLED=true" "$global_config" 2>/dev/null; then
|
|
25465
|
+
echo -e " Build analytics: ${GREEN}enabled${NC} (you opted in; opt out with: loki telemetry analytics off)"
|
|
25466
|
+
else
|
|
25467
|
+
echo -e " Build analytics: ${YELLOW}off${NC} (the default; opt in with: loki telemetry analytics on)"
|
|
25468
|
+
fi
|
|
25469
|
+
;;
|
|
25470
|
+
*)
|
|
25471
|
+
echo -e "${RED}Unknown analytics action: $_an_action${NC}"
|
|
25472
|
+
echo "Usage: loki telemetry analytics on|off|status"
|
|
25473
|
+
return 1
|
|
25474
|
+
;;
|
|
25475
|
+
esac
|
|
25476
|
+
;;
|
|
25477
|
+
|
|
25170
25478
|
--help|-h|help)
|
|
25171
25479
|
echo -e "${BOLD}loki telemetry${NC} - OpenTelemetry management"
|
|
25172
25480
|
echo ""
|
|
@@ -25180,6 +25488,8 @@ TELEM_DISABLE_PY
|
|
|
25180
25488
|
echo " off Opt out of all anonymous diagnostics (telemetry + crash)"
|
|
25181
25489
|
echo " start Alias for 'on' (persistent opt-in across all sessions)"
|
|
25182
25490
|
echo " stop Alias for 'off' (persistent opt-out across all sessions)"
|
|
25491
|
+
echo " analytics on|off|status"
|
|
25492
|
+
echo " Opt in/out of build-outcome analytics (off by default)"
|
|
25183
25493
|
echo ""
|
|
25184
25494
|
echo "Anonymous diagnostics are OFF by default. Nothing is collected or"
|
|
25185
25495
|
echo "sent unless you opt in. See docs/PRIVACY.md for the full disclosure."
|
|
@@ -30978,136 +31288,6 @@ cmd_outcomes() {
|
|
|
30978
31288
|
LOKI_DIR="${LOKI_DIR:-.loki}" python3 "$lib" "$@"
|
|
30979
31289
|
}
|
|
30980
31290
|
|
|
30981
|
-
# loki verdict: the five measured trust signals, in one block a reviewer reads
|
|
30982
|
-
# in ten seconds.
|
|
30983
|
-
#
|
|
30984
|
-
# WHY A COMMAND AND NOT JUST A LIBRARY. We measure five things nobody else does
|
|
30985
|
-
# -- did the work survive, does the spec still match intent, was this the agent
|
|
30986
|
-
# or a human rescue, does the completion claim name real work, which model
|
|
30987
|
-
# decided -- and each was correct, tested, and unreachable. A moat nobody can
|
|
30988
|
-
# see is not a moat. This is the surface.
|
|
30989
|
-
#
|
|
30990
|
-
# There is deliberately NO composite score. Averaging a revert count, a hash
|
|
30991
|
-
# comparison, a diff hash, a path match and a model id yields a number whose
|
|
30992
|
-
# movement nobody can explain, which is what competitors already ship. UNKNOWN
|
|
30993
|
-
# is PRINTED, never suppressed: a reviewer must tell "we checked and it is fine"
|
|
30994
|
-
# from "we could not check". See autonomy/lib/verdict.py.
|
|
30995
|
-
cmd_verdict() {
|
|
30996
|
-
local lib="${_LOKI_SCRIPT_DIR}/lib/verdict.py"
|
|
30997
|
-
if [ ! -f "$lib" ]; then
|
|
30998
|
-
echo "verdict renderer is not installed at $lib" >&2
|
|
30999
|
-
return 2
|
|
31000
|
-
fi
|
|
31001
|
-
case "${1:-}" in
|
|
31002
|
-
--help|-h|help)
|
|
31003
|
-
echo -e "${BOLD}loki verdict${NC} - the five measured trust signals, in one readable block"
|
|
31004
|
-
echo ""
|
|
31005
|
-
echo "Usage: loki verdict [--json]"
|
|
31006
|
-
echo ""
|
|
31007
|
-
echo "Prints one line each for outcome, intent, authorship, grounding and"
|
|
31008
|
-
echo "model: what survived, whether the spec still matches intent, whether"
|
|
31009
|
-
echo "this was the agent or a human rescue, whether the completion claim"
|
|
31010
|
-
echo "named real work, and which model decided."
|
|
31011
|
-
echo ""
|
|
31012
|
-
echo "Every line is either a measured fact or an explicit UNKNOWN. There is"
|
|
31013
|
-
echo "no composite score: a number nobody can explain is not evidence."
|
|
31014
|
-
echo "Read-only: it never writes to the repo it analyses."
|
|
31015
|
-
return 0
|
|
31016
|
-
;;
|
|
31017
|
-
esac
|
|
31018
|
-
LOKI_DIR="${LOKI_DIR:-.loki}" python3 "$lib" "$@"
|
|
31019
|
-
}
|
|
31020
|
-
|
|
31021
|
-
# loki readiness: can an autonomous agent verify its own work in THIS repo?
|
|
31022
|
-
#
|
|
31023
|
-
# WHY THIS IS NOT A COPY OF FACTORY AI'S AGENT READINESS MODEL. Theirs is
|
|
31024
|
-
# LLM-scored -- their report objects record modelUsed and reasoningEffort, so
|
|
31025
|
-
# the number is a model's opinion and two runs can disagree about the same
|
|
31026
|
-
# commit. Every criterion here is a file that exists or does not, a command
|
|
31027
|
-
# present or absent: same commit, same answer, every machine, no key, no spend.
|
|
31028
|
-
#
|
|
31029
|
-
# No percentage and no letter grade. A composite invites ranking, ranking
|
|
31030
|
-
# invites gaming, and the individual signals are the actionable part -- "there
|
|
31031
|
-
# is no test command" tells you what to do, "readiness 62%" does not. Criteria
|
|
31032
|
-
# that cannot be determined report UNKNOWN by name rather than counting as
|
|
31033
|
-
# failures. See autonomy/lib/agent_readiness.py.
|
|
31034
|
-
cmd_gates() {
|
|
31035
|
-
local lib="${_LOKI_SCRIPT_DIR}/lib/gate_policy.py"
|
|
31036
|
-
if [ ! -f "$lib" ]; then
|
|
31037
|
-
echo "gate policy reporter is not installed at $lib" >&2
|
|
31038
|
-
return 2
|
|
31039
|
-
fi
|
|
31040
|
-
case "${1:-}" in
|
|
31041
|
-
--help|-h|help)
|
|
31042
|
-
echo -e "${BOLD}loki gates${NC} - what blocks here, and what only advises"
|
|
31043
|
-
echo ""
|
|
31044
|
-
echo "Usage: loki gates [.loki-dir] [--json]"
|
|
31045
|
-
echo ""
|
|
31046
|
-
echo "Ona's Veto Exec ships an audit-first ladder: start in audit mode,"
|
|
31047
|
-
echo "review what matched, then promote the confirmed rules to block. The"
|
|
31048
|
-
echo "middle step is the load-bearing one -- a policy you cannot safely"
|
|
31049
|
-
echo "turn on is a policy nobody turns on."
|
|
31050
|
-
echo ""
|
|
31051
|
-
echo "We had both ends and nothing between them: gates are advisory or"
|
|
31052
|
-
echo "blocking, three promotion knobs exist, and the failure ledger has"
|
|
31053
|
-
echo "counted per-gate hits all along. Nothing joined them, so deciding"
|
|
31054
|
-
echo "whether to promote a gate meant guessing."
|
|
31055
|
-
echo ""
|
|
31056
|
-
echo "For each gate this prints its mode, how many times it has fired,"
|
|
31057
|
-
echo "and -- for an advisory one -- the exact variable that promotes it."
|
|
31058
|
-
echo ""
|
|
31059
|
-
echo "Deterministic: reads two files and the environment. No model, no"
|
|
31060
|
-
echo "key, no spend. Counts come from"
|
|
31061
|
-
echo ".loki/quality/gate-failure-count.json; open it and count them"
|
|
31062
|
-
echo "yourself."
|
|
31063
|
-
echo ""
|
|
31064
|
-
echo "A gate with no ledger entry reports 'not measured', never 0: an"
|
|
31065
|
-
echo "absent measurement is not evidence a gate never fired."
|
|
31066
|
-
echo ""
|
|
31067
|
-
echo "This command NEVER promotes a gate. Promotion stays an explicit"
|
|
31068
|
-
echo "operator act via the named variable."
|
|
31069
|
-
return 0
|
|
31070
|
-
;;
|
|
31071
|
-
esac
|
|
31072
|
-
python3 "$lib" "$@"
|
|
31073
|
-
}
|
|
31074
|
-
|
|
31075
|
-
cmd_readiness() {
|
|
31076
|
-
local lib="${_LOKI_SCRIPT_DIR}/lib/agent_readiness.py"
|
|
31077
|
-
if [ ! -f "$lib" ]; then
|
|
31078
|
-
echo "readiness assessor is not installed at $lib" >&2
|
|
31079
|
-
return 2
|
|
31080
|
-
fi
|
|
31081
|
-
case "${1:-}" in
|
|
31082
|
-
--help|-h|help)
|
|
31083
|
-
echo -e "${BOLD}loki readiness${NC} - can an agent verify its own work in this repo?"
|
|
31084
|
-
echo ""
|
|
31085
|
-
echo "Usage: loki readiness [path] [--json] [--fix]"
|
|
31086
|
-
echo ""
|
|
31087
|
-
echo "Measures whether this repo gives an agent a way to check itself: a"
|
|
31088
|
-
echo "test command, a build, CI config, typed sources, a lockfile. Not"
|
|
31089
|
-
echo "general code quality -- the narrower question every agent depends on."
|
|
31090
|
-
echo ""
|
|
31091
|
-
echo "Deterministic: no model, no key, no spend. Same commit, same answer."
|
|
31092
|
-
echo "Criteria that cannot be determined report UNKNOWN rather than failing."
|
|
31093
|
-
echo ""
|
|
31094
|
-
echo -e "${BOLD}--fix${NC} writes the missing files whose content can be derived"
|
|
31095
|
-
echo "honestly (README.md, AGENTS.md, .gitignore) as TODO stubs, then"
|
|
31096
|
-
echo "re-measures and reports what is true AFTER the change."
|
|
31097
|
-
echo ""
|
|
31098
|
-
echo "It deliberately REFUSES to generate a test command, a lockfile or a"
|
|
31099
|
-
echo "CI config. Guessing one writes a line that lies: an invented"
|
|
31100
|
-
echo "'npm test' in a repo with no runner fails forever, and this check"
|
|
31101
|
-
echo "would then report the criterion present for something that does not"
|
|
31102
|
-
echo "work. Those stay reported, never generated."
|
|
31103
|
-
echo ""
|
|
31104
|
-
echo "Without --fix it is read-only and never writes to the repo."
|
|
31105
|
-
return 0
|
|
31106
|
-
;;
|
|
31107
|
-
esac
|
|
31108
|
-
python3 "$lib" "$@"
|
|
31109
|
-
}
|
|
31110
|
-
|
|
31111
31291
|
cmd_wiki() {
|
|
31112
31292
|
local subcmd="${1:-}"
|
|
31113
31293
|
shift 2>/dev/null || true
|