loki-mode 7.104.1 → 7.104.3
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/loki +28 -6
- package/autonomy/run.sh +230 -50
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +146 -3
- package/docs/INSTALLATION.md +1 -1
- package/docs/TASKLIST-ACCURACY-PLAN.md +355 -0
- package/loki-ts/dist/loki.js +103 -101
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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 v7.104.
|
|
6
|
+
# Loki Mode v7.104.3
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.104.
|
|
411
|
+
**v7.104.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.104.
|
|
1
|
+
7.104.3
|
package/autonomy/loki
CHANGED
|
@@ -9957,12 +9957,27 @@ cmd_doctor() {
|
|
|
9957
9957
|
echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
|
|
9958
9958
|
pass_count=$((pass_count + 1))
|
|
9959
9959
|
elif command -v claude &>/dev/null; then
|
|
9960
|
-
# Zero-network check:
|
|
9961
|
-
#
|
|
9962
|
-
#
|
|
9963
|
-
#
|
|
9960
|
+
# Zero-network login check. v7.104.2: prefer the CLI's own local
|
|
9961
|
+
# auth-status (JSON with "loggedIn"), which is authoritative for BOTH the
|
|
9962
|
+
# file-based and the native/Keychain login. This fixes doctor missing a
|
|
9963
|
+
# genuine Keychain login (native install writes NO ~/.claude/.credentials
|
|
9964
|
+
# .json). Fall back to the expiry stamp in the file for older CLIs.
|
|
9965
|
+
_claude_login="$(claude auth status 2>/dev/null | python3 -c "
|
|
9966
|
+
import json, sys
|
|
9967
|
+
try:
|
|
9968
|
+
v = json.load(sys.stdin).get('loggedIn')
|
|
9969
|
+
# Only the explicit booleans are trusted; a missing/renamed field is
|
|
9970
|
+
# inconclusive and falls through to the file-expiry check (never a spurious
|
|
9971
|
+
# 'not logged in' warning). Matches the run.sh helper + bun doctor.
|
|
9972
|
+
if v is True:
|
|
9973
|
+
print('yes')
|
|
9974
|
+
elif v is False:
|
|
9975
|
+
print('no')
|
|
9976
|
+
except Exception:
|
|
9977
|
+
pass
|
|
9978
|
+
" 2>/dev/null)"
|
|
9964
9979
|
_claude_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
9965
|
-
|
|
9980
|
+
_claude_expired="$( [ -s "$_claude_creds" ] && python3 -c "
|
|
9966
9981
|
import json, sys, time
|
|
9967
9982
|
try:
|
|
9968
9983
|
d = json.load(open(sys.argv[1]))
|
|
@@ -9970,7 +9985,14 @@ try:
|
|
|
9970
9985
|
print('expired' if isinstance(exp,(int,float)) and exp>0 and (exp/1000.0)<=(time.time()+60) else 'ok')
|
|
9971
9986
|
except Exception:
|
|
9972
9987
|
print('ok')
|
|
9973
|
-
" "$_claude_creds" 2>/dev/null || echo ok)"
|
|
9988
|
+
" "$_claude_creds" 2>/dev/null || echo ok )"
|
|
9989
|
+
if [ "$_claude_login" = "yes" ]; then
|
|
9990
|
+
echo -e " ${GREEN}PASS${NC} Claude CLI is logged in (subscription/OAuth login)"
|
|
9991
|
+
pass_count=$((pass_count + 1))
|
|
9992
|
+
elif [ "$_claude_login" = "no" ]; then
|
|
9993
|
+
echo -e " ${YELLOW}WARN${NC} Claude CLI is NOT logged in -- run 'claude login' before a build (it would otherwise stall)"
|
|
9994
|
+
warn_count=$((warn_count + 1))
|
|
9995
|
+
elif [ "$_claude_expired" = "expired" ]; then
|
|
9974
9996
|
echo -e " ${YELLOW}WARN${NC} Claude login has EXPIRED -- run 'claude login' before a build (it would otherwise stall)"
|
|
9975
9997
|
warn_count=$((warn_count + 1))
|
|
9976
9998
|
else
|
package/autonomy/run.sh
CHANGED
|
@@ -2023,6 +2023,118 @@ _loki_check_git_present() {
|
|
|
2023
2023
|
return 0
|
|
2024
2024
|
}
|
|
2025
2025
|
|
|
2026
|
+
# _loki_claude_login_state: report the Claude Code login state WITHOUT a network
|
|
2027
|
+
# call, printing exactly one of: loggedin | expired | loggedout | unknown.
|
|
2028
|
+
#
|
|
2029
|
+
# v7.104.2: the previous preflight assumed OAuth credentials always live in
|
|
2030
|
+
# ~/.claude/.credentials.json. That is false for the native install (Claude Code
|
|
2031
|
+
# 2.x on macOS), which stores the login in the macOS Keychain under the service
|
|
2032
|
+
# "Claude Code-credentials" and creates NO .credentials.json. A genuinely logged
|
|
2033
|
+
# in subscription user was therefore falsely told "installed but not logged in"
|
|
2034
|
+
# and blocked from every build -- the exact opposite of the zero-friction intent.
|
|
2035
|
+
#
|
|
2036
|
+
# Design: prefer the durable, CLI-owned signal (`claude auth status`, local +
|
|
2037
|
+
# fast + non-interactive, JSON with "loggedIn"), because the credential STORE
|
|
2038
|
+
# location has now moved twice (file -> keychain) and will move again. Fall back
|
|
2039
|
+
# to the file, then the keychain, for older CLIs that lack `auth status`. Crucial
|
|
2040
|
+
# rule (inconclusive-never-false-fails applied to auth): only ever return a hard
|
|
2041
|
+
# "loggedout" on POSITIVE evidence of no login; when we cannot tell, return
|
|
2042
|
+
# "unknown" and let the caller fail OPEN (warn, proceed, let the real call
|
|
2043
|
+
# decide) -- never hard-block a paying user on uncertainty.
|
|
2044
|
+
_loki_claude_login_state() {
|
|
2045
|
+
# 1) Primary: the CLI's own local auth-status (zero-network, ~0.2s). Newer
|
|
2046
|
+
# claude CLIs print JSON with a boolean "loggedIn"; parse defensively.
|
|
2047
|
+
if command -v claude >/dev/null 2>&1; then
|
|
2048
|
+
local _status_json
|
|
2049
|
+
_status_json="$(claude auth status 2>/dev/null)"
|
|
2050
|
+
if [[ -n "$_status_json" ]]; then
|
|
2051
|
+
local _parsed
|
|
2052
|
+
_parsed="$(printf '%s' "$_status_json" | python3 -c "
|
|
2053
|
+
import json, sys
|
|
2054
|
+
try:
|
|
2055
|
+
d = json.load(sys.stdin)
|
|
2056
|
+
v = d.get('loggedIn')
|
|
2057
|
+
# ONLY the two explicit booleans are trusted. Anything else -- a missing
|
|
2058
|
+
# field, a renamed schema, a JSON error object -- is inconclusive and MUST
|
|
2059
|
+
# fall through to the file/keychain checks, never a hard 'loggedout' (that
|
|
2060
|
+
# would block a genuinely logged-in user with a valid creds file, violating
|
|
2061
|
+
# fail-open). Mirrors the bun doctor's === true / === false / else shape.
|
|
2062
|
+
if v is True:
|
|
2063
|
+
print('loggedin')
|
|
2064
|
+
elif v is False:
|
|
2065
|
+
print('loggedout')
|
|
2066
|
+
# else: print nothing -> fall through
|
|
2067
|
+
except Exception:
|
|
2068
|
+
pass # not JSON (older CLI / different output) -> stay silent, fall through
|
|
2069
|
+
" 2>/dev/null)"
|
|
2070
|
+
if [[ "$_parsed" == "loggedin" ]]; then
|
|
2071
|
+
echo "loggedin"; return 0
|
|
2072
|
+
elif [[ "$_parsed" == "loggedout" ]]; then
|
|
2073
|
+
echo "loggedout"; return 0
|
|
2074
|
+
fi
|
|
2075
|
+
# auth status ran but was inconclusive (unparseable, or valid JSON
|
|
2076
|
+
# without an explicit loggedIn boolean) -> do NOT trust it as a
|
|
2077
|
+
# negative; fall through to the credential-store checks below.
|
|
2078
|
+
fi
|
|
2079
|
+
fi
|
|
2080
|
+
|
|
2081
|
+
# 2) Fallback: a VALID (non-expired) OAuth credentials file (older CLIs write
|
|
2082
|
+
# it here). Compute the expiry once; a valid file is a positive login.
|
|
2083
|
+
# IMPORTANT: we do NOT conclude "expired" here yet -- a stale expired file
|
|
2084
|
+
# can sit next to a live macOS-Keychain login (the native install writes
|
|
2085
|
+
# the file once, then rotates only the Keychain). Concluding "expired" off
|
|
2086
|
+
# the file before consulting the Keychain would wrongly block a genuinely
|
|
2087
|
+
# logged-in user -- the exact class of bug this whole change fixes. So the
|
|
2088
|
+
# Keychain (step 3, a POSITIVE signal) is checked BEFORE we ever return
|
|
2089
|
+
# "expired" (step 4). Unknown schema -> treat as valid (fail open).
|
|
2090
|
+
local _creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
2091
|
+
local _file_state="" # ""=absent, "valid"=present+fresh, "expired"=present+stale
|
|
2092
|
+
if [[ -s "$_creds" ]]; then
|
|
2093
|
+
_file_state="$(python3 -c "
|
|
2094
|
+
import json, sys, time
|
|
2095
|
+
try:
|
|
2096
|
+
d = json.load(open(sys.argv[1]))
|
|
2097
|
+
exp = d.get('claudeAiOauth', {}).get('expiresAt')
|
|
2098
|
+
if isinstance(exp, (int, float)) and exp > 0 and (exp / 1000.0) <= (time.time() + 60):
|
|
2099
|
+
print('expired')
|
|
2100
|
+
else:
|
|
2101
|
+
print('valid')
|
|
2102
|
+
except Exception:
|
|
2103
|
+
print('valid') # unreadable/unknown -> fail open (treat as valid)
|
|
2104
|
+
" "$_creds" 2>/dev/null || echo valid)"
|
|
2105
|
+
fi
|
|
2106
|
+
if [[ "$_file_state" == "valid" ]]; then
|
|
2107
|
+
echo "loggedin"; return 0
|
|
2108
|
+
fi
|
|
2109
|
+
|
|
2110
|
+
# 3) macOS Keychain (native install stores the login here, often with NO file
|
|
2111
|
+
# at all, or alongside a now-stale file). A present Keychain entry is a
|
|
2112
|
+
# POSITIVE login signal and OVERRIDES an expired file. Existence check
|
|
2113
|
+
# ONLY -- never `-w` (reading the secret can pop a GUI keychain prompt that
|
|
2114
|
+
# would hang a headless/CI build). Guarded to Darwin.
|
|
2115
|
+
if [[ "$(uname 2>/dev/null)" == "Darwin" ]] && command -v security >/dev/null 2>&1; then
|
|
2116
|
+
if security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then
|
|
2117
|
+
echo "loggedin"; return 0
|
|
2118
|
+
fi
|
|
2119
|
+
fi
|
|
2120
|
+
|
|
2121
|
+
# 4) Only now, with no valid file and no Keychain login, is an expired file a
|
|
2122
|
+
# real "expired" state (genuine expiry that would 401 mid-build).
|
|
2123
|
+
if [[ "$_file_state" == "expired" ]]; then
|
|
2124
|
+
echo "expired"; return 0
|
|
2125
|
+
fi
|
|
2126
|
+
|
|
2127
|
+
# 5) No positive login evidence at all. On macOS (where the file AND the
|
|
2128
|
+
# Keychain are the two real stores, both absent) with the CLI present, that
|
|
2129
|
+
# is confident "logged out". Anywhere else we cannot prove absence of a
|
|
2130
|
+
# login (no Keychain to consult), so stay "unknown" and let the caller fail
|
|
2131
|
+
# open.
|
|
2132
|
+
if [[ "$(uname 2>/dev/null)" == "Darwin" ]] && command -v claude >/dev/null 2>&1; then
|
|
2133
|
+
echo "loggedout"; return 0
|
|
2134
|
+
fi
|
|
2135
|
+
echo "unknown"; return 0
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2026
2138
|
# _loki_check_workspace_writable: the build writes state, proofs, and source into
|
|
2027
2139
|
# the working directory and its .loki/ subtree on every iteration. A read-only
|
|
2028
2140
|
# volume, a permission-denied directory, or a full disk would otherwise fail
|
|
@@ -2108,21 +2220,33 @@ validate_api_keys() {
|
|
|
2108
2220
|
|
|
2109
2221
|
# Zero-friction auth preflight for LOCAL runs (must run BEFORE the early
|
|
2110
2222
|
# return below, which exits for non-Docker/K8s envs). When claude is the
|
|
2111
|
-
# provider
|
|
2112
|
-
#
|
|
2113
|
-
#
|
|
2114
|
-
# 401 -- the worst first impression --
|
|
2115
|
-
#
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2223
|
+
# provider and there is no ANTHROPIC_API_KEY, ask the login-state helper
|
|
2224
|
+
# (auth-status -> file -> keychain, all zero-network) whether the user is
|
|
2225
|
+
# actually logged in. A never-logged-in user would otherwise enter the build,
|
|
2226
|
+
# make a failing call, and 401 -- the worst first impression -- so we fail
|
|
2227
|
+
# fast with the one-step fix. But we ONLY hard-block on a confident
|
|
2228
|
+
# "loggedout"; an "expired" login gets its own message; "unknown" (we cannot
|
|
2229
|
+
# prove the login state, e.g. an older CLI on a non-macOS box with no file)
|
|
2230
|
+
# fails OPEN so a genuinely logged-in user is never blocked. This is the
|
|
2231
|
+
# v7.104.2 fix for the native/Keychain login being misread as logged out.
|
|
2232
|
+
# Opt out entirely with LOKI_SKIP_AUTH_PREFLIGHT=1.
|
|
2233
|
+
if [[ "$provider" == "claude" && "${LOKI_SKIP_AUTH_PREFLIGHT:-}" != "1" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
2234
|
+
local _login_state
|
|
2235
|
+
_login_state="$(_loki_claude_login_state)"
|
|
2236
|
+
if [[ "$_login_state" == "loggedout" ]]; then
|
|
2120
2237
|
log_error "Claude Code is installed but not logged in -- the build would stall instead of running."
|
|
2121
2238
|
log_error "Log in once, then retry:"
|
|
2122
2239
|
log_error " claude login"
|
|
2123
2240
|
log_error "(or set ANTHROPIC_API_KEY, or LOKI_SKIP_AUTH_PREFLIGHT=1 to bypass this check)"
|
|
2124
2241
|
return 1
|
|
2242
|
+
elif [[ "$_login_state" == "expired" ]]; then
|
|
2243
|
+
log_error "Your Claude Code login has expired -- the build would stall instead of running."
|
|
2244
|
+
log_error "Fix it in one step, then retry:"
|
|
2245
|
+
log_error " claude login"
|
|
2246
|
+
log_error "(or set ANTHROPIC_API_KEY, or LOKI_SKIP_AUTH_PREFLIGHT=1 to bypass this check)"
|
|
2247
|
+
return 1
|
|
2125
2248
|
fi
|
|
2249
|
+
# "loggedin" or "unknown" -> proceed (fail open on uncertainty).
|
|
2126
2250
|
fi
|
|
2127
2251
|
|
|
2128
2252
|
# CLI tools (claude, codex, cline, aider) use their own login sessions.
|
|
@@ -6396,52 +6520,108 @@ EFF_EOF
|
|
|
6396
6520
|
[ ! -f "$completed_file" ] && echo "[]" > "$completed_file"
|
|
6397
6521
|
[ ! -f "$failed_file" ] && echo "[]" > "$failed_file"
|
|
6398
6522
|
|
|
6399
|
-
#
|
|
6400
|
-
|
|
6401
|
-
{
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
# Add to appropriate queue
|
|
6523
|
+
# Build the completed iteration record + move it out of in-progress, ATOMICALLY.
|
|
6524
|
+
# v7.104.3 task-list accuracy fix: the old writer emitted a THIN body
|
|
6525
|
+
# {id,type,title:"Iteration N",status,exitCode,provider} with no description
|
|
6526
|
+
# and no logs, and then DELETED the rich in-progress record -- so the done
|
|
6527
|
+
# column showed empty cards. We now LIFT the honest per-iteration parts (logs,
|
|
6528
|
+
# startedAt) from the in-progress record BEFORE removing it, and give the card
|
|
6529
|
+
# an HONEST iteration-scoped title/description derived from values in scope
|
|
6530
|
+
# ($phase/$exit_code/$duration_ms). We deliberately do NOT reuse the borrowed
|
|
6531
|
+
# PRD-story title: because pending stories never leave the queue, the
|
|
6532
|
+
# in-progress title is always pending[0] ("server.js..."), so carrying it onto
|
|
6533
|
+
# N done cards would falsely imply that story was built N times (fake-green).
|
|
6534
|
+
# We also upsert-by-id (no cross-sub-run "iteration-1 x5" accumulation) and
|
|
6535
|
+
# keep completed/failed mutually exclusive per id.
|
|
6414
6536
|
local target_file="$completed_file"
|
|
6415
|
-
|
|
6416
|
-
|
|
6537
|
+
local other_file="$failed_file"
|
|
6538
|
+
[ "$exit_code" != "0" ] && { target_file="$failed_file"; other_file="$completed_file"; }
|
|
6539
|
+
local _completed_ts; _completed_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
6540
|
+
_LOKI_TASK_ID="$task_id" \
|
|
6541
|
+
_LOKI_ITER="$iteration" \
|
|
6542
|
+
_LOKI_PHASE="$phase" \
|
|
6543
|
+
_LOKI_EXIT="$exit_code" \
|
|
6544
|
+
_LOKI_DUR="$duration_ms" \
|
|
6545
|
+
_LOKI_PROVIDER="${PROVIDER_NAME:-claude}" \
|
|
6546
|
+
_LOKI_COMPLETED_TS="$_completed_ts" \
|
|
6547
|
+
_LOKI_TARGET="$target_file" \
|
|
6548
|
+
_LOKI_OTHER="$other_file" \
|
|
6549
|
+
_LOKI_INPROG="$in_progress_file" \
|
|
6417
6550
|
python3 -c "
|
|
6418
|
-
import
|
|
6551
|
+
import json, os
|
|
6552
|
+
|
|
6553
|
+
tid = os.environ['_LOKI_TASK_ID']
|
|
6554
|
+
it = os.environ['_LOKI_ITER']
|
|
6555
|
+
phase = os.environ.get('_LOKI_PHASE') or 'unknown'
|
|
6556
|
+
exit_code = os.environ.get('_LOKI_EXIT', '1')
|
|
6557
|
+
dur = os.environ.get('_LOKI_DUR', '')
|
|
6558
|
+
provider = os.environ.get('_LOKI_PROVIDER', 'claude')
|
|
6559
|
+
completed_ts = os.environ['_LOKI_COMPLETED_TS']
|
|
6560
|
+
target = os.environ['_LOKI_TARGET']
|
|
6561
|
+
other = os.environ['_LOKI_OTHER']
|
|
6562
|
+
inprog = os.environ['_LOKI_INPROG']
|
|
6563
|
+
ok = (exit_code == '0')
|
|
6564
|
+
|
|
6565
|
+
def load(p):
|
|
6566
|
+
try:
|
|
6567
|
+
with open(p) as f:
|
|
6568
|
+
d = json.load(f)
|
|
6569
|
+
return d if isinstance(d, list) else (d.get('tasks', []) if isinstance(d, dict) else [])
|
|
6570
|
+
except Exception:
|
|
6571
|
+
return []
|
|
6572
|
+
|
|
6573
|
+
# Lift the honest body from the in-progress record before removing it.
|
|
6574
|
+
prior = next((t for t in load(inprog) if isinstance(t, dict) and t.get('id') == tid), {})
|
|
6575
|
+
logs = prior.get('logs') if isinstance(prior.get('logs'), list) else []
|
|
6576
|
+
started = prior.get('startedAt')
|
|
6577
|
+
|
|
6578
|
+
dur_s = ''
|
|
6419
6579
|
try:
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6580
|
+
if dur not in ('', None):
|
|
6581
|
+
_ms = int(float(dur))
|
|
6582
|
+
dur_s = f\", {_ms}ms\" if _ms < 1000 else f\", {round(_ms/1000)}s\"
|
|
6583
|
+
except Exception:
|
|
6584
|
+
dur_s = ''
|
|
6585
|
+
|
|
6586
|
+
if ok:
|
|
6587
|
+
title = f'Iteration {it} complete - {phase}'
|
|
6588
|
+
desc = f'Iteration {it} finished cleanly in the {phase} phase (exit 0{dur_s}).'
|
|
6589
|
+
else:
|
|
6590
|
+
title = f'Iteration {it} failed (exit {exit_code})'
|
|
6591
|
+
desc = f'Iteration {it} ended in the {phase} phase with exit code {exit_code}{dur_s}.'
|
|
6592
|
+
|
|
6593
|
+
entry = {
|
|
6594
|
+
'id': tid,
|
|
6595
|
+
'type': 'iteration',
|
|
6596
|
+
'title': title,
|
|
6597
|
+
'description': desc,
|
|
6598
|
+
'status': 'completed' if ok else 'failed',
|
|
6599
|
+
'phase': phase,
|
|
6600
|
+
'completedAt': completed_ts,
|
|
6601
|
+
'exitCode': int(exit_code) if str(exit_code).lstrip('-').isdigit() else exit_code,
|
|
6602
|
+
'provider': provider,
|
|
6603
|
+
'logs': logs,
|
|
6604
|
+
}
|
|
6605
|
+
if started:
|
|
6606
|
+
entry['startedAt'] = started
|
|
6607
|
+
|
|
6608
|
+
# Upsert-by-id into target (drop any stale same-id), cap at 50.
|
|
6609
|
+
data = [t for t in load(target) if not (isinstance(t, dict) and t.get('id') == tid)]
|
|
6610
|
+
data.append(entry)
|
|
6426
6611
|
data = data[-50:]
|
|
6427
|
-
with open(
|
|
6612
|
+
with open(target, 'w') as f:
|
|
6428
6613
|
json.dump(data, f, indent=2)
|
|
6429
|
-
" 2>/dev/null || echo "[$task_json]" > "$target_file"
|
|
6430
6614
|
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
except:
|
|
6442
|
-
pass
|
|
6443
|
-
" 2>/dev/null || true
|
|
6444
|
-
fi
|
|
6615
|
+
# Mutual exclusion: remove this id from the OTHER terminal file.
|
|
6616
|
+
odata = [t for t in load(other) if not (isinstance(t, dict) and t.get('id') == tid)]
|
|
6617
|
+
with open(other, 'w') as f:
|
|
6618
|
+
json.dump(odata, f, indent=2)
|
|
6619
|
+
|
|
6620
|
+
# Remove from in-progress.
|
|
6621
|
+
idata = [t for t in load(inprog) if not (isinstance(t, dict) and t.get('id') == tid)]
|
|
6622
|
+
with open(inprog, 'w') as f:
|
|
6623
|
+
json.dump(idata, f, indent=2)
|
|
6624
|
+
" 2>/dev/null || echo "[{\"id\":\"$task_id\",\"type\":\"iteration\",\"title\":\"Iteration $iteration\",\"status\":\"$([ "$exit_code" = "0" ] && echo completed || echo failed)\",\"completedAt\":\"$_completed_ts\",\"exitCode\":$exit_code,\"provider\":\"${PROVIDER_NAME:-claude}\"}]" > "$target_file"
|
|
6445
6625
|
|
|
6446
6626
|
# BUG-ST-014: Atomic current-task.json clear via temp file + mv
|
|
6447
6627
|
local ct_tmp=".loki/queue/current-task.json.tmp.$$"
|
|
@@ -19023,7 +19203,7 @@ main() {
|
|
|
19023
19203
|
_handoff_dir="${TARGET_DIR:-.}"
|
|
19024
19204
|
_handoff_md="$_handoff_dir/HANDOFF.md"
|
|
19025
19205
|
_handoff_tmp="$_handoff_dir/.HANDOFF.md.tmp"
|
|
19026
|
-
if python3 "$_own_render" --loki-dir "$LOKI_DIR" --md > "$_handoff_tmp" 2>/dev/null; then
|
|
19206
|
+
if python3 "$_own_render" --loki-dir "${LOKI_DIR:-${TARGET_DIR:-.}/.loki}" --md > "$_handoff_tmp" 2>/dev/null; then
|
|
19027
19207
|
mv -f "$_handoff_tmp" "$_handoff_md" 2>/dev/null || rm -f "$_handoff_tmp" 2>/dev/null || true
|
|
19028
19208
|
else
|
|
19029
19209
|
rm -f "$_handoff_tmp" 2>/dev/null || true
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -1816,10 +1816,30 @@ async def list_tasks(
|
|
|
1816
1816
|
}
|
|
1817
1817
|
for _f in ("acceptance_criteria", "notes", "logs", "user_story",
|
|
1818
1818
|
"project", "source", "specification", "provider",
|
|
1819
|
-
"startedAt", "full_content"
|
|
1819
|
+
"startedAt", "full_content",
|
|
1820
|
+
# v7.104.3: iteration terminal fields so the card
|
|
1821
|
+
# can show real outcome (and the read-time honest
|
|
1822
|
+
# description synthesis for legacy thin markers can
|
|
1823
|
+
# see exitCode). exitCode may be 0, which is falsy,
|
|
1824
|
+
# so it is guarded separately below.
|
|
1825
|
+
"completedAt", "phase", "exitCode"):
|
|
1820
1826
|
_v = task.get(_f)
|
|
1821
1827
|
if _v not in (None, "", [], {}):
|
|
1822
1828
|
task_entry[_f] = _v
|
|
1829
|
+
# exitCode == 0 is a real, meaningful value but falsy; the
|
|
1830
|
+
# loop above drops it via the "not in" filter, so carry it
|
|
1831
|
+
# explicitly when the source record has the key at all.
|
|
1832
|
+
if "exitCode" in task and task.get("exitCode") == 0:
|
|
1833
|
+
task_entry["exitCode"] = 0
|
|
1834
|
+
# v7.104.3: preserve the record's OWN terminal outcome. Both
|
|
1835
|
+
# the "completed" and "failed" groups map to the "done"
|
|
1836
|
+
# column, so `status` alone can no longer tell success from
|
|
1837
|
+
# failure. The honest-description synthesis and the dedup
|
|
1838
|
+
# tie-break both need to distinguish them, and NEVER call a
|
|
1839
|
+
# failed iteration "completed" (fake-green). Derived from the
|
|
1840
|
+
# source group key, which is authoritative.
|
|
1841
|
+
if group_key in ("completed", "failed"):
|
|
1842
|
+
task_entry["_terminal_outcome"] = group_key
|
|
1823
1843
|
all_tasks.append(task_entry)
|
|
1824
1844
|
except (json.JSONDecodeError, KeyError):
|
|
1825
1845
|
pass
|
|
@@ -1846,12 +1866,32 @@ async def list_tasks(
|
|
|
1846
1866
|
items = raw_items
|
|
1847
1867
|
else:
|
|
1848
1868
|
items = []
|
|
1869
|
+
# v7.104.3: which terminal outcome (if any) does THIS queue
|
|
1870
|
+
# file represent? completed.json -> completed; failed/dead-
|
|
1871
|
+
# letter -> failed. Non-terminal files (pending/in-progress)
|
|
1872
|
+
# have none.
|
|
1873
|
+
_qf_terminal = None
|
|
1874
|
+
if queue_file == "completed.json":
|
|
1875
|
+
_qf_terminal = "completed"
|
|
1876
|
+
elif queue_file in ("failed.json", "dead-letter.json"):
|
|
1877
|
+
_qf_terminal = "failed"
|
|
1849
1878
|
if isinstance(items, list):
|
|
1850
1879
|
for i, item in enumerate(items):
|
|
1851
1880
|
if isinstance(item, dict):
|
|
1852
1881
|
tid = item.get("id", f"q-{q_status}-{i}")
|
|
1853
|
-
# Skip if already in all_tasks
|
|
1854
|
-
|
|
1882
|
+
# Skip if already in all_tasks -- EXCEPT for
|
|
1883
|
+
# terminal (completed/failed) records. v7.104.3:
|
|
1884
|
+
# the old unconditional skip dropped a failed
|
|
1885
|
+
# sibling before it could be tagged with
|
|
1886
|
+
# _terminal_outcome, so the failed-outranks-
|
|
1887
|
+
# completed dedup below never saw it and a
|
|
1888
|
+
# completed card masked a real failure (a
|
|
1889
|
+
# fake-green from the queue source). Let terminal
|
|
1890
|
+
# records always enter, tagged; the global dedup
|
|
1891
|
+
# then resolves the collision honestly. Non-
|
|
1892
|
+
# terminal records keep the skip (they cannot
|
|
1893
|
+
# override an existing terminal entry anyway).
|
|
1894
|
+
if _qf_terminal is None and any(t["id"] == tid for t in all_tasks):
|
|
1855
1895
|
continue
|
|
1856
1896
|
task_entry = {
|
|
1857
1897
|
"id": tid,
|
|
@@ -1876,6 +1916,20 @@ async def list_tasks(
|
|
|
1876
1916
|
task_entry["notes"] = item["notes"]
|
|
1877
1917
|
if isinstance(item.get("logs"), list):
|
|
1878
1918
|
task_entry["logs"] = item["logs"]
|
|
1919
|
+
# v7.104.3: iteration terminal fields + the
|
|
1920
|
+
# source-file terminal outcome, so the honest
|
|
1921
|
+
# description synthesis can distinguish a clean
|
|
1922
|
+
# exit from a failure and never mislabels a
|
|
1923
|
+
# failed iteration "completed".
|
|
1924
|
+
for _qf in ("provider", "startedAt", "completedAt", "phase"):
|
|
1925
|
+
if item.get(_qf) not in (None, "", [], {}):
|
|
1926
|
+
task_entry[_qf] = item[_qf]
|
|
1927
|
+
if "exitCode" in item and isinstance(item.get("exitCode"), int):
|
|
1928
|
+
task_entry["exitCode"] = item["exitCode"]
|
|
1929
|
+
if queue_file == "completed.json":
|
|
1930
|
+
task_entry["_terminal_outcome"] = "completed"
|
|
1931
|
+
elif queue_file in ("failed.json", "dead-letter.json"):
|
|
1932
|
+
task_entry["_terminal_outcome"] = "failed"
|
|
1879
1933
|
all_tasks.append(task_entry)
|
|
1880
1934
|
except (json.JSONDecodeError, KeyError):
|
|
1881
1935
|
pass
|
|
@@ -1903,6 +1957,95 @@ async def list_tasks(
|
|
|
1903
1957
|
except Exception:
|
|
1904
1958
|
pass
|
|
1905
1959
|
|
|
1960
|
+
# v7.104.3 task-list accuracy: global dedup-by-id with a terminal-wins rule.
|
|
1961
|
+
# The dashboard-state.json groups (read first, above) can carry the same id in
|
|
1962
|
+
# more than one column when the underlying queue files hold stale entries -
|
|
1963
|
+
# e.g. real anonima data showed iteration-13 in BOTH inProgress and completed,
|
|
1964
|
+
# and completed listing iteration-1 five times (a pre-fix blind-append run).
|
|
1965
|
+
# The run.sh write-side fix (upsert-by-id + terminal mutual-exclusion) stops
|
|
1966
|
+
# NEW collisions, but the dashboard must render correctly against state that
|
|
1967
|
+
# was written before the fix, or during a partial write. We keep, per id, the
|
|
1968
|
+
# single most-authoritative entry.
|
|
1969
|
+
#
|
|
1970
|
+
# Priority is TERMINAL-WINS, deliberately NOT the naive
|
|
1971
|
+
# in_progress > pending > done: the reported symptom is a completed iteration
|
|
1972
|
+
# still showing in in-progress/todo, so letting in_progress outrank done would
|
|
1973
|
+
# perpetuate exactly that bug. A record that reached a terminal state (has a
|
|
1974
|
+
# completedAt, i.e. status == done) is the truth for that id; among non-terminal
|
|
1975
|
+
# states we fall back to in_progress > review > pending. This is safe globally
|
|
1976
|
+
# here because pending PRD-story ids (prd-NNN) and iteration ids (iteration-N)
|
|
1977
|
+
# never share an id, so no task is legitimately both pending and done.
|
|
1978
|
+
#
|
|
1979
|
+
# Within the terminal (done) column, a same-id collision between a completed
|
|
1980
|
+
# and a failed record must resolve to FAILED, never completed: masking a real
|
|
1981
|
+
# failure behind a success is a fake-green the trust model forbids. So the
|
|
1982
|
+
# authority key is a 2-tuple (column-rank, terminal-outcome-rank) where a
|
|
1983
|
+
# failed terminal record outranks a completed one; non-terminal states carry a
|
|
1984
|
+
# neutral 0 in the second slot.
|
|
1985
|
+
_rank = {"done": 3, "in_progress": 2, "review": 1, "pending": 0}
|
|
1986
|
+
_term = {"failed": 1, "completed": 0}
|
|
1987
|
+
|
|
1988
|
+
def _authority(_task):
|
|
1989
|
+
return (_rank.get(_task.get("status"), -1),
|
|
1990
|
+
_term.get(_task.get("_terminal_outcome"), 0))
|
|
1991
|
+
|
|
1992
|
+
_by_id: dict = {}
|
|
1993
|
+
_order: list = []
|
|
1994
|
+
for _t in all_tasks:
|
|
1995
|
+
_id = _t.get("id")
|
|
1996
|
+
if _id is None:
|
|
1997
|
+
_order.append(_t) # cannot dedup an id-less entry; keep as-is
|
|
1998
|
+
continue
|
|
1999
|
+
_prev = _by_id.get(_id)
|
|
2000
|
+
if _prev is None:
|
|
2001
|
+
_by_id[_id] = _t
|
|
2002
|
+
_order.append(_id)
|
|
2003
|
+
elif _authority(_t) > _authority(_prev):
|
|
2004
|
+
_by_id[_id] = _t # higher-authority state replaces in place (order kept)
|
|
2005
|
+
all_tasks = [t if not isinstance(t, str) else _by_id[t] for t in _order]
|
|
2006
|
+
|
|
2007
|
+
# v7.104.3 task-list accuracy: HONEST render-time description for legacy
|
|
2008
|
+
# iteration cards. Iterations completed BEFORE the run.sh write-side fix were
|
|
2009
|
+
# persisted as thin markers - they carry real captured fields (status,
|
|
2010
|
+
# exitCode, provider, completedAt) but no description or logs, so they render
|
|
2011
|
+
# as empty "Iteration N" cards with nothing inside (the exact symptom the
|
|
2012
|
+
# dashboard showed). These iterations will never re-run, so the write-side
|
|
2013
|
+
# fix cannot backfill them. We synthesize a one-line description at READ time
|
|
2014
|
+
# from the fields that ARE present - never mutating the stored state (no
|
|
2015
|
+
# fabrication, fully reversible) and never inventing an outcome the record
|
|
2016
|
+
# does not attest. New (post-fix) cards already carry a real description and
|
|
2017
|
+
# are left untouched.
|
|
2018
|
+
for _t in all_tasks:
|
|
2019
|
+
if _t.get("type") == "iteration" and not _t.get("description"):
|
|
2020
|
+
_ec = _t.get("exitCode")
|
|
2021
|
+
_prov = _t.get("provider")
|
|
2022
|
+
# The record's OWN terminal outcome (completed vs failed) is
|
|
2023
|
+
# authoritative. Both map to the "done" column, so status alone can
|
|
2024
|
+
# not tell them apart - a failed iteration must NEVER be described as
|
|
2025
|
+
# "completed" (fake-green). Prefer the exit code when it is a real
|
|
2026
|
+
# integer; otherwise fall back to the terminal outcome; and when even
|
|
2027
|
+
# that is absent, use a neutral verb that asserts no success.
|
|
2028
|
+
_failed = (_t.get("_terminal_outcome") == "failed")
|
|
2029
|
+
_outcome = None
|
|
2030
|
+
if isinstance(_ec, int) and _ec == 0 and not _failed:
|
|
2031
|
+
_outcome = "completed cleanly (exit 0)"
|
|
2032
|
+
elif isinstance(_ec, int) and _ec != 0:
|
|
2033
|
+
_outcome = f"failed (exit {_ec})"
|
|
2034
|
+
elif _failed:
|
|
2035
|
+
# failed record without a usable exit code: honest, no exit claim
|
|
2036
|
+
_outcome = "failed"
|
|
2037
|
+
elif _t.get("_terminal_outcome") == "completed":
|
|
2038
|
+
_outcome = "completed"
|
|
2039
|
+
elif _t.get("status") == "done":
|
|
2040
|
+
# terminal but neither outcome nor exit code is known: neutral
|
|
2041
|
+
# verb that does not assert success or failure
|
|
2042
|
+
_outcome = "finished"
|
|
2043
|
+
if _outcome:
|
|
2044
|
+
_desc = f"Iteration {_outcome}"
|
|
2045
|
+
if _prov:
|
|
2046
|
+
_desc += f", built by {_prov}"
|
|
2047
|
+
_t["description"] = _desc + "."
|
|
2048
|
+
|
|
1906
2049
|
# Apply project_id filter if provided
|
|
1907
2050
|
if project_id is not None:
|
|
1908
2051
|
all_tasks = [t for t in all_tasks if t.get("project_id") == project_id]
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v7.104.
|
|
5
|
+
**Version:** v7.104.3
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|