loki-mode 8.0.3 → 8.2.0

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/README.md CHANGED
@@ -15,7 +15,7 @@ _The free, source-available autonomous coding agent by [Autonomi](https://www.au
15
15
 
16
16
  [Website](https://www.autonomi.dev/) | [Documentation](wiki/Home.md) | [Installation](docs/INSTALLATION.md) | [Changelog](CHANGELOG.md) | [Purple Lab -- deprecated v7.44.0](#purple-lab)
17
17
 
18
- **Current release: v8.0.0**
18
+ **Current release: v8.2.0**
19
19
 
20
20
  </div>
21
21
 
@@ -25,6 +25,37 @@ _The free, source-available autonomous coding agent by [Autonomi](https://www.au
25
25
 
26
26
  ---
27
27
 
28
+ ## Already have a codebase? Start read-only.
29
+
30
+ Most agents are built to create new apps. The harder, more valuable problem is
31
+ the ten-year-old repo that pays the bills. Loki works on both, and on an
32
+ existing codebase it starts by **changing nothing**:
33
+
34
+ ```bash
35
+ loki modernize heal ./your-repo --assess # read-only. no writes, no commits.
36
+ loki modernize heal ./your-repo --assess --json # same, machine-readable
37
+ ```
38
+
39
+ You get a modernization readiness report: language mix, a 4-level maturity
40
+ rating, technical-debt signals (test coverage, TODO density, oversized files,
41
+ dependency staleness), and a **ranked list of where to start** -- ordered by
42
+ blast radius, so the first change is the one least likely to break something.
43
+
44
+ Then, if you want it to act:
45
+
46
+ ```bash
47
+ loki modernize heal ./your-repo --strict # block ALL behavioral change without approval
48
+ loki modernize heal ./your-repo --phase archaeology # extract knowledge only
49
+ loki modernize heal ./your-repo --compliance healthcare # or fintech | government
50
+ ```
51
+
52
+ The healing pipeline runs in phases -- archaeology, stabilize, isolate,
53
+ modernize, validate -- and the validate phase checks **behavioral equivalence
54
+ against the pre-change baseline**, not just that the tests are green. Friction
55
+ points (the weird code that exists for a reason nobody remembers) are cataloged
56
+ before anything touches them, because in a legacy system the strange code is
57
+ usually load-bearing.
58
+
28
59
  ## The Evidence Receipt: don't trust the agent, check it
29
60
 
30
61
  Every coding agent tells you it finished. Loki hands you something you can
@@ -239,18 +270,31 @@ This needs Bun on your PATH (the SDK loop runs on the Bun runtime). `loki doctor
239
270
 
240
271
  **With a coding-agent CLI.** The classic path, and still the default: Loki drives a separate CLI (Claude Code is the recommended one) plus a couple of common tools on your PATH.
241
272
 
242
- **With a different model or provider.** Loki is not tied to Anthropic. Anything
243
- that speaks the Anthropic Messages API works: OpenRouter, Ollama, LiteLLM, vLLM,
244
- or your own gateway. Point `ANTHROPIC_BASE_URL` at the endpoint and name the
245
- model with `LOKI_MODEL_OVERRIDE`:
273
+ **With a different model or provider.** Loki is not tied to Anthropic, but
274
+ *how* you reach another model depends on which API the endpoint speaks. There
275
+ are two routes, and picking the wrong one fails confusingly.
276
+
277
+ *Route 1 -- OpenAI-shaped endpoints (OpenRouter, and most hosted open models).*
278
+ Use a provider that speaks that API natively. `aider` and `cline` both do, and
279
+ Loki now defaults them to open-weight models rather than Claude:
246
280
 
247
281
  ```bash
248
- # OpenRouter (hundreds of models, one key)
249
- export ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1
250
- export ANTHROPIC_API_KEY=sk-or-...
251
- export LOKI_MODEL_OVERRIDE=<model-id-from-openrouter.ai/models>
252
- loki start prd.md
282
+ loki provider set aider
283
+ export OPENROUTER_API_KEY=sk-or-...
284
+ loki start prd.md # defaults to deepseek-v3.2
253
285
 
286
+ export LOKI_AIDER_MODEL=openrouter/z-ai/glm-4.6 # or pick your own
287
+ ```
288
+
289
+ OpenRouter serves **only** the OpenAI-shaped `/v1/chat/completions`; it has no
290
+ Anthropic `/v1/messages` endpoint. Pointing `ANTHROPIC_BASE_URL` at it does not
291
+ work, which earlier versions of this README incorrectly suggested.
292
+
293
+ *Route 2 -- Anthropic-protocol gateways.* `ANTHROPIC_BASE_URL` routes Claude
294
+ Code itself, so the endpoint must speak the Anthropic Messages API. LiteLLM,
295
+ Bedrock proxies, and self-hosted gateways can:
296
+
297
+ ```bash
254
298
  # Ollama, fully local (no API key, no per-token cost)
255
299
  export ANTHROPIC_BASE_URL=http://localhost:11434/v1
256
300
  export LOKI_MODEL_OVERRIDE=<model you have pulled, e.g. the output of `ollama list`>
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 v8.0.3
6
+ # Loki Mode v8.2.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -469,4 +469,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
469
469
 
470
470
  ---
471
471
 
472
- **v8.0.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.2.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.0.3
1
+ 8.2.0
@@ -2267,11 +2267,17 @@ PYEOF
2267
2267
  if [ -z "$_nm_cc_dir" ] || [ ! -f "$_nm_scanner" ]; then
2268
2268
  _nm_status="INCONCLUSIVE:scanner_unavailable"
2269
2269
  else
2270
+ # STDIN, not an env var: a single env string is capped at
2271
+ # MAX_ARG_STRLEN (131071 bytes) on Linux, so a large changed-file
2272
+ # union makes execve fail with E2BIG and this gate silently
2273
+ # degrades to inconclusive (pass-through). macOS has no per-string
2274
+ # cap, so that failure mode was Linux-only.
2270
2275
  _nm_status=$(
2271
- _NM_FILES="$_nm_files" \
2272
- _NM_TREE="." \
2273
- python3 -I "$_nm_scanner" 2>/dev/null \
2274
- || echo "INCONCLUSIVE:detector_error"
2276
+ printf '%s\n' "$_nm_files" | {
2277
+ _NM_TREE="." \
2278
+ python3 -I "$_nm_scanner" 2>/dev/null \
2279
+ || echo "INCONCLUSIVE:detector_error"
2280
+ }
2275
2281
  )
2276
2282
  fi
2277
2283
  case "$_nm_status" in
@@ -3135,6 +3141,25 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
3135
3141
  _provider_rc=$?
3136
3142
  fi
3137
3143
  ;;
3144
+ *)
3145
+ # v8.2.0 TIMEOUT SEAM. Any provider exposing provider_invoke_argv can
3146
+ # cast a real council vote instead of falling straight to the
3147
+ # heuristic. argv is a REAL command, so `timeout` bounds it exactly as
3148
+ # it bounds the named arms above (providers/claude.sh:321).
3149
+ #
3150
+ # SEMANTICS PRESERVED: _provider_rc is captured the same way, so the
3151
+ # bash-F4 safe default below still forces a conservative REJECT on a
3152
+ # timeout kill (124/137/143). An empty verdict falls to
3153
+ # council_heuristic_review, identical to a missing CLI today.
3154
+ if type provider_invoke_argv >/dev/null 2>&1; then
3155
+ provider_invoke_argv fast "$prompt"
3156
+ # caveman HARD-SUPPRESS: this vote is parsed for "VOTE:".
3157
+ verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
3158
+ env CAVEMAN_DEFAULT_MODE=off \
3159
+ "${_LOKI_INVOKE_ARGV[@]+"${_LOKI_INVOKE_ARGV[@]}"}" 2>/dev/null)
3160
+ _provider_rc=$?
3161
+ fi
3162
+ ;;
3138
3163
  esac
3139
3164
 
3140
3165
  # bash-F4 (WAVE10 SAFE-DEFAULT): a provider timeout (124, incl. 128+SIGTERM
@@ -3303,6 +3328,22 @@ REASON: your reasoning"
3303
3328
  verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null)
3304
3329
  fi
3305
3330
  ;;
3331
+ *)
3332
+ # v8.2.0 TIMEOUT SEAM (contrarian / devil's-advocate vote). Same
3333
+ # rationale as the member vote: a real argv keeps the `timeout`
3334
+ # bound that a shell function would silently remove.
3335
+ #
3336
+ # SEMANTICS PRESERVED: this path tracks no _provider_rc by design --
3337
+ # an empty verdict (timeout or failure) already routes to the
3338
+ # conservative REJECT fallback immediately below.
3339
+ if type provider_invoke_argv >/dev/null 2>&1; then
3340
+ provider_invoke_argv fast "$prompt"
3341
+ # caveman HARD-SUPPRESS: parsed for "VOTE:".
3342
+ verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" \
3343
+ env CAVEMAN_DEFAULT_MODE=off \
3344
+ "${_LOKI_INVOKE_ARGV[@]+"${_LOKI_INVOKE_ARGV[@]}"}" 2>/dev/null)
3345
+ fi
3346
+ ;;
3306
3347
  esac
3307
3348
 
3308
3349
  if [ -z "$verdict" ]; then
@@ -82,6 +82,9 @@ council_v2_vote() {
82
82
  local first=true
83
83
  local approve_count=0
84
84
  local reject_count=0
85
+ # Reviewers we could not obtain a verdict from. Never folded into
86
+ # reject_count -- see the TRUST comment in the tally below.
87
+ local inconclusive_count=0
85
88
  for vote_file in "${vote_files[@]}"; do
86
89
  if [ -f "$vote_file" ]; then
87
90
  local vote_content
@@ -95,16 +98,40 @@ council_v2_vote() {
95
98
 
96
99
  local verdict
97
100
  verdict=$(echo "$vote_content" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "UNKNOWN")
101
+ # TRUST: a reviewer that could not be REACHED did not vote REJECT.
102
+ #
103
+ # Every provider arm below used to substitute a literal
104
+ # {"verdict":"REJECT","reasoning":"review execution failed"} on any
105
+ # miss -- CLI absent, timeout, transient error, unparseable output --
106
+ # and this tally counted "anything not APPROVE" as a rejection. The
107
+ # engine therefore fabricated a reviewer vote the model never gave,
108
+ # and the run was BLOCKED by it.
109
+ #
110
+ # That is the same defect class as a receipt attesting to a diff stat
111
+ # it did not measure: the artifact claims a fact nobody established.
112
+ # On a weak model that formats poorly it is worse still -- low format
113
+ # compliance becomes a permanent BLOCK that reads to the user as
114
+ # "Loki is broken" rather than "your model could not answer".
115
+ #
116
+ # INCONCLUSIVE is counted separately and never as a rejection.
98
117
  if [ "$verdict" = "APPROVE" ]; then
99
118
  ((approve_count++))
100
- else
119
+ elif [ "$verdict" = "REJECT" ]; then
101
120
  ((reject_count++))
121
+ else
122
+ ((inconclusive_count++))
102
123
  fi
103
124
  fi
104
125
  done
105
126
  votes_json="$votes_json]"
106
127
 
107
- log_info "Blind review results: $approve_count APPROVE / $reject_count REJECT"
128
+ if [ "$inconclusive_count" -gt 0 ]; then
129
+ log_warn "Blind review results: $approve_count APPROVE / $reject_count REJECT / $inconclusive_count INCONCLUSIVE"
130
+ log_warn " $inconclusive_count reviewer(s) produced no verdict (CLI missing, timeout, or unparseable output)."
131
+ log_warn " These are NOT rejections. The council decides on the verdicts it actually obtained."
132
+ else
133
+ log_info "Blind review results: $approve_count APPROVE / $reject_count REJECT"
134
+ fi
108
135
 
109
136
  # Step 4: Sycophancy detection
110
137
  local sycophancy_score
@@ -137,8 +164,13 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
137
164
 
138
165
  if [ -f "$da_vote" ]; then
139
166
  local da_verdict
140
- da_verdict=$(cat "$da_vote" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "REJECT")
141
- if [ "$da_verdict" = "REJECT" ]; then
167
+ # Same trust rule as the main tally: an unparseable devil's
168
+ # advocate did not vote REJECT. Defaulting to REJECT here would
169
+ # silently overturn a unanimous APPROVE on a transient failure.
170
+ da_verdict=$(cat "$da_vote" | python3 -c "import sys,json; print(json.load(sys.stdin).get('verdict','').upper())" 2>/dev/null || echo "INCONCLUSIVE")
171
+ if [ "$da_verdict" = "INCONCLUSIVE" ]; then
172
+ log_warn "Devil's advocate produced no verdict -- unanimous approval left UNCHANGED (not overturned)"
173
+ elif [ "$da_verdict" = "REJECT" ]; then
142
174
  log_warn "Devil's advocate REJECTED unanimous approval"
143
175
  approve_count=$((approve_count - 1))
144
176
  reject_count=$((reject_count + 1))
@@ -374,42 +406,64 @@ except Exception:
374
406
  fi
375
407
  # Fall through to the plain text call (+ sed-carve) on any miss.
376
408
  if [ -z "$result" ]; then
377
- result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
409
+ result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
378
410
  fi
379
411
  else
380
- result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
412
+ result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
381
413
  fi
382
414
  ;;
383
415
  codex)
384
416
  if command -v codex &>/dev/null; then
385
- result=$(codex exec -q "$full_prompt" 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
417
+ result=$(codex exec -q "$full_prompt" 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
386
418
  else
387
- result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
419
+ result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
388
420
  fi
389
421
  ;;
390
422
  gemini)
391
423
  if command -v gemini &>/dev/null; then
392
- result=$(echo "$full_prompt" | gemini 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
424
+ result=$(echo "$full_prompt" | gemini 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
393
425
  else
394
- result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
426
+ result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
395
427
  fi
396
428
  ;;
397
429
  cline)
398
430
  if command -v cline &>/dev/null; then
399
- result=$(cline -y "$full_prompt" 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
431
+ result=$(cline -y "$full_prompt" 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
400
432
  else
401
- result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
433
+ result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
402
434
  fi
403
435
  ;;
404
436
  aider)
405
437
  if command -v aider &>/dev/null; then
406
- result=$(aider --message "$full_prompt" --yes-always --no-auto-commits --no-git 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
438
+ result=$(aider --message "$full_prompt" --yes-always --no-auto-commits --no-git 2>/dev/null || echo '{"verdict":"INCONCLUSIVE","reasoning":"review execution failed (no verdict obtained; NOT a rejection)","issues":[]}')
407
439
  else
408
- result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
440
+ result='{"verdict":"INCONCLUSIVE","reasoning":"reviewer CLI unavailable (no verdict obtained; NOT a rejection)","issues":[]}'
409
441
  fi
410
442
  ;;
411
443
  *)
412
- result='{"verdict":"REJECT","reasoning":"review not supported for this provider","issues":[]}'
444
+ # v8.2.0 TIMEOUT SEAM. A provider exposing provider_invoke_argv gets
445
+ # a real reviewer instead of an automatic INCONCLUSIVE. The argv is a
446
+ # REAL command so `timeout` genuinely bounds it (providers/claude.sh:321);
447
+ # routing this through a shell function would silently drop the bound.
448
+ #
449
+ # VERDICT SEMANTICS UNCHANGED: on empty output, non-zero exit, or a
450
+ # timeout kill (124/137/143) we fall back to the SAME INCONCLUSIVE
451
+ # string used below -- never REJECT. A judge that produced no
452
+ # judgement has not rejected anything (v8.1.0 TRUST-3).
453
+ result=''
454
+ if type provider_invoke_argv >/dev/null 2>&1; then
455
+ local _c2_seam_rc=0
456
+ provider_invoke_argv fast "$full_prompt"
457
+ # caveman HARD-SUPPRESS: this verdict is parsed for the JSON
458
+ # "verdict" field; compression would reword it.
459
+ result="$(timeout "${LOKI_SDK_REVIEW_TIMEOUT:-180}" \
460
+ env CAVEMAN_DEFAULT_MODE=off \
461
+ "${_LOKI_INVOKE_ARGV[@]+"${_LOKI_INVOKE_ARGV[@]}"}" 2>/dev/null)" || _c2_seam_rc=$?
462
+ [ "$_c2_seam_rc" -ne 0 ] && result=''
463
+ fi
464
+ if [ -z "$result" ]; then
465
+ result='{"verdict":"INCONCLUSIVE","reasoning":"review not supported for this provider (no verdict obtained; NOT a rejection)","issues":[]}'
466
+ fi
413
467
  ;;
414
468
  esac
415
469
 
@@ -420,8 +474,52 @@ except Exception:
420
474
  # Try removing markdown fencing
421
475
  extracted=$(echo "$result" | sed 's/^```json//;s/^```//' | sed -n '/^{/,/^}/p' | head -50)
422
476
  fi
477
+ # STRUCTURE-TOLERANT RECOVERY (v8.2.0).
478
+ #
479
+ # A strict JSON carve is the single most model-sensitive contract in the
480
+ # engine: schema adherence is exactly what varies most across models, while
481
+ # every coding model can state a verdict in prose. Measured elsewhere (Forge
482
+ # replication): scaffolding drove tool-call errors 42 -> 0 while
483
+ # advanced-reasoning accuracy stayed flat -- i.e. a harness CAN rescue
484
+ # format compliance but cannot manufacture judgment. So recovering a verdict
485
+ # the model genuinely expressed is legitimate; inventing one is not.
486
+ #
487
+ # This runs ONLY when the JSON carve produced nothing, and it accepts a
488
+ # verdict only when the model stated it UNAMBIGUOUSLY (exactly one of
489
+ # APPROVE/REJECT appears as a standalone word). A response mentioning both,
490
+ # or neither, stays INCONCLUSIVE -- never guessed.
491
+ if [ -z "$extracted" ] && [ -n "${result:-}" ]; then
492
+ local _recovered
493
+ _recovered="$(printf '%s' "$result" | _LOKI_RAW="$result" python3 -c '
494
+ import os, re, sys, json
495
+ raw = os.environ.get("_LOKI_RAW", "")
496
+ # Standalone words only: "APPROVE" not "approved-by", and not inside a URL.
497
+ approve = len(re.findall(r"(?<![A-Za-z0-9_-])APPROVE(?![A-Za-z0-9_-])", raw, re.I))
498
+ reject = len(re.findall(r"(?<![A-Za-z0-9_-])REJECT(?![A-Za-z0-9_-])", raw, re.I))
499
+ if approve and not reject:
500
+ v = "APPROVE"
501
+ elif reject and not approve:
502
+ v = "REJECT"
503
+ else:
504
+ sys.exit(1) # ambiguous or absent -> stay inconclusive
505
+ print(json.dumps({
506
+ "verdict": v,
507
+ "reasoning": "recovered from unstructured output (model stated %s in prose)" % v,
508
+ "issues": [],
509
+ "recovered": True,
510
+ }))
511
+ ' 2>/dev/null)" || _recovered=""
512
+ if [ -n "$_recovered" ]; then
513
+ extracted="$_recovered"
514
+ fi
515
+ fi
516
+
423
517
  if [ -z "$extracted" ]; then
424
- extracted='{"verdict":"REJECT","reasoning":"failed to parse review output","issues":[]}'
518
+ # The weak-model case, and the reason this matters most: a model whose
519
+ # prose could not be carved into JSON did not vote REJECT. Recording one
520
+ # here would turn "your model formats poorly" into "your code was
521
+ # rejected" -- a block the user cannot act on and did not earn.
522
+ extracted='{"verdict":"INCONCLUSIVE","reasoning":"failed to parse review output (no verdict obtained; NOT a rejection)","issues":[]}'
425
523
  fi
426
524
 
427
525
  echo "$extracted" > "$output_file"
package/autonomy/grill.sh CHANGED
@@ -158,6 +158,12 @@ grill_check_provider() {
158
158
  fi
159
159
  ;;
160
160
  *)
161
+ # v8.2.0: capability, not identity. Must stay in lockstep with the
162
+ # matching arm in grill_invoke_provider -- this gate runs FIRST, so
163
+ # rejecting here would make that arm dead code.
164
+ if type provider_invoke_argv >/dev/null 2>&1; then
165
+ return 0
166
+ fi
161
167
  _grill_err "grill currently supports the claude and codex providers (got: $provider)"
162
168
  return $GRILL_EXIT_ERROR
163
169
  ;;
@@ -278,6 +284,32 @@ grill_invoke_provider() {
278
284
  return 0
279
285
  ;;
280
286
  *)
287
+ # v8.2.0 TIMEOUT SEAM. Previously any other provider was a hard
288
+ # error. A provider exposing provider_invoke_argv builds a REAL argv
289
+ # (not a shell function), so _grill_with_timeout still bounds it --
290
+ # the whole reason the seam exists (providers/claude.sh:321).
291
+ # Nothing to preserve on this arm, so nothing can regress: it
292
+ # produced no output at all before.
293
+ #
294
+ # Deliberate: no --disallowedTools. That flag is claude-specific with
295
+ # no portable equivalent, so a seam-provider grill CAN write to the
296
+ # tree. Accepted; the alternative is the hard error below.
297
+ if type provider_invoke_argv >/dev/null 2>&1; then
298
+ local out
299
+ provider_invoke_argv fast "$prompt"
300
+ # env CAVEMAN_DEFAULT_MODE=off: grill output is parsed downstream
301
+ # and caveman compression would reword the questions. `env` (not a
302
+ # bare prefix) because _grill_with_timeout execs its first token.
303
+ out="$(_grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" \
304
+ env CAVEMAN_DEFAULT_MODE=off \
305
+ "${_LOKI_INVOKE_ARGV[@]+"${_LOKI_INVOKE_ARGV[@]}"}" 2>/dev/null)"
306
+ if [ -z "$out" ]; then
307
+ _grill_err "provider returned no output (timeout or invocation error)"
308
+ return $GRILL_EXIT_ERROR
309
+ fi
310
+ printf '%s\n' "$out"
311
+ return 0
312
+ fi
281
313
  _grill_err "grill currently supports the claude and codex providers (got: $provider)"
282
314
  return $GRILL_EXIT_ERROR
283
315
  ;;
@@ -159,12 +159,24 @@ except Exception:
159
159
  return 0
160
160
  }
161
161
 
162
- # Decide whether model verification can be attempted. Returns 0 (ok) only when
163
- # the active provider is claude and not degraded. Mirrors
164
- # _loki_prd_enrich_provider_ok (autonomy/lib/prd-enrich.sh:65).
162
+ # Decide whether model verification can be attempted.
163
+ #
164
+ # v8.2.0: this used to require LOKI_PROVIDER=claude specifically, so a user on
165
+ # codex/opencode/cline/aider silently lost done-recognition. That is a
166
+ # CAPABILITY question, not an identity question -- the real requirement is "can
167
+ # we reach a model AND bound the call with a timeout". Any provider exposing the
168
+ # argv seam (provider_invoke_argv, see providers/claude.sh) satisfies both.
169
+ #
170
+ # Falls back to the historical claude-binary check when the seam is absent, so
171
+ # nothing regresses for existing installs.
165
172
  _loki_done_recog_provider_ok() {
166
- [ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
167
173
  [ "${PROVIDER_DEGRADED:-false}" != "true" ] || return 1
174
+ # Preferred: a provider that can build a timeout-able argv.
175
+ if type provider_invoke_argv >/dev/null 2>&1; then
176
+ return 0
177
+ fi
178
+ # Legacy path: claude binary present.
179
+ [ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
168
180
  command -v claude >/dev/null 2>&1 || return 1
169
181
  return 0
170
182
  }