loki-mode 9.53.0 → 9.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/autonomy/run.sh CHANGED
@@ -9064,6 +9064,10 @@ setup_agent_branch() {
9064
9064
  # Controlled by LOKI_BRANCH_PROTECTION env var (default: true). Set it to
9065
9065
  # "false" to opt out fully and work on the current branch (back-compat).
9066
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
9067
9071
 
9068
9072
  if [ "$branch_protection" != "true" ]; then
9069
9073
  log_info "Branch protection disabled (LOKI_BRANCH_PROTECTION=${branch_protection})"
@@ -9098,11 +9102,15 @@ setup_agent_branch() {
9098
9102
 
9099
9103
  # Already on a loki branch (session-* or delegate-*): idempotent reuse, do
9100
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).
9101
9108
  case "$cur" in
9102
9109
  loki/*)
9103
9110
  log_info "Already on loki branch ${cur}"
9104
9111
  mkdir -p .loki/state 2>/dev/null || true
9105
9112
  printf '%s\n' "$cur" > .loki/state/agent-branch.txt 2>/dev/null || true
9113
+ _loki_resume_snapshot
9106
9114
  return 0
9107
9115
  ;;
9108
9116
  esac
@@ -9113,8 +9121,14 @@ setup_agent_branch() {
9113
9121
  if [ -s .loki/state/agent-branch.txt ]; then
9114
9122
  recorded="$(cat .loki/state/agent-branch.txt 2>/dev/null || true)"
9115
9123
  if [ -n "$recorded" ] && git rev-parse --verify "$recorded" >/dev/null 2>&1; then
9116
- if git checkout "$recorded" >/dev/null 2>&1; then
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
9117
9130
  log_info "Resuming on recorded agent branch: ${recorded}"
9131
+ _loki_resume_snapshot
9118
9132
  return 0
9119
9133
  fi
9120
9134
  log_warn "Recorded agent branch ${recorded} could not be checked out - creating a new one"
@@ -9133,19 +9147,13 @@ setup_agent_branch() {
9133
9147
 
9134
9148
  log_info "Branch protection enabled - creating agent branch: $branch_name (base: $cur)"
9135
9149
 
9136
- # Record the user's own untracked (not ignored) files, repo-wide,
9137
- # NUL-delimited and relative to the repo top, so commit_session_changes
9138
- # and the receipt (autonomy/lib/workspace_diff.py) leave them alone. Taken
9139
- # only when a branch is minted: on resume, untracked files can be the
9140
- # agent's own uncommitted work. .loki/ is self-ignored above.
9141
- local top="" snap=".loki/state/preexisting-untracked.z"
9142
- if top="$(git rev-parse --show-toplevel 2>/dev/null)" \
9143
- && git -C "$top" ls-files -z --others --exclude-standard > "$snap.tmp" 2>/dev/null \
9144
- && mv -f "$snap.tmp" "$snap" 2>/dev/null; then
9145
- :
9146
- else
9147
- rm -f "$snap.tmp" "$snap" 2>/dev/null
9148
- log_warn "Could not record pre-existing untracked files; the session commit may include them"
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)"
9149
9157
  fi
9150
9158
 
9151
9159
  # Create and checkout the feature branch
@@ -9154,6 +9162,16 @@ setup_agent_branch() {
9154
9162
  return 1
9155
9163
  fi
9156
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
+
9157
9175
  # Store the branch name for later use (PR creation, cleanup)
9158
9176
  printf '%s\n' "$branch_name" > .loki/state/agent-branch.txt 2>/dev/null
9159
9177
 
@@ -9162,6 +9180,180 @@ setup_agent_branch() {
9162
9180
  echo "$branch_name"
9163
9181
  }
9164
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
+
9165
9357
  # RUN-25 iter 21 (Wave D #2): the two secret matchers now live in one sourceable
9166
9358
  # lib so the commit gate (here) and the completion evidence gate (completion-
9167
9359
  # council.sh) share a single implementation. Sourced with a guard so a re-source
@@ -9226,13 +9418,20 @@ commit_session_changes() {
9226
9418
  ':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
9227
9419
  ':!id_rsa*' ':!*.token' ':!credentials*' 2>/dev/null || true
9228
9420
 
9229
- # Unstage exactly the files that were untracked when the branch was minted
9230
- # (setup_agent_branch): they are the user's, not this session's work, and
9231
- # committing them here would delete them from disk on a later checkout of
9232
- # the base. Agent-created files stay staged. Skip an empty snapshot: an
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
9233
9427
  # empty --pathspec-from-file resets the WHOLE index. No snapshot (older
9234
9428
  # session): behave as before. If the unstage fails (git < 2.25), commit
9235
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
9236
9435
  local preexisting="$PWD/.loki/state/preexisting-untracked.z" top=""
9237
9436
  if [ -s "$preexisting" ]; then
9238
9437
  if ! { top="$(git rev-parse --show-toplevel 2>/dev/null)" \
@@ -9244,8 +9443,12 @@ commit_session_changes() {
9244
9443
  fi
9245
9444
  fi
9246
9445
 
9247
- # Nothing staged = clean no-op, never an error.
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.
9248
9450
  if git diff --cached --quiet 2>/dev/null; then
9451
+ rm -f .loki/state/session-created.z 2>/dev/null
9249
9452
  return 0
9250
9453
  fi
9251
9454
 
@@ -9287,7 +9490,9 @@ commit_session_changes() {
9287
9490
  return 0
9288
9491
  fi
9289
9492
 
9290
- git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result:-0})" 2>/dev/null || true
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
9291
9496
  audit_agent_action "git_commit" "Committed session changes" "iterations=${ITERATION_COUNT:-0},result=${result:-0}" || true
9292
9497
  return 0
9293
9498
  }
@@ -12573,9 +12778,19 @@ os.replace(tmp, out)
12573
12778
  fi
12574
12779
 
12575
12780
  if [ "$test_passed" = "true" ]; then
12576
- touch "$quality_dir/unit-tests.pass"
12577
12781
  rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
12578
- log_info "Test suite gate: $test_runner passed"
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
12579
12794
  # Coverage block is distinct from tests-red: tests passed, but enforced
12580
12795
  # coverage is below threshold. Return nonzero to gate WITHOUT writing the
12581
12796
  # TESTS_FAILED signal or removing unit-tests.pass.
@@ -19583,7 +19798,11 @@ load_state() {
19583
19798
  if command -v python3 &> /dev/null; then
19584
19799
  # BUG-ST-006: Validate checkpoint integrity before loading state
19585
19800
  local state_valid
19586
- state_valid=$(LOKI_STATE_FILE="$state_file" python3 -c "
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 ('', '.')]
19587
19806
  import json, os, sys
19588
19807
  try:
19589
19808
  with open(os.environ['LOKI_STATE_FILE']) as f:
@@ -19609,15 +19828,20 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
19609
19828
  ITERATION_COUNT=0
19610
19829
  # Back up corrupted state file for diagnosis
19611
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
19612
19836
  return
19613
19837
  fi
19614
19838
 
19615
19839
  # Load retry count, iteration count, and status from previous session
19616
19840
  local prev_status
19617
- 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")
19618
- 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")
19619
19843
  # BUG-RUN-003: Restore ITERATION_COUNT from persisted state
19620
- 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")
19621
19845
 
19622
19846
  # Reset retry count + iteration count if previous session ended in a
19623
19847
  # terminal state. A fresh `loki start` after a terminal run is a NEW
@@ -19678,6 +19902,18 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
19678
19902
  else
19679
19903
  RETRY_COUNT=0
19680
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
19681
19917
  }
19682
19918
 
19683
19919
  # Load tasks from queue files for prompt injection
@@ -23908,6 +24144,12 @@ if __name__ == "__main__":
23908
24144
  # v7.5.12: Provider invocation finished (or was killed by trap).
23909
24145
  LOKI_PROVIDER_ACTIVE=0
23910
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
+
23911
24153
  echo ""
23912
24154
  echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
23913
24155
  echo ""
@@ -26208,6 +26450,10 @@ cleanup() {
26208
26450
  fi
26209
26451
  # v7.5.12: Kill any running provider pipeline first, before slow cleanup.
26210
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
26211
26457
  rm -f "$loki_dir/STOP" "$loki_dir/PAUSE" "$loki_dir/PAUSED.md" 2>/dev/null
26212
26458
  # UT2-13: Clear cli-provider marker on session end.
26213
26459
  rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
@@ -26282,6 +26528,8 @@ except (json.JSONDecodeError, OSError): pass
26282
26528
  log_warn "Loki Mode interrupted -- shutting down (double Ctrl+C)"
26283
26529
  # v7.5.12: Kill provider pipeline immediately so we don't wait on it.
26284
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
26285
26533
  # Write STOP signal so any peer processes (dashboard, etc.) also stop.
26286
26534
  mkdir -p "$loki_dir" 2>/dev/null && touch "$loki_dir/STOP" 2>/dev/null || true
26287
26535
  if type app_runner_cleanup &>/dev/null; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.53.0"
10
+ __version__ = "9.54.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v9.53.0
5
+ **Version:** v9.54.0
6
6
 
7
7
  ---
8
8
 
@@ -90,6 +90,47 @@ Status values: todo, in progress, shipped (version), parked (reason).
90
90
  59. **Agent edits to pre-existing untracked files are omitted from the receipt (new in v9.53.0).** Store a blob hash per snapshot entry; list a path whose hash changed (for example `preexisting_modified`) while still never committing the user's file (cycle-2 council). Not a false pass: no verdict reads the list.
91
91
  60. **Bun `runTestCoverage` accepts a stale `test-results.json`** (no freshness check against `.loki/quality/.test-results.iter`), and an inconclusive coverage still lands in `GateOutcome.passed[]` with an "(gate did not run)" log line.
92
92
 
93
+ 61. **`evidence-block.json` still writes the raw `$test_pass`** (`completion-council.sh` ~2792, block path), so `tests.pass` can read true for an inconclusive test axis when another axis blocks.
94
+ 62. **`enforce_test_coverage` monorepo-custom-rejected path** records `pass: true` / `verified` and touches `unit-tests.pass` when `LOKI_MONOREPO_TEST_CMD` is rejected and the gate is skipped.
95
+ 63. **`council_augment_from_managed_memory`** (`completion-council.sh` ~228) runs `cd "${PROJECT_DIR:-$(pwd)}"` then `python3 -m memory.managed_memory.retrieve`; with `PROJECT_DIR` unset it can run the agent repo's own `memory/` package into the council prompt.
96
+ 64. **A test script like `exit 0` still reads as a pass on both routes** (the zero-test detector acts only on positive evidence of zero tests); `node --test` behind an npm script is not detected. The Bun freshness marker is agent-writable.
97
+ 65. **(Fixed for interrupt and kill in cycle 3 via `.loki/state/session-created.z`; see 86 for limits.) Previous-session leftovers become "pre-existing" after a crash** (cycle-3 disclosure): files left uncommitted by a crashed or secret-scan-aborted session are recorded as pre-existing on the next run and stay on disk, uncommitted.
98
+
99
+ 66. **Ignored `dir/` snapshot entries hide agent files under them.** If the agent un-ignores `build/` and creates `build/agent.js`, the literal `build/` reset unstages it and the receipt's coverage check hides it; an agent edit to a user file under that entry is not disclosed (no per-file hash for dir entries). At least report new or changed paths covered only by a dir entry (cycle-3 council).
100
+ 67. **Deletions and ignored edits are not disclosed.** An agent `rm` of a pre-existing untracked user file, or an edit to a still-ignored pre-existing file (`.env.local`), leaves the receipt silent; iterate the recorded hash map (missing = deleted, changed = modified), with care for gate-regenerated noise (cycle-3 council).
101
+ 68. **The resume union never prunes entries that became tracked**: a file the user committed between sessions is still unstaged by the session commit while the receipt lists it as modified. Drop union entries present in `git ls-files -z --cached`.
102
+ 69. **Snapshot hashing cost on large untracked data trees**: every file entry is sha256'd at every session start (30k small files: 1.9s; tens of GB of untracked parquet or weights would stall for minutes). Add a size cap or a size+mtime+inode fingerprint.
103
+ 70. **Snapshot and hash files live in agent-writable `.loki/state`**: an agent can add entries (hide or uncommit files) or delete the hash file (suppress `preexisting_modified`). Predates cycle 3; matters for adversarial agents.
104
+ 71. **`evidence-gate-details` `tests.ok` stays true for an inconclusive test axis**; `surface_evidence_gate_details` prints `tests_ok=True` next to `tests_inconclusive`.
105
+
106
+ 72. **Refused resume keeps the old `base-branch.txt` and says too little.** Now that `--no-overwrite-ignore` makes the mint fallback a normal path, session 2 keeps session 1's recorded base (wrong PR `--base` advice if it started elsewhere), and the warning does not name the ignored file or where session 1's commits live.
107
+
108
+ 73. **Bash records a test run that exits 0 but reports failures as a pass** (a jest printing "Tests: 1 failed, 2 passed" with exit 0 wrote `pass: true`, `verified`). Parse the runner's own summary for failures (cycle-3 council round 2).
109
+ 74. **Agent self-commits bypass the snapshot (data risk).** The snapshot guards only Loki's session commit: an agent that rewrites `.gitignore` and runs `git add -A && git commit` itself puts the user's untracked and ignored files on the session branch, and a base checkout removes them from disk. The RARV prompt tells every provider to checkpoint-commit; only `providers/claude.sh` (and the Bun override) forbids `git add -A`. Intersect `git diff --diff-filter=A <base>..HEAD` with the snapshot in `commit_session_changes`, `git rm --cached` hits, warn, and add the hygiene line to codex/cline/opencode/aider prompts. Also: an agent moving a user file to a new path gets the user's bytes committed under an agent-created name.
110
+ 75. **Unregistered suites:** `tests/council/test_managed_completion_flag.sh`, `tests/council/test_managed_review*`, `tests/test-voter-agents-json.sh` and `tests/test-evidence-gate-no-tests.sh` are never run by `tests/run-all-tests.sh` (named only in comments).
111
+
112
+ 76. **`done-recognition.sh` tests_axis (434-479): a zero-test record falls through** to an affirmative branch (predates cycle 3; council round 3).
113
+ 77. **Every Bun test-results.json is stale by construction:** only bash `enforce_test_coverage` writes `.test-results.iter`, so the Bun gate always re-runs `npm test` or reports inconclusive, and an unknown zero-test detector result makes every exit-0 npm run inconclusive. Fails safe; decide whether Bun should write its own marker.
114
+ 78. **Leading-directory pathspec mismatch:** a pre-existing untracked file `a` also matches `a/b` in the literal reset, but `workspace_diff._covered` only treats `a/` entries as covering children, so the commit omits `a/b` while the receipt lists it (the receipt is the honest side).
115
+ 79. **A previous session's `static-analysis.pass` survives iteration 0** and reads as a pass of the exogenous static_analysis gate when the new session never runs static analysis (cannot produce VERIFIED alone). Add it to the iteration-0 drop.
116
+ 80. **Refused resume on a paused or interrupted state keeps session 1's `start-sha`** while the new branch is minted from the current base, so base commits made between sessions can appear in the receipt diff (extends 72).
117
+
118
+ 81. **Two more verdict-relevant inline Python sites without D7:** `run.sh` `_declared_test_script` (~11981) and `_ws_script` (~12101) run plain `python3 -c` with the cwd in the agent repo (extends 54).
119
+ 82. **`proof-generator._collect_quality_gates` reads `test-results.json` status with no freshness check** against `.test-results.iter` (the iteration-0 drop covers the new-session case; a stale file mid-session is still read).
120
+ 83. **An embedded git repo with no commits under an un-ignored directory makes `git add -A` fail as a whole**; `commit_session_changes` then silently commits nothing with no "Left uncommitted" warning (fails safe, predates cycle 3).
121
+ 84. **`-E` also drops `PYTHONDONTWRITEBYTECODE`/`PYTHONIOENCODING`/`PYTHONUTF8`** for the guarded helpers; the council-v2 swarm imports may write `__pycache__` into the install dir (cosmetic).
122
+
123
+ 85. **The test gate's pytest writes `__pycache__/*.pyc` into the user's repo**, and the session commit includes them when the repo does not ignore `__pycache__` (seen in a receipt: `__pycache__/helper.cpython-313.pyc`). `P6.no-gate-artifacts-committed` covers only the static-analysis gate; run tests with `PYTHONDONTWRITEBYTECODE=1` or a pycache prefix under `.loki` (cycle-3 interrupt fix).
124
+ 86. **Session-created record limits:** the provider turn in flight when the process dies is not recorded (SIGHUP is not trapped, like SIGKILL), so those files are treated as the user's (kept, never committed); a refused resume starts an empty record (warns by name); a path created then deleted by the agent and later re-created by the user is committed as the session's; the snapshot now requires python3 (fails closed without it).
125
+
126
+ 87. **Resume rehash hides earlier edits:** the union deletes and rehashes `.sha.z`, so after a SIGKILL or pod loss (no session-1 receipt) session 1's edit to a pre-existing untracked file never appears as `preexisting_modified`. Keep existing hashes for entries already in the base list; hash only new paths.
127
+ 88. **Old git (< 2.18) receipt attribution:** a failed mint snapshot deletes the list, so the receipt names every user untracked file as the run's work (names and counts only; the commit path fails closed). Keep an `ls-files --others --exclude-standard` fallback list for the receipt.
128
+ 89. **Bash and Bun read `failed_count` differently:** the bash evidence gate reads `pass: true` as PASS whatever `failed_count` says; Bun now fails it.
129
+
130
+ 90. **Data risk, next in line: `LOKI_BRANCH_PROTECTION=false` on a leftover `loki/session-*` branch** returns before any snapshot (`run.sh` ~9073), and `commit_session_changes` then commits with the earlier session's stale snapshot, so a file the user made since is committed and lost from disk on checkout of the base (also in v9.53.0; breaks the LOCK A1 opt-out promise). One guard: commit nothing unless this run took a snapshot, or skip the session commit when branch protection is off (cycle-3 council round 6).
131
+ 91. **Zero-test stage events:** a zero-test run still returns 0 from `enforce_test_coverage`, so the loop emits `stage_complete test_suite pass` (~24590) although the result is inconclusive.
132
+ 92. **Receipt path bases mix under a subdirectory `TARGET_DIR`:** tracked entries are repo-top-relative, untracked and `preexisting_modified` entries are cwd-relative.
133
+
93
134
  ## Later milestones
94
135
 
95
136
  M1 one command, M2 Seal and Wall, M3 assign like a teammate, M4 system map, M5 the line, M6 ship and operate, M7 one screen, M8 legacy lane, M9 enterprise readiness, M10 simplify and ship v10.0.0. Items get broken out here when their milestone comes up.
@@ -42,6 +42,7 @@ produced it. "Not measured" is a real value here: nothing is estimated.
42
42
 
43
43
  | Property | Status | Command |
44
44
  |---|---|---|
45
- | all 9 | 1 of 9 proven (P6); 48 cases, 25 pass, 23 pending with milestones (2026-09-26, v9.53.0) | `bash tests/moat/run.sh` |
45
+ | all 9 | 1 of 9 proven (P6); 55 cases, 32 pass, 23 pending with milestones (2026-09-26, v9.54.0) | `bash tests/moat/run.sh` |
46
+ | history | v9.53.0: 1 of 9, 48 cases, 25 pass, 23 pending | - |
46
47
  | history | v9.52.0: 0 of 9, 45 cases, 21 pass, 24 pending | - |
47
48
  | runtime | about 15s locally (9 scripts in parallel), about 1m15s CI job | `time bash tests/moat/run.sh` |
@@ -27,6 +27,7 @@
27
27
 
28
28
  ## Shipped in v10 program
29
29
 
30
+ - **v9.54.0** (2026-09-26): resume never sweeps, overwrites or loses user files (between-session files, gitignored files, interrupted sessions, old git); previous-session test evidence never read as this session's; D7 on checklist verification, council helpers and load_state.
30
31
  - **v9.53.0** (2026-09-26): P6 in-place brownfield proven (moat 1 of 9); user untracked files never swept into session commits; no bytecode from the syntax gate; Bun gate never passes an inconclusive result; council and runner verdict paths cannot import from the agent repo.
31
32
  - **v9.52.0** (2026-09-25): moat suite `tests/moat/` (45 cases, 0 of 9 properties proven, 24 pending with milestones, ratchet baseline set by this release); verifier fixes (Bun `--jwks`, stripped signature, malformed tokens, strict flags and help, `python3 -E` on every verify path, exit contract 64/66); council `pass` readers fail closed; honest `doctor --airgap`; main-red fix.
32
33
 
@@ -37,6 +38,22 @@
37
38
  - Council: 3 of 3 APPROVE on the first round. Non-blocking data-safety findings recorded as BACKLOG 57-60.
38
39
  - Released as v9.53.0.
39
40
 
41
+ ### Cycle 3 (2026-09-26)
42
+
43
+ - v9.53.0 verified on all channels (npm latest + gitHead `d7fb8674`, tag, GitHub release, Docker amd64/arm64, Homebrew sha256).
44
+ - Dev fleet (3 worktree slices): resume re-snapshot, gitignored files, receipt discloses edits to user files (BACKLOG 57-59); D7 on checklist verification and council helpers (53, 54 partly); zero-test and stale results never affirmative on either route (55, 56, 60).
45
+ - Moat: still 1 of 9 proven (P6); 54 cases registered (48 in the release-tag union), 23 pending; ratchet against v9.52.0 and v9.53.0. New cases: P6.resume-does-not-sweep, P6.ignored-files-not-swept, P6.preexisting-edit-disclosed, P6.resume-keeps-ignored-user-file, P2.checklist-verify-not-shadowed, P2.zero-test-never-affirmative.
46
+ - Council rounds: r1 CONCERN x2 (resume overwrote ignored user files; stale Bun pass) -> fixed 55ee9747; r2 CONCERN (previous test-results.json still reported) -> fixed d55b13f4; r3 CONCERN (snapshot on git < 2.18 fell back to sweeping everything) -> fixed f71ab6a1; r4 3 of 3 APPROVE but an interrupt regression found -> fixed 5faa666e; r5 CONCERN (session-created record held directory entries) -> fixed 32d3831a; r6 **3 of 3 APPROVE**.
47
+ - Moat at release: 1 of 9 proven; 55 registered, 32 pass, 23 pending.
48
+ - Released as v9.54.0.
49
+ - Open items the slices found: BACKLOG 61-65.
50
+
51
+ ## Next (after cycle 3)
52
+
53
+ 1. BACKLOG 90 (branch-protection-off stale snapshot: data risk), then 74 (agent self-commits bypass the snapshot), 85 (test-gate `__pycache__`), 73 (exit-0 runs with failures).
54
+ 2. Honest verdict: 81, 82, 89, 91; council helper D7 remainder (54).
55
+ 3. Then P7 (no fabricated data), P9 (Rule of Two), M0 measurement.
56
+
40
57
  ## Next (after cycle 2)
41
58
 
42
59
  1. Brownfield data safety: BACKLOG 57, 58, 59 (resume re-snapshot, gitignored files, receipt omission).