loki-mode 9.53.0 → 9.54.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +28 -5
- 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/proof-generator.py +36 -5
- 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 +503 -52
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +4 -0
- package/dashboard/audit.py +9 -1
- package/dashboard/auth.py +16 -4
- package/dashboard/registry.py +34 -12
- package/dashboard/rigour_integration.py +25 -17
- package/dashboard/server.py +252 -104
- package/dashboard/static/cost.html +55 -26
- package/dashboard/static/index.html +475 -1544
- package/dashboard/static/proofs.html +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/air-gapped.md +7 -2
- package/docs/image-provenance.md +4 -2
- package/docs/v10/BACKLOG.md +82 -2
- package/docs/v10/BOARD.md +105 -0
- package/docs/v10/DECISIONS.md +71 -0
- package/docs/v10/METRICS.md +2 -1
- package/docs/v10/PROGRESS.md +60 -0
- package/docs/v10/SWARM-PROMPT.md +98 -0
- package/docs/v10/SWARM.md +237 -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/providers/aider.sh +8 -0
- package/providers/cline.sh +8 -0
- package/providers/codex.sh +9 -0
- package/providers/opencode.sh +9 -0
- package/web-app/dist/assets/AdminPage-CZrBkzRF.js +32 -0
- package/web-app/dist/assets/Avatar-gQIp6FBI.js +11 -0
- package/web-app/dist/assets/{Badge-BF3mhT57.js → Badge-D2_fBTFY.js} +1 -1
- package/web-app/dist/assets/{Button-BDp9aJ9q.js → Button-D5qDd2ef.js} +1 -1
- package/web-app/dist/assets/CockpitPage-DVMk2RUK.js +47 -0
- package/web-app/dist/assets/ComparePage-NMLyE1xL.js +1 -0
- package/web-app/dist/assets/{ErrorBoundary-ut3wrUmu.js → ErrorBoundary-BRABI-BT.js} +1 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-D2LNKqUx.js +1 -0
- package/web-app/dist/assets/GitHubIssuesPanel-BC5GzKUN.js +7 -0
- package/web-app/dist/assets/{GitHubPRsPanel-DurZgSaj.js → GitHubPRsPanel-BSlmoVTS.js} +3 -3
- package/web-app/dist/assets/HomePage-D_CDMoen.js +12 -0
- package/web-app/dist/assets/{LoginPage-HeRGJeZy.js → LoginPage-BBNA_N9t.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CUKrqsZs.js → MagicPage-CnFRZCMx.js} +1 -1
- package/web-app/dist/assets/MetricsPage-CPfLMRlh.js +6 -0
- package/web-app/dist/assets/{NotFoundPage-BZDZ-B77.js → NotFoundPage-lA0XSGTz.js} +1 -1
- package/web-app/dist/assets/ProjectPage-CNAPbr9z.js +256 -0
- package/web-app/dist/assets/ProjectsPage-CMtm8goN.js +6 -0
- package/web-app/dist/assets/SettingsPage-BhIjVMYE.js +10 -0
- package/web-app/dist/assets/{ShowcasePage-BGS2jTGH.js → ShowcasePage-BndtGeYo.js} +1 -1
- package/web-app/dist/assets/SystemSettingsPage-euXI-KXE.js +1 -0
- package/web-app/dist/assets/TeamsPage-ClUKecun.js +11 -0
- package/web-app/dist/assets/TemplatesPage-BWHWVk0L.js +29 -0
- package/web-app/dist/assets/{TerminalOutput-togzv9Zl.js → TerminalOutput-Cv3N3Szf.js} +1 -1
- package/web-app/dist/assets/{activity-Dreyb_Kr.js → activity-dEhz-bXR.js} +1 -1
- package/web-app/dist/assets/{bell-DWH0drES.js → bell-B3XUMq1t.js} +1 -1
- package/web-app/dist/assets/{bot-78MoCuDl.js → bot-X-ssNlwR.js} +1 -1
- package/web-app/dist/assets/{check-ikfylPst.js → check-C_9Nx7kb.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DXUqkd8J.js → chevron-left-tvNswo3A.js} +1 -1
- package/web-app/dist/assets/{circle-alert-Da74VO8P.js → circle-alert-CSB88nEs.js} +1 -1
- package/web-app/dist/assets/{clock-mlGCFq8l.js → clock-C8p2e4NI.js} +1 -1
- package/web-app/dist/assets/{cloud-B_Y5ZrIO.js → cloud-CKp2ZvhG.js} +1 -1
- package/web-app/dist/assets/{code-xml-Bbom7YWV.js → code-xml-dY70LCHD.js} +1 -1
- package/web-app/dist/assets/{database-CLcofiVp.js → database-DObn5fmn.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-C5mkjXIM.js → dollar-sign-DSu6n3K3.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CVrkaOQP.js → file-code-corner-DpCnS73z.js} +1 -1
- package/web-app/dist/assets/{file-plus-BV9240gX.js → file-plus-D3IPKCf1.js} +1 -1
- package/web-app/dist/assets/{globe-aZ0Zso4I.js → globe-C9QXkgH_.js} +1 -1
- package/web-app/dist/assets/{hammer-EgOQNukG.js → hammer-D_FtWe1y.js} +1 -1
- package/web-app/dist/assets/index-CI54rara.css +1 -0
- package/web-app/dist/assets/{index-DvZ0Kk2x.js → index-Cz7q1bvC.js} +2 -2
- package/web-app/dist/assets/{layers-BOVKmepE.js → layers-DsW7jnJX.js} +1 -1
- package/web-app/dist/assets/{loader-circle-0K8yLgHl.js → loader-circle-B3HspjKR.js} +1 -1
- package/web-app/dist/assets/{lock-XzlDMy8M.js → lock-DSI29pct.js} +1 -1
- package/web-app/dist/assets/{package-BtXTKusI.js → package-CLNxtA2x.js} +1 -1
- package/web-app/dist/assets/{plus-PxeiD8ju.js → plus-DyI4WC4_.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-CjqzYczt.js → refresh-cw-D2dv0xA0.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CY0os4DH.js → rotate-ccw-CBvihA-a.js} +1 -1
- package/web-app/dist/assets/scroll-text-DvoOvAl_.js +6 -0
- package/web-app/dist/assets/{server-pIIs9vNp.js → server-DenWvSUt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CsDqKXwA.js → shield-alert-40cX_FvG.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CAh3HVzH.js → thumbs-up-BrjZ6QuV.js} +12 -7
- package/web-app/dist/assets/trash-2-C6evwkEH.js +16 -0
- package/web-app/dist/assets/{trending-down-DrtBd_C9.js → trending-down-CfbZis3O.js} +1 -1
- package/web-app/dist/assets/{trending-up-D-DbCHXm.js → trending-up-DWuEK3WC.js} +1 -1
- package/web-app/dist/assets/{upload-D4l6DCOt.js → upload-B9DFBcPG.js} +1 -1
- package/web-app/dist/assets/{usePolling-D0aoTX80.js → usePolling-BryVC-T1.js} +1 -1
- package/web-app/dist/assets/{user-BQf_XJO_.js → user-DMRXwZiw.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/server.py +122 -47
- package/web-app/dist/assets/AdminPage-y6_b2Xo4.js +0 -32
- package/web-app/dist/assets/Avatar-BFPkXhIJ.js +0 -6
- package/web-app/dist/assets/CockpitPage-5TKICCoA.js +0 -47
- package/web-app/dist/assets/ComparePage-BSuNsUHy.js +0 -1
- package/web-app/dist/assets/EvidenceReceiptPanel-CcaBwEI-.js +0 -1
- package/web-app/dist/assets/GitHubIssuesPanel-DJeYgzAa.js +0 -12
- package/web-app/dist/assets/HomePage-Bs451LAy.js +0 -28
- package/web-app/dist/assets/MetricsPage-DvY1csgz.js +0 -6
- package/web-app/dist/assets/ProjectPage-CqHkEZkl.js +0 -257
- package/web-app/dist/assets/ProjectsPage-CLvCK1Y8.js +0 -6
- package/web-app/dist/assets/SettingsPage-DP7OmdwP.js +0 -10
- package/web-app/dist/assets/SystemSettingsPage-P_rlLh0x.js +0 -1
- package/web-app/dist/assets/TeamsPage-DxtBDcBC.js +0 -11
- package/web-app/dist/assets/TemplatesPage-5e-faH3W.js +0 -29
- package/web-app/dist/assets/folder-open-D_6yV78G.js +0 -11
- package/web-app/dist/assets/index-BSgmPwC0.css +0 -1
- package/web-app/dist/assets/lightbulb-DdASgqOG.js +0 -6
- package/web-app/dist/assets/mail-COu670_W.js +0 -6
- package/web-app/dist/assets/minus-bQpUt9ff.js +0 -6
- package/web-app/dist/assets/save-v8qep5cY.js +0 -6
- package/web-app/dist/assets/trash-2-Dt6urTyY.js +0 -6
package/autonomy/run.sh
CHANGED
|
@@ -8765,10 +8765,20 @@ start_resource_monitor() {
|
|
|
8765
8765
|
# Initial check
|
|
8766
8766
|
check_system_resources
|
|
8767
8767
|
|
|
8768
|
-
# Background monitoring loop
|
|
8768
|
+
# Background monitoring loop. BACKLOG 22: a bare `sleep N` inside this
|
|
8769
|
+
# subshell survives a TERM to the subshell -- caught traps do not carry
|
|
8770
|
+
# into a foreground child, so the sleep reparents to init and outlives the
|
|
8771
|
+
# run for up to RESOURCE_CHECK_INTERVAL seconds. Backgrounding the sleep
|
|
8772
|
+
# and recording its PID lets a TERM trap kill it directly; `wait` returns
|
|
8773
|
+
# immediately once the trap fires, so shutdown is not delayed either.
|
|
8769
8774
|
(
|
|
8775
|
+
_loki_rm_sleep_pid=""
|
|
8776
|
+
trap '[ -n "$_loki_rm_sleep_pid" ] && kill "$_loki_rm_sleep_pid" 2>/dev/null; exit 0' TERM
|
|
8770
8777
|
while true; do
|
|
8771
|
-
sleep "$RESOURCE_CHECK_INTERVAL"
|
|
8778
|
+
sleep "$RESOURCE_CHECK_INTERVAL" &
|
|
8779
|
+
_loki_rm_sleep_pid=$!
|
|
8780
|
+
wait "$_loki_rm_sleep_pid"
|
|
8781
|
+
_loki_rm_sleep_pid=""
|
|
8772
8782
|
check_system_resources
|
|
8773
8783
|
done
|
|
8774
8784
|
) &
|
|
@@ -9064,6 +9074,10 @@ setup_agent_branch() {
|
|
|
9064
9074
|
# Controlled by LOKI_BRANCH_PROTECTION env var (default: true). Set it to
|
|
9065
9075
|
# "false" to opt out fully and work on the current branch (back-compat).
|
|
9066
9076
|
local branch_protection="${LOKI_BRANCH_PROTECTION:-true}"
|
|
9077
|
+
# Set to 1 only once THIS run has a current snapshot (mint or resume
|
|
9078
|
+
# union); _loki_record_session_created records nothing otherwise, since a
|
|
9079
|
+
# leftover or stale snapshot would make it claim a user's file.
|
|
9080
|
+
_LOKI_SNAPSHOT_THIS_RUN=0
|
|
9067
9081
|
|
|
9068
9082
|
if [ "$branch_protection" != "true" ]; then
|
|
9069
9083
|
log_info "Branch protection disabled (LOKI_BRANCH_PROTECTION=${branch_protection})"
|
|
@@ -9087,8 +9101,10 @@ setup_agent_branch() {
|
|
|
9087
9101
|
[ -f .loki/.gitignore ] || printf '*\n' > .loki/.gitignore 2>/dev/null || true
|
|
9088
9102
|
|
|
9089
9103
|
# Capture the ref Loki was run from. Detached HEAD yields the literal "HEAD".
|
|
9104
|
+
# symbolic-ref also names an unborn branch (fresh `git init`), where
|
|
9105
|
+
# rev-parse printed "HEAD" and failed, recording "HEAD" twice as the base.
|
|
9090
9106
|
local cur=""
|
|
9091
|
-
cur="$(git
|
|
9107
|
+
cur="$(git symbolic-ref --short -q HEAD 2>/dev/null || echo HEAD)"
|
|
9092
9108
|
|
|
9093
9109
|
# Detached HEAD: do NOT branch, do NOT fabricate a base (LOCK A2/A6).
|
|
9094
9110
|
if [ "$cur" = "HEAD" ]; then
|
|
@@ -9098,11 +9114,15 @@ setup_agent_branch() {
|
|
|
9098
9114
|
|
|
9099
9115
|
# Already on a loki branch (session-* or delegate-*): idempotent reuse, do
|
|
9100
9116
|
# not nest a branch off a loki branch (LOCK A5/A7).
|
|
9117
|
+
# Both reuse paths add what is untracked now to the pre-existing list (a
|
|
9118
|
+
# file the user made between sessions is theirs), except the files an
|
|
9119
|
+
# unfinished previous session created (_loki_resume_snapshot).
|
|
9101
9120
|
case "$cur" in
|
|
9102
9121
|
loki/*)
|
|
9103
9122
|
log_info "Already on loki branch ${cur}"
|
|
9104
9123
|
mkdir -p .loki/state 2>/dev/null || true
|
|
9105
9124
|
printf '%s\n' "$cur" > .loki/state/agent-branch.txt 2>/dev/null || true
|
|
9125
|
+
_loki_resume_snapshot
|
|
9106
9126
|
return 0
|
|
9107
9127
|
;;
|
|
9108
9128
|
esac
|
|
@@ -9113,39 +9133,37 @@ setup_agent_branch() {
|
|
|
9113
9133
|
if [ -s .loki/state/agent-branch.txt ]; then
|
|
9114
9134
|
recorded="$(cat .loki/state/agent-branch.txt 2>/dev/null || true)"
|
|
9115
9135
|
if [ -n "$recorded" ] && git rev-parse --verify "$recorded" >/dev/null 2>&1; then
|
|
9116
|
-
|
|
9136
|
+
# --no-overwrite-ignore: git's default checkout silently replaces a
|
|
9137
|
+
# gitignored user file at a path the session branch tracks, and the
|
|
9138
|
+
# file is gone when the user switches back. On conflict the resume
|
|
9139
|
+
# is refused and a fresh session branch (with a fresh snapshot) is
|
|
9140
|
+
# minted below.
|
|
9141
|
+
if git checkout --no-overwrite-ignore "$recorded" >/dev/null 2>&1; then
|
|
9117
9142
|
log_info "Resuming on recorded agent branch: ${recorded}"
|
|
9143
|
+
_loki_resume_snapshot
|
|
9118
9144
|
return 0
|
|
9119
9145
|
fi
|
|
9120
9146
|
log_warn "Recorded agent branch ${recorded} could not be checked out - creating a new one"
|
|
9121
9147
|
fi
|
|
9122
9148
|
fi
|
|
9123
9149
|
|
|
9124
|
-
# Fresh run:
|
|
9125
|
-
#
|
|
9150
|
+
# Fresh run: mint and check out the feature branch, then persist its base
|
|
9151
|
+
# (fresh-run-only, LOCK A2).
|
|
9126
9152
|
local timestamp
|
|
9127
9153
|
timestamp=$(date +%s)
|
|
9128
9154
|
local branch_name="loki/session-${timestamp}-$$"
|
|
9129
9155
|
|
|
9130
9156
|
mkdir -p .loki/state 2>/dev/null || true
|
|
9131
|
-
# Persist the base only once per run tree; never overwrite an existing base.
|
|
9132
|
-
[ ! -s .loki/state/base-branch.txt ] && printf '%s\n' "$cur" > .loki/state/base-branch.txt 2>/dev/null
|
|
9133
9157
|
|
|
9134
9158
|
log_info "Branch protection enabled - creating agent branch: $branch_name (base: $cur)"
|
|
9135
9159
|
|
|
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"
|
|
9160
|
+
# Record the user's own untracked and gitignored files so
|
|
9161
|
+
# commit_session_changes and the receipt leave them alone.
|
|
9162
|
+
local snap_ok=1 leftover=""
|
|
9163
|
+
if ! _loki_snapshot_or_fail_closed; then
|
|
9164
|
+
snap_ok=0
|
|
9165
|
+
rm -f .loki/state/preexisting-untracked.z .loki/state/preexisting-untracked.sha.z 2>/dev/null
|
|
9166
|
+
log_warn "Could not record pre-existing untracked files; this session will commit nothing (review and commit manually)"
|
|
9149
9167
|
fi
|
|
9150
9168
|
|
|
9151
9169
|
# Create and checkout the feature branch
|
|
@@ -9154,6 +9172,25 @@ setup_agent_branch() {
|
|
|
9154
9172
|
return 1
|
|
9155
9173
|
fi
|
|
9156
9174
|
|
|
9175
|
+
# A new session branch starts with an empty session-created record. Files
|
|
9176
|
+
# an unfinished earlier session left (reached here after a refused resume)
|
|
9177
|
+
# were just snapshotted as pre-existing: kept on disk, never committed.
|
|
9178
|
+
leftover="$(_loki_nul_names .loki/state/session-created.z)"
|
|
9179
|
+
if [ -n "$leftover" ]; then
|
|
9180
|
+
log_warn "Left uncommitted from an earlier unfinished session, now treated as your files: $leftover"
|
|
9181
|
+
fi
|
|
9182
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9183
|
+
if [ "$snap_ok" = 1 ]; then _LOKI_SNAPSHOT_THIS_RUN=1; fi
|
|
9184
|
+
|
|
9185
|
+
# Every mint (a refused resume included) records its own base and the
|
|
9186
|
+
# commit it starts at (the empty tree when unborn); the resume paths above
|
|
9187
|
+
# keep both. A base left by an earlier session can name a branch deleted
|
|
9188
|
+
# since, and _loki_untrack_agent_committed_user_files then could not find
|
|
9189
|
+
# the user files the agent commits.
|
|
9190
|
+
printf '%s\n' "$cur" > .loki/state/base-branch.txt 2>/dev/null
|
|
9191
|
+
{ git rev-parse --verify -q HEAD || git hash-object -t tree /dev/null; } \
|
|
9192
|
+
> .loki/state/session-start-sha 2>/dev/null
|
|
9193
|
+
|
|
9157
9194
|
# Store the branch name for later use (PR creation, cleanup)
|
|
9158
9195
|
printf '%s\n' "$branch_name" > .loki/state/agent-branch.txt 2>/dev/null
|
|
9159
9196
|
|
|
@@ -9162,6 +9199,180 @@ setup_agent_branch() {
|
|
|
9162
9199
|
echo "$branch_name"
|
|
9163
9200
|
}
|
|
9164
9201
|
|
|
9202
|
+
# Snapshot, or fail closed. Any snapshot failure (for example git older than
|
|
9203
|
+
# 2.18, whose status lacks --no-renames / --ignored=matching) leaves a marker
|
|
9204
|
+
# that makes commit_session_changes commit nothing: a missing snapshot must
|
|
9205
|
+
# only ever mean an older session, never "sweep everything".
|
|
9206
|
+
_loki_snapshot_or_fail_closed() {
|
|
9207
|
+
if _loki_snapshot_preexisting "$@"; then
|
|
9208
|
+
rm -f .loki/state/preexisting-untracked.failed 2>/dev/null
|
|
9209
|
+
return 0
|
|
9210
|
+
fi
|
|
9211
|
+
mkdir -p .loki/state 2>/dev/null
|
|
9212
|
+
: > .loki/state/preexisting-untracked.failed 2>/dev/null
|
|
9213
|
+
return 1
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9216
|
+
# _loki_snapshot_preexisting [union]
|
|
9217
|
+
# Write .loki/state/preexisting-untracked.z: every path git does not track,
|
|
9218
|
+
# untracked and gitignored, repo-wide, NUL-delimited, relative to the repo top.
|
|
9219
|
+
# commit_session_changes unstages these and the receipt
|
|
9220
|
+
# (autonomy/lib/workspace_diff.py) does not claim them. --ignored=matching lists
|
|
9221
|
+
# a directory only when an ignore pattern matches it, so node_modules/ is one
|
|
9222
|
+
# "dir/" entry covering its subtree, while logs/ holding only *.log files is
|
|
9223
|
+
# listed file by file (a new logs/app.json is still the agent's). "union" keeps
|
|
9224
|
+
# the paths already recorded. The sibling .sha.z (content hash per file entry)
|
|
9225
|
+
# lets the receipt list a pre-existing file the run changed; it is removed
|
|
9226
|
+
# first, so a failed hash step means no detection, never stale hashes. Returns
|
|
9227
|
+
# non-zero, leaving any earlier list in place, when git cannot list.
|
|
9228
|
+
_loki_snapshot_preexisting() {
|
|
9229
|
+
local snap=".loki/state/preexisting-untracked.z" top="" base="" exclude=""
|
|
9230
|
+
rm -f "${snap%.z}.sha.z" 2>/dev/null
|
|
9231
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
9232
|
+
mkdir -p .loki/state 2>/dev/null || return 1
|
|
9233
|
+
# "union" never adds a path listed in session-created.z: those files were
|
|
9234
|
+
# made by a session that did not finish (interrupt, pod loss), so the
|
|
9235
|
+
# session that finishes the work commits them. If the user edits one of
|
|
9236
|
+
# them between sessions it is still the session's file and is committed
|
|
9237
|
+
# with the edit. Recorded entries only block additions; they never remove
|
|
9238
|
+
# a path already in the list.
|
|
9239
|
+
if [ "${1:-}" = union ]; then
|
|
9240
|
+
base="$snap"
|
|
9241
|
+
exclude=".loki/state/session-created.z"
|
|
9242
|
+
fi
|
|
9243
|
+
if ! _loki_untracked_status "$snap.status" \
|
|
9244
|
+
|| ! _loki_untracked_merge "$snap.status" "$base" "$exclude" exact "$snap"; then
|
|
9245
|
+
rm -f "$snap.status" "$snap.tmp"
|
|
9246
|
+
return 1
|
|
9247
|
+
fi
|
|
9248
|
+
rm -f "$snap.status"
|
|
9249
|
+
python3 -E "$SCRIPT_DIR/lib/workspace_diff.py" hash-snapshot "$top" "$snap" >/dev/null 2>&1 \
|
|
9250
|
+
|| log_warn "Could not hash your pre-existing untracked files; the receipt cannot list the ones this run changes"
|
|
9251
|
+
return 0
|
|
9252
|
+
}
|
|
9253
|
+
|
|
9254
|
+
# _loki_untracked_status <out>: the one enumeration behind both the snapshot
|
|
9255
|
+
# and the session-created record, so the two always agree: raw
|
|
9256
|
+
# `git status --porcelain -z` records for the whole repo, .loki excluded.
|
|
9257
|
+
# Non-zero, writing nothing, when git cannot list.
|
|
9258
|
+
_loki_untracked_status() {
|
|
9259
|
+
local top="" prefix=""
|
|
9260
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
|
|
9261
|
+
prefix="$(git rev-parse --show-prefix 2>/dev/null)" || return 1
|
|
9262
|
+
git -C "$top" --no-optional-locks status --porcelain -z --no-renames -uall \
|
|
9263
|
+
--ignored=matching --ignore-submodules=all -- ":(exclude,literal)${prefix}.loki" \
|
|
9264
|
+
> "$1" 2>/dev/null && return 0
|
|
9265
|
+
rm -f "$1"
|
|
9266
|
+
return 1
|
|
9267
|
+
}
|
|
9268
|
+
|
|
9269
|
+
# _loki_untracked_merge <status> <base> <exclude> exact|cover <out>
|
|
9270
|
+
# Atomically write <out>: the NUL-delimited paths of <base>, plus every
|
|
9271
|
+
# untracked ("??") or ignored ("!!") path in <status> that <exclude> does not
|
|
9272
|
+
# hold. With "cover", an <exclude> entry ending in "/" also holds every path
|
|
9273
|
+
# below it (workspace_diff._covered). An empty or missing <base> or <exclude>
|
|
9274
|
+
# is an empty list; any other read or write failure is non-zero and leaves
|
|
9275
|
+
# <out> as it was. Sorted bytewise, like `LC_ALL=C sort -z -u`. One python
|
|
9276
|
+
# process: bash 3.2 has no associative arrays, and a bash loop cost 300ms per
|
|
9277
|
+
# 6,000 entries on /bin/bash. -E and no cwd on sys.path (D7): the cwd is the
|
|
9278
|
+
# agent's repo.
|
|
9279
|
+
_loki_untracked_merge() {
|
|
9280
|
+
python3 -E -c 'import sys
|
|
9281
|
+
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
9282
|
+
import os
|
|
9283
|
+
status, base, exclude, mode, out = sys.argv[1:6]
|
|
9284
|
+
def entries(path, missing_ok=True):
|
|
9285
|
+
try:
|
|
9286
|
+
with open(path, "rb") as fh:
|
|
9287
|
+
return [p for p in fh.read().split(b"\0") if p]
|
|
9288
|
+
except FileNotFoundError:
|
|
9289
|
+
if missing_ok:
|
|
9290
|
+
return []
|
|
9291
|
+
raise
|
|
9292
|
+
held = set(entries(exclude)) if exclude else set()
|
|
9293
|
+
def covered(path):
|
|
9294
|
+
if path in held:
|
|
9295
|
+
return True
|
|
9296
|
+
cut = path.find(b"/") if mode == "cover" else -1
|
|
9297
|
+
while cut != -1:
|
|
9298
|
+
if path[:cut + 1] in held:
|
|
9299
|
+
return True
|
|
9300
|
+
cut = path.find(b"/", cut + 1)
|
|
9301
|
+
return False
|
|
9302
|
+
paths = set(entries(base)) if base else set()
|
|
9303
|
+
for rec in entries(status, missing_ok=False):
|
|
9304
|
+
# The session-created record ("cover" mode) never holds a directory entry:
|
|
9305
|
+
# a whole-directory entry (logs/, out/) would stop the resume union from
|
|
9306
|
+
# adopting a file the user later creates inside it, and a .gitignore
|
|
9307
|
+
# rewrite would then sweep that file. Adopting such a directory as
|
|
9308
|
+
# pre-existing fails safe (the agent files inside stay uncommitted).
|
|
9309
|
+
if mode == "cover" and rec.endswith(b"/"):
|
|
9310
|
+
continue
|
|
9311
|
+
if rec[:3] in (b"?? ", b"!! ") and not covered(rec[3:]):
|
|
9312
|
+
paths.add(rec[3:])
|
|
9313
|
+
with open(out + ".tmp", "wb") as fh:
|
|
9314
|
+
fh.write(b"".join(p + b"\0" for p in sorted(paths)))
|
|
9315
|
+
os.replace(out + ".tmp", out)' "$@" 2>/dev/null
|
|
9316
|
+
}
|
|
9317
|
+
|
|
9318
|
+
# _loki_record_session_created
|
|
9319
|
+
# Union into .loki/state/session-created.z (NUL-delimited, repo-top-relative,
|
|
9320
|
+
# atomic) the paths git does not track now that the pre-existing snapshot does
|
|
9321
|
+
# not cover: the files this session created. The resume union never adds these
|
|
9322
|
+
# to the snapshot, so a session that is interrupted (cleanup) or killed (pod
|
|
9323
|
+
# loss: no trap runs, hence the call after every provider turn) still has its
|
|
9324
|
+
# own files committed by the session that finishes. commit_session_changes
|
|
9325
|
+
# clears the record after a normal commit; a new session branch starts empty.
|
|
9326
|
+
# Records nothing unless this run took a current snapshot (a leftover or stale
|
|
9327
|
+
# one would make it claim a user's file). On a failure the record is left as it
|
|
9328
|
+
# was, never partial, and preexisting-untracked.failed is never touched; the
|
|
9329
|
+
# next successful call recomputes everything, so only a failure on the final
|
|
9330
|
+
# call before a kill loses paths (those are then treated as the user's: kept on
|
|
9331
|
+
# disk, not committed).
|
|
9332
|
+
_loki_record_session_created() {
|
|
9333
|
+
local snap=".loki/state/preexisting-untracked.z" out=".loki/state/session-created.z"
|
|
9334
|
+
[ "${_LOKI_SNAPSHOT_THIS_RUN:-0}" = 1 ] || return 0
|
|
9335
|
+
if [ -f "$snap" ] && _loki_untracked_status "$out.status" \
|
|
9336
|
+
&& _loki_untracked_merge "$out.status" "$out" "$snap" cover "$out"; then
|
|
9337
|
+
rm -f "$out.status"
|
|
9338
|
+
return 0
|
|
9339
|
+
fi
|
|
9340
|
+
rm -f "$out.status" "$out.tmp" 2>/dev/null
|
|
9341
|
+
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"
|
|
9342
|
+
return 1
|
|
9343
|
+
}
|
|
9344
|
+
|
|
9345
|
+
# _loki_resume_snapshot: the resume paths' union, naming the files carried over
|
|
9346
|
+
# from an unfinished previous session (this session's commit includes them).
|
|
9347
|
+
_loki_resume_snapshot() {
|
|
9348
|
+
local carried=""
|
|
9349
|
+
if ! _loki_snapshot_or_fail_closed union; then
|
|
9350
|
+
log_warn "Could not add your current untracked files to the pre-existing list; this session will commit nothing (review and commit manually)"
|
|
9351
|
+
return 0
|
|
9352
|
+
fi
|
|
9353
|
+
_LOKI_SNAPSHOT_THIS_RUN=1
|
|
9354
|
+
carried="$(_loki_nul_names .loki/state/session-created.z)"
|
|
9355
|
+
if [ -n "$carried" ]; then
|
|
9356
|
+
log_info "Carried over as this session's work, not adopted as your files (created by the unfinished previous session): $carried"
|
|
9357
|
+
fi
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
# _loki_nul_names <file>: the entries of a NUL-delimited repo-top-relative list
|
|
9361
|
+
# that exist now, comma-separated, the first 20 then "(and N more)".
|
|
9362
|
+
_loki_nul_names() {
|
|
9363
|
+
local p top="" n=0 out=""
|
|
9364
|
+
[ -s "$1" ] || return 0
|
|
9365
|
+
top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 0
|
|
9366
|
+
while IFS= read -r -d '' p; do
|
|
9367
|
+
if [ -e "$top/$p" ] || [ -L "$top/$p" ]; then
|
|
9368
|
+
n=$((n + 1))
|
|
9369
|
+
if [ "$n" -le 20 ]; then out="${out}${out:+, }${p}"; fi
|
|
9370
|
+
fi
|
|
9371
|
+
done < "$1"
|
|
9372
|
+
if [ "$n" -gt 20 ]; then out="${out} (and $((n - 20)) more)"; fi
|
|
9373
|
+
printf '%s' "$out"
|
|
9374
|
+
}
|
|
9375
|
+
|
|
9165
9376
|
# RUN-25 iter 21 (Wave D #2): the two secret matchers now live in one sourceable
|
|
9166
9377
|
# lib so the commit gate (here) and the completion evidence gate (completion-
|
|
9167
9378
|
# council.sh) share a single implementation. Sourced with a guard so a re-source
|
|
@@ -9175,6 +9386,101 @@ fi
|
|
|
9175
9386
|
if ! type _commit_scan_secret_file >/dev/null 2>&1; then _commit_scan_secret_file() { return 1; }; fi
|
|
9176
9387
|
if ! type _commit_path_looks_secret >/dev/null 2>&1; then _commit_path_looks_secret() { return 1; }; fi
|
|
9177
9388
|
|
|
9389
|
+
# _loki_covered_paths <paths> <snapshot> <top> <out>
|
|
9390
|
+
# Atomically write <out>: the NUL-delimited entries of <paths> that <snapshot>
|
|
9391
|
+
# covers (the entry itself or a parent "dir/" entry, as workspace_diff._covered)
|
|
9392
|
+
# and that still exist under <top>. Non-zero on a read or write failure. -E and
|
|
9393
|
+
# no cwd on sys.path (D7): the cwd is the agent's repo.
|
|
9394
|
+
_loki_covered_paths() {
|
|
9395
|
+
python3 -E -c 'import sys
|
|
9396
|
+
sys.path[:] = [p for p in sys.path if p not in ("", ".")]
|
|
9397
|
+
import os
|
|
9398
|
+
paths, snap, top, out = sys.argv[1:5]
|
|
9399
|
+
def entries(path):
|
|
9400
|
+
with open(path, "rb") as fh:
|
|
9401
|
+
return [p for p in fh.read().split(b"\0") if p]
|
|
9402
|
+
held = set(entries(snap))
|
|
9403
|
+
def covered(path):
|
|
9404
|
+
if path in held:
|
|
9405
|
+
return True
|
|
9406
|
+
cut = path.find(b"/")
|
|
9407
|
+
while cut != -1:
|
|
9408
|
+
if path[:cut + 1] in held:
|
|
9409
|
+
return True
|
|
9410
|
+
cut = path.find(b"/", cut + 1)
|
|
9411
|
+
return False
|
|
9412
|
+
# A file gone from disk stays tracked: untracking it would leave its bytes
|
|
9413
|
+
# only in history.
|
|
9414
|
+
top = os.fsencode(top)
|
|
9415
|
+
hits = [p for p in entries(paths) if covered(p) and os.path.lexists(os.path.join(top, p))]
|
|
9416
|
+
with open(out + ".tmp", "wb") as fh:
|
|
9417
|
+
fh.write(b"".join(p + b"\0" for p in hits))
|
|
9418
|
+
os.replace(out + ".tmp", out)' "$@" 2>/dev/null
|
|
9419
|
+
}
|
|
9420
|
+
|
|
9421
|
+
# _loki_untrack_agent_committed_user_files <branch> (BACKLOG 74)
|
|
9422
|
+
# An agent that rewrites .gitignore and runs `git add -A && git commit` itself
|
|
9423
|
+
# puts the user's untracked or ignored files on the session branch, and a
|
|
9424
|
+
# checkout of the base then deletes them from disk. Files ADDED on the branch
|
|
9425
|
+
# since it forked from the recorded base that the pre-existing snapshot covers
|
|
9426
|
+
# are removed from the index (they stay on disk) and that removal is committed
|
|
9427
|
+
# on its own, before the session commit: it adds no content, and no later exit
|
|
9428
|
+
# (a secret abort, a failed unstage) can put the files back on the branch tip.
|
|
9429
|
+
# Records this session's removals in .loki/state/agent-committed-user-files.z
|
|
9430
|
+
# and warns that the branch history still holds them. The fork is the merge
|
|
9431
|
+
# base with the recorded base branch, else the session's recorded start commit
|
|
9432
|
+
# (.loki/state/session-start-sha). Non-zero when the removal could not be
|
|
9433
|
+
# committed, or when neither fork resolves or the check fails (the tip may hold
|
|
9434
|
+
# user files): the caller then makes no session commit, whose `git add -A`
|
|
9435
|
+
# would stage the files again.
|
|
9436
|
+
_loki_untrack_agent_committed_user_files() {
|
|
9437
|
+
local snap=".loki/state/preexisting-untracked.z" rec=".loki/state/agent-committed-user-files.z"
|
|
9438
|
+
local base="" fork="" start="" top="" names=""
|
|
9439
|
+
local unchecked="Before switching branches, run git rm --cached on each of your files the agent committed and commit, or a checkout of the base deletes them. Left uncommitted: no session commit."
|
|
9440
|
+
[ -s "$snap" ] || return 0
|
|
9441
|
+
# No commit on the branch yet: the agent committed nothing.
|
|
9442
|
+
git rev-parse --verify -q HEAD >/dev/null 2>&1 || return 0
|
|
9443
|
+
[ -s .loki/state/base-branch.txt ] && base="$(cat .loki/state/base-branch.txt 2>/dev/null)"
|
|
9444
|
+
if [ -n "$base" ]; then fork="$(git merge-base HEAD "$base" 2>/dev/null)" || fork=""; fi
|
|
9445
|
+
if [ -z "$fork" ] && [ -s .loki/state/session-start-sha ]; then
|
|
9446
|
+
start="$(cat .loki/state/session-start-sha 2>/dev/null)"
|
|
9447
|
+
# A commit on this branch, or the empty tree of an unborn mint.
|
|
9448
|
+
if git merge-base --is-ancestor "$start" HEAD 2>/dev/null \
|
|
9449
|
+
|| [ "$(git cat-file -t "$start" 2>/dev/null)" = tree ]; then
|
|
9450
|
+
fork="$start"
|
|
9451
|
+
fi
|
|
9452
|
+
fi
|
|
9453
|
+
if [ -z "$fork" ]; then
|
|
9454
|
+
log_warn "Could not check whether the agent committed your pre-existing untracked files on $1: base branch '${base:-unknown}' not found and the recorded session start commit is missing or not on this branch. ${unchecked}"
|
|
9455
|
+
return 1
|
|
9456
|
+
fi
|
|
9457
|
+
# --no-renames: a user file swept in beside a deleted tracked file must
|
|
9458
|
+
# read as added, not as a rename of that file.
|
|
9459
|
+
if ! top="$(git rev-parse --show-toplevel 2>/dev/null)" \
|
|
9460
|
+
|| ! git -C "$top" diff --name-only -z --no-renames --diff-filter=A "$fork" HEAD > "$rec.added" 2>/dev/null \
|
|
9461
|
+
|| ! _loki_covered_paths "$rec.added" "$snap" "$top" "$rec.new"; then
|
|
9462
|
+
rm -f "$rec.added" "$rec.new" "$rec.new.tmp"
|
|
9463
|
+
log_warn "Could not check whether the agent committed your pre-existing untracked files on $1. ${unchecked}"
|
|
9464
|
+
return 1
|
|
9465
|
+
fi
|
|
9466
|
+
rm -f "$rec.added"
|
|
9467
|
+
if [ ! -s "$rec.new" ]; then
|
|
9468
|
+
rm -f "$rec.new"
|
|
9469
|
+
return 0
|
|
9470
|
+
fi
|
|
9471
|
+
mv -f "$rec.new" "$rec"
|
|
9472
|
+
names="$(_loki_nul_names "$rec")"
|
|
9473
|
+
git reset -q >/dev/null 2>&1 || true
|
|
9474
|
+
if ! git -C "$top" update-index -z --force-remove --stdin < "$rec" >/dev/null 2>&1 \
|
|
9475
|
+
|| ! git commit -q -m "Loki Mode: untrack pre-existing user files the agent committed" >/dev/null 2>&1; then
|
|
9476
|
+
log_warn "The agent committed your pre-existing files on $1 and Loki could not remove them from the branch: ${names}. They are on disk. Before switching branches, run git rm --cached on each and commit, or a checkout of the base deletes them. Left uncommitted: no session commit."
|
|
9477
|
+
return 1
|
|
9478
|
+
fi
|
|
9479
|
+
log_warn "The agent committed your pre-existing untracked or ignored files on $1: ${names}. Loki removed them from the branch tip; they stay on disk, untracked. The branch history still holds them: do not push $1 as-is. To drop them from its history: git reset --soft ${fork} && git commit"
|
|
9480
|
+
audit_agent_action "git_untrack_user_files" "Removed pre-existing user files the agent committed" "files=${names}" || true
|
|
9481
|
+
return 0
|
|
9482
|
+
}
|
|
9483
|
+
|
|
9178
9484
|
commit_session_changes() {
|
|
9179
9485
|
# Squash the session's work into one honest session-end commit on the agent
|
|
9180
9486
|
# branch (LOCK A3/A4/A8). Commit-always (incl. failed runs) so the user is
|
|
@@ -9209,6 +9515,25 @@ commit_session_changes() {
|
|
|
9209
9515
|
;;
|
|
9210
9516
|
esac
|
|
9211
9517
|
|
|
9518
|
+
# Commit nothing unless THIS run took a current snapshot (BACKLOG 90). The
|
|
9519
|
+
# LOCK A1 opt-out takes none, so it never commits, even on a leftover
|
|
9520
|
+
# loki/session-* branch whose agent-branch.txt and snapshot belong to an
|
|
9521
|
+
# earlier session: that stale snapshot would sweep in a file the user made
|
|
9522
|
+
# since, and a checkout of the base would delete it. Checked before any
|
|
9523
|
+
# staging, so the opt-out leaves the user's index alone.
|
|
9524
|
+
if [ "${_LOKI_SNAPSHOT_THIS_RUN:-0}" != 1 ]; then
|
|
9525
|
+
if [ "${LOKI_BRANCH_PROTECTION:-true}" != "true" ]; then
|
|
9526
|
+
log_info "Branch protection off (LOKI_BRANCH_PROTECTION=${LOKI_BRANCH_PROTECTION}): no session commit; the work stays uncommitted on ${cur}"
|
|
9527
|
+
elif [ -f "$PWD/.loki/state/preexisting-untracked.failed" ]; then
|
|
9528
|
+
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."
|
|
9529
|
+
else
|
|
9530
|
+
log_warn "Left uncommitted: this run recorded no snapshot of your pre-existing untracked files, so it makes no session commit. Review and commit manually."
|
|
9531
|
+
fi
|
|
9532
|
+
return 0
|
|
9533
|
+
fi
|
|
9534
|
+
|
|
9535
|
+
_loki_untrack_agent_committed_user_files "$cur" || return 0
|
|
9536
|
+
|
|
9212
9537
|
# Stage everything except .loki/ runtime state and a secret-path denylist.
|
|
9213
9538
|
# These excludes are defense-in-depth ONLY for the top-level cases git
|
|
9214
9539
|
# pathspec handles cleanly. We deliberately do NOT add nested globs like
|
|
@@ -9226,13 +9551,16 @@ commit_session_changes() {
|
|
|
9226
9551
|
':!*.key' ':!*.pem' ':!*.p12' ':!*.keystore' \
|
|
9227
9552
|
':!id_rsa*' ':!*.token' ':!credentials*' 2>/dev/null || true
|
|
9228
9553
|
|
|
9229
|
-
# Unstage exactly the
|
|
9230
|
-
# (setup_agent_branch
|
|
9231
|
-
#
|
|
9232
|
-
#
|
|
9554
|
+
# Unstage exactly the paths recorded as untracked or gitignored when the
|
|
9555
|
+
# session started (setup_agent_branch; a "dir/" entry covers its subtree):
|
|
9556
|
+
# they are the user's, not this session's work, and committing them here
|
|
9557
|
+
# would delete them from disk on a later checkout of the base (an agent
|
|
9558
|
+
# rewrite of .gitignore can expose ignored files to `git add -A`).
|
|
9559
|
+
# Agent-created files stay staged. Skip an empty snapshot: an
|
|
9233
9560
|
# empty --pathspec-from-file resets the WHOLE index. No snapshot (older
|
|
9234
9561
|
# session): behave as before. If the unstage fails (git < 2.25), commit
|
|
9235
|
-
# nothing rather than sweep the user's files in.
|
|
9562
|
+
# nothing rather than sweep the user's files in. A failed snapshot never
|
|
9563
|
+
# gets here (the snapshot guard above returns first).
|
|
9236
9564
|
local preexisting="$PWD/.loki/state/preexisting-untracked.z" top=""
|
|
9237
9565
|
if [ -s "$preexisting" ]; then
|
|
9238
9566
|
if ! { top="$(git rev-parse --show-toplevel 2>/dev/null)" \
|
|
@@ -9244,8 +9572,12 @@ commit_session_changes() {
|
|
|
9244
9572
|
fi
|
|
9245
9573
|
fi
|
|
9246
9574
|
|
|
9247
|
-
# Nothing staged = clean no-op, never an error.
|
|
9575
|
+
# Nothing staged = clean no-op, never an error. A session that ends here or
|
|
9576
|
+
# commits below ended normally: its session-created record is spent (what
|
|
9577
|
+
# it listed is committed, or was never committable), so the next session
|
|
9578
|
+
# starts clean.
|
|
9248
9579
|
if git diff --cached --quiet 2>/dev/null; then
|
|
9580
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9249
9581
|
return 0
|
|
9250
9582
|
fi
|
|
9251
9583
|
|
|
@@ -9287,7 +9619,9 @@ commit_session_changes() {
|
|
|
9287
9619
|
return 0
|
|
9288
9620
|
fi
|
|
9289
9621
|
|
|
9290
|
-
git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result:-0})" 2>/dev/null
|
|
9622
|
+
if git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result:-0})" 2>/dev/null; then
|
|
9623
|
+
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9624
|
+
fi
|
|
9291
9625
|
audit_agent_action "git_commit" "Committed session changes" "iterations=${ITERATION_COUNT:-0},result=${result:-0}" || true
|
|
9292
9626
|
return 0
|
|
9293
9627
|
}
|
|
@@ -11884,6 +12218,12 @@ ZT_MATCHES_EOF
|
|
|
11884
12218
|
}
|
|
11885
12219
|
|
|
11886
12220
|
enforce_test_coverage() {
|
|
12221
|
+
# Every suite this gate launches (pytest, unittest, the coverage pass, a
|
|
12222
|
+
# package script that runs Python) runs inside the user's repo: no
|
|
12223
|
+
# __pycache__/*.pyc there, so none can land in the session commit when the
|
|
12224
|
+
# repo does not ignore them (BACKLOG 85). Bytecode caching only; the same
|
|
12225
|
+
# tests run. Scoped to this call.
|
|
12226
|
+
local -x PYTHONDONTWRITEBYTECODE=1
|
|
11887
12227
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
11888
12228
|
local quality_dir="$loki_dir/quality"
|
|
11889
12229
|
mkdir -p "$quality_dir" "$loki_dir/signals"
|
|
@@ -11913,7 +12253,9 @@ enforce_test_coverage() {
|
|
|
11913
12253
|
# that is the legitimate case they were written for (a package that ships
|
|
11914
12254
|
# a runner but no npm script).
|
|
11915
12255
|
local _declared_test_script=""
|
|
11916
|
-
|
|
12256
|
+
# BACKLOG 81 (D7): -E plus the sys.path filter, so the agent's repo (the
|
|
12257
|
+
# cwd) cannot supply json or sitecustomize to this reader.
|
|
12258
|
+
_declared_test_script=$(_LOKI_PKG="${TARGET_DIR:-.}/package.json" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
11917
12259
|
import json, os, sys
|
|
11918
12260
|
try:
|
|
11919
12261
|
with open(os.environ['_LOKI_PKG']) as f:
|
|
@@ -12033,7 +12375,8 @@ sys.stdout.write(t.strip())
|
|
|
12033
12375
|
# npm test), so the grep only ever picked the LABEL, never
|
|
12034
12376
|
# what ran. This corrects the evidence, not the execution.
|
|
12035
12377
|
local _ws_script
|
|
12036
|
-
|
|
12378
|
+
# BACKLOG 81 (D7): same guard as the root reader above.
|
|
12379
|
+
_ws_script=$(_LOKI_PKG="$pkg_json" python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
12037
12380
|
import json, os, sys
|
|
12038
12381
|
try:
|
|
12039
12382
|
with open(os.environ['_LOKI_PKG']) as f:
|
|
@@ -12353,6 +12696,9 @@ TREOF
|
|
|
12353
12696
|
# run instead of re-running the suite. Single source of truth for "tests
|
|
12354
12697
|
# ran this iteration", set on every return path that writes results.
|
|
12355
12698
|
printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
|
|
12699
|
+
# BACKLOG 91: non-blocking, but nothing was measured: the loop's
|
|
12700
|
+
# stage_complete event reads this and says not_run, not pass.
|
|
12701
|
+
_LOKI_TEST_SUITE_STATUS=not_run
|
|
12356
12702
|
return 0
|
|
12357
12703
|
fi
|
|
12358
12704
|
|
|
@@ -12364,6 +12710,49 @@ TREOF
|
|
|
12364
12710
|
# first, then delete every remaining control char (\000-\037, incl. ESC).
|
|
12365
12711
|
details=$(echo "$details" | tr '"' "'" | tr '\n' ' ' | tr -d '\000-\037' | head -c 500)
|
|
12366
12712
|
|
|
12713
|
+
# BACKLOG 73: a runner that exits 0 while its own summary reports failures
|
|
12714
|
+
# (`jest || true`, a script that swallows the status) failed. The failure
|
|
12715
|
+
# count comes from the runner's summary lines in the FULL output (the details
|
|
12716
|
+
# tail can drop them: the jest branch keeps 3 lines). The highest count
|
|
12717
|
+
# across summaries wins, so output that concatenates several runs (npm test
|
|
12718
|
+
# --workspaces, `jest; vitest`) cannot hide a failed run behind a later
|
|
12719
|
+
# green one. Shapes: jest "Tests: 1 failed, ...", vitest "Tests 1 failed |
|
|
12720
|
+
# ...", pytest "=== 1 failed, 2 passed in 0.1s ===" (or its -q line), node
|
|
12721
|
+
# TAP "# fail 1" and the spec reporter's "<info sign> fail 1", counted only
|
|
12722
|
+
# right after the matching pass line (node TAP echoes a test's own stdout
|
|
12723
|
+
# as "# ..." lines, which must not count), mocha "1 failing", go "--- FAIL:"
|
|
12724
|
+
# lines. A read summary with no failures is a measured 0. With no recognised
|
|
12725
|
+
# summary the old best-effort parse of the tail still applies (null when it
|
|
12726
|
+
# finds nothing). Either way a recorded count above zero is a failure: the
|
|
12727
|
+
# council evidence gate and the Bun gate both read failed_count > 0 as one.
|
|
12728
|
+
# LC_ALL=C: bytes, so any runner output parses; [ \t] not [[:space:]] for
|
|
12729
|
+
# old mawk.
|
|
12730
|
+
local _tr_passed_n _tr_failed_n _tr_summary_red=false
|
|
12731
|
+
_tr_failed_n=$(printf '%s\n' "${output:-}" | LC_ALL=C sed "s/$(printf '\033')\[[0-9;]*[A-Za-z]//g" | LC_ALL=C awk '
|
|
12732
|
+
function upd(n) { if (n > f) f = n; seen = 1 }
|
|
12733
|
+
/^Tests:[ \t]/ || /^[ \t]*Tests[ \t]+[0-9]/ ||
|
|
12734
|
+
/^=+ .*[0-9]+ (passed|failed)/ || /^[0-9]+ (passed|failed).* in [0-9.]+s/ {
|
|
12735
|
+
n = 0; if (match($0, /[0-9]+ failed/)) n = substr($0, RSTART, RLENGTH) + 0; upd(n) }
|
|
12736
|
+
/^[^ A-Za-z0-9]+ fail [0-9]+[ \t]*$/ { if (prev ~ /^[^ A-Za-z0-9]+ pass [0-9]+[ \t]*$/) upd($3 + 0) }
|
|
12737
|
+
/^[ \t]*[0-9]+ passing \(/ { upd(0) }
|
|
12738
|
+
/^[ \t]*[0-9]+ failing[ \t]*$/ { upd($1 + 0) }
|
|
12739
|
+
/^--- FAIL: / { go++ }
|
|
12740
|
+
{ prev = $0 }
|
|
12741
|
+
END { if (go > f) { f = go; seen = 1 } if (seen) print f + 0 }')
|
|
12742
|
+
_tr_passed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+' | head -1)
|
|
12743
|
+
[ -n "$_tr_failed_n" ] || _tr_failed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+' | head -1)
|
|
12744
|
+
# node --test emits TAP-ish "# pass N" / "# fail N" summary lines, which the
|
|
12745
|
+
# "N passed"/"N failed" pattern above does not match. Fall back to those.
|
|
12746
|
+
[ -n "$_tr_passed_n" ] || _tr_passed_n=$(printf '%s' "$details" | grep -oE '# pass [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12747
|
+
[ -n "$_tr_failed_n" ] || _tr_failed_n=$(printf '%s' "$details" | grep -oE '# fail [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12748
|
+
[ -n "$_tr_passed_n" ] || _tr_passed_n=null
|
|
12749
|
+
[ -n "$_tr_failed_n" ] || _tr_failed_n=null
|
|
12750
|
+
if [ "$test_passed" = "true" ] && [ "$_tr_failed_n" != "null" ] && [ "$_tr_failed_n" -gt 0 ] 2>/dev/null; then
|
|
12751
|
+
test_passed=false
|
|
12752
|
+
_tr_summary_red=true
|
|
12753
|
+
log_warn "Test suite gate: $test_runner exited 0 but its summary reports $_tr_failed_n failed -- recording a failure"
|
|
12754
|
+
fi
|
|
12755
|
+
|
|
12367
12756
|
# Evidence Receipt provenance (v7.85.0): record the deterministic FACTS a
|
|
12368
12757
|
# non-forgeable receipt needs -- the command that ran, its exit code, and a
|
|
12369
12758
|
# status enum -- alongside the legacy pass/runner/min_coverage keys the
|
|
@@ -12381,6 +12770,8 @@ TREOF
|
|
|
12381
12770
|
*) _tr_cmd="$test_runner" ;;
|
|
12382
12771
|
esac
|
|
12383
12772
|
if [ "$test_passed" = "true" ]; then _tr_exit=0; _tr_status="verified"; else _tr_exit=1; _tr_status="failed"; fi
|
|
12773
|
+
# BACKLOG 73: the runner itself exited 0; record that fact, not a made-up 1.
|
|
12774
|
+
if [ "$_tr_summary_red" = "true" ]; then _tr_exit=0; fi
|
|
12384
12775
|
|
|
12385
12776
|
# #82 (zero-test-file hardening): a runner that EXITED 0 but executed ZERO
|
|
12386
12777
|
# real tests is a mini fake-green -- it records "verified" while proving
|
|
@@ -12402,17 +12793,6 @@ TREOF
|
|
|
12402
12793
|
log_warn "Verification gap: $test_runner exited 0 but ran ZERO tests -- recording inconclusive (no_tests_run), not verified"
|
|
12403
12794
|
fi
|
|
12404
12795
|
|
|
12405
|
-
# Best-effort pass/fail counts from the summary text (null when not found).
|
|
12406
|
-
local _tr_passed_n _tr_failed_n
|
|
12407
|
-
_tr_passed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+' | head -1)
|
|
12408
|
-
_tr_failed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+' | head -1)
|
|
12409
|
-
# node --test emits TAP-ish "# pass N" / "# fail N" summary lines, which the
|
|
12410
|
-
# "N passed"/"N failed" pattern above does not match. Fall back to those.
|
|
12411
|
-
[ -n "$_tr_passed_n" ] || _tr_passed_n=$(printf '%s' "$details" | grep -oE '# pass [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12412
|
-
[ -n "$_tr_failed_n" ] || _tr_failed_n=$(printf '%s' "$details" | grep -oE '# fail [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12413
|
-
[ -n "$_tr_passed_n" ] || _tr_passed_n=null
|
|
12414
|
-
[ -n "$_tr_failed_n" ] || _tr_failed_n=null
|
|
12415
|
-
|
|
12416
12796
|
# verification_gap is "none" whenever a real runner executed AND ran tests:
|
|
12417
12797
|
# the suite ran, so there is no docs-without-execution / source-without-tests
|
|
12418
12798
|
# gap. The #82 zero-test case is the exception -- a runner ran but executed
|
|
@@ -12573,9 +12953,21 @@ os.replace(tmp, out)
|
|
|
12573
12953
|
fi
|
|
12574
12954
|
|
|
12575
12955
|
if [ "$test_passed" = "true" ]; then
|
|
12576
|
-
touch "$quality_dir/unit-tests.pass"
|
|
12577
12956
|
rm -f "$loki_dir/signals/TESTS_FAILED" 2>/dev/null || true
|
|
12578
|
-
|
|
12957
|
+
# BACKLOG 55: the #82 zero-test run exited 0 but proved nothing. It is
|
|
12958
|
+
# inconclusive, so it must not leave the pass marker the receipt reads
|
|
12959
|
+
# as "unit_tests passed" (proof-generator.py _collect_quality_gates).
|
|
12960
|
+
# Still non-blocking (return 0 below) and still not a failure (no
|
|
12961
|
+
# TESTS_FAILED), so the council decides it, per the #82 design.
|
|
12962
|
+
if [ "$_tr_zero_tests" = "true" ]; then
|
|
12963
|
+
rm -f "$quality_dir/unit-tests.pass" 2>/dev/null || true
|
|
12964
|
+
log_warn "Test suite gate: $test_runner ran zero tests -- inconclusive (not passed, not failed)"
|
|
12965
|
+
_LOKI_TEST_SUITE_STATUS=not_run # BACKLOG 91, see the no-runner return
|
|
12966
|
+
else
|
|
12967
|
+
touch "$quality_dir/unit-tests.pass"
|
|
12968
|
+
log_info "Test suite gate: $test_runner passed"
|
|
12969
|
+
_LOKI_TEST_SUITE_STATUS=pass
|
|
12970
|
+
fi
|
|
12579
12971
|
# Coverage block is distinct from tests-red: tests passed, but enforced
|
|
12580
12972
|
# coverage is below threshold. Return nonzero to gate WITHOUT writing the
|
|
12581
12973
|
# TESTS_FAILED signal or removing unit-tests.pass.
|
|
@@ -19583,7 +19975,11 @@ load_state() {
|
|
|
19583
19975
|
if command -v python3 &> /dev/null; then
|
|
19584
19976
|
# BUG-ST-006: Validate checkpoint integrity before loading state
|
|
19585
19977
|
local state_valid
|
|
19586
|
-
|
|
19978
|
+
# -E and the sys.path filter (D7): the cwd is the target repo, and a
|
|
19979
|
+
# committed json.py must not decide the loaded iteration count
|
|
19980
|
+
# (a fake non-zero count would skip the iteration-0 evidence drop).
|
|
19981
|
+
state_valid=$(LOKI_STATE_FILE="$state_file" python3 -E -c "
|
|
19982
|
+
import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
|
|
19587
19983
|
import json, os, sys
|
|
19588
19984
|
try:
|
|
19589
19985
|
with open(os.environ['LOKI_STATE_FILE']) as f:
|
|
@@ -19609,15 +20005,20 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
19609
20005
|
ITERATION_COUNT=0
|
|
19610
20006
|
# Back up corrupted state file for diagnosis
|
|
19611
20007
|
mv "$state_file" "${state_file}.corrupt.$(date +%s)" 2>/dev/null || true
|
|
20008
|
+
# Starts at iteration 0: drop the previous session's test
|
|
20009
|
+
# evidence, as the iteration-0 block at the end does.
|
|
20010
|
+
rm -f "${TARGET_DIR:-.}/.loki/quality/.test-results.iter" \
|
|
20011
|
+
"${TARGET_DIR:-.}/.loki/quality/unit-tests.pass" \
|
|
20012
|
+
"${TARGET_DIR:-.}/.loki/quality/test-results.json" 2>/dev/null || true
|
|
19612
20013
|
return
|
|
19613
20014
|
fi
|
|
19614
20015
|
|
|
19615
20016
|
# Load retry count, iteration count, and status from previous session
|
|
19616
20017
|
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")
|
|
20018
|
+
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")
|
|
20019
|
+
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
20020
|
# 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")
|
|
20021
|
+
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
20022
|
|
|
19622
20023
|
# Reset retry count + iteration count if previous session ended in a
|
|
19623
20024
|
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
@@ -19678,6 +20079,18 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
19678
20079
|
else
|
|
19679
20080
|
RETRY_COUNT=0
|
|
19680
20081
|
fi
|
|
20082
|
+
# A session that starts at iteration 0 has run nothing yet, so a leftover
|
|
20083
|
+
# freshness marker or unit-tests.pass is a previous session's evidence.
|
|
20084
|
+
# Iterations restart at 0, so keeping them would let an old pass:true read
|
|
20085
|
+
# as this iteration's result.
|
|
20086
|
+
if [ "${ITERATION_COUNT:-0}" = "0" ]; then
|
|
20087
|
+
# Same path the writer (enforce_test_coverage) and the freshness
|
|
20088
|
+
# readers use.
|
|
20089
|
+
local _q="${TARGET_DIR:-.}/.loki/quality"
|
|
20090
|
+
# test-results.json too: with the marker gone the receipt's quality
|
|
20091
|
+
# gates fall back to its status, reporting a previous session's run.
|
|
20092
|
+
rm -f "$_q/.test-results.iter" "$_q/unit-tests.pass" "$_q/test-results.json" 2>/dev/null || true
|
|
20093
|
+
fi
|
|
19681
20094
|
}
|
|
19682
20095
|
|
|
19683
20096
|
# Load tasks from queue files for prompt injection
|
|
@@ -23908,6 +24321,12 @@ if __name__ == "__main__":
|
|
|
23908
24321
|
# v7.5.12: Provider invocation finished (or was killed by trap).
|
|
23909
24322
|
LOKI_PROVIDER_ACTIVE=0
|
|
23910
24323
|
|
|
24324
|
+
# Record the files this session has created so far. A pod loss is a
|
|
24325
|
+
# SIGKILL (no trap runs), so this per-turn record is what lets the
|
|
24326
|
+
# resumed session commit them. One git status; no-op without a
|
|
24327
|
+
# session snapshot.
|
|
24328
|
+
_loki_record_session_created || true
|
|
24329
|
+
|
|
23911
24330
|
echo ""
|
|
23912
24331
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
23913
24332
|
echo ""
|
|
@@ -24345,8 +24764,12 @@ EOF
|
|
|
24345
24764
|
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
24346
24765
|
# F49: isolate HOME so the project's suite cannot pollute the
|
|
24347
24766
|
# user's real home when it execs the generated app.
|
|
24767
|
+
_LOKI_TEST_SUITE_STATUS=""
|
|
24348
24768
|
if _loki_with_app_sandbox enforce_test_coverage; then
|
|
24349
24769
|
clear_gate_failure "test_coverage"
|
|
24770
|
+
# BACKLOG 91: a zero-test or no-runner run returns 0 without
|
|
24771
|
+
# measuring anything; its stage status is not_run, not pass.
|
|
24772
|
+
if [ "$_LOKI_TEST_SUITE_STATUS" = "not_run" ]; then _stg_ok=not_run; fi
|
|
24350
24773
|
else
|
|
24351
24774
|
_stg_ok=fail
|
|
24352
24775
|
local tc_count
|
|
@@ -25446,11 +25869,33 @@ kill_provider_child() {
|
|
|
25446
25869
|
fi
|
|
25447
25870
|
kill -TERM "$child_pid" 2>/dev/null && killed=1
|
|
25448
25871
|
done
|
|
25449
|
-
# Also kill provider leaf processes
|
|
25450
|
-
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25872
|
+
# Also kill provider leaf processes in case they were reparented (to init,
|
|
25873
|
+
# once their immediate parent -- a shell or the provider CLI itself --
|
|
25874
|
+
# exited). A reparented process changes PARENT, not PROCESS GROUP: unless
|
|
25875
|
+
# a process explicitly calls setpgid/setsid, it keeps the pgid it was
|
|
25876
|
+
# launched into, which for every invoke_* call above is this shell's own
|
|
25877
|
+
# pgid ($$). Scope the sweep to that pgid, matched by process name, rather
|
|
25878
|
+
# than a bare `pkill -f "^claude( |$)"` -- that matched the FULL COMMAND
|
|
25879
|
+
# LINE of every "claude", "codex", "aider" or "cline" process on the whole
|
|
25880
|
+
# machine, with no scoping at all, and killed unrelated Claude Code
|
|
25881
|
+
# sessions in other terminals the moment ANY loki-mode run finished,
|
|
25882
|
+
# double-Ctrl-C'd, or Ctrl-C'd in perpetual mode (build prompt D14 and
|
|
25883
|
+
# `feedback-pkill-f-substring-killed-the-session`). Reproduced: a decoy
|
|
25884
|
+
# `claude` process in a DIFFERENT process group survived; one launched
|
|
25885
|
+
# inside this shell's job (matching how a real reparented leaf would sit)
|
|
25886
|
+
# was still caught.
|
|
25887
|
+
local proc my_pgid
|
|
25888
|
+
my_pgid="$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')"
|
|
25889
|
+
if [ -n "$my_pgid" ]; then
|
|
25890
|
+
for proc in claude codex aider cline; do
|
|
25891
|
+
local leaf_pid leaf_pgid
|
|
25892
|
+
for leaf_pid in $(pgrep -f "^${proc}( |$)" 2>/dev/null); do
|
|
25893
|
+
leaf_pgid="$(ps -o pgid= -p "$leaf_pid" 2>/dev/null | tr -d ' ')"
|
|
25894
|
+
[ -n "$leaf_pgid" ] && [ "$leaf_pgid" = "$my_pgid" ] || continue
|
|
25895
|
+
kill -TERM "$leaf_pid" 2>/dev/null && killed=1
|
|
25896
|
+
done
|
|
25897
|
+
done
|
|
25898
|
+
fi
|
|
25454
25899
|
|
|
25455
25900
|
# Brief wait for graceful exit (max ~2s).
|
|
25456
25901
|
local i=0
|
|
@@ -26208,6 +26653,10 @@ cleanup() {
|
|
|
26208
26653
|
fi
|
|
26209
26654
|
# v7.5.12: Kill any running provider pipeline first, before slow cleanup.
|
|
26210
26655
|
kill_provider_child 2>/dev/null || true
|
|
26656
|
+
# This exit makes no session commit: record the files this session
|
|
26657
|
+
# created (after the provider is gone, so none are added later), so a
|
|
26658
|
+
# resume commits them instead of adopting them as the user's.
|
|
26659
|
+
_loki_record_session_created || true
|
|
26211
26660
|
rm -f "$loki_dir/STOP" "$loki_dir/PAUSE" "$loki_dir/PAUSED.md" 2>/dev/null
|
|
26212
26661
|
# UT2-13: Clear cli-provider marker on session end.
|
|
26213
26662
|
rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
|
|
@@ -26282,6 +26731,8 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
26282
26731
|
log_warn "Loki Mode interrupted -- shutting down (double Ctrl+C)"
|
|
26283
26732
|
# v7.5.12: Kill provider pipeline immediately so we don't wait on it.
|
|
26284
26733
|
kill_provider_child 2>/dev/null || true
|
|
26734
|
+
# No session commit on this exit either (see the branch above).
|
|
26735
|
+
_loki_record_session_created || true
|
|
26285
26736
|
# Write STOP signal so any peer processes (dashboard, etc.) also stop.
|
|
26286
26737
|
mkdir -p "$loki_dir" 2>/dev/null && touch "$loki_dir/STOP" 2>/dev/null || true
|
|
26287
26738
|
if type app_runner_cleanup &>/dev/null; then
|