loki-mode 9.17.0 → 9.18.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/issue-providers.sh +0 -24
- package/autonomy/lib/agent_readiness.py +1 -79
- package/autonomy/lib/outcome_ledger.py +0 -122
- package/autonomy/lib/proof-generator.py +4 -71
- package/autonomy/lib/verdict.py +4 -25
- package/autonomy/loki +444 -264
- package/autonomy/notify.sh +1 -70
- package/autonomy/queue-consumer.sh +18 -290
- package/autonomy/run.sh +280 -189
- package/autonomy/trigger-server.py +518 -17
- package/autonomy/verify.sh +100 -12
- package/bin/loki +7 -1
- package/completions/_loki +0 -3
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +1 -168
- package/dashboard/static/index.html +222 -249
- package/docs/COMPETITIVE-SCORECARD.md +38 -0
- package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +475 -0
- package/docs/DEPLOYMENT.md +542 -0
- package/docs/STALE-STATE-AUDIT.md +174 -0
- package/docs/VERIFICATION-COST.md +46 -166
- package/loki-ts/dist/loki.js +311 -305
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +0 -25
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/autonomy/lib/gate_policy.py +0 -166
- package/docs/QUEUE-OPERATIONS.md +0 -107
package/autonomy/run.sh
CHANGED
|
@@ -1519,6 +1519,22 @@ PYREG
|
|
|
1519
1519
|
# if any other project is still running (KEEP) it is left up. NEVER uses a
|
|
1520
1520
|
# blanket pkill and NEVER touches another folder's pids. Best-effort and
|
|
1521
1521
|
# failure-swallowed: teardown bookkeeping must never block a clean exit.
|
|
1522
|
+
# Is this pid plausibly OUR dashboard, or a recycled number now naming something
|
|
1523
|
+
# else? Fails OPEN (returns 0) whenever ps cannot tell us, so the only behavior
|
|
1524
|
+
# change is refusing to kill a process that is positively identified as NOT a
|
|
1525
|
+
# dashboard. See the call site for the measured stale-file evidence.
|
|
1526
|
+
_loki_pid_looks_like_dashboard() {
|
|
1527
|
+
local _p="$1" _cmd
|
|
1528
|
+
case "$_p" in ''|*[!0-9]*) return 1 ;; esac
|
|
1529
|
+
[ "$_p" -gt 1 ] 2>/dev/null || return 1 # never signal pid 0/1
|
|
1530
|
+
_cmd="$(ps -o command= -p "$_p" 2>/dev/null)"
|
|
1531
|
+
[ -n "$_cmd" ] || return 0 # ps unavailable: behave as before
|
|
1532
|
+
case "$_cmd" in
|
|
1533
|
+
*dashboard*|*uvicorn*|*loki*) return 0 ;;
|
|
1534
|
+
esac
|
|
1535
|
+
return 1
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1522
1538
|
loki_mark_project_stopped_and_maybe_kill_shared_dashboard() {
|
|
1523
1539
|
local _skill="${LOKI_SKILL_DIR:-${PROJECT_DIR:-$SCRIPT_DIR/..}}"
|
|
1524
1540
|
local _shared_pidfile="${HOME}/.loki/dashboard/dashboard.pid"
|
|
@@ -1566,7 +1582,19 @@ PYCHECK
|
|
|
1566
1582
|
if [ -f "$_shared_pidfile" ]; then
|
|
1567
1583
|
local _shared_pid
|
|
1568
1584
|
_shared_pid=$(cat "$_shared_pidfile" 2>/dev/null)
|
|
1569
|
-
|
|
1585
|
+
# Identity check before signalling. dashboard.pid is removed only on
|
|
1586
|
+
# the explicit stop paths (:16608, :16658, :17399) -- nothing covers
|
|
1587
|
+
# a crash, so the file outlives its dashboard. Measured in this very
|
|
1588
|
+
# checkout: .loki/dashboard/dashboard.pid held a DEAD 87992 with an
|
|
1589
|
+
# 8-day-old mtime. PIDs recycle, so a stale number eventually names
|
|
1590
|
+
# an unrelated LIVE process and this `kill -9` hits it.
|
|
1591
|
+
#
|
|
1592
|
+
# kill -0 is NOT sufficient: a recycled pid is alive by definition,
|
|
1593
|
+
# which is exactly the insufficiency the loki.pgid self-check had.
|
|
1594
|
+
# Mirrors _app_runner_pid_is_ours (app-runner.sh:242) and fails OPEN
|
|
1595
|
+
# the same way -- if `ps` says nothing we signal as before, so a
|
|
1596
|
+
# legitimate dashboard is never left running by this check.
|
|
1597
|
+
if [ -n "$_shared_pid" ] && _loki_pid_looks_like_dashboard "$_shared_pid"; then
|
|
1570
1598
|
kill "$_shared_pid" 2>/dev/null || true
|
|
1571
1599
|
sleep 0.5
|
|
1572
1600
|
kill -9 "$_shared_pid" 2>/dev/null || true
|
|
@@ -2049,40 +2077,7 @@ except Exception:
|
|
|
2049
2077
|
pass
|
|
2050
2078
|
" 2>/dev/null || true)"
|
|
2051
2079
|
fi
|
|
2052
|
-
|
|
2053
|
-
_fields="${_fields} | \$${_cost}"
|
|
2054
|
-
# PROJECTED spend at the iteration cap, shown beside the actual.
|
|
2055
|
-
#
|
|
2056
|
-
# WHY. MAX_ITERATIONS (default 25) is the ONLY backstop on a run's cost:
|
|
2057
|
-
# LOKI_BUDGET_LIMIT defaults to "" and LOKI_MAX_DURATION to 0, both
|
|
2058
|
-
# documented at run.sh:1118-1121, and the runtime says so out loud at
|
|
2059
|
-
# :21333. That is a defensible default -- a run killed mid-flight at a
|
|
2060
|
-
# dollar threshold the user never chose is worse than one that finishes.
|
|
2061
|
-
# But it left the user unable to SEE where the run was heading: $4.20 at
|
|
2062
|
-
# iteration 3 of 25 reads as cheap right up to the moment it is not.
|
|
2063
|
-
#
|
|
2064
|
-
# Linear extrapolation, and labelled "proj" rather than presented as a
|
|
2065
|
-
# forecast: later iterations are usually cheaper than early ones (more
|
|
2066
|
-
# cache hits, smaller diffs), so this is an upper bound, not a promise.
|
|
2067
|
-
# Shown only when it would actually tell the user something -- from
|
|
2068
|
-
# iteration 2 (one data point cannot extrapolate) and only when the
|
|
2069
|
-
# projection is meaningfully above what has already been spent.
|
|
2070
|
-
if [ "${_iter:-0}" -ge 2 ] && [ "${_max:-0}" -gt 0 ]; then
|
|
2071
|
-
local _proj
|
|
2072
|
-
_proj="$(LOKI_C="$_cost" LOKI_I="$_iter" LOKI_M="$_max" python3 -c "
|
|
2073
|
-
import os
|
|
2074
|
-
try:
|
|
2075
|
-
c = float(os.environ['LOKI_C']); i = int(os.environ['LOKI_I']); m = int(os.environ['LOKI_M'])
|
|
2076
|
-
if i > 0 and m > i:
|
|
2077
|
-
p = c / i * m
|
|
2078
|
-
if p >= c * 1.5:
|
|
2079
|
-
print('%.2f' % p)
|
|
2080
|
-
except Exception:
|
|
2081
|
-
pass
|
|
2082
|
-
" 2>/dev/null || true)"
|
|
2083
|
-
[ -n "$_proj" ] && _fields="${_fields} (proj \$${_proj} at ${_max})"
|
|
2084
|
-
fi
|
|
2085
|
-
fi
|
|
2080
|
+
[ -n "$_cost" ] && _fields="${_fields} | \$${_cost}"
|
|
2086
2081
|
|
|
2087
2082
|
# Files changed (+ins/-del and file count) vs the run start SHA. Reuse the
|
|
2088
2083
|
# build_completion_summary diff approach incl. the .loki/.git exclude pathspec.
|
|
@@ -3349,34 +3344,10 @@ detect_complexity() {
|
|
|
3349
3344
|
file_count="${file_count:-0}"
|
|
3350
3345
|
file_count="${file_count//[^0-9]/}"
|
|
3351
3346
|
|
|
3352
|
-
# Check for external integrations
|
|
3353
|
-
#
|
|
3354
|
-
# THE EXCLUDES ARE LOAD-BEARING. This grep used to prune nothing while the
|
|
3355
|
-
# find eleven lines above it prunes node_modules/.git/vendor/dist/build --
|
|
3356
|
-
# same function, same intent, inconsistent implementation. With --include
|
|
3357
|
-
# "*.json" that meant ANY transitive dependency whose package.json mentions
|
|
3358
|
-
# azure, stripe or aws-sdk set has_external=true.
|
|
3359
|
-
#
|
|
3360
|
-
# And has_external does not merely block "simple": in the classifier below it
|
|
3361
|
-
# jumps straight to "complex", skipping "standard". So a one-liner in any
|
|
3362
|
-
# repo that has ever run npm install landed on the MOST expensive tier, which
|
|
3363
|
-
# then runs the architecture doc suite (up to 300s of silence per attempt)
|
|
3364
|
-
# and holds the council's forced minimum-iteration floor at 3 instead of 1.
|
|
3365
|
-
#
|
|
3366
|
-
# Reproduced from scratch before fixing: a project with ONE dependency naming
|
|
3367
|
-
# @azure/core classified complex; adding --exclude-dir=node_modules made the
|
|
3368
|
-
# identical project classify simple. It also fired TRUE on this repo.
|
|
3369
|
-
#
|
|
3370
|
-
# A prior incident matches exactly -- a coffee landing page took 1h34m over
|
|
3371
|
-
# 11 iterations because the simple fast-path never engaged. The fast path was
|
|
3372
|
-
# correctly built and correctly wired the whole time; this one missing prune
|
|
3373
|
-
# was what made it unreachable.
|
|
3347
|
+
# Check for external integrations
|
|
3374
3348
|
local has_external=false
|
|
3375
3349
|
if grep -rq "oauth\|SAML\|OIDC\|stripe\|twilio\|aws-sdk\|@google-cloud\|azure" \
|
|
3376
|
-
"$target_dir" --include="*.json" --include="*.ts" --include="*.js"
|
|
3377
|
-
--exclude-dir=node_modules --exclude-dir=.git --exclude-dir=vendor \
|
|
3378
|
-
--exclude-dir=dist --exclude-dir=build --exclude-dir=__pycache__ \
|
|
3379
|
-
--exclude-dir=.venv --exclude-dir=venv 2>/dev/null; then
|
|
3350
|
+
"$target_dir" --include="*.json" --include="*.ts" --include="*.js" 2>/dev/null; then
|
|
3380
3351
|
has_external=true
|
|
3381
3352
|
fi
|
|
3382
3353
|
|
|
@@ -11399,7 +11370,58 @@ enforce_test_coverage() {
|
|
|
11399
11370
|
if [ -f "${TARGET_DIR:-.}/package.json" ]; then
|
|
11400
11371
|
# BUG-EC-014: Wrap test runners with timeout to prevent hanging indefinitely
|
|
11401
11372
|
local gate_timeout="${LOKI_GATE_TIMEOUT:-300}" # 5 minutes default
|
|
11402
|
-
|
|
11373
|
+
# A DECLARED scripts.test wins over an installed package.
|
|
11374
|
+
#
|
|
11375
|
+
# The v7.41.x fix below already established that grep false-positives on
|
|
11376
|
+
# devDependencies -- read its comment -- but it only guarded the `else`
|
|
11377
|
+
# branch, so the three grep branches AHEAD of it still shadowed it. This
|
|
11378
|
+
# repo is the proof: jest is a devDependency with no jest config while
|
|
11379
|
+
# scripts.test runs `bash -n` + `node --test`, and the grep branch
|
|
11380
|
+
# hijacked the runner, ran jest over 895 non-jest files and failed every
|
|
11381
|
+
# one of them.
|
|
11382
|
+
#
|
|
11383
|
+
# _has_declared_test_script is true only when the project DECLARES a real
|
|
11384
|
+
# test script, which is the project's own statement of its runner. When
|
|
11385
|
+
# it declares nothing, the grep branches still apply exactly as before --
|
|
11386
|
+
# that is the legitimate case they were written for (a package that ships
|
|
11387
|
+
# a runner but no npm script).
|
|
11388
|
+
local _declared_test_script=""
|
|
11389
|
+
_declared_test_script=$(_LOKI_PKG="${TARGET_DIR:-.}/package.json" python3 -c "
|
|
11390
|
+
import json, os, sys
|
|
11391
|
+
try:
|
|
11392
|
+
with open(os.environ['_LOKI_PKG']) as f:
|
|
11393
|
+
d = json.load(f)
|
|
11394
|
+
except Exception:
|
|
11395
|
+
sys.exit(0)
|
|
11396
|
+
if not isinstance(d, dict):
|
|
11397
|
+
sys.exit(0)
|
|
11398
|
+
t = (d.get('scripts') or {}).get('test') or ''
|
|
11399
|
+
if 'no test specified' in t.lower():
|
|
11400
|
+
sys.exit(0)
|
|
11401
|
+
sys.stdout.write(t.strip())
|
|
11402
|
+
" 2>/dev/null || echo "")
|
|
11403
|
+
if [ -n "$_declared_test_script" ]; then
|
|
11404
|
+
# Run the DECLARED script. This body used to live in the trailing
|
|
11405
|
+
# `else`; a no-op `:` here would have terminated the if/elif chain
|
|
11406
|
+
# and left test_runner=none, turning a false BLOCK into a silently
|
|
11407
|
+
# unmeasured gate -- strictly worse than the bug being fixed.
|
|
11408
|
+
#
|
|
11409
|
+
# LOKI_TEST_COMMAND lets an operator override the invocation; the
|
|
11410
|
+
# default is the project's own `npm test`.
|
|
11411
|
+
local _test_cmd="${LOKI_TEST_COMMAND:-npm test}"
|
|
11412
|
+
# Label the runner by what the script invokes so evidence is honest
|
|
11413
|
+
# (node --test, vitest, jest, etc. all surface here).
|
|
11414
|
+
case "$_declared_test_script" in
|
|
11415
|
+
*"node --test"*|*"node:test"*) test_runner="node-test" ;;
|
|
11416
|
+
*vitest*) test_runner="vitest" ;;
|
|
11417
|
+
*jest*) test_runner="jest" ;;
|
|
11418
|
+
*mocha*) test_runner="mocha" ;;
|
|
11419
|
+
*) test_runner="npm-test" ;;
|
|
11420
|
+
esac
|
|
11421
|
+
local output
|
|
11422
|
+
output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" sh -c "$_test_cmd" 2>&1) || test_passed=false
|
|
11423
|
+
details="$test_runner ($_test_cmd): $(echo "$output" | tail -5 | tr '\n' ' ')"
|
|
11424
|
+
elif grep -q '"vitest"' "${TARGET_DIR:-.}/package.json" 2>/dev/null; then
|
|
11403
11425
|
test_runner="vitest"
|
|
11404
11426
|
local output
|
|
11405
11427
|
output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" npx vitest run --reporter=json 2>&1) || test_passed=false
|
|
@@ -11425,37 +11447,13 @@ enforce_test_coverage() {
|
|
|
11425
11447
|
# would false-positive on devDeps / unrelated keys), then run the
|
|
11426
11448
|
# configured command. This MUST sit before the monorepo/python/go/rust
|
|
11427
11449
|
# checks, all of which gate on test_runner=="none".
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
sys.exit(0)
|
|
11436
|
-
t = (d.get('scripts') or {}).get('test') or ''
|
|
11437
|
-
# npm's default placeholder; treat as 'no test'.
|
|
11438
|
-
if 'no test specified' in t.lower():
|
|
11439
|
-
sys.exit(0)
|
|
11440
|
-
sys.stdout.write(t.strip())
|
|
11441
|
-
" 2>/dev/null || echo "")
|
|
11442
|
-
if [ -n "$_pkg_test_script" ]; then
|
|
11443
|
-
# LOKI_TEST_COMMAND lets an operator override the invocation; the
|
|
11444
|
-
# default is the project's own `npm test`.
|
|
11445
|
-
local _test_cmd="${LOKI_TEST_COMMAND:-npm test}"
|
|
11446
|
-
# Label the runner by what the script invokes so evidence is
|
|
11447
|
-
# honest (node --test, vitest, jest, etc. all surface here).
|
|
11448
|
-
case "$_pkg_test_script" in
|
|
11449
|
-
*"node --test"*|*"node:test"*) test_runner="node-test" ;;
|
|
11450
|
-
*vitest*) test_runner="vitest" ;;
|
|
11451
|
-
*jest*) test_runner="jest" ;;
|
|
11452
|
-
*mocha*) test_runner="mocha" ;;
|
|
11453
|
-
*) test_runner="npm-test" ;;
|
|
11454
|
-
esac
|
|
11455
|
-
local output
|
|
11456
|
-
output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" sh -c "$_test_cmd" 2>&1) || test_passed=false
|
|
11457
|
-
details="$test_runner ($_test_cmd): $(echo "$output" | tail -5 | tr '\n' ' ')"
|
|
11458
|
-
fi
|
|
11450
|
+
#
|
|
11451
|
+
# That handler now runs in the FIRST branch above, because a declared
|
|
11452
|
+
# script must win over an installed devDependency. Nothing is left to
|
|
11453
|
+
# do here: reaching this point means the project declares no test
|
|
11454
|
+
# script AND ships no recognised runner, so test_runner stays "none"
|
|
11455
|
+
# and the monorepo/python/go/rust detection below takes over.
|
|
11456
|
+
:
|
|
11459
11457
|
fi
|
|
11460
11458
|
fi
|
|
11461
11459
|
|
|
@@ -11497,15 +11495,61 @@ sys.stdout.write(t.strip())
|
|
|
11497
11495
|
"${TARGET_DIR:-.}"/apps/*/package.json \
|
|
11498
11496
|
"${TARGET_DIR:-.}"/services/*/package.json; do
|
|
11499
11497
|
[ -f "$pkg_json" ] || continue
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11498
|
+
# Read the workspace's DECLARED test script, same rule as the
|
|
11499
|
+
# single-package path above. A bare grep for '"jest"' matches
|
|
11500
|
+
# a devDependency, so a workspace that merely depends on jest
|
|
11501
|
+
# got labelled monorepo-jest.
|
|
11502
|
+
#
|
|
11503
|
+
# LOWER SEVERITY than the single-package case, and worth being
|
|
11504
|
+
# precise about why: all three branches below dispatch the
|
|
11505
|
+
# project's own script (turbo test / pnpm test --recursive /
|
|
11506
|
+
# npm test), so the grep only ever picked the LABEL, never
|
|
11507
|
+
# what ran. This corrects the evidence, not the execution.
|
|
11508
|
+
local _ws_script
|
|
11509
|
+
_ws_script=$(_LOKI_PKG="$pkg_json" python3 -c "
|
|
11510
|
+
import json, os, sys
|
|
11511
|
+
try:
|
|
11512
|
+
with open(os.environ['_LOKI_PKG']) as f:
|
|
11513
|
+
d = json.load(f)
|
|
11514
|
+
except Exception:
|
|
11515
|
+
sys.exit(0)
|
|
11516
|
+
if not isinstance(d, dict):
|
|
11517
|
+
sys.exit(0)
|
|
11518
|
+
t = (d.get('scripts') or {}).get('test') or ''
|
|
11519
|
+
if 'no test specified' in t.lower():
|
|
11520
|
+
sys.exit(0)
|
|
11521
|
+
sys.stdout.write(t.strip())
|
|
11522
|
+
" 2>/dev/null || echo "")
|
|
11523
|
+
case "$_ws_script" in
|
|
11524
|
+
*vitest*) workspace_runner="vitest"; break ;;
|
|
11525
|
+
*jest*) workspace_runner="jest"; break ;;
|
|
11526
|
+
*mocha*) workspace_runner="mocha"; break ;;
|
|
11527
|
+
"") : ;;
|
|
11528
|
+
*) workspace_runner="npm-test"; break ;;
|
|
11529
|
+
esac
|
|
11507
11530
|
done
|
|
11508
11531
|
|
|
11532
|
+
# FAIL-SAFE. If no workspace DECLARES a test script but one has a
|
|
11533
|
+
# runner installed, still run the monorepo suite -- labelled
|
|
11534
|
+
# "installed" so the evidence does not claim a declaration that
|
|
11535
|
+
# does not exist.
|
|
11536
|
+
#
|
|
11537
|
+
# Without this the change would trade a wrong label for a SKIPPED
|
|
11538
|
+
# GATE, which is the dangerous direction: the old grep at least
|
|
11539
|
+
# dispatched `npm test`. Reporting a runner inaccurately is a
|
|
11540
|
+
# documentation bug; silently not testing a monorepo is not.
|
|
11541
|
+
if [ -z "$workspace_runner" ]; then
|
|
11542
|
+
for pkg_json in "${TARGET_DIR:-.}"/packages/*/package.json \
|
|
11543
|
+
"${TARGET_DIR:-.}"/apps/*/package.json \
|
|
11544
|
+
"${TARGET_DIR:-.}"/services/*/package.json; do
|
|
11545
|
+
[ -f "$pkg_json" ] || continue
|
|
11546
|
+
if grep -qE '"(vitest|jest|mocha)"' "$pkg_json" 2>/dev/null; then
|
|
11547
|
+
workspace_runner="installed"
|
|
11548
|
+
break
|
|
11549
|
+
fi
|
|
11550
|
+
done
|
|
11551
|
+
fi
|
|
11552
|
+
|
|
11509
11553
|
if [ -n "$workspace_runner" ]; then
|
|
11510
11554
|
test_runner="monorepo-$workspace_runner"
|
|
11511
11555
|
local output
|
|
@@ -12355,14 +12399,6 @@ auto_generate_docs_if_needed() {
|
|
|
12355
12399
|
elif command -v timeout >/dev/null 2>&1; then
|
|
12356
12400
|
_doc_cmd=(timeout "${_doc_to}s")
|
|
12357
12401
|
fi
|
|
12358
|
-
# SAY WHAT IS HAPPENING BEFORE GOING QUIET. This call discards child output
|
|
12359
|
-
# and can run for the full timeout (default 300s), so without this line the
|
|
12360
|
-
# user sees a single "Auto-documentation" header and then minutes of nothing.
|
|
12361
|
-
# A silent gap reads as a hang: the observed incident was a build stuck ~55
|
|
12362
|
-
# min here with the work committed but never pushed, and nothing on screen
|
|
12363
|
-
# said which step owned the time. Naming the step and its cap turns an
|
|
12364
|
-
# apparent freeze into a bounded wait the user can reason about.
|
|
12365
|
-
log_info "Auto-documentation: generating architecture suite (no output until it finishes; up to ${_doc_to}s)"
|
|
12366
12402
|
if "${_doc_cmd[@]}" "$loki_bin" docs generate "$project_dir" >/dev/null 2>&1; then
|
|
12367
12403
|
:
|
|
12368
12404
|
else
|
|
@@ -12421,10 +12457,6 @@ run_magic_debate_gate() {
|
|
|
12421
12457
|
# verdict on genuinely thin input, not a spurious process block.
|
|
12422
12458
|
log_info "Magic Modules: running debate on '$latest_name'"
|
|
12423
12459
|
local debate_out debate_rc
|
|
12424
|
-
# Captured to a variable, so nothing reaches the screen for up to 300s. Same
|
|
12425
|
-
# reasoning as the doc suite above: name the step and its cap so a bounded
|
|
12426
|
-
# wait does not read as a hang.
|
|
12427
|
-
log_info "Magic debate: reviewing $latest_name (2 rounds, output shown when it finishes; up to 300s)"
|
|
12428
12460
|
debate_out=$(cd "$TARGET_DIR" && PYTHONPATH="$PROJECT_DIR" LOKI_PROVIDER="${PROVIDER_NAME:-claude}" \
|
|
12429
12461
|
timeout 300 "$PROJECT_DIR/autonomy/loki" magic debate "$latest_name" --rounds 2 2>&1) \
|
|
12430
12462
|
&& debate_rc=0 || debate_rc=$?
|
|
@@ -20089,11 +20121,8 @@ except Exception:
|
|
|
20089
20121
|
if [ -n "$gate_escalation_context" ]; then
|
|
20090
20122
|
_legacy_priority="${_legacy_priority}${_legacy_priority:+ }${gate_escalation_context}"
|
|
20091
20123
|
fi
|
|
20092
|
-
# Same cap, same reason, same default as the degraded path above --
|
|
20093
|
-
# a pasted spec has to be bounded, but the bound must not silently
|
|
20094
|
-
# eat requirements. 4000 bytes dropped anything past ~600 words.
|
|
20095
20124
|
if [ -n "$prd" ] && [ -f "$prd" ]; then
|
|
20096
|
-
_legacy_prd_content=$(head -c
|
|
20125
|
+
_legacy_prd_content=$(head -c 4000 "$prd")
|
|
20097
20126
|
fi
|
|
20098
20127
|
if [ $retry -eq 0 ]; then
|
|
20099
20128
|
if [ -n "$prd" ]; then
|
|
@@ -20144,35 +20173,9 @@ except Exception:
|
|
|
20144
20173
|
|
|
20145
20174
|
if [ "${PROVIDER_DEGRADED:-false}" = "true" ]; then
|
|
20146
20175
|
# Degraded providers: simpler wording, but still static-first.
|
|
20147
|
-
#
|
|
20148
|
-
# THE CAP IS NOW ANNOUNCED, NOT SILENT. This path PASTES the spec text
|
|
20149
|
-
# (a degraded provider cannot be told "read the file at this path" the
|
|
20150
|
-
# way claude/cline/opencode are at :20196), so it has to be bounded. It
|
|
20151
|
-
# was bounded at 4000 bytes with no notice: a requirement past ~600 words
|
|
20152
|
-
# was dropped mid-sentence and the model never knew a spec existed beyond
|
|
20153
|
-
# what it saw. Demonstrated on a 4229-byte spec -- the requirement on the
|
|
20154
|
-
# last line was simply absent from what the model received.
|
|
20155
|
-
#
|
|
20156
|
-
# That is the same class of defect spec-expand.sh:5-7 already names for
|
|
20157
|
-
# OpenAPI ("a 40-operation file loses 21 of 40 ops") and fixed for
|
|
20158
|
-
# contracts only. Markdown specs still had it, and only for the two
|
|
20159
|
-
# degraded providers -- so codex and aider users silently got a worse
|
|
20160
|
-
# build than claude users from the identical spec.
|
|
20161
|
-
#
|
|
20162
|
-
# Raised to 24000 (a large PRD fits whole) and, when the spec still
|
|
20163
|
-
# exceeds it, the model is TOLD so and given the path to read the rest.
|
|
20164
|
-
# An unannounced truncation makes the model confidently build the wrong
|
|
20165
|
-
# thing; an announced one makes it go look.
|
|
20166
|
-
local _prd_cap="${LOKI_DEGRADED_PRD_CAP:-24000}"
|
|
20167
20176
|
local prd_content=""
|
|
20168
|
-
local _prd_truncated=0
|
|
20169
20177
|
if [ -n "$prd" ] && [ -f "$prd" ]; then
|
|
20170
|
-
prd_content=$(head -c
|
|
20171
|
-
local _prd_bytes
|
|
20172
|
-
_prd_bytes=$(wc -c < "$prd" 2>/dev/null | tr -d ' ')
|
|
20173
|
-
if [ -n "$_prd_bytes" ] && [ "$_prd_bytes" -gt "$_prd_cap" ] 2>/dev/null; then
|
|
20174
|
-
_prd_truncated=1
|
|
20175
|
-
fi
|
|
20178
|
+
prd_content=$(head -c 4000 "$prd")
|
|
20176
20179
|
fi
|
|
20177
20180
|
|
|
20178
20181
|
local degraded_prd_anchor="Loki Mode"
|
|
@@ -20201,38 +20204,6 @@ except Exception:
|
|
|
20201
20204
|
[ -n "$queue_tasks" ] && printf 'Tasks: %s\n' "$queue_tasks"
|
|
20202
20205
|
if [ -n "$prd" ]; then
|
|
20203
20206
|
printf 'PRD contents: %s\n' "$prd_content"
|
|
20204
|
-
# Announce the cut. Silence here is what made the old 4000-byte cap
|
|
20205
|
-
# dangerous: the model treated a partial spec as the whole spec and
|
|
20206
|
-
# built confidently against requirements it had never seen. Naming
|
|
20207
|
-
# the file lets it read the remainder itself.
|
|
20208
|
-
if [ "${_prd_truncated:-0}" = "1" ]; then
|
|
20209
|
-
printf 'NOTE: the spec above is TRUNCATED at %s bytes. The full spec is at %s -- read it before deciding the work is complete.\n' \
|
|
20210
|
-
"$_prd_cap" "$prd"
|
|
20211
|
-
fi
|
|
20212
|
-
fi
|
|
20213
|
-
|
|
20214
|
-
# FIRST-PASS EXCELLENCE FOR DEGRADED PROVIDERS.
|
|
20215
|
-
#
|
|
20216
|
-
# This directive existed only for Claude. providers/claude.sh:322 injects
|
|
20217
|
-
# it via --append-system-prompt, a flag codex/aider do not have, so the
|
|
20218
|
-
# one mechanism built specifically to make a WEAKER model land complete
|
|
20219
|
-
# on iteration 1 reached only the strongest one. Measured before writing
|
|
20220
|
-
# this: grep for FIRST_PASS_EXCELLENCE returns 0 in codex.sh, aider.sh,
|
|
20221
|
-
# cline.sh and opencode.sh.
|
|
20222
|
-
#
|
|
20223
|
-
# It matters most exactly where it was missing. The premise (recorded
|
|
20224
|
-
# when the Claude version was built) is that iteration count is a proxy
|
|
20225
|
-
# for how much the first pass missed, and that for a weak model context
|
|
20226
|
-
# quality beats iteration count. Codex is also the free on-ramp, so the
|
|
20227
|
-
# users least able to absorb a bad build were the ones getting no help.
|
|
20228
|
-
#
|
|
20229
|
-
# Condensed rather than byte-mirrored: the Claude text is ~4.3KB of
|
|
20230
|
-
# system prompt, and these providers take it inline in the user turn
|
|
20231
|
-
# where budget is tighter. The four load-bearing instructions are kept --
|
|
20232
|
-
# build fully, wire the backend, verify by RUNNING, commit to one design.
|
|
20233
|
-
# Same iteration-1 gate and same env var, so one switch controls both.
|
|
20234
|
-
if [ "${LOKI_FIRST_PASS_EXCELLENCE:-1}" != "0" ] && [ "${iteration:-1}" -le 1 ] 2>/dev/null; then
|
|
20235
|
-
printf '%s\n' '[FIRST-PASS EXCELLENCE] Treat THIS pass as your one shot to ship a complete, working solution. The loop is a safety net, not a plan. 1) BUILD IT FULLY: no stubs, no TODOs, no placeholder or mock data where real logic belongs. If the spec implies a backend (auth, persistence, a form that submits), WIRE IT so it actually persists -- a UI whose buttons do nothing is the most common failure. 2) VERIFY BY RUNNING each acceptance path, not by reading the code. 3) DECIDE the architecture now rather than refactoring later. 4) Commit to ONE specific design; avoid the generic purple-gradient default look.'
|
|
20236
20207
|
fi
|
|
20237
20208
|
printf '</dynamic_context>\n'
|
|
20238
20209
|
return 0
|
|
@@ -24725,6 +24696,58 @@ kill_provider_child() {
|
|
|
24725
24696
|
# ~run.sh:15034); in interactive foreground we share the user's shell group,
|
|
24726
24697
|
# leave the pgid absent, and skip this reap entirely -- so Ctrl+C semantics
|
|
24727
24698
|
# and the user's shell are untouched.
|
|
24699
|
+
# --- pgid stamp helpers (P0: stale-pgid session killer) ---------------------
|
|
24700
|
+
# The pgid file is written as `pgid=<n> boot=<id> started=<epoch>` so a reader
|
|
24701
|
+
# can tell a live record from a week-old orphan. See reap_own_process_group.
|
|
24702
|
+
|
|
24703
|
+
# Extract one field from a stamp. A bare number (legacy file) yields the pgid
|
|
24704
|
+
# and nothing else, so the reader's fail-closed branch rejects it.
|
|
24705
|
+
_loki_pgid_field() {
|
|
24706
|
+
local _s="$1" _k="$2"
|
|
24707
|
+
case "$_s" in
|
|
24708
|
+
*=*) ;;
|
|
24709
|
+
*) [ "$_k" = "pgid" ] && printf '%s' "$(printf '%s' "$_s" | tr -d ' ')"; return 0 ;;
|
|
24710
|
+
esac
|
|
24711
|
+
# Prefix/suffix trimming, not `for _tok in $_s`. Two reasons:
|
|
24712
|
+
# - `while read` in a pipeline runs in a SUBSHELL and drops a final field
|
|
24713
|
+
# with no trailing newline, silently returning an empty `started`.
|
|
24714
|
+
# - unquoted `$_s` in a `for` relies on word splitting, which bash does
|
|
24715
|
+
# and ZSH DOES NOT. run.sh is bash, but this file gets sourced and
|
|
24716
|
+
# probed from other shells, and a helper whose correctness depends on
|
|
24717
|
+
# the caller's shell is a trap. This form needs no word splitting.
|
|
24718
|
+
local _rest="$_s" _tok
|
|
24719
|
+
while [ -n "$_rest" ]; do
|
|
24720
|
+
_tok="${_rest%% *}" # first space-delimited token
|
|
24721
|
+
case "$_tok" in
|
|
24722
|
+
"$_k"=*) printf '%s' "${_tok#*=}"; return 0 ;;
|
|
24723
|
+
esac
|
|
24724
|
+
case "$_rest" in
|
|
24725
|
+
*" "*) _rest="${_rest#* }" ;; # advance past this token
|
|
24726
|
+
*) _rest="" ;; # last token: stop
|
|
24727
|
+
esac
|
|
24728
|
+
done
|
|
24729
|
+
}
|
|
24730
|
+
|
|
24731
|
+
# A value that changes on every reboot, so a pgid recorded before a reboot can
|
|
24732
|
+
# never validate afterwards. Empty when unavailable -- callers fail closed.
|
|
24733
|
+
_loki_boot_id() {
|
|
24734
|
+
if [ -r /proc/stat ]; then
|
|
24735
|
+
awk '/^btime /{print $2; exit}' /proc/stat 2>/dev/null && return 0
|
|
24736
|
+
fi
|
|
24737
|
+
# macOS: `kern.boottime` prints `{ sec = 1234567890, usec = 0 } ...`
|
|
24738
|
+
sysctl -n kern.boottime 2>/dev/null | sed -n 's/.*sec *= *\([0-9][0-9]*\).*/\1/p'
|
|
24739
|
+
}
|
|
24740
|
+
|
|
24741
|
+
# Start time of a pid as a unix epoch, or empty when it cannot be determined.
|
|
24742
|
+
_loki_proc_start_epoch() {
|
|
24743
|
+
local _p="${1:-$$}" _lstart
|
|
24744
|
+
_lstart="$(ps -o lstart= -p "$_p" 2>/dev/null)"
|
|
24745
|
+
[ -n "$_lstart" ] || return 0
|
|
24746
|
+
date -j -f '%a %b %e %T %Y' "$_lstart" '+%s' 2>/dev/null \
|
|
24747
|
+
|| date -d "$_lstart" '+%s' 2>/dev/null \
|
|
24748
|
+
|| true
|
|
24749
|
+
}
|
|
24750
|
+
|
|
24728
24751
|
reap_own_process_group() {
|
|
24729
24752
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
24730
24753
|
# Resolve the pgid file the same way main() recorded it (global or per-session).
|
|
@@ -24735,8 +24758,13 @@ reap_own_process_group() {
|
|
|
24735
24758
|
_reap_pgid_file="${_reap_pgid_file%.pid}.pgid"
|
|
24736
24759
|
[ -f "$_reap_pgid_file" ] || return 0 # interactive / no own session: skip
|
|
24737
24760
|
|
|
24738
|
-
|
|
24739
|
-
|
|
24761
|
+
# Parse the stamped format `pgid=<n> boot=<id> started=<epoch>`, falling back
|
|
24762
|
+
# to a bare number for a file written by an older version.
|
|
24763
|
+
local _stamp _pgid _boot _started
|
|
24764
|
+
_stamp=$(cat "$_reap_pgid_file" 2>/dev/null | tr -d '\n')
|
|
24765
|
+
_pgid=$(_loki_pgid_field "$_stamp" pgid)
|
|
24766
|
+
_boot=$(_loki_pgid_field "$_stamp" boot)
|
|
24767
|
+
_started=$(_loki_pgid_field "$_stamp" started)
|
|
24740
24768
|
case "$_pgid" in ''|*[!0-9]*) return 0 ;; esac
|
|
24741
24769
|
[ "$_pgid" -gt 1 ] 2>/dev/null || return 0 # never touch pgid 0/1
|
|
24742
24770
|
|
|
@@ -24747,6 +24775,40 @@ reap_own_process_group() {
|
|
|
24747
24775
|
_my_pgid=$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')
|
|
24748
24776
|
[ -n "$_my_pgid" ] && [ "$_pgid" = "$_my_pgid" ] || return 0
|
|
24749
24777
|
|
|
24778
|
+
# BOOT + AGE GUARD (the fix for the session-killer).
|
|
24779
|
+
#
|
|
24780
|
+
# The self-check above is NOT sufficient on its own. loki.pgid was removed
|
|
24781
|
+
# only on the normal exit path, so a Ctrl+C'd or crashed run left the file
|
|
24782
|
+
# behind indefinitely -- measured on a real machine: two orphans aged 155h
|
|
24783
|
+
# and 202h. PIDs recycle (macOS wraps near 99999; max observed 99762), so a
|
|
24784
|
+
# week-old pgid eventually matches a LIVE, unrelated shell group. The
|
|
24785
|
+
# self-check then passes and every sibling in the user's terminal -- their
|
|
24786
|
+
# editor, another agent session -- is TERM'd then KILL'd.
|
|
24787
|
+
#
|
|
24788
|
+
# A recycled pgid cannot forge both of these:
|
|
24789
|
+
# - boot id: a file from a previous boot can never match this boot.
|
|
24790
|
+
# - started: the recording run must not predate THIS process.
|
|
24791
|
+
#
|
|
24792
|
+
# Fails CLOSED. A missing or malformed stamp means we do nothing: an
|
|
24793
|
+
# orphaned agent (the v7.41.x problem this reap was added for) is strictly
|
|
24794
|
+
# less harmful than killing a user's editor.
|
|
24795
|
+
local _now_boot
|
|
24796
|
+
_now_boot="$(_loki_boot_id)"
|
|
24797
|
+
if [ -z "$_boot" ] || [ -z "$_started" ]; then
|
|
24798
|
+
return 0 # unstamped legacy file: never trust it
|
|
24799
|
+
fi
|
|
24800
|
+
if [ -z "$_now_boot" ] || [ "$_boot" != "$_now_boot" ]; then
|
|
24801
|
+
return 0 # different boot (or boot id unavailable): refuse
|
|
24802
|
+
fi
|
|
24803
|
+
case "$_started" in ''|*[!0-9]*) return 0 ;; esac
|
|
24804
|
+
local _self_started
|
|
24805
|
+
_self_started="$(_loki_proc_start_epoch $$)"
|
|
24806
|
+
# The stamp must not predate this process by more than a small clock skew.
|
|
24807
|
+
# A stale file is always OLDER than the process now reading it.
|
|
24808
|
+
if [ -n "$_self_started" ] && [ "$_started" -lt "$(( _self_started - 5 ))" ]; then
|
|
24809
|
+
return 0
|
|
24810
|
+
fi
|
|
24811
|
+
|
|
24750
24812
|
# Collect protected pids (dashboard, app-runner, registered children) so the
|
|
24751
24813
|
# reap never takes down the shared dashboard if it happens to share our
|
|
24752
24814
|
# group. Mirrors the `loki stop` / dashboard reaper protection set.
|
|
@@ -24889,17 +24951,7 @@ check_human_intervention() {
|
|
|
24889
24951
|
# Security: Check it's a regular file (not symlink) to prevent symlink attacks
|
|
24890
24952
|
if [ -f "$loki_dir/HUMAN_INPUT.md" ] && [ ! -L "$loki_dir/HUMAN_INPUT.md" ]; then
|
|
24891
24953
|
# Security: Prompt injection disabled by default for enterprise security
|
|
24892
|
-
|
|
24893
|
-
# `loki steer` prints "Enable it: export LOKI_PROMPT_INJECTION=1" and
|
|
24894
|
-
# then "The next iteration will read and apply it." A user who followed
|
|
24895
|
-
# that instruction exactly had their steering note moved to
|
|
24896
|
-
# logs/human-input-REJECTED-*.md and never read -- the CLI reported
|
|
24897
|
-
# success, the runner silently discarded it, and the only warning went
|
|
24898
|
-
# to the runner log rather than to the person who typed the command.
|
|
24899
|
-
#
|
|
24900
|
-
# Both forms is the convention here (LOKI_GATE_SEMANTIC_TESTS_BLOCK and
|
|
24901
|
-
# LOKI_DURABLE_STATE each test for both); this flag was the outlier.
|
|
24902
|
-
if [ "${LOKI_PROMPT_INJECTION:-false}" != "true" ] && [ "${LOKI_PROMPT_INJECTION:-false}" != "1" ]; then
|
|
24954
|
+
if [ "${LOKI_PROMPT_INJECTION:-false}" != "true" ]; then
|
|
24903
24955
|
log_warn "HUMAN_INPUT.md detected but prompt injection is DISABLED"
|
|
24904
24956
|
log_warn "To enable, set LOKI_PROMPT_INJECTION=true (only in trusted environments)"
|
|
24905
24957
|
# Move to rejected instead of processed
|
|
@@ -25158,6 +25210,20 @@ _loki_remove_temp_self_copy() {
|
|
|
25158
25210
|
fi
|
|
25159
25211
|
}
|
|
25160
25212
|
|
|
25213
|
+
# P0 (stale-pgid session killer): the pgid file must never outlive its run.
|
|
25214
|
+
# Recorded by the writer (see _LOKI_PGID_FILE below) rather than re-derived, so
|
|
25215
|
+
# the removal hits the exact path written -- global OR per-session -- and can
|
|
25216
|
+
# never delete a concurrent run's file.
|
|
25217
|
+
_loki_remove_pgid_file() {
|
|
25218
|
+
[ -n "${_LOKI_PGID_FILE:-}" ] || return 0
|
|
25219
|
+
# A `trap ... EXIT` fires in SUBSHELLS too, and a subshell exiting must not
|
|
25220
|
+
# delete a LIVE parent's file (repo scar: a bare trap deleted a parent's
|
|
25221
|
+
# lock). $$ does NOT change in a subshell -- BASHPID does, so it is the only
|
|
25222
|
+
# real discriminator here.
|
|
25223
|
+
[ "${BASHPID:-$$}" = "${_LOKI_PGID_OWNER:-$$}" ] || return 0
|
|
25224
|
+
rm -f "$_LOKI_PGID_FILE" 2>/dev/null || true
|
|
25225
|
+
}
|
|
25226
|
+
|
|
25161
25227
|
_loki_session_exit_cleanup() {
|
|
25162
25228
|
local exit_code=$?
|
|
25163
25229
|
_loki_terminal_record 2>/dev/null || true
|
|
@@ -25165,6 +25231,12 @@ _loki_session_exit_cleanup() {
|
|
|
25165
25231
|
&& type safe_release_lock >/dev/null 2>&1; then
|
|
25166
25232
|
safe_release_lock "$_LOKI_SESSION_LOCK_FILE" 2>/dev/null || true
|
|
25167
25233
|
fi
|
|
25234
|
+
# Covers EXIT plus INT/TERM: every cleanup() path that actually terminates
|
|
25235
|
+
# does so via `exit`, which fires this trap. Deliberately NOT called from
|
|
25236
|
+
# cleanup() itself -- its perpetual/pause branches RETURN and the run
|
|
25237
|
+
# continues, so removing the file there would silently disable the
|
|
25238
|
+
# completion-path reap for the rest of the run.
|
|
25239
|
+
_loki_remove_pgid_file
|
|
25168
25240
|
_loki_remove_temp_self_copy
|
|
25169
25241
|
return "$exit_code"
|
|
25170
25242
|
}
|
|
@@ -25833,6 +25905,10 @@ main() {
|
|
|
25833
25905
|
fi
|
|
25834
25906
|
else
|
|
25835
25907
|
# Lock helper not loaded (lib/lock.sh missing). PID-only fallback.
|
|
25908
|
+
# This branch still falls through to the pgid writer below, so it needs
|
|
25909
|
+
# the same EXIT cleanup or the pgid file leaks on every interrupt here.
|
|
25910
|
+
# An empty lock file is safe: the release is [ -n ]-guarded.
|
|
25911
|
+
_loki_arm_session_exit_cleanup ""
|
|
25836
25912
|
if [ -f "$pid_file" ]; then
|
|
25837
25913
|
local existing_pid
|
|
25838
25914
|
existing_pid=$(cat "$pid_file" 2>/dev/null)
|
|
@@ -25863,7 +25939,22 @@ main() {
|
|
|
25863
25939
|
if [ "${LOKI_OWN_SESSION:-}" = "1" ]; then
|
|
25864
25940
|
_loki_pgid="$(ps -o pgid= -p $$ 2>/dev/null | tr -d ' ')"
|
|
25865
25941
|
if [ -n "$_loki_pgid" ]; then
|
|
25866
|
-
|
|
25942
|
+
# STAMPED, not a bare number. The reader requires boot id + start
|
|
25943
|
+
# epoch to match before it will signal anything, because this file
|
|
25944
|
+
# outlived its run (measured: 155h and 202h orphans) and PIDs
|
|
25945
|
+
# recycle -- a stale bare pgid eventually matches a live shell group
|
|
25946
|
+
# and the reap killed the user's other terminal sessions.
|
|
25947
|
+
_loki_pgid_started="$(_loki_proc_start_epoch $$)"
|
|
25948
|
+
[ -n "$_loki_pgid_started" ] || _loki_pgid_started="$(date +%s 2>/dev/null)"
|
|
25949
|
+
# Record the EXACT path written (global or per-session) plus the
|
|
25950
|
+
# owning shell, so the EXIT trap removes THIS file and only from
|
|
25951
|
+
# this process. Without removal on the interrupt/crash paths the
|
|
25952
|
+
# file outlived its run -- measured orphans aged 155h and 202h.
|
|
25953
|
+
_LOKI_PGID_FILE="${pid_file%.pid}.pgid"
|
|
25954
|
+
_LOKI_PGID_OWNER="${BASHPID:-$$}"
|
|
25955
|
+
printf 'pgid=%s boot=%s started=%s\n' \
|
|
25956
|
+
"$_loki_pgid" "$(_loki_boot_id)" "$_loki_pgid_started" \
|
|
25957
|
+
> "$_LOKI_PGID_FILE" 2>/dev/null || true
|
|
25867
25958
|
fi
|
|
25868
25959
|
fi
|
|
25869
25960
|
# Store session ID in state for dashboard/status visibility
|