loki-mode 8.70.0 → 8.72.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/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.70.0
6
+ # Loki Mode v8.72.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.70.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
472
+ **v8.72.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 8.70.0
1
+ 8.72.0
package/autonomy/loki CHANGED
@@ -4137,6 +4137,85 @@ cmd_ship() {
4137
4137
  return 0
4138
4138
  }
4139
4139
 
4140
+ # Per-error_class remediation, shared by BOTH cmd_why paths (human + --json).
4141
+ #
4142
+ # WHY THIS IS A SHELL VARIABLE. cmd_why runs two SEPARATE python3 heredocs, so a
4143
+ # helper defined in one is invisible to the other. Copy-pasting the map into
4144
+ # both is exactly the drift this file warns against everywhere else (see the
4145
+ # GUIDE / _loki_next_action "kept in lockstep" comments). Defining it once and
4146
+ # injecting it into both makes divergence impossible rather than merely
4147
+ # discouraged.
4148
+ #
4149
+ # SCOPE. The GUIDE map keys on run STATUS (council_approved / failed / ...).
4150
+ # This keys on the LAST_ERROR error_class -- a different, orthogonal axis. A
4151
+ # `failed` run got the same "read the logs" line whether the cause was a 401 or
4152
+ # a timeout; this is what makes the two cases differ.
4153
+ #
4154
+ # run.sh WRITES the classification (_loki_classify_iteration_error, :1857) and
4155
+ # injects a heal hint naming the class (:1889), but neither carries remediation
4156
+ # text -- so there is no second surface to mirror. This is the sole home for the
4157
+ # wording. If run.sh ever gains one, keep them in lockstep.
4158
+ #
4159
+ # HONESTY. last_error_action() returns None for any class not in the map --
4160
+ # including "unknown", which run.sh writes when it will not guess. A None means
4161
+ # the caller must print the recorded brief verbatim and infer NOTHING. Never add
4162
+ # a fallback action here: a generic string on an unknown class is a fabricated
4163
+ # diagnosis wearing an action's clothes.
4164
+ read -r -d '' _LOKI_WHY_ACTIONS_PY <<'WHYACTIONS' || true
4165
+ # error_class -> (action). Each is a command or env var the reader can act on.
4166
+ LAST_ERROR_ACTIONS = {
4167
+ "rate_limited":
4168
+ "Wait for the provider limit to reset, then re-run: loki start <spec>",
4169
+ "build_timeout":
4170
+ "Raise the per-iteration limit, or narrow the spec so one iteration does less:\n"
4171
+ "LOKI_ITERATION_TIMEOUT=3600 loki start <spec>",
4172
+ "provider_empty_output":
4173
+ "The provider returned nothing. Check provider health first: loki doctor\n"
4174
+ "then re-run. If doctor is clean, try another provider: LOKI_PROVIDER=<name> loki start <spec>",
4175
+ }
4176
+
4177
+ # Auth remediation is PROVIDER-SPECIFIC and the LAST_ERROR schema records no
4178
+ # provider. So we use LOKI_PROVIDER when it is set, and when it is not we list
4179
+ # every option LABELLED rather than presenting one provider's fix as the answer.
4180
+ PROVIDER_AUTH = {
4181
+ "claude": "claude login (or set ANTHROPIC_API_KEY)",
4182
+ "codex": "codex login (or set OPENAI_API_KEY)",
4183
+ "aider": "set the API key for your aider model (OPENAI_API_KEY / ANTHROPIC_API_KEY)",
4184
+ "cline": "re-enter the API key in Cline's provider settings",
4185
+ "opencode": "opencode auth login",
4186
+ }
4187
+
4188
+
4189
+ def last_error_action(rec, provider=""):
4190
+ """Concrete next action for a LAST_ERROR record, or None if unrecognized.
4191
+
4192
+ None is a real answer: it means we have no mapped action and the caller must
4193
+ fall back to the recorded brief verbatim. Do not turn it into a default.
4194
+ """
4195
+ if not isinstance(rec, dict):
4196
+ return None
4197
+ ec = str(rec.get("error_class") or "").strip()
4198
+ if ec == "auth_error":
4199
+ p = str(provider or "").strip().lower()
4200
+ if p in PROVIDER_AUTH:
4201
+ return "Re-authenticate %s: %s" % (p, PROVIDER_AUTH[p])
4202
+ lines = ["Re-authenticate the provider in use (LOKI_PROVIDER is unset, so "
4203
+ "the provider was not recorded):"]
4204
+ for _n in sorted(PROVIDER_AUTH):
4205
+ lines.append(" %-9s %s" % (_n, PROVIDER_AUTH[_n]))
4206
+ return "\n".join(lines)
4207
+ action = LAST_ERROR_ACTIONS.get(ec)
4208
+ if action is None:
4209
+ return None
4210
+ if ec == "rate_limited":
4211
+ # retry_after is NOT in the documented schema. Read it defensively and
4212
+ # print nothing when absent -- a placeholder would read as measured.
4213
+ _r = rec.get("retry_after", rec.get("retry_after_seconds"))
4214
+ if _r not in (None, ""):
4215
+ action += "\nProvider reported retry-after: %s" % _r
4216
+ return action
4217
+ WHYACTIONS
4218
+
4140
4219
  # loki why -- actionable failure/outcome diagnosis (B5).
4141
4220
  # Reads the already-captured run artifacts (no new state): the terminal run state
4142
4221
  # (.loki/<autonomy-state>.json: status, lastExitCode, iterationCount), the durable
@@ -4173,8 +4252,11 @@ cmd_why() {
4173
4252
  _LOKI_WHY_STATE="$state_file" _LOKI_WHY_COMPLETION="$completion_file" \
4174
4253
  _LOKI_WHY_HEAD_SHA="$_why_json_head_sha" \
4175
4254
  _LOKI_WHY_LAST_ERROR="$loki_dir/state/LAST_ERROR.json" \
4255
+ _LOKI_WHY_ACTIONS_PY="$_LOKI_WHY_ACTIONS_PY" \
4176
4256
  _LOKI_WHY_EFFICIENCY="$loki_dir/metrics/efficiency" python3 - <<'WHYJSON'
4177
4257
  import json, os
4258
+ # Shared per-class action map (single definition; see _LOKI_WHY_ACTIONS_PY).
4259
+ exec(os.environ.get("_LOKI_WHY_ACTIONS_PY", ""))
4178
4260
  def load(p):
4179
4261
  try:
4180
4262
  with open(p) as f: return json.load(f)
@@ -4235,10 +4317,38 @@ if _effdir:
4235
4317
  except Exception:
4236
4318
  _rework = None
4237
4319
 
4320
+ # The machine-readable surface got the raw record and no action, so every
4321
+ # consumer had to re-derive the remediation and drift from the printed report.
4322
+ # Emit the SAME mapping the human path prints, plus an explicit honesty triple
4323
+ # so a consumer can distinguish the three no-action cases instead of seeing one
4324
+ # indistinguishable null:
4325
+ # present=False -> no failure was recorded (NOT "it succeeded")
4326
+ # present=True, readable=False-> a record exists but is malformed
4327
+ # recognized=False -> real record, class we have no action for
4328
+ _le_path = os.environ.get("_LOKI_WHY_LAST_ERROR", "")
4329
+ _le_present = bool(_le_path) and os.path.exists(_le_path)
4330
+ _le_readable = isinstance(last_error, dict) and bool(last_error)
4331
+ if _le_present and not _le_readable:
4332
+ # load() collapses unreadable and absent into {}. Re-read to tell them
4333
+ # apart -- reporting a malformed record as "no failure" is the exact lie
4334
+ # this command exists to prevent.
4335
+ try:
4336
+ with open(_le_path) as _lf:
4337
+ _parsed = json.load(_lf)
4338
+ _le_readable = isinstance(_parsed, dict)
4339
+ except Exception:
4340
+ _le_readable = False
4341
+ _le_action = last_error_action(last_error, os.environ.get("LOKI_PROVIDER", "")) \
4342
+ if _le_readable else None
4343
+
4238
4344
  print(json.dumps({
4239
4345
  "state": state,
4240
4346
  "completion": comp,
4241
4347
  "last_error": last_error,
4348
+ "last_error_present": _le_present,
4349
+ "last_error_readable": _le_readable,
4350
+ "last_error_recognized": _le_action is not None,
4351
+ "last_error_action": _le_action,
4242
4352
  "completion_is_stale": comp_is_stale,
4243
4353
  "head_sha": head_sha or None,
4244
4354
  "rework": _rework,
@@ -4260,8 +4370,11 @@ WHYJSON
4260
4370
  _LOKI_WHY_CONVERGENCE="$loki_dir/council/convergence.log" \
4261
4371
  _LOKI_WHY_GATE="$loki_dir/signals/GATE_ESCALATION.json" \
4262
4372
  _LOKI_WHY_TARGET="${TARGET_DIR:-$(dirname "$loki_dir")}" \
4373
+ _LOKI_WHY_ACTIONS_PY="$_LOKI_WHY_ACTIONS_PY" \
4263
4374
  _LOKI_WHY_HANDOFFS="$loki_dir/memory/handoffs" python3 - <<'WHYTXT'
4264
4375
  import json, os, glob
4376
+ # Shared per-class action map (single definition; see _LOKI_WHY_ACTIONS_PY).
4377
+ exec(os.environ.get("_LOKI_WHY_ACTIONS_PY", ""))
4265
4378
  def load(p):
4266
4379
  try:
4267
4380
  with open(p) as f: return json.load(f)
@@ -4490,11 +4603,29 @@ if _unc and os.path.exists(_unc):
4490
4603
  # (that would be a fake-green-adjacent lie) - using the same SUCCESS set below.
4491
4604
  SUCCESS_STATUSES = {"council_approved", "completion_promise_fulfilled", "complete", "completed"}
4492
4605
  le = {}
4606
+ _le_path = os.environ.get("_LOKI_WHY_LAST_ERROR", "")
4607
+ _le_bad = ""
4493
4608
  try:
4494
- with open(os.environ.get("_LOKI_WHY_LAST_ERROR", "")) as f:
4609
+ with open(_le_path) as f:
4495
4610
  le = json.load(f)
4496
- except Exception:
4611
+ if not isinstance(le, dict):
4612
+ # A bare list/string/null is not a record. Matches the isinstance guard
4613
+ # below rather than silently rendering an empty one.
4614
+ _le_bad = "record is a %s, expected a JSON object" % type(le).__name__
4615
+ le = {}
4616
+ except IOError:
4617
+ le = {} # absent: genuinely no recorded failure. Say nothing here.
4618
+ except ValueError as _e:
4619
+ # Present but unparseable. Reporting silence here would read as "no failure
4620
+ # recorded", which is a different and false claim.
4497
4621
  le = {}
4622
+ _le_bad = str(_e)
4623
+ if _le_bad and status not in SUCCESS_STATUSES:
4624
+ print()
4625
+ print(f" Last error : a failure record exists but could not be read.")
4626
+ print(f" File : {_le_path}")
4627
+ print(f" Reason: {_le_bad}")
4628
+ print(f" Nothing is inferred from an unreadable record.")
4498
4629
  if isinstance(le, dict) and le.get("error_class") and status not in SUCCESS_STATUSES:
4499
4630
  print()
4500
4631
  _it = le.get("iteration")
@@ -4502,6 +4633,24 @@ if isinstance(le, dict) and le.get("error_class") and status not in SUCCESS_STAT
4502
4633
  print(f" Last error : {le.get('error_class')}{_it_s}")
4503
4634
  if le.get("brief"):
4504
4635
  print(f" {le.get('brief')}")
4636
+ # Per-class remediation. Naming the error_class told the user WHAT broke and
4637
+ # nothing about what to do; the GUIDE map above is keyed on run STATUS, so a
4638
+ # failed run got the same generic "read the logs" line whether the cause was
4639
+ # a 401 or a timeout. This maps the CLASS to its own concrete action.
4640
+ #
4641
+ # Kept in lockstep with LAST_ERROR_ACTIONS in the --json path above -- the
4642
+ # two must not drift (same norm as the GUIDE / _loki_next_action pair).
4643
+ # run.sh classifies the error but carries NO remediation wording, so this is
4644
+ # the sole home for these strings.
4645
+ _act = last_error_action(le, os.environ.get("LOKI_PROVIDER", ""))
4646
+ if _act:
4647
+ for _i, _line in enumerate(_act.splitlines()):
4648
+ print(f" {'What to do :' if _i == 0 else ' '} {_line}")
4649
+ else:
4650
+ # Unrecognized class: the brief above is all we honestly know. Say that
4651
+ # rather than emitting a generic action that implies a diagnosis.
4652
+ print(" (Unrecognized error class -- the description above is "
4653
+ "the recorded text, verbatim. No cause is inferred.)")
4505
4654
 
4506
4655
  # Surface the latest structured handoff (already-captured context), honestly.
4507
4656
  hd = sorted(glob.glob(os.path.join(os.environ.get("_LOKI_WHY_HANDOFFS",""), "*.md")))
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "8.70.0"
10
+ __version__ = "8.72.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: