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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +18 -4
- 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 +106 -12
- package/autonomy/prd-checklist.sh +19 -11
- package/autonomy/run.sh +274 -26
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BACKLOG.md +41 -0
- package/docs/v10/METRICS.md +2 -1
- package/docs/v10/PROGRESS.md +17 -0
- 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
|
@@ -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
|
-
|
|
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
|
|
9137
|
-
#
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
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
|
|
9230
|
-
# (setup_agent_branch
|
|
9231
|
-
#
|
|
9232
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
5
|
+
**Version:** v9.54.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/docs/v10/BACKLOG.md
CHANGED
|
@@ -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.
|
package/docs/v10/METRICS.md
CHANGED
|
@@ -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);
|
|
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` |
|
package/docs/v10/PROGRESS.md
CHANGED
|
@@ -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).
|