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 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.1
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.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.104.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.104.1
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: warn if the Claude OAuth login has expired. An
9961
- # expired token would otherwise pass and then 401 mid-build (the build
9962
- # stalls at BOOTSTRAP). Mirror run.sh's fail-fast preflight here so a
9963
- # user catches it with `loki doctor` BEFORE starting a build.
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
- if [ -s "$_claude_creds" ] && [ "$(python3 -c "
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)" = "expired" ]; then
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, the claude CLI is on PATH, there is no ANTHROPIC_API_KEY, and no
2112
- # OAuth credentials file exists (the user installed claude but never ran
2113
- # `claude login`), the build would otherwise enter, make a failing call, and
2114
- # 401 -- the worst first impression -- forcing the user to run `loki why`.
2115
- # Fail fast with the one-step fix instead. Opt out with LOKI_SKIP_AUTH_PREFLIGHT=1.
2116
- if [[ "$provider" == "claude" && "${LOKI_SKIP_AUTH_PREFLIGHT:-}" != "1" && -z "${ANTHROPIC_API_KEY:-}" ]] \
2117
- && command -v claude >/dev/null 2>&1; then
2118
- local _local_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
2119
- if [[ ! -s "$_local_creds" ]]; then
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
- # Create completed task entry
6400
- local task_json=$(cat <<EOF
6401
- {
6402
- "id": "$task_id",
6403
- "type": "iteration",
6404
- "title": "Iteration $iteration",
6405
- "status": "$([ "$exit_code" = "0" ] && echo "completed" || echo "failed")",
6406
- "completedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
6407
- "exitCode": $exit_code,
6408
- "provider": "${PROVIDER_NAME:-claude}"
6409
- }
6410
- EOF
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
- [ "$exit_code" != "0" ] && target_file="$failed_file"
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 sys, json
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
- with open('$target_file', 'r') as f:
6421
- data = json.load(f)
6422
- except:
6423
- data = []
6424
- data.append($task_json)
6425
- # Keep only last 50 entries
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('$target_file', 'w') as f:
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
- # Remove from in-progress
6432
- if [ -f "$in_progress_file" ]; then
6433
- python3 -c "
6434
- import sys, json
6435
- try:
6436
- with open('$in_progress_file', 'r') as f:
6437
- data = json.load(f)
6438
- data = [t for t in data if t.get('id') != '$task_id']
6439
- with open('$in_progress_file', 'w') as f:
6440
- json.dump(data, f, indent=2)
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.104.1"
10
+ __version__ = "7.104.3"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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
- if any(t["id"] == tid for t in all_tasks):
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]
@@ -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.1
5
+ **Version:** v7.104.3
6
6
 
7
7
  ---
8
8