loki-mode 9.52.0 → 9.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +84 -48
- package/autonomy/council-v2.sh +13 -7
- package/autonomy/lib/done-recognition.sh +24 -16
- package/autonomy/lib/proof-check.sh +11 -3
- package/autonomy/lib/voter-agents.sh +8 -3
- package/autonomy/lib/workspace_diff.py +112 -1
- package/autonomy/prd-checklist.sh +19 -11
- package/autonomy/run.sh +336 -39
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BACKLOG.md +51 -0
- package/docs/v10/METRICS.md +3 -1
- package/docs/v10/PROGRESS.md +35 -4
- package/loki-ts/dist/loki.js +323 -322
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -2678,7 +2678,7 @@ _advance_current_phase() {
|
|
|
2678
2678
|
[ -f "$orch" ] || return 0
|
|
2679
2679
|
# Values are passed via argv (not interpolated into the source) so a phase or
|
|
2680
2680
|
# path containing quotes can never break or inject into the python.
|
|
2681
|
-
python3 -c "
|
|
2681
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
2682
2682
|
import json, sys
|
|
2683
2683
|
f, phase = sys.argv[1], sys.argv[2]
|
|
2684
2684
|
try:
|
|
@@ -4377,7 +4377,7 @@ _loki_receipt_facts() {
|
|
|
4377
4377
|
local pj="$loki_dir/proofs/$rid/proof.json"
|
|
4378
4378
|
[ -f "$pj" ] || return 0
|
|
4379
4379
|
local headline=""
|
|
4380
|
-
headline="$(python3 -c "
|
|
4380
|
+
headline="$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
4381
4381
|
import json, sys
|
|
4382
4382
|
try:
|
|
4383
4383
|
d = json.load(open(sys.argv[1]))
|
|
@@ -4999,7 +4999,7 @@ except Exception:
|
|
|
4999
4999
|
_LOKI_CS_ASSUMPTIONS_HIGH="$assumptions_high" \
|
|
5000
5000
|
_LOKI_CS_OUT_FILE="$loki_dir/state/completion.json" \
|
|
5001
5001
|
_LOKI_CS_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
|
|
5002
|
-
python3 -c "
|
|
5002
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
5003
5003
|
import json, os, tempfile
|
|
5004
5004
|
out = os.environ['_LOKI_CS_OUT_FILE']
|
|
5005
5005
|
def i(v):
|
|
@@ -5219,7 +5219,7 @@ print_completion_card() {
|
|
|
5219
5219
|
# split would collapse adjacent empties). Any failure leaves the card
|
|
5220
5220
|
# unrendered. Trailing-newline guard: NUL-free, fields are single-line.
|
|
5221
5221
|
local _fields
|
|
5222
|
-
_fields="$(python3 -c "
|
|
5222
|
+
_fields="$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
5223
5223
|
import json,sys
|
|
5224
5224
|
try:
|
|
5225
5225
|
d=json.load(open(sys.argv[1]))
|
|
@@ -5541,7 +5541,8 @@ validate_exec_manifest_result() {
|
|
|
5541
5541
|
result_file=$(mktemp "${TARGET_DIR}/.loki/.exec-manifest-result.XXXXXX") || return 1
|
|
5542
5542
|
LOKI_RESULT_FILE="$result_file" LOKI_RESULT_STREAM="$stream_name" \
|
|
5543
5543
|
LOKI_RESULT_BASE="$base_sha" LOKI_RESULT_BRANCH="$branch" \
|
|
5544
|
-
LOKI_RESULT_REPO="$TARGET_DIR" python3 <<'PY'
|
|
5544
|
+
LOKI_RESULT_REPO="$TARGET_DIR" python3 -E - <<'PY'
|
|
5545
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
5545
5546
|
import json, os, subprocess
|
|
5546
5547
|
paths = subprocess.check_output(
|
|
5547
5548
|
["git", "-C", os.environ["LOKI_RESULT_REPO"], "diff", "--name-only",
|
|
@@ -8098,7 +8099,7 @@ generate_proof_of_run() {
|
|
|
8098
8099
|
ITERATION_COUNT="${ITERATION_COUNT:-0}" \
|
|
8099
8100
|
PROVIDER_NAME="$provider" \
|
|
8100
8101
|
PRD_PATH="${prd_path:-}" \
|
|
8101
|
-
python3 "$gen" \
|
|
8102
|
+
python3 -E "$gen" \
|
|
8102
8103
|
--loki-dir "$loki_dir" \
|
|
8103
8104
|
--loki-version "$ver" \
|
|
8104
8105
|
--provider "$provider" \
|
|
@@ -8125,7 +8126,7 @@ generate_proof_of_run() {
|
|
|
8125
8126
|
ITERATION_COUNT="${ITERATION_COUNT:-0}" \
|
|
8126
8127
|
PROVIDER_NAME="$provider" \
|
|
8127
8128
|
PRD_PATH="${prd_path:-}" \
|
|
8128
|
-
python3 "$gen" \
|
|
8129
|
+
python3 -E "$gen" \
|
|
8129
8130
|
--loki-dir "$loki_dir" \
|
|
8130
8131
|
--loki-version "$ver" \
|
|
8131
8132
|
--provider "$provider" \
|
|
@@ -9063,6 +9064,10 @@ setup_agent_branch() {
|
|
|
9063
9064
|
# Controlled by LOKI_BRANCH_PROTECTION env var (default: true). Set it to
|
|
9064
9065
|
# "false" to opt out fully and work on the current branch (back-compat).
|
|
9065
9066
|
local branch_protection="${LOKI_BRANCH_PROTECTION:-true}"
|
|
9067
|
+
# Set to 1 only once THIS run has a current snapshot (mint or resume
|
|
9068
|
+
# union); _loki_record_session_created records nothing otherwise, since a
|
|
9069
|
+
# leftover or stale snapshot would make it claim a user's file.
|
|
9070
|
+
_LOKI_SNAPSHOT_THIS_RUN=0
|
|
9066
9071
|
|
|
9067
9072
|
if [ "$branch_protection" != "true" ]; then
|
|
9068
9073
|
log_info "Branch protection disabled (LOKI_BRANCH_PROTECTION=${branch_protection})"
|
|
@@ -9097,11 +9102,15 @@ setup_agent_branch() {
|
|
|
9097
9102
|
|
|
9098
9103
|
# Already on a loki branch (session-* or delegate-*): idempotent reuse, do
|
|
9099
9104
|
# not nest a branch off a loki branch (LOCK A5/A7).
|
|
9105
|
+
# Both reuse paths add what is untracked now to the pre-existing list (a
|
|
9106
|
+
# file the user made between sessions is theirs), except the files an
|
|
9107
|
+
# unfinished previous session created (_loki_resume_snapshot).
|
|
9100
9108
|
case "$cur" in
|
|
9101
9109
|
loki/*)
|
|
9102
9110
|
log_info "Already on loki branch ${cur}"
|
|
9103
9111
|
mkdir -p .loki/state 2>/dev/null || true
|
|
9104
9112
|
printf '%s\n' "$cur" > .loki/state/agent-branch.txt 2>/dev/null || true
|
|
9113
|
+
_loki_resume_snapshot
|
|
9105
9114
|
return 0
|
|
9106
9115
|
;;
|
|
9107
9116
|
esac
|
|
@@ -9112,8 +9121,14 @@ setup_agent_branch() {
|
|
|
9112
9121
|
if [ -s .loki/state/agent-branch.txt ]; then
|
|
9113
9122
|
recorded="$(cat .loki/state/agent-branch.txt 2>/dev/null || true)"
|
|
9114
9123
|
if [ -n "$recorded" ] && git rev-parse --verify "$recorded" >/dev/null 2>&1; then
|
|
9115
|
-
|
|
9124
|
+
# --no-overwrite-ignore: git's default checkout silently replaces a
|
|
9125
|
+
# gitignored user file at a path the session branch tracks, and the
|
|
9126
|
+
# file is gone when the user switches back. On conflict the resume
|
|
9127
|
+
# is refused and a fresh session branch (with a fresh snapshot) is
|
|
9128
|
+
# minted below.
|
|
9129
|
+
if git checkout --no-overwrite-ignore "$recorded" >/dev/null 2>&1; then
|
|
9116
9130
|
log_info "Resuming on recorded agent branch: ${recorded}"
|
|
9131
|
+
_loki_resume_snapshot
|
|
9117
9132
|
return 0
|
|
9118
9133
|
fi
|
|
9119
9134
|
log_warn "Recorded agent branch ${recorded} could not be checked out - creating a new one"
|
|
@@ -9132,12 +9147,31 @@ setup_agent_branch() {
|
|
|
9132
9147
|
|
|
9133
9148
|
log_info "Branch protection enabled - creating agent branch: $branch_name (base: $cur)"
|
|
9134
9149
|
|
|
9150
|
+
# Record the user's own untracked and gitignored files so
|
|
9151
|
+
# commit_session_changes and the receipt leave them alone.
|
|
9152
|
+
local snap_ok=1 leftover=""
|
|
9153
|
+
if ! _loki_snapshot_or_fail_closed; then
|
|
9154
|
+
snap_ok=0
|
|
9155
|
+
rm -f .loki/state/preexisting-untracked.z .loki/state/preexisting-untracked.sha.z 2>/dev/null
|
|
9156
|
+
log_warn "Could not record pre-existing untracked files; this session will commit nothing (review and commit manually)"
|
|
9157
|
+
fi
|
|
9158
|
+
|
|
9135
9159
|
# Create and checkout the feature branch
|
|
9136
9160
|
if ! git checkout -b "$branch_name" 2>/dev/null; then
|
|
9137
9161
|
log_error "Failed to create agent branch: $branch_name"
|
|
9138
9162
|
return 1
|
|
9139
9163
|
fi
|
|
9140
9164
|
|
|
9165
|
+
# A new session branch starts with an empty session-created record. Files
|
|
9166
|
+
# an unfinished earlier session left (reached here after a refused resume)
|
|
9167
|
+
# were just snapshotted as pre-existing: kept on disk, never committed.
|
|
9168
|
+
leftover="$(_loki_nul_names .loki/state/session-created.z)"
|
|
9169
|
+
if [ -n "$leftover" ]; then
|
|
9170
|
+
log_warn "Left uncommitted from an earlier unfinished session, now treated as your files: $leftover"
|
|
9171
|
+
fi
|
|
9172
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9173
|
+
if [ "$snap_ok" = 1 ]; then _LOKI_SNAPSHOT_THIS_RUN=1; fi
|
|
9174
|
+
|
|
9141
9175
|
# Store the branch name for later use (PR creation, cleanup)
|
|
9142
9176
|
printf '%s\n' "$branch_name" > .loki/state/agent-branch.txt 2>/dev/null
|
|
9143
9177
|
|
|
@@ -9146,6 +9180,180 @@ setup_agent_branch() {
|
|
|
9146
9180
|
echo "$branch_name"
|
|
9147
9181
|
}
|
|
9148
9182
|
|
|
9183
|
+
# Snapshot, or fail closed. Any snapshot failure (for example git older than
|
|
9184
|
+
# 2.18, whose status lacks --no-renames / --ignored=matching) leaves a marker
|
|
9185
|
+
# that makes commit_session_changes commit nothing: a missing snapshot must
|
|
9186
|
+
# only ever mean an older session, never "sweep everything".
|
|
9187
|
+
_loki_snapshot_or_fail_closed() {
|
|
9188
|
+
if _loki_snapshot_preexisting "$@"; then
|
|
9189
|
+
rm -f .loki/state/preexisting-untracked.failed 2>/dev/null
|
|
9190
|
+
return 0
|
|
9191
|
+
fi
|
|
9192
|
+
mkdir -p .loki/state 2>/dev/null
|
|
9193
|
+
: > .loki/state/preexisting-untracked.failed 2>/dev/null
|
|
9194
|
+
return 1
|
|
9195
|
+
}
|
|
9196
|
+
|
|
9197
|
+
# _loki_snapshot_preexisting [union]
|
|
9198
|
+
# Write .loki/state/preexisting-untracked.z: every path git does not track,
|
|
9199
|
+
# untracked and gitignored, repo-wide, NUL-delimited, relative to the repo top.
|
|
9200
|
+
# commit_session_changes unstages these and the receipt
|
|
9201
|
+
# (autonomy/lib/workspace_diff.py) does not claim them. --ignored=matching lists
|
|
9202
|
+
# a directory only when an ignore pattern matches it, so node_modules/ is one
|
|
9203
|
+
# "dir/" entry covering its subtree, while logs/ holding only *.log files is
|
|
9204
|
+
# listed file by file (a new logs/app.json is still the agent's). "union" keeps
|
|
9205
|
+
# the paths already recorded. The sibling .sha.z (content hash per file entry)
|
|
9206
|
+
# lets the receipt list a pre-existing file the run changed; it is removed
|
|
9207
|
+
# first, so a failed hash step means no detection, never stale hashes. Returns
|
|
9208
|
+
# non-zero, leaving any earlier list in place, when git cannot list.
|
|
9209
|
+
_loki_snapshot_preexisting() {
|
|
9210
|
+
local snap=".loki/state/preexisting-untracked.z" top="" base="" exclude=""
|
|
9211
|
+
rm -f "${snap%.z}.sha.z" 2>/dev/null
|
|
9212
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
9213
|
+
mkdir -p .loki/state 2>/dev/null || return 1
|
|
9214
|
+
# "union" never adds a path listed in session-created.z: those files were
|
|
9215
|
+
# made by a session that did not finish (interrupt, pod loss), so the
|
|
9216
|
+
# session that finishes the work commits them. If the user edits one of
|
|
9217
|
+
# them between sessions it is still the session's file and is committed
|
|
9218
|
+
# with the edit. Recorded entries only block additions; they never remove
|
|
9219
|
+
# a path already in the list.
|
|
9220
|
+
if [ "${1:-}" = union ]; then
|
|
9221
|
+
base="$snap"
|
|
9222
|
+
exclude=".loki/state/session-created.z"
|
|
9223
|
+
fi
|
|
9224
|
+
if ! _loki_untracked_status "$snap.status" \
|
|
9225
|
+
|| ! _loki_untracked_merge "$snap.status" "$base" "$exclude" exact "$snap"; then
|
|
9226
|
+
rm -f "$snap.status" "$snap.tmp"
|
|
9227
|
+
return 1
|
|
9228
|
+
fi
|
|
9229
|
+
rm -f "$snap.status"
|
|
9230
|
+
python3 -E "$SCRIPT_DIR/lib/workspace_diff.py" hash-snapshot "$top" "$snap" >/dev/null 2>&1 \
|
|
9231
|
+
|| log_warn "Could not hash your pre-existing untracked files; the receipt cannot list the ones this run changes"
|
|
9232
|
+
return 0
|
|
9233
|
+
}
|
|
9234
|
+
|
|
9235
|
+
# _loki_untracked_status <out>: the one enumeration behind both the snapshot
|
|
9236
|
+
# and the session-created record, so the two always agree: raw
|
|
9237
|
+
# `git status --porcelain -z` records for the whole repo, .loki excluded.
|
|
9238
|
+
# Non-zero, writing nothing, when git cannot list.
|
|
9239
|
+
_loki_untracked_status() {
|
|
9240
|
+
local top="" prefix=""
|
|
9241
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
9242
|
+
prefix="$(git rev-parse --show-prefix 2>/dev/null)" || return 1
|
|
9243
|
+
git -C "$top" --no-optional-locks status --porcelain -z --no-renames -uall \
|
|
9244
|
+
--ignored=matching --ignore-submodules=all -- ":(exclude,literal)${prefix}.loki" \
|
|
9245
|
+
> "$1" 2>/dev/null && return 0
|
|
9246
|
+
rm -f "$1"
|
|
9247
|
+
return 1
|
|
9248
|
+
}
|
|
9249
|
+
|
|
9250
|
+
# _loki_untracked_merge <status> <base> <exclude> exact|cover <out>
|
|
9251
|
+
# Atomically write <out>: the NUL-delimited paths of <base>, plus every
|
|
9252
|
+
# untracked ("??") or ignored ("!!") path in <status> that <exclude> does not
|
|
9253
|
+
# hold. With "cover", an <exclude> entry ending in "/" also holds every path
|
|
9254
|
+
# below it (workspace_diff._covered). An empty or missing <base> or <exclude>
|
|
9255
|
+
# is an empty list; any other read or write failure is non-zero and leaves
|
|
9256
|
+
# <out> as it was. Sorted bytewise, like `LC_ALL=C sort -z -u`. One python
|
|
9257
|
+
# process: bash 3.2 has no associative arrays, and a bash loop cost 300ms per
|
|
9258
|
+
# 6,000 entries on /bin/bash. -E and no cwd on sys.path (D7): the cwd is the
|
|
9259
|
+
# agent's repo.
|
|
9260
|
+
_loki_untracked_merge() {
|
|
9261
|
+
python3 -E -c 'import sys
|
|
9262
|
+
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
9263
|
+
import os
|
|
9264
|
+
status, base, exclude, mode, out = sys.argv[1:6]
|
|
9265
|
+
def entries(path, missing_ok=True):
|
|
9266
|
+
try:
|
|
9267
|
+
with open(path, "rb") as fh:
|
|
9268
|
+
return [p for p in fh.read().split(b"\0") if p]
|
|
9269
|
+
except FileNotFoundError:
|
|
9270
|
+
if missing_ok:
|
|
9271
|
+
return []
|
|
9272
|
+
raise
|
|
9273
|
+
held = set(entries(exclude)) if exclude else set()
|
|
9274
|
+
def covered(path):
|
|
9275
|
+
if path in held:
|
|
9276
|
+
return True
|
|
9277
|
+
cut = path.find(b"/") if mode == "cover" else -1
|
|
9278
|
+
while cut != -1:
|
|
9279
|
+
if path[:cut + 1] in held:
|
|
9280
|
+
return True
|
|
9281
|
+
cut = path.find(b"/", cut + 1)
|
|
9282
|
+
return False
|
|
9283
|
+
paths = set(entries(base)) if base else set()
|
|
9284
|
+
for rec in entries(status, missing_ok=False):
|
|
9285
|
+
# The session-created record ("cover" mode) never holds a directory entry:
|
|
9286
|
+
# a whole-directory entry (logs/, out/) would stop the resume union from
|
|
9287
|
+
# adopting a file the user later creates inside it, and a .gitignore
|
|
9288
|
+
# rewrite would then sweep that file. Adopting such a directory as
|
|
9289
|
+
# pre-existing fails safe (the agent files inside stay uncommitted).
|
|
9290
|
+
if mode == "cover" and rec.endswith(b"/"):
|
|
9291
|
+
continue
|
|
9292
|
+
if rec[:3] in (b"?? ", b"!! ") and not covered(rec[3:]):
|
|
9293
|
+
paths.add(rec[3:])
|
|
9294
|
+
with open(out + ".tmp", "wb") as fh:
|
|
9295
|
+
fh.write(b"".join(p + b"\0" for p in sorted(paths)))
|
|
9296
|
+
os.replace(out + ".tmp", out)' "$@" 2>/dev/null
|
|
9297
|
+
}
|
|
9298
|
+
|
|
9299
|
+
# _loki_record_session_created
|
|
9300
|
+
# Union into .loki/state/session-created.z (NUL-delimited, repo-top-relative,
|
|
9301
|
+
# atomic) the paths git does not track now that the pre-existing snapshot does
|
|
9302
|
+
# not cover: the files this session created. The resume union never adds these
|
|
9303
|
+
# to the snapshot, so a session that is interrupted (cleanup) or killed (pod
|
|
9304
|
+
# loss: no trap runs, hence the call after every provider turn) still has its
|
|
9305
|
+
# own files committed by the session that finishes. commit_session_changes
|
|
9306
|
+
# clears the record after a normal commit; a new session branch starts empty.
|
|
9307
|
+
# Records nothing unless this run took a current snapshot (a leftover or stale
|
|
9308
|
+
# one would make it claim a user's file). On a failure the record is left as it
|
|
9309
|
+
# was, never partial, and preexisting-untracked.failed is never touched; the
|
|
9310
|
+
# next successful call recomputes everything, so only a failure on the final
|
|
9311
|
+
# call before a kill loses paths (those are then treated as the user's: kept on
|
|
9312
|
+
# disk, not committed).
|
|
9313
|
+
_loki_record_session_created() {
|
|
9314
|
+
local snap=".loki/state/preexisting-untracked.z" out=".loki/state/session-created.z"
|
|
9315
|
+
[ "${_LOKI_SNAPSHOT_THIS_RUN:-0}" = 1 ] || return 0
|
|
9316
|
+
if [ -f "$snap" ] && _loki_untracked_status "$out.status" \
|
|
9317
|
+
&& _loki_untracked_merge "$out.status" "$out" "$snap" cover "$out"; then
|
|
9318
|
+
rm -f "$out.status"
|
|
9319
|
+
return 0
|
|
9320
|
+
fi
|
|
9321
|
+
rm -f "$out.status" "$out.tmp" 2>/dev/null
|
|
9322
|
+
log_warn "Could not record the files this session created; if the run stops before the next record, they are treated as yours and left uncommitted"
|
|
9323
|
+
return 1
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9326
|
+
# _loki_resume_snapshot: the resume paths' union, naming the files carried over
|
|
9327
|
+
# from an unfinished previous session (this session's commit includes them).
|
|
9328
|
+
_loki_resume_snapshot() {
|
|
9329
|
+
local carried=""
|
|
9330
|
+
if ! _loki_snapshot_or_fail_closed union; then
|
|
9331
|
+
log_warn "Could not add your current untracked files to the pre-existing list; this session will commit nothing (review and commit manually)"
|
|
9332
|
+
return 0
|
|
9333
|
+
fi
|
|
9334
|
+
_LOKI_SNAPSHOT_THIS_RUN=1
|
|
9335
|
+
carried="$(_loki_nul_names .loki/state/session-created.z)"
|
|
9336
|
+
if [ -n "$carried" ]; then
|
|
9337
|
+
log_info "Carried over as this session's work, not adopted as your files (created by the unfinished previous session): $carried"
|
|
9338
|
+
fi
|
|
9339
|
+
}
|
|
9340
|
+
|
|
9341
|
+
# _loki_nul_names <file>: the entries of a NUL-delimited repo-top-relative list
|
|
9342
|
+
# that exist now, comma-separated, the first 20 then "(and N more)".
|
|
9343
|
+
_loki_nul_names() {
|
|
9344
|
+
local p top="" n=0 out=""
|
|
9345
|
+
[ -s "$1" ] || return 0
|
|
9346
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
|
|
9347
|
+
while IFS= read -r -d '' p; do
|
|
9348
|
+
if [ -e "$top/$p" ] || [ -L "$top/$p" ]; then
|
|
9349
|
+
n=$((n + 1))
|
|
9350
|
+
if [ "$n" -le 20 ]; then out="${out}${out:+, }${p}"; fi
|
|
9351
|
+
fi
|
|
9352
|
+
done < "$1"
|
|
9353
|
+
if [ "$n" -gt 20 ]; then out="${out} (and $((n - 20)) more)"; fi
|
|
9354
|
+
printf '%s' "$out"
|
|
9355
|
+
}
|
|
9356
|
+
|
|
9149
9357
|
# RUN-25 iter 21 (Wave D #2): the two secret matchers now live in one sourceable
|
|
9150
9358
|
# lib so the commit gate (here) and the completion evidence gate (completion-
|
|
9151
9359
|
# council.sh) share a single implementation. Sourced with a guard so a re-source
|
|
@@ -9210,8 +9418,37 @@ commit_session_changes() {
|
|
|
9210
9418
|
':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
|
|
9211
9419
|
':!id_rsa*' ':!*.token' ':!credentials*' 2>/dev/null || true
|
|
9212
9420
|
|
|
9213
|
-
#
|
|
9421
|
+
# Unstage exactly the paths recorded as untracked or gitignored when the
|
|
9422
|
+
# session started (setup_agent_branch; a "dir/" entry covers its subtree):
|
|
9423
|
+
# they are the user's, not this session's work, and committing them here
|
|
9424
|
+
# would delete them from disk on a later checkout of the base (an agent
|
|
9425
|
+
# rewrite of .gitignore can expose ignored files to `git add -A`).
|
|
9426
|
+
# Agent-created files stay staged. Skip an empty snapshot: an
|
|
9427
|
+
# empty --pathspec-from-file resets the WHOLE index. No snapshot (older
|
|
9428
|
+
# session): behave as before. If the unstage fails (git < 2.25), commit
|
|
9429
|
+
# nothing rather than sweep the user's files in.
|
|
9430
|
+
if [ -f "$PWD/.loki/state/preexisting-untracked.failed" ]; then
|
|
9431
|
+
git reset -q >/dev/null 2>&1 || true
|
|
9432
|
+
log_warn "Left uncommitted: could not record your pre-existing untracked files at session start (the snapshot needs git 2.18+, excluding them needs 2.25+). Review and commit manually."
|
|
9433
|
+
return 0
|
|
9434
|
+
fi
|
|
9435
|
+
local preexisting="$PWD/.loki/state/preexisting-untracked.z" top=""
|
|
9436
|
+
if [ -s "$preexisting" ]; then
|
|
9437
|
+
if ! { top="$(git rev-parse --show-toplevel 2>/dev/null)" \
|
|
9438
|
+
&& git -C "$top" --literal-pathspecs reset -q \
|
|
9439
|
+
--pathspec-from-file="$preexisting" --pathspec-file-nul >/dev/null 2>&1; }; then
|
|
9440
|
+
git reset -q >/dev/null 2>&1 || true
|
|
9441
|
+
log_warn "Left uncommitted: could not exclude your pre-existing untracked files from the session commit (needs git 2.25+). Review and commit manually."
|
|
9442
|
+
return 0
|
|
9443
|
+
fi
|
|
9444
|
+
fi
|
|
9445
|
+
|
|
9446
|
+
# Nothing staged = clean no-op, never an error. A session that ends here or
|
|
9447
|
+
# commits below ended normally: its session-created record is spent (what
|
|
9448
|
+
# it listed is committed, or was never committable), so the next session
|
|
9449
|
+
# starts clean.
|
|
9214
9450
|
if git diff --cached --quiet 2>/dev/null; then
|
|
9451
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9215
9452
|
return 0
|
|
9216
9453
|
fi
|
|
9217
9454
|
|
|
@@ -9253,7 +9490,9 @@ commit_session_changes() {
|
|
|
9253
9490
|
return 0
|
|
9254
9491
|
fi
|
|
9255
9492
|
|
|
9256
|
-
git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result:-0})" 2>/dev/null
|
|
9493
|
+
if git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result:-0})" 2>/dev/null; then
|
|
9494
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9495
|
+
fi
|
|
9257
9496
|
audit_agent_action "git_commit" "Committed session changes" "iterations=${ITERATION_COUNT:-0},result=${result:-0}" || true
|
|
9258
9497
|
return 0
|
|
9259
9498
|
}
|
|
@@ -10227,7 +10466,8 @@ _loki_supervised_build_result_passes() {
|
|
|
10227
10466
|
local result_file="$1"
|
|
10228
10467
|
! loki_is_supervised_simple_web && return 0
|
|
10229
10468
|
[ -s "$result_file" ] || return 1
|
|
10230
|
-
python3 - "$result_file" <<'PYEOF' 2>/dev/null
|
|
10469
|
+
python3 -E - "$result_file" <<'PYEOF' 2>/dev/null
|
|
10470
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
10231
10471
|
import json
|
|
10232
10472
|
import sys
|
|
10233
10473
|
|
|
@@ -10712,7 +10952,8 @@ SAEMPTYEOF
|
|
|
10712
10952
|
# not product-specific copy or design opinions.
|
|
10713
10953
|
local _ui_contract_out="" _ui_contract_rc=0
|
|
10714
10954
|
_ui_contract_out=$(
|
|
10715
|
-
_LOKI_CHANGED_FILES="$changed_files" python3 - "${TARGET_DIR:-.}" <<'PYEOF' 2>/dev/null
|
|
10955
|
+
_LOKI_CHANGED_FILES="$changed_files" python3 -E - "${TARGET_DIR:-.}" <<'PYEOF' 2>/dev/null
|
|
10956
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
10716
10957
|
import os
|
|
10717
10958
|
import re
|
|
10718
10959
|
import sys
|
|
@@ -10778,8 +11019,14 @@ PYEOF
|
|
|
10778
11019
|
[ -f "${TARGET_DIR:-.}/$f" ] || continue
|
|
10779
11020
|
total_checked=$((total_checked + 1))
|
|
10780
11021
|
local _py_compile_rc=0
|
|
11022
|
+
# Same check as `python3 -m py_compile` (importlib's source_to_code
|
|
11023
|
+
# is compile(bytes, path, "exec", dont_inherit=True)), but it
|
|
11024
|
+
# writes no __pycache__/*.pyc into the user's repo, where the
|
|
11025
|
+
# session commit would pick it up. -E and the sys.path filter
|
|
11026
|
+
# (D7): the repo under check must not supply modules to the gate.
|
|
10781
11027
|
LOKI_DEADLINE_IDLE_TIMEOUT=0 _loki_with_deadline "$gate_timeout" \
|
|
10782
|
-
python3 -
|
|
11028
|
+
python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]; compile(open(sys.argv[1], "rb").read(), sys.argv[1], "exec", dont_inherit=True)' \
|
|
11029
|
+
"${TARGET_DIR:-.}/$f" 2>&1 || _py_compile_rc=$?
|
|
10783
11030
|
if [ "$_py_compile_rc" -ne 0 ]; then
|
|
10784
11031
|
findings=$((findings + 1))
|
|
10785
11032
|
if [ "$_py_compile_rc" -eq 124 ]; then
|
|
@@ -11096,7 +11343,7 @@ SECEMPTY
|
|
|
11096
11343
|
|
|
11097
11344
|
# Run the scanner. exit 0 = no findings, 1 = findings, 2 = bad input.
|
|
11098
11345
|
local raw rc=0
|
|
11099
|
-
raw=$(python3 "$scanner" "${TARGET_DIR:-.}" --json 2>/dev/null) || rc=$?
|
|
11346
|
+
raw=$(python3 -E "$scanner" "${TARGET_DIR:-.}" --json 2>/dev/null) || rc=$?
|
|
11100
11347
|
if [ "$rc" -eq 2 ] || [ -z "$raw" ]; then
|
|
11101
11348
|
cat > "$out_file" << 'SECEMPTY'
|
|
11102
11349
|
{"rules_version":null,"findings":[],"summary":{"total":0,"by_severity":{}},"skipped":"scanner-error"}
|
|
@@ -11110,7 +11357,7 @@ SECEMPTY
|
|
|
11110
11357
|
# It prints a final line: ACTIVE_HIGH=<n>\tACTIVE_TOTAL=<n>\tWAIVED=<n>
|
|
11111
11358
|
# and writes the enriched receipt (findings carry a "waived" bool).
|
|
11112
11359
|
local verdict
|
|
11113
|
-
verdict=$(_SEC_RAW="$raw" _SEC_WAIVERS="$waivers_file" _SEC_OUT="$out_file" python3 -c '
|
|
11360
|
+
verdict=$(_SEC_RAW="$raw" _SEC_WAIVERS="$waivers_file" _SEC_OUT="$out_file" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
11114
11361
|
import json, os, sys
|
|
11115
11362
|
raw = os.environ.get("_SEC_RAW", "")
|
|
11116
11363
|
waivers_file = os.environ.get("_SEC_WAIVERS", "")
|
|
@@ -11610,7 +11857,7 @@ measure_test_coverage() {
|
|
|
11610
11857
|
--coverage.reportsDirectory=.loki/quality/vitest-cov >/dev/null 2>&1) || true
|
|
11611
11858
|
local f="$target_dir/.loki/quality/vitest-cov/coverage-summary.json"
|
|
11612
11859
|
if [ -f "$f" ]; then
|
|
11613
|
-
COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
|
|
11860
|
+
COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
11614
11861
|
import json, os, sys
|
|
11615
11862
|
try:
|
|
11616
11863
|
d=json.load(open(os.environ['_LOKI_COV_F']))
|
|
@@ -11629,7 +11876,7 @@ except Exception:
|
|
|
11629
11876
|
--coverageDirectory=.loki/quality/jest-cov --passWithNoTests >/dev/null 2>&1) || true
|
|
11630
11877
|
local f="$target_dir/.loki/quality/jest-cov/coverage-summary.json"
|
|
11631
11878
|
if [ -f "$f" ]; then
|
|
11632
|
-
COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -c "
|
|
11879
|
+
COVERAGE_PCT=$(_LOKI_COV_F="$f" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
11633
11880
|
import json, os, sys
|
|
11634
11881
|
try:
|
|
11635
11882
|
d=json.load(open(os.environ['_LOKI_COV_F']))
|
|
@@ -11649,7 +11896,7 @@ except Exception:
|
|
|
11649
11896
|
_loki_run_pytest_with_timeout "$target_dir" \
|
|
11650
11897
|
--cov --cov-report="json:$pyc_json" -q >/dev/null 2>&1 || true
|
|
11651
11898
|
if [ -f "$pyc_json" ]; then
|
|
11652
|
-
COVERAGE_PCT=$(_LOKI_COV_F="$pyc_json" python3 -c "
|
|
11899
|
+
COVERAGE_PCT=$(_LOKI_COV_F="$pyc_json" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
11653
11900
|
import json, os, sys
|
|
11654
11901
|
try:
|
|
11655
11902
|
d=json.load(open(os.environ['_LOKI_COV_F']))
|
|
@@ -11689,7 +11936,7 @@ except Exception:
|
|
|
11689
11936
|
local out
|
|
11690
11937
|
out=$(cd "$target_dir" && timeout "$gate_timeout" cargo llvm-cov --json 2>/dev/null) || true
|
|
11691
11938
|
if [ -n "$out" ]; then
|
|
11692
|
-
COVERAGE_PCT=$(_LOKI_COV_JSON="$out" python3 -c "
|
|
11939
|
+
COVERAGE_PCT=$(_LOKI_COV_JSON="$out" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
11693
11940
|
import json, os, sys
|
|
11694
11941
|
try:
|
|
11695
11942
|
d=json.loads(os.environ['_LOKI_COV_JSON'])
|
|
@@ -12412,7 +12659,7 @@ TREOF
|
|
|
12412
12659
|
local cov_below=false
|
|
12413
12660
|
if [ "$COVERAGE_MEASURED" = "true" ] && [ -n "$COVERAGE_PCT" ]; then
|
|
12414
12661
|
# Float-safe compare via python3 (pct may be e.g. 87.5).
|
|
12415
|
-
if _LOKI_COV_PCT="$COVERAGE_PCT" _LOKI_COV_MIN="$min_coverage" python3 -c "
|
|
12662
|
+
if _LOKI_COV_PCT="$COVERAGE_PCT" _LOKI_COV_MIN="$min_coverage" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
12416
12663
|
import os, sys
|
|
12417
12664
|
try:
|
|
12418
12665
|
pct=float(os.environ['_LOKI_COV_PCT']); mn=float(os.environ['_LOKI_COV_MIN'])
|
|
@@ -12437,7 +12684,7 @@ sys.exit(0 if pct < mn else 1)
|
|
|
12437
12684
|
_LOKI_COV_BLOCKED="$coverage_block" \
|
|
12438
12685
|
_LOKI_COV_RUNNER="$test_runner" \
|
|
12439
12686
|
_LOKI_COV_OUT="$quality_dir/coverage.json" \
|
|
12440
|
-
python3 -c "
|
|
12687
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
12441
12688
|
import json, os, tempfile
|
|
12442
12689
|
out=os.environ['_LOKI_COV_OUT']
|
|
12443
12690
|
measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
|
|
@@ -12498,7 +12745,7 @@ os.replace(tmp, out)
|
|
|
12498
12745
|
_LOKI_COV_BLOCKED="false" \
|
|
12499
12746
|
_LOKI_COV_RUNNER="$test_runner" \
|
|
12500
12747
|
_LOKI_COV_OUT="$quality_dir/coverage.json" \
|
|
12501
|
-
python3 -c "
|
|
12748
|
+
python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
12502
12749
|
import json, os, tempfile
|
|
12503
12750
|
out=os.environ['_LOKI_COV_OUT']
|
|
12504
12751
|
measured = os.environ.get('_LOKI_COV_MEASURED','false') == 'true'
|
|
@@ -12531,9 +12778,19 @@ os.replace(tmp, out)
|
|
|
12531
12778
|
fi
|
|
12532
12779
|
|
|
12533
12780
|
if [ "$test_passed" = "true" ]; then
|
|
12534
|
-
touch "$quality_dir/unit-tests.pass"
|
|
12535
12781
|
rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
|
|
12536
|
-
|
|
12782
|
+
# BACKLOG 55: the #82 zero-test run exited 0 but proved nothing. It is
|
|
12783
|
+
# inconclusive, so it must not leave the pass marker the receipt reads
|
|
12784
|
+
# as "unit_tests passed" (proof-generator.py _collect_quality_gates).
|
|
12785
|
+
# Still non-blocking (return 0 below) and still not a failure (no
|
|
12786
|
+
# TESTS_FAILED), so the council decides it, per the #82 design.
|
|
12787
|
+
if [ "$_tr_zero_tests" = "true" ]; then
|
|
12788
|
+
rm -f "$quality_dir/unit-tests.pass" 2>/dev/null || true
|
|
12789
|
+
log_warn "Test suite gate: $test_runner ran zero tests -- inconclusive (not passed, not failed)"
|
|
12790
|
+
else
|
|
12791
|
+
touch "$quality_dir/unit-tests.pass"
|
|
12792
|
+
log_info "Test suite gate: $test_runner passed"
|
|
12793
|
+
fi
|
|
12537
12794
|
# Coverage block is distinct from tests-red: tests passed, but enforced
|
|
12538
12795
|
# coverage is below threshold. Return nonzero to gate WITHOUT writing the
|
|
12539
12796
|
# TESTS_FAILED signal or removing unit-tests.pass.
|
|
@@ -12767,7 +13024,7 @@ run_doc_quality_gate() {
|
|
|
12767
13024
|
local manifest="$project_dir/.loki/docs/docs-manifest.json"
|
|
12768
13025
|
if [ -f "$manifest" ]; then
|
|
12769
13026
|
local doc_sha
|
|
12770
|
-
doc_sha=$(python3 -c "import json; print(json.load(open('$manifest')).get('git_sha', ''))" 2>/dev/null)
|
|
13027
|
+
doc_sha=$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json; print(json.load(open('$manifest')).get('git_sha', ''))" 2>/dev/null)
|
|
12771
13028
|
if [ -n "$doc_sha" ]; then
|
|
12772
13029
|
local behind
|
|
12773
13030
|
behind=$(git -C "$project_dir" rev-list --count "$doc_sha..HEAD" 2>/dev/null || echo "0")
|
|
@@ -13198,7 +13455,7 @@ enforce_lsp_diagnostics() {
|
|
|
13198
13455
|
fi
|
|
13199
13456
|
|
|
13200
13457
|
if [ -f "$lsp_file" ]; then
|
|
13201
|
-
verdict=$(_LOKI_LSP_FILE="$lsp_file" python3 -c '
|
|
13458
|
+
verdict=$(_LOKI_LSP_FILE="$lsp_file" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
13202
13459
|
import json, os, sys
|
|
13203
13460
|
try:
|
|
13204
13461
|
with open(os.environ["_LOKI_LSP_FILE"], encoding="utf-8") as handle:
|
|
@@ -13537,7 +13794,8 @@ council_verdicts_to_txt_files() {
|
|
|
13537
13794
|
local out_dir_env="$review_dir"
|
|
13538
13795
|
export LOKI_COUNCIL_OUT_DIR="$out_dir_env"
|
|
13539
13796
|
export LOKI_COUNCIL_VERDICTS_JSON="$verdicts_json"
|
|
13540
|
-
python3 << 'COUNCIL_WRITE'
|
|
13797
|
+
python3 -E - << 'COUNCIL_WRITE'
|
|
13798
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
13541
13799
|
import json
|
|
13542
13800
|
import os
|
|
13543
13801
|
import re
|
|
@@ -13626,6 +13884,9 @@ _run_managed_review_council() {
|
|
|
13626
13884
|
|
|
13627
13885
|
local result_json
|
|
13628
13886
|
result_json=$(python3 << 'MANAGED_REVIEW' 2>&1
|
|
13887
|
+
# Drops the cwd (the agent's repo) from sys.path. No -E here: the
|
|
13888
|
+
# LOKI_MANAGED_REVIEW_FAKE_MODULE test hook below is found through PYTHONPATH.
|
|
13889
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
13629
13890
|
import json
|
|
13630
13891
|
import os
|
|
13631
13892
|
import sys
|
|
@@ -13724,7 +13985,7 @@ MANAGED_REVIEW
|
|
|
13724
13985
|
fi
|
|
13725
13986
|
|
|
13726
13987
|
local status
|
|
13727
|
-
status=$(printf '%s' "$result_json" | python3 -c "import json,sys; d=json.loads(sys.stdin.read() or '{}'); print(d.get('status',''))" 2>/dev/null || echo "")
|
|
13988
|
+
status=$(printf '%s' "$result_json" | python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json,sys; d=json.loads(sys.stdin.read() or '{}'); print(d.get('status',''))" 2>/dev/null || echo "")
|
|
13728
13989
|
|
|
13729
13990
|
if [ "$status" != "ok" ]; then
|
|
13730
13991
|
local reason
|
|
@@ -16384,7 +16645,8 @@ REVIEW_WAVE_TIMING
|
|
|
16384
16645
|
export LOKI_REVIEW_AGG_QSCORE="$quality_score"
|
|
16385
16646
|
export LOKI_REVIEW_AGG_QMED="$nonblocking_medium"
|
|
16386
16647
|
export LOKI_REVIEW_AGG_QLOW="$nonblocking_low"
|
|
16387
|
-
python3 << 'AGG_SCRIPT'
|
|
16648
|
+
python3 -E - << 'AGG_SCRIPT'
|
|
16649
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
16388
16650
|
import json, os
|
|
16389
16651
|
result = {
|
|
16390
16652
|
"review_id": os.environ["LOKI_REVIEW_AGG_ID"],
|
|
@@ -16527,7 +16789,8 @@ AGG_SCRIPT
|
|
|
16527
16789
|
|
|
16528
16790
|
_LOKI_DA_AGG_FILE="$review_dir/$review_id/aggregate.json" \
|
|
16529
16791
|
_LOKI_DA_STATUS="$da_status" _LOKI_DA_RC="$da_dispatch_rc" \
|
|
16530
|
-
_LOKI_DA_SPECULATIVE="$da_speculative" python3 <<'DA_AGG_PATCH' 2>/dev/null || true
|
|
16792
|
+
_LOKI_DA_SPECULATIVE="$da_speculative" python3 -E - <<'DA_AGG_PATCH' 2>/dev/null || true
|
|
16793
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
16531
16794
|
import json
|
|
16532
16795
|
import os
|
|
16533
16796
|
|
|
@@ -17895,7 +18158,7 @@ is_completed() {
|
|
|
17895
18158
|
# Check orchestrator state
|
|
17896
18159
|
if [ -f ".loki/state/orchestrator.json" ]; then
|
|
17897
18160
|
if command -v python3 &> /dev/null; then
|
|
17898
|
-
local phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', ''))" 2>/dev/null || echo "")
|
|
18161
|
+
local phase=$(python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', ''))" 2>/dev/null || echo "")
|
|
17899
18162
|
# Accept various completion states
|
|
17900
18163
|
if [ "$phase" = "COMPLETED" ] || [ "$phase" = "complete" ] || [ "$phase" = "finalized" ] || [ "$phase" = "growth-loop" ]; then
|
|
17901
18164
|
return 0
|
|
@@ -19535,7 +19798,11 @@ load_state() {
|
|
|
19535
19798
|
if command -v python3 &> /dev/null; then
|
|
19536
19799
|
# BUG-ST-006: Validate checkpoint integrity before loading state
|
|
19537
19800
|
local state_valid
|
|
19538
|
-
|
|
19801
|
+
# -E and the sys.path filter (D7): the cwd is the target repo, and a
|
|
19802
|
+
# committed json.py must not decide the loaded iteration count
|
|
19803
|
+
# (a fake non-zero count would skip the iteration-0 evidence drop).
|
|
19804
|
+
state_valid=$(LOKI_STATE_FILE="$state_file" python3 -E -c "
|
|
19805
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
19539
19806
|
import json, os, sys
|
|
19540
19807
|
try:
|
|
19541
19808
|
with open(os.environ['LOKI_STATE_FILE']) as f:
|
|
@@ -19561,15 +19828,20 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
19561
19828
|
ITERATION_COUNT=0
|
|
19562
19829
|
# Back up corrupted state file for diagnosis
|
|
19563
19830
|
mv "$state_file" "${state_file}.corrupt.$(date +%s)" 2>/dev/null || true
|
|
19831
|
+
# Starts at iteration 0: drop the previous session's test
|
|
19832
|
+
# evidence, as the iteration-0 block at the end does.
|
|
19833
|
+
rm -f "${TARGET_DIR:-.}/.loki/quality/.test-results.iter" \
|
|
19834
|
+
"${TARGET_DIR:-.}/.loki/quality/unit-tests.pass" \
|
|
19835
|
+
"${TARGET_DIR:-.}/.loki/quality/test-results.json" 2>/dev/null || true
|
|
19564
19836
|
return
|
|
19565
19837
|
fi
|
|
19566
19838
|
|
|
19567
19839
|
# Load retry count, iteration count, and status from previous session
|
|
19568
19840
|
local prev_status
|
|
19569
|
-
prev_status=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
19570
|
-
RETRY_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
19841
|
+
prev_status=$(LOKI_STATE_FILE="$state_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
19842
|
+
RETRY_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
19571
19843
|
# BUG-RUN-003: Restore ITERATION_COUNT from persisted state
|
|
19572
|
-
ITERATION_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('iterationCount', 0))" 2>/dev/null || echo "0")
|
|
19844
|
+
ITERATION_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('iterationCount', 0))" 2>/dev/null || echo "0")
|
|
19573
19845
|
|
|
19574
19846
|
# Reset retry count + iteration count if previous session ended in a
|
|
19575
19847
|
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
@@ -19630,6 +19902,18 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
19630
19902
|
else
|
|
19631
19903
|
RETRY_COUNT=0
|
|
19632
19904
|
fi
|
|
19905
|
+
# A session that starts at iteration 0 has run nothing yet, so a leftover
|
|
19906
|
+
# freshness marker or unit-tests.pass is a previous session's evidence.
|
|
19907
|
+
# Iterations restart at 0, so keeping them would let an old pass:true read
|
|
19908
|
+
# as this iteration's result.
|
|
19909
|
+
if [ "${ITERATION_COUNT:-0}" = "0" ]; then
|
|
19910
|
+
# Same path the writer (enforce_test_coverage) and the freshness
|
|
19911
|
+
# readers use.
|
|
19912
|
+
local _q="${TARGET_DIR:-.}/.loki/quality"
|
|
19913
|
+
# test-results.json too: with the marker gone the receipt's quality
|
|
19914
|
+
# gates fall back to its status, reporting a previous session's run.
|
|
19915
|
+
rm -f "$_q/.test-results.iter" "$_q/unit-tests.pass" "$_q/test-results.json" 2>/dev/null || true
|
|
19916
|
+
fi
|
|
19633
19917
|
}
|
|
19634
19918
|
|
|
19635
19919
|
# Load tasks from queue files for prompt injection
|
|
@@ -21909,7 +22193,8 @@ _loki_supervised_completion_gates_pass() {
|
|
|
21909
22193
|
local test_iteration="$quality_dir/.test-results.iter"
|
|
21910
22194
|
if [ ! -s "$test_results" ] || [ ! -s "$test_iteration" ] \
|
|
21911
22195
|
|| [ "$(tr -d '[:space:]' < "$test_iteration" 2>/dev/null)" != "${ITERATION_COUNT:-0}" ] \
|
|
21912
|
-
|| ! python3 - "$test_results" <<'PYEOF' 2>/dev/null
|
|
22196
|
+
|| ! python3 -E - "$test_results" <<'PYEOF' 2>/dev/null
|
|
22197
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
21913
22198
|
import json
|
|
21914
22199
|
import sys
|
|
21915
22200
|
|
|
@@ -23859,6 +24144,12 @@ if __name__ == "__main__":
|
|
|
23859
24144
|
# v7.5.12: Provider invocation finished (or was killed by trap).
|
|
23860
24145
|
LOKI_PROVIDER_ACTIVE=0
|
|
23861
24146
|
|
|
24147
|
+
# Record the files this session has created so far. A pod loss is a
|
|
24148
|
+
# SIGKILL (no trap runs), so this per-turn record is what lets the
|
|
24149
|
+
# resumed session commit them. One git status; no-op without a
|
|
24150
|
+
# session snapshot.
|
|
24151
|
+
_loki_record_session_created || true
|
|
24152
|
+
|
|
23862
24153
|
echo ""
|
|
23863
24154
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
23864
24155
|
echo ""
|
|
@@ -26159,6 +26450,10 @@ cleanup() {
|
|
|
26159
26450
|
fi
|
|
26160
26451
|
# v7.5.12: Kill any running provider pipeline first, before slow cleanup.
|
|
26161
26452
|
kill_provider_child 2>/dev/null || true
|
|
26453
|
+
# This exit makes no session commit: record the files this session
|
|
26454
|
+
# created (after the provider is gone, so none are added later), so a
|
|
26455
|
+
# resume commits them instead of adopting them as the user's.
|
|
26456
|
+
_loki_record_session_created || true
|
|
26162
26457
|
rm -f "$loki_dir/STOP" "$loki_dir/PAUSE" "$loki_dir/PAUSED.md" 2>/dev/null
|
|
26163
26458
|
# UT2-13: Clear cli-provider marker on session end.
|
|
26164
26459
|
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
@@ -26233,6 +26528,8 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
26233
26528
|
log_warn "Loki Mode interrupted -- shutting down (double Ctrl+C)"
|
|
26234
26529
|
# v7.5.12: Kill provider pipeline immediately so we don't wait on it.
|
|
26235
26530
|
kill_provider_child 2>/dev/null || true
|
|
26531
|
+
# No session commit on this exit either (see the branch above).
|
|
26532
|
+
_loki_record_session_created || true
|
|
26236
26533
|
# Write STOP signal so any peer processes (dashboard, etc.) also stop.
|
|
26237
26534
|
mkdir -p "$loki_dir" 2>/dev/null && touch "$loki_dir/STOP" 2>/dev/null || true
|
|
26238
26535
|
if type app_runner_cleanup &>/dev/null; then
|
|
@@ -27102,7 +27399,7 @@ main() {
|
|
|
27102
27399
|
local _terminal_status=""
|
|
27103
27400
|
local _terminal_state_file
|
|
27104
27401
|
_terminal_state_file="$(_loki_state_file)"
|
|
27105
|
-
_terminal_status=$(LOKI_STATE_FILE="$_terminal_state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status',''))" 2>/dev/null || true)
|
|
27402
|
+
_terminal_status=$(LOKI_STATE_FILE="$_terminal_state_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status',''))" 2>/dev/null || true)
|
|
27106
27403
|
case "$_terminal_status" in
|
|
27107
27404
|
deterministic_gates_passed|council_approved|council_force_approved|completion_promise_fulfilled|reuse_already_satisfied)
|
|
27108
27405
|
if [ "$result" = "0" ]; then
|
|
@@ -27163,7 +27460,7 @@ main() {
|
|
|
27163
27460
|
# Refresh its durable files against the final HEAD without notifying twice.
|
|
27164
27461
|
local _completion_file="${TARGET_DIR:-.}/.loki/state/completion.json"
|
|
27165
27462
|
local _completion_outcome=""
|
|
27166
|
-
_completion_outcome=$(LOKI_COMPLETION_FILE="$_completion_file" python3 -c '
|
|
27463
|
+
_completion_outcome=$(LOKI_COMPLETION_FILE="$_completion_file" python3 -E -c 'import sys; sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
27167
27464
|
import json, os
|
|
27168
27465
|
try:
|
|
27169
27466
|
value = json.load(open(os.environ["LOKI_COMPLETION_FILE"], encoding="utf-8")).get("outcome", "")
|
|
@@ -27288,7 +27585,7 @@ except Exception:
|
|
|
27288
27585
|
if [ "${LOKI_DURABLE_STATE:-0}" = "1" ]; then
|
|
27289
27586
|
local _final_status _final_state_file
|
|
27290
27587
|
_final_state_file="$(_loki_state_file)"
|
|
27291
|
-
_final_status=$(LOKI_STATE_FILE="$_final_state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
|
27588
|
+
_final_status=$(LOKI_STATE_FILE="$_final_state_file" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]; import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
|
27292
27589
|
case "$_final_status" in
|
|
27293
27590
|
council_approved|council_force_approved|deterministic_gates_passed|completion_promise_fulfilled|paused|interrupted|stopped)
|
|
27294
27591
|
result=0 ;;
|