loki-mode 9.54.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 +10 -1
- package/autonomy/lib/proof-generator.py +36 -5
- package/autonomy/run.sh +234 -31
- 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 +41 -2
- package/docs/v10/BOARD.md +105 -0
- package/docs/v10/DECISIONS.md +71 -0
- package/docs/v10/PROGRESS.md +43 -0
- package/docs/v10/SWARM-PROMPT.md +98 -0
- package/docs/v10/SWARM.md +237 -0
- package/loki-ts/dist/loki.js +2 -2
- 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/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v9.54.
|
|
6
|
+
# Loki Mode v9.54.2
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
470
470
|
|
|
471
471
|
---
|
|
472
472
|
|
|
473
|
-
**v9.54.
|
|
473
|
+
**v9.54.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.54.
|
|
1
|
+
9.54.2
|
|
@@ -1948,6 +1948,15 @@ runner = d.get('runner', 'none')
|
|
|
1948
1948
|
# an unrecorded outcome is not a pass. None falls to INCONCLUSIVE below.
|
|
1949
1949
|
passed = d.get('pass')
|
|
1950
1950
|
status = d.get('status', '')
|
|
1951
|
+
# BACKLOG 89: a recorded failure count above zero is a failure whatever "pass"
|
|
1952
|
+
# says, as the Bun gate reads it (quality_gates.ts artifactCount): a numeric
|
|
1953
|
+
# failed_count wins, the legacy numeric failed is the fallback, and anything
|
|
1954
|
+
# else (null, missing, a bool) is unmeasured, never a failure and never 0.
|
|
1955
|
+
def _count(v):
|
|
1956
|
+
return v if isinstance(v, (int, float)) and not isinstance(v, bool) else None
|
|
1957
|
+
failed_n = _count(d.get('failed_count'))
|
|
1958
|
+
if failed_n is None:
|
|
1959
|
+
failed_n = _count(d.get('failed'))
|
|
1951
1960
|
#82 (zero-test-file hardening): a runner that ran but executed ZERO real tests
|
|
1952
1961
|
# (node --test on a *.test.js with no test() calls; jest --passWithNoTests with
|
|
1953
1962
|
# no suites) records pass:"inconclusive" + status:"no_tests_run" -- a mini
|
|
@@ -1958,7 +1967,7 @@ status = d.get('status', '')
|
|
|
1958
1967
|
# Only the boolean True passes as affirmative; only the boolean False blocks.
|
|
1959
1968
|
if runner == 'none':
|
|
1960
1969
|
print('PASS:none:true')
|
|
1961
|
-
elif passed is False:
|
|
1970
|
+
elif passed is False or (failed_n or 0) > 0:
|
|
1962
1971
|
print('FAIL:%s:false' % runner)
|
|
1963
1972
|
elif status == 'no_tests_run' or passed is not True:
|
|
1964
1973
|
# Both are INCONCLUSIVE; NO_PASS names the second: no boolean pass was
|
|
@@ -319,6 +319,32 @@ def _is_exogenous(gate):
|
|
|
319
319
|
return _gate_provenance(gate.get("name")) == "exogenous"
|
|
320
320
|
|
|
321
321
|
|
|
322
|
+
def _test_results_fresh(quality_dir):
|
|
323
|
+
"""BACKLOG 82: the gates' freshness rule (ensure_completion_test_evidence,
|
|
324
|
+
Bun testResultsFresh). enforce_test_coverage writes unit-tests.pass and
|
|
325
|
+
test-results.json together with .test-results.iter; they are this
|
|
326
|
+
iteration's evidence only when that marker holds this iteration's number.
|
|
327
|
+
Both loop callers always pass the iteration as ITERATION_COUNT (run.sh
|
|
328
|
+
generate_proof_of_run, Bun runner/proof.ts); a set but empty value is not
|
|
329
|
+
fresh.
|
|
330
|
+
ponytail: ITERATION_COUNT absent altogether means a hand run outside any
|
|
331
|
+
loop, with no iteration to compare against, so the pre-82 reading applies
|
|
332
|
+
there (Bun's gate calls that stale). Tighten when a caller exists that
|
|
333
|
+
omits it."""
|
|
334
|
+
raw = os.environ.get("ITERATION_COUNT")
|
|
335
|
+
if raw is None:
|
|
336
|
+
return True
|
|
337
|
+
iteration = raw.strip()
|
|
338
|
+
if not iteration:
|
|
339
|
+
return False
|
|
340
|
+
try:
|
|
341
|
+
with open(os.path.join(quality_dir, ".test-results.iter"), "r",
|
|
342
|
+
encoding="utf-8") as handle:
|
|
343
|
+
return handle.read(64).strip() == iteration
|
|
344
|
+
except OSError:
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
|
|
322
348
|
def _collect_quality_gates(loki_dir):
|
|
323
349
|
gates_raw = _read_json(
|
|
324
350
|
os.path.join(loki_dir, "state", "quality-gates.json"), default=None
|
|
@@ -351,6 +377,8 @@ def _collect_quality_gates(loki_dir):
|
|
|
351
377
|
# never an LLM opinion. Only fills gates the aggregate did not already cover.
|
|
352
378
|
if not gates:
|
|
353
379
|
quality_dir = os.path.join(loki_dir, "quality")
|
|
380
|
+
tests_json = os.path.join(quality_dir, "test-results.json")
|
|
381
|
+
tests_fresh = _test_results_fresh(quality_dir)
|
|
354
382
|
seen = set()
|
|
355
383
|
# (gate name in proof, marker/result filename stem under quality/)
|
|
356
384
|
markers = [
|
|
@@ -361,7 +389,12 @@ def _collect_quality_gates(loki_dir):
|
|
|
361
389
|
pass_marker = os.path.join(quality_dir, stem + ".pass")
|
|
362
390
|
result_json = os.path.join(quality_dir, stem + ".json")
|
|
363
391
|
status = None
|
|
364
|
-
if
|
|
392
|
+
if gate_name == "unit_tests" and not tests_fresh:
|
|
393
|
+
# BACKLOG 82: a test result another iteration wrote is not
|
|
394
|
+
# this iteration's evidence, pass or fail. Name the gap.
|
|
395
|
+
if any(os.path.exists(p) for p in (pass_marker, result_json, tests_json)):
|
|
396
|
+
status = "not_run"
|
|
397
|
+
elif os.path.exists(pass_marker):
|
|
365
398
|
status = "passed"
|
|
366
399
|
elif os.path.exists(result_json):
|
|
367
400
|
rj = _read_json(result_json, default=None)
|
|
@@ -390,10 +423,8 @@ def _collect_quality_gates(loki_dir):
|
|
|
390
423
|
passed += 1
|
|
391
424
|
# test-results.json carries the suite outcome even when no .pass marker
|
|
392
425
|
# (e.g. {status:"verified"} = tests ran and passed).
|
|
393
|
-
if "unit_tests" not in seen:
|
|
394
|
-
tr = _read_json(
|
|
395
|
-
os.path.join(quality_dir, "test-results.json"), default=None
|
|
396
|
-
)
|
|
426
|
+
if "unit_tests" not in seen and tests_fresh:
|
|
427
|
+
tr = _read_json(tests_json, default=None)
|
|
397
428
|
if isinstance(tr, dict):
|
|
398
429
|
st = str(tr.get("status") or "")
|
|
399
430
|
if st in ("verified", "pass", "passed"):
|
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
|
) &
|
|
@@ -9091,8 +9101,10 @@ setup_agent_branch() {
|
|
|
9091
9101
|
[ -f .loki/.gitignore ] || printf '*\n' > .loki/.gitignore 2>/dev/null || true
|
|
9092
9102
|
|
|
9093
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.
|
|
9094
9106
|
local cur=""
|
|
9095
|
-
cur="$(git
|
|
9107
|
+
cur="$(git symbolic-ref --short -q HEAD 2>/dev/null || echo HEAD)"
|
|
9096
9108
|
|
|
9097
9109
|
# Detached HEAD: do NOT branch, do NOT fabricate a base (LOCK A2/A6).
|
|
9098
9110
|
if [ "$cur" = "HEAD" ]; then
|
|
@@ -9135,15 +9147,13 @@ setup_agent_branch() {
|
|
|
9135
9147
|
fi
|
|
9136
9148
|
fi
|
|
9137
9149
|
|
|
9138
|
-
# Fresh run:
|
|
9139
|
-
#
|
|
9150
|
+
# Fresh run: mint and check out the feature branch, then persist its base
|
|
9151
|
+
# (fresh-run-only, LOCK A2).
|
|
9140
9152
|
local timestamp
|
|
9141
9153
|
timestamp=$(date +%s)
|
|
9142
9154
|
local branch_name="loki/session-${timestamp}-$$"
|
|
9143
9155
|
|
|
9144
9156
|
mkdir -p .loki/state 2>/dev/null || true
|
|
9145
|
-
# Persist the base only once per run tree; never overwrite an existing base.
|
|
9146
|
-
[ ! -s .loki/state/base-branch.txt ] && printf '%s\n' "$cur" > .loki/state/base-branch.txt 2>/dev/null
|
|
9147
9157
|
|
|
9148
9158
|
log_info "Branch protection enabled - creating agent branch: $branch_name (base: $cur)"
|
|
9149
9159
|
|
|
@@ -9172,6 +9182,15 @@ setup_agent_branch() {
|
|
|
9172
9182
|
rm -f .loki/state/session-created.z 2>/dev/null
|
|
9173
9183
|
if [ "$snap_ok" = 1 ]; then _LOKI_SNAPSHOT_THIS_RUN=1; fi
|
|
9174
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
|
+
|
|
9175
9194
|
# Store the branch name for later use (PR creation, cleanup)
|
|
9176
9195
|
printf '%s\n' "$branch_name" > .loki/state/agent-branch.txt 2>/dev/null
|
|
9177
9196
|
|
|
@@ -9367,6 +9386,101 @@ fi
|
|
|
9367
9386
|
if ! type _commit_scan_secret_file >/dev/null 2>&1; then _commit_scan_secret_file() { return 1; }; fi
|
|
9368
9387
|
if ! type _commit_path_looks_secret >/dev/null 2>&1; then _commit_path_looks_secret() { return 1; }; fi
|
|
9369
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
|
+
|
|
9370
9484
|
commit_session_changes() {
|
|
9371
9485
|
# Squash the session's work into one honest session-end commit on the agent
|
|
9372
9486
|
# branch (LOCK A3/A4/A8). Commit-always (incl. failed runs) so the user is
|
|
@@ -9401,6 +9515,25 @@ commit_session_changes() {
|
|
|
9401
9515
|
;;
|
|
9402
9516
|
esac
|
|
9403
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
|
+
|
|
9404
9537
|
# Stage everything except .loki/ runtime state and a secret-path denylist.
|
|
9405
9538
|
# These excludes are defense-in-depth ONLY for the top-level cases git
|
|
9406
9539
|
# pathspec handles cleanly. We deliberately do NOT add nested globs like
|
|
@@ -9426,12 +9559,8 @@ commit_session_changes() {
|
|
|
9426
9559
|
# Agent-created files stay staged. Skip an empty snapshot: an
|
|
9427
9560
|
# empty --pathspec-from-file resets the WHOLE index. No snapshot (older
|
|
9428
9561
|
# session): behave as before. If the unstage fails (git < 2.25), commit
|
|
9429
|
-
# nothing rather than sweep the user's files in.
|
|
9430
|
-
|
|
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
|
|
9562
|
+
# nothing rather than sweep the user's files in. A failed snapshot never
|
|
9563
|
+
# gets here (the snapshot guard above returns first).
|
|
9435
9564
|
local preexisting="$PWD/.loki/state/preexisting-untracked.z" top=""
|
|
9436
9565
|
if [ -s "$preexisting" ]; then
|
|
9437
9566
|
if ! { top="$(git rev-parse --show-toplevel 2>/dev/null)" \
|
|
@@ -12089,6 +12218,12 @@ ZT_MATCHES_EOF
|
|
|
12089
12218
|
}
|
|
12090
12219
|
|
|
12091
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
|
|
12092
12227
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
12093
12228
|
local quality_dir="$loki_dir/quality"
|
|
12094
12229
|
mkdir -p "$quality_dir" "$loki_dir/signals"
|
|
@@ -12118,7 +12253,9 @@ enforce_test_coverage() {
|
|
|
12118
12253
|
# that is the legitimate case they were written for (a package that ships
|
|
12119
12254
|
# a runner but no npm script).
|
|
12120
12255
|
local _declared_test_script=""
|
|
12121
|
-
|
|
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 ('', '.')]
|
|
12122
12259
|
import json, os, sys
|
|
12123
12260
|
try:
|
|
12124
12261
|
with open(os.environ['_LOKI_PKG']) as f:
|
|
@@ -12238,7 +12375,8 @@ sys.stdout.write(t.strip())
|
|
|
12238
12375
|
# npm test), so the grep only ever picked the LABEL, never
|
|
12239
12376
|
# what ran. This corrects the evidence, not the execution.
|
|
12240
12377
|
local _ws_script
|
|
12241
|
-
|
|
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 ('', '.')]
|
|
12242
12380
|
import json, os, sys
|
|
12243
12381
|
try:
|
|
12244
12382
|
with open(os.environ['_LOKI_PKG']) as f:
|
|
@@ -12558,6 +12696,9 @@ TREOF
|
|
|
12558
12696
|
# run instead of re-running the suite. Single source of truth for "tests
|
|
12559
12697
|
# ran this iteration", set on every return path that writes results.
|
|
12560
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
|
|
12561
12702
|
return 0
|
|
12562
12703
|
fi
|
|
12563
12704
|
|
|
@@ -12569,6 +12710,49 @@ TREOF
|
|
|
12569
12710
|
# first, then delete every remaining control char (\000-\037, incl. ESC).
|
|
12570
12711
|
details=$(echo "$details" | tr '"' "'" | tr '\n' ' ' | tr -d '\000-\037' | head -c 500)
|
|
12571
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
|
+
|
|
12572
12756
|
# Evidence Receipt provenance (v7.85.0): record the deterministic FACTS a
|
|
12573
12757
|
# non-forgeable receipt needs -- the command that ran, its exit code, and a
|
|
12574
12758
|
# status enum -- alongside the legacy pass/runner/min_coverage keys the
|
|
@@ -12586,6 +12770,8 @@ TREOF
|
|
|
12586
12770
|
*) _tr_cmd="$test_runner" ;;
|
|
12587
12771
|
esac
|
|
12588
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
|
|
12589
12775
|
|
|
12590
12776
|
# #82 (zero-test-file hardening): a runner that EXITED 0 but executed ZERO
|
|
12591
12777
|
# real tests is a mini fake-green -- it records "verified" while proving
|
|
@@ -12607,17 +12793,6 @@ TREOF
|
|
|
12607
12793
|
log_warn "Verification gap: $test_runner exited 0 but ran ZERO tests -- recording inconclusive (no_tests_run), not verified"
|
|
12608
12794
|
fi
|
|
12609
12795
|
|
|
12610
|
-
# Best-effort pass/fail counts from the summary text (null when not found).
|
|
12611
|
-
local _tr_passed_n _tr_failed_n
|
|
12612
|
-
_tr_passed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+' | head -1)
|
|
12613
|
-
_tr_failed_n=$(printf '%s' "$details" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+' | head -1)
|
|
12614
|
-
# node --test emits TAP-ish "# pass N" / "# fail N" summary lines, which the
|
|
12615
|
-
# "N passed"/"N failed" pattern above does not match. Fall back to those.
|
|
12616
|
-
[ -n "$_tr_passed_n" ] || _tr_passed_n=$(printf '%s' "$details" | grep -oE '# pass [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12617
|
-
[ -n "$_tr_failed_n" ] || _tr_failed_n=$(printf '%s' "$details" | grep -oE '# fail [0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
12618
|
-
[ -n "$_tr_passed_n" ] || _tr_passed_n=null
|
|
12619
|
-
[ -n "$_tr_failed_n" ] || _tr_failed_n=null
|
|
12620
|
-
|
|
12621
12796
|
# verification_gap is "none" whenever a real runner executed AND ran tests:
|
|
12622
12797
|
# the suite ran, so there is no docs-without-execution / source-without-tests
|
|
12623
12798
|
# gap. The #82 zero-test case is the exception -- a runner ran but executed
|
|
@@ -12787,9 +12962,11 @@ os.replace(tmp, out)
|
|
|
12787
12962
|
if [ "$_tr_zero_tests" = "true" ]; then
|
|
12788
12963
|
rm -f "$quality_dir/unit-tests.pass" 2>/dev/null || true
|
|
12789
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
|
|
12790
12966
|
else
|
|
12791
12967
|
touch "$quality_dir/unit-tests.pass"
|
|
12792
12968
|
log_info "Test suite gate: $test_runner passed"
|
|
12969
|
+
_LOKI_TEST_SUITE_STATUS=pass
|
|
12793
12970
|
fi
|
|
12794
12971
|
# Coverage block is distinct from tests-red: tests passed, but enforced
|
|
12795
12972
|
# coverage is below threshold. Return nonzero to gate WITHOUT writing the
|
|
@@ -24587,8 +24764,12 @@ EOF
|
|
|
24587
24764
|
local _stg_t0=$(date +%s 2>/dev/null); local _stg_ok=pass
|
|
24588
24765
|
# F49: isolate HOME so the project's suite cannot pollute the
|
|
24589
24766
|
# user's real home when it execs the generated app.
|
|
24767
|
+
_LOKI_TEST_SUITE_STATUS=""
|
|
24590
24768
|
if _loki_with_app_sandbox enforce_test_coverage; then
|
|
24591
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
|
|
24592
24773
|
else
|
|
24593
24774
|
_stg_ok=fail
|
|
24594
24775
|
local tc_count
|
|
@@ -25688,11 +25869,33 @@ kill_provider_child() {
|
|
|
25688
25869
|
fi
|
|
25689
25870
|
kill -TERM "$child_pid" 2>/dev/null && killed=1
|
|
25690
25871
|
done
|
|
25691
|
-
# Also kill provider leaf processes
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
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
|
|
25696
25899
|
|
|
25697
25900
|
# Brief wait for graceful exit (max ~2s).
|
|
25698
25901
|
local i=0
|
package/dashboard/__init__.py
CHANGED
package/dashboard/api_v2.py
CHANGED
|
@@ -894,6 +894,10 @@ async def verify_audit_integrity():
|
|
|
894
894
|
|
|
895
895
|
return {
|
|
896
896
|
"valid": aggregate["valid"],
|
|
897
|
+
# verified/status: whether anything was checked (valid stays True for
|
|
898
|
+
# an empty chain); see audit.verify_all_logs_in_dir.
|
|
899
|
+
"verified": aggregate["verified"],
|
|
900
|
+
"status": aggregate["status"],
|
|
897
901
|
"files_checked": aggregate["files_checked"],
|
|
898
902
|
"files_skipped": aggregate.get("files_skipped", 0),
|
|
899
903
|
"entries_checked": aggregate.get("entries_checked", 0),
|
package/dashboard/audit.py
CHANGED
|
@@ -657,7 +657,8 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
657
657
|
if not audit_dir.exists():
|
|
658
658
|
return {"valid": True, "files_checked": 0, "files_skipped": 0,
|
|
659
659
|
"entries_checked": 0, "first_tampered_file": None,
|
|
660
|
-
"first_tampered_line": None, "genesis_file": None
|
|
660
|
+
"first_tampered_line": None, "genesis_file": None,
|
|
661
|
+
"verified": False, "status": "nothing_checked"}
|
|
661
662
|
# v7.7.15 council fix (Opus 2): rotated files have name shape
|
|
662
663
|
# `audit-YYYY-MM-DD.HHMMSS.jsonl` (from `_rotate_logs_if_needed` at
|
|
663
664
|
# line 167). Lexicographic sort puts `audit-2026-05-04.123456.jsonl`
|
|
@@ -689,8 +690,13 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
689
690
|
"first_tampered_file": str(log_file),
|
|
690
691
|
"first_tampered_line": result.get("first_tampered_line"),
|
|
691
692
|
"genesis_file": genesis_file,
|
|
693
|
+
"verified": False,
|
|
694
|
+
"status": "tampered",
|
|
692
695
|
}
|
|
693
696
|
prev_hash = result.get("last_hash", prev_hash)
|
|
697
|
+
# `valid` means "no break found" and stays True for an empty chain (the
|
|
698
|
+
# cross-chain verifier reads it). `verified` and `status` say whether
|
|
699
|
+
# anything was actually checked: zero files checked is not a verified chain.
|
|
694
700
|
return {
|
|
695
701
|
"valid": True,
|
|
696
702
|
"files_checked": files_checked,
|
|
@@ -699,6 +705,8 @@ def verify_all_logs_in_dir(audit_dir) -> dict:
|
|
|
699
705
|
"first_tampered_file": None,
|
|
700
706
|
"first_tampered_line": None,
|
|
701
707
|
"genesis_file": genesis_file,
|
|
708
|
+
"verified": files_checked > 0,
|
|
709
|
+
"status": "verified" if files_checked > 0 else "nothing_checked",
|
|
702
710
|
}
|
|
703
711
|
|
|
704
712
|
|
package/dashboard/auth.py
CHANGED
|
@@ -343,6 +343,18 @@ def list_tokens(include_revoked: bool = False) -> list[dict]:
|
|
|
343
343
|
return result
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
def _deadline_passed(value) -> bool:
|
|
347
|
+
"""True when an ISO-8601 deadline is in the past, or cannot be read.
|
|
348
|
+
|
|
349
|
+
Fails closed: an unparseable value, or a naive timestamp that cannot be
|
|
350
|
+
ordered against an aware "now", is treated as passed.
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
return datetime.now(timezone.utc) > datetime.fromisoformat(str(value))
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
|
|
346
358
|
def validate_token(raw_token: str) -> Optional[dict]:
|
|
347
359
|
"""
|
|
348
360
|
Validate a raw token.
|
|
@@ -372,10 +384,10 @@ def validate_token(raw_token: str) -> Optional[dict]:
|
|
|
372
384
|
if matched_token.get("revoked"):
|
|
373
385
|
return None
|
|
374
386
|
|
|
375
|
-
# Check expiration
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if
|
|
387
|
+
# Check expiration, and the grace period of a rotated-out key
|
|
388
|
+
# (api_keys.rotate_key sets rotation_expires_at on the old key).
|
|
389
|
+
for field in ("expires_at", "rotation_expires_at"):
|
|
390
|
+
if matched_token.get(field) and _deadline_passed(matched_token[field]):
|
|
379
391
|
return None
|
|
380
392
|
|
|
381
393
|
# Update last used
|