loki-mode 11.17.34 → 11.17.36

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.
Files changed (75) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/lib/proof-generator.py +53 -0
  4. package/autonomy/loki +171 -14
  5. package/completions/_loki +1 -0
  6. package/completions/loki.bash +1 -1
  7. package/dashboard/__init__.py +1 -1
  8. package/docs/CLI-REFERENCE.md +1 -1
  9. package/docs/INSTALLATION.md +1 -1
  10. package/docs/v10/BOARD.md +10 -10
  11. package/docs/v10/DECISIONS.md +8 -0
  12. package/docs/v10/PROGRESS.md +6 -0
  13. package/loki-ts/dist/loki.js +974 -974
  14. package/mcp/__init__.py +1 -1
  15. package/package.json +2 -2
  16. package/packages/control-plane/dist/server.js +461 -419
  17. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  18. package/web-app/dist/assets/{AdminPage-o3gtaCnU.js → AdminPage-B-I1NRtL.js} +1 -1
  19. package/web-app/dist/assets/{Avatar-C02t-Irl.js → Avatar-DT_8Mdba.js} +1 -1
  20. package/web-app/dist/assets/{Badge-BuN80MKq.js → Badge-C09l4W-0.js} +1 -1
  21. package/web-app/dist/assets/{Button-lPXsKVcs.js → Button-aC5gMeUz.js} +1 -1
  22. package/web-app/dist/assets/{CockpitPage-8CNxkZSh.js → CockpitPage-CpH1eekK.js} +1 -1
  23. package/web-app/dist/assets/{ComparePage-j-XhFiwH.js → ComparePage-hM6QwfSW.js} +1 -1
  24. package/web-app/dist/assets/{ErrorBoundary-D8nyc_y_.js → ErrorBoundary-6c3dc7IQ.js} +1 -1
  25. package/web-app/dist/assets/{EvidenceReceiptPanel-DtTSMxZm.js → EvidenceReceiptPanel-DKE4l5Tk.js} +1 -1
  26. package/web-app/dist/assets/{GitHubIssuesPanel-Bzmm3gBY.js → GitHubIssuesPanel-abdxEU9p.js} +1 -1
  27. package/web-app/dist/assets/{GitHubPRsPanel-Dh6vSHLE.js → GitHubPRsPanel-pHo-s2C4.js} +1 -1
  28. package/web-app/dist/assets/{HomePage-CKKPOMOg.js → HomePage-_-344_LT.js} +2 -2
  29. package/web-app/dist/assets/{LoginPage-49FrYkL9.js → LoginPage-frrXakkY.js} +1 -1
  30. package/web-app/dist/assets/{MagicPage-D5psAu69.js → MagicPage-BhafCYFh.js} +1 -1
  31. package/web-app/dist/assets/{MetricsPage-oQXV5q4J.js → MetricsPage-C5zf292O.js} +1 -1
  32. package/web-app/dist/assets/{NotFoundPage-C2hj6k1f.js → NotFoundPage-D6M43Y9l.js} +1 -1
  33. package/web-app/dist/assets/{ProjectPage-Bo8ocjwn.js → ProjectPage-D4kvygK3.js} +3 -3
  34. package/web-app/dist/assets/{ProjectsPage-QUhbz991.js → ProjectsPage-Cu7lmyKY.js} +1 -1
  35. package/web-app/dist/assets/{SettingsPage-6XW8I3LO.js → SettingsPage-8itZwlRh.js} +1 -1
  36. package/web-app/dist/assets/{ShowcasePage-9CHGu9uW.js → ShowcasePage-CYlATNwJ.js} +1 -1
  37. package/web-app/dist/assets/{SystemSettingsPage-gZZK7T45.js → SystemSettingsPage-DBDfGnoQ.js} +1 -1
  38. package/web-app/dist/assets/{TeamsPage-DSlKvZhA.js → TeamsPage-unFdeJaG.js} +1 -1
  39. package/web-app/dist/assets/{TemplatesPage-BVDR5mF2.js → TemplatesPage-c6QupvoO.js} +1 -1
  40. package/web-app/dist/assets/{TerminalOutput-DnOFdmTG.js → TerminalOutput-DzuhSLY5.js} +1 -1
  41. package/web-app/dist/assets/{activity-D22UFJWA.js → activity-C3i3sjXj.js} +1 -1
  42. package/web-app/dist/assets/{bell-B2Mfgp26.js → bell-BXgcpNiL.js} +1 -1
  43. package/web-app/dist/assets/{bot-CTXo6aac.js → bot-gYi5RFNf.js} +1 -1
  44. package/web-app/dist/assets/{check-D9kJod1H.js → check--JEYw3iQ.js} +1 -1
  45. package/web-app/dist/assets/{chevron-left-XI4pnrlY.js → chevron-left-CJAjz61W.js} +1 -1
  46. package/web-app/dist/assets/{circle-alert-CvS3mi0t.js → circle-alert-1GfPVQQS.js} +1 -1
  47. package/web-app/dist/assets/{clock-CCgwQ80p.js → clock-DjpWVOcn.js} +1 -1
  48. package/web-app/dist/assets/{cloud-S-SuEsEp.js → cloud-Cgitkgr5.js} +1 -1
  49. package/web-app/dist/assets/{code-xml-PRnhUKWP.js → code-xml-CxZaU6C9.js} +1 -1
  50. package/web-app/dist/assets/{database-C_a7H_3N.js → database-DOrPow1f.js} +1 -1
  51. package/web-app/dist/assets/{dollar-sign-FDCzdLrB.js → dollar-sign-DqAZbHwb.js} +1 -1
  52. package/web-app/dist/assets/{file-code-corner-28-hDPSO.js → file-code-corner-whhcIwS0.js} +1 -1
  53. package/web-app/dist/assets/{file-plus-c0yygSC8.js → file-plus-Dp5DUu4j.js} +1 -1
  54. package/web-app/dist/assets/{globe-DOA9Y_1n.js → globe-Dq6fTDw8.js} +1 -1
  55. package/web-app/dist/assets/{hammer-Omzy4vB3.js → hammer--bO0oBWq.js} +1 -1
  56. package/web-app/dist/assets/{index-Dp6emuTo.js → index-M9OMp27I.js} +3 -3
  57. package/web-app/dist/assets/{layers-D7ZjWXJQ.js → layers-CDURqqkV.js} +1 -1
  58. package/web-app/dist/assets/{loader-circle-CJ2SKm0h.js → loader-circle-C1bCbfqC.js} +1 -1
  59. package/web-app/dist/assets/{lock-HKmv1nIN.js → lock-DVMr4ren.js} +1 -1
  60. package/web-app/dist/assets/{package-FQl71HNO.js → package-CpLzC77t.js} +1 -1
  61. package/web-app/dist/assets/{plus-DY8mlU6I.js → plus-CXi3LhCN.js} +1 -1
  62. package/web-app/dist/assets/{refresh-cw-CJ_pkRQQ.js → refresh-cw-26KajS9u.js} +1 -1
  63. package/web-app/dist/assets/{rotate-ccw-rGU_udZ1.js → rotate-ccw-DAXQ_5XQ.js} +1 -1
  64. package/web-app/dist/assets/{scroll-text-CeVAfOXM.js → scroll-text-ipwVF4bE.js} +1 -1
  65. package/web-app/dist/assets/{server-D-N_u9OT.js → server-B6Yla6UI.js} +1 -1
  66. package/web-app/dist/assets/{shield-alert-CJfx3HCX.js → shield-alert-BunS6EV9.js} +1 -1
  67. package/web-app/dist/assets/{thumbs-up-BrC-E9mS.js → thumbs-up-X1xmkCyf.js} +1 -1
  68. package/web-app/dist/assets/{trash-2-Bb3mW1oq.js → trash-2-DrY1jsTr.js} +1 -1
  69. package/web-app/dist/assets/{trending-down-CrgUJZLA.js → trending-down-D5A0-8g2.js} +1 -1
  70. package/web-app/dist/assets/{trending-up-Dk_GS57K.js → trending-up-BUVdVPXq.js} +1 -1
  71. package/web-app/dist/assets/{upload-BAxHyFqH.js → upload-BMHttdxa.js} +1 -1
  72. package/web-app/dist/assets/{usePolling-CkTdc2YU.js → usePolling-C108x_AT.js} +1 -1
  73. package/web-app/dist/assets/{user-Dr6XCReg.js → user-C0rsicvA.js} +1 -1
  74. package/web-app/dist/index.html +1 -1
  75. package/web-app/server.py +164 -132
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v11.17.34
6
+ # Loki Mode v11.17.36
7
7
 
8
8
  **Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
9
9
 
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
483
483
 
484
484
  ---
485
485
 
486
- **v11.17.34 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
486
+ **v11.17.36 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.17.34
1
+ 11.17.36
@@ -782,6 +782,8 @@ def _collect_security(loki_dir):
782
782
  make here. Stated explicitly because those two say so in their own
783
783
  docstrings and this one did not, leaving the behaviour to be inferred from
784
784
  silence -- which is the exact failure the honesty ledger exists to prevent.
785
+ A scan that ran and came back clean IS read by _exogenous_corroboration
786
+ (TEN-GATES-10): it can only stand beside a green suite, never cause a gap.
785
787
  """
786
788
  out = {
787
789
  "ran": False, "total": 0, "active": 0, "waived": 0,
@@ -853,6 +855,8 @@ def _collect_functional(loki_dir):
853
855
  NOT change what "Verified" means. Making functional-satisfaction gate the green
854
856
  headline is a trust-semantics product decision (council + founder), the second
855
857
  half of FV-2. Recording it first lets the signal be seen and validated safely.
858
+ Exception (TEN-GATES-10): functional_status verified is read by
859
+ _exogenous_corroboration as agent-independent evidence beside a green suite.
856
860
  """
857
861
  out = {"ran": False, "functional_status": "not_run",
858
862
  "passed": 0, "failed": 0, "inconclusive": 0}
@@ -892,6 +896,8 @@ def _collect_healthcheck(loki_dir):
892
896
  NOT read by _compute_headline / _compute_degraded, so it does not change what
893
897
  "Verified" means. Gating on it is the founder-gated trust decision (mirrors the
894
898
  FV-2 opt-in gate). ponytail: reuses the health.json app-runner already writes.
899
+ Exception (TEN-GATES-10): ok:true is read by _exogenous_corroboration as
900
+ agent-independent evidence beside a green suite.
895
901
  """
896
902
  out = {"ran": False, "ok": False, "status": "not_run", "checked_at": ""}
897
903
  raw = _read_json(
@@ -1960,14 +1966,61 @@ def _compute_degraded(facts):
1960
1966
  if rec.get("state") == "gap":
1961
1967
  out.append({"item": "functionality:%s" % axis, "status": "failed",
1962
1968
  "reason": rec.get("reason") or "axis disproven"})
1969
+ # TEN-GATES-10 (D105): the agent writes both the code and the suite, so a
1970
+ # green suite alone is the agent grading its own work. When the suite is the
1971
+ # ONLY passing fact, record the gap here (pre-headline, so proof-verify.py
1972
+ # re-derives the same amber headline from the recorded ledger) and VERIFIED
1973
+ # becomes VERIFIED WITH GAPS. LOKI_PROOF_REQUIRE_EXOGENOUS=0 opts out.
1974
+ tests_verified = (
1975
+ tests.get("status") == "verified"
1976
+ and bool(tests.get("command"))
1977
+ and tests.get("exit_code") == 0
1978
+ )
1979
+ if (tests_verified
1980
+ and os.environ.get("LOKI_PROOF_REQUIRE_EXOGENOUS", "1").strip() != "0"
1981
+ and not _exogenous_corroboration(facts)):
1982
+ out.append({"item": "exogenous_evidence", "status": "not_run",
1983
+ "reason": "no agent-independent evidence (only the agent's "
1984
+ "own test suite passed)"})
1963
1985
  return out
1964
1986
 
1965
1987
 
1988
+ def _exogenous_corroboration(facts):
1989
+ """Names of agent-independent facts that ran and passed (TEN-GATES-10).
1990
+
1991
+ Counts: build verified, healthcheck ok, functional verified, a security scan
1992
+ that actually ran and came back clean, or an EXOGENOUS quality gate that
1993
+ passed. An advisory (model-authored) gate pass never counts: it is the same
1994
+ plausibility signal the agent's own suite already is. Absent facts count as
1995
+ nothing (fail-closed).
1996
+ """
1997
+ names = []
1998
+ if (facts.get("build") or {}).get("status") == "verified":
1999
+ names.append("build")
2000
+ hc = facts.get("healthcheck") or {}
2001
+ if hc.get("ran") and hc.get("ok") is True:
2002
+ names.append("healthcheck")
2003
+ fn = facts.get("functional") or {}
2004
+ if fn.get("ran") and str(fn.get("functional_status") or "").lower() in (
2005
+ "verified", "passed"):
2006
+ names.append("functional")
2007
+ sec = facts.get("security") or {}
2008
+ if sec.get("ran") and sec.get("status") == "clean":
2009
+ names.append("security")
2010
+ for g in facts.get("quality_gates") or []:
2011
+ if g.get("status") == "passed" and _is_exogenous(g):
2012
+ names.append("quality_gate:%s" % g.get("name", ""))
2013
+ return names
2014
+
2015
+
1966
2016
  def _compute_headline(facts, degraded):
1967
2017
  """Deterministic headline. NEVER green from an LLM opinion or a bare
1968
2018
  pass:true. Rules:
1969
2019
  - VERIFIED only when tests.status == verified AND there are no degraded
1970
2020
  items AND the diff is non-empty AND tests recorded a real command.
2021
+ (TEN-GATES-10: _compute_degraded adds an exogenous_evidence gap when
2022
+ the suite is the only passing fact, so VERIFIED also needs one
2023
+ agent-independent fact; see _exogenous_corroboration.)
1971
2024
  - VERIFIED WITH GAPS when some facts verified but degraded is non-empty.
1972
2025
  - NOT VERIFIED otherwise.
1973
2026
  """
package/autonomy/loki CHANGED
@@ -10009,7 +10009,7 @@ _deploy_latest_receipt() {
10009
10009
  return 0
10010
10010
  }
10011
10011
 
10012
- # _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination] [deployed_url] [health_url_source]
10012
+ # _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination] [deployed_url] [health_url_source] [env_keys_csv] [env_source]
10013
10013
  # Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
10014
10014
  # receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
10015
10015
  # authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
@@ -10034,6 +10034,9 @@ _deploy_record() {
10034
10034
  local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
10035
10035
  local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}" dest_rec="${20:-}"
10036
10036
  local deployed_url="${21:-}" hsrc="${22:-}"
10037
+ # --env-file: key NAMES only (comma-joined, validated identifiers) and the file
10038
+ # path. A value is never passed here, so the signed record cannot carry one.
10039
+ local env_keys_csv="${23:-}" env_source="${24:-}"
10037
10040
  local ddir="$dir/.loki/deploys"
10038
10041
  [ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
10039
10042
  mkdir -p "$ddir" 2>/dev/null || return 0
@@ -10042,6 +10045,7 @@ _deploy_record() {
10042
10045
  DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
10043
10046
  DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
10044
10047
  DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" DR_DEST="$dest_rec" DR_DURL="$deployed_url" DR_HSRC="$hsrc" \
10048
+ DR_ENVK="$env_keys_csv" DR_ENVS="$env_source" \
10045
10049
  python3 -c '
10046
10050
  import json, os, datetime
10047
10051
  state = os.environ.get("DR_STATE") or "anchored"
@@ -10073,6 +10077,8 @@ json.dump({
10073
10077
  "destination": os.environ.get("DR_DEST", ""),
10074
10078
  "deployed_url": os.environ.get("DR_DURL", ""),
10075
10079
  "health_url_source": os.environ.get("DR_HSRC", ""),
10080
+ "env_keys": sorted(k for k in os.environ.get("DR_ENVK", "").split(",") if k),
10081
+ "env_source": os.environ.get("DR_ENVS", ""),
10076
10082
  "health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
10077
10083
  }, open(os.environ["DR_OUT"], "w"), indent=2)
10078
10084
  ' 2>/dev/null || return 0
@@ -10241,6 +10247,59 @@ sys.exit(0 if ok else 1)' "$f" "$sha" 2>/dev/null || continue
10241
10247
  return 1
10242
10248
  }
10243
10249
 
10250
+ # _deploy_env_load <env_file> <example_file>
10251
+ # Parses KEY=VALUE lines with NO shell evaluation (python, never source/eval) and
10252
+ # writes NUL-terminated records to stdout: one "KEY=VALUE" per key (sorted by
10253
+ # key), then "__LOKI_ENV_OK__". On any problem it writes a single
10254
+ # "__LOKI_ENV_ERR__:<reason>" record instead. A reason names keys and line
10255
+ # numbers only, never a value. When <example_file> exists, every key it lists
10256
+ # must be present in <env_file>, else the reason names the missing keys.
10257
+ _deploy_env_load() {
10258
+ python3 - "$1" "$2" <<'PYEOF'
10259
+ import os, re, sys
10260
+ KEY = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
10261
+ def out(s):
10262
+ sys.stdout.buffer.write(s.encode("utf-8", "surrogateescape") + b"\0")
10263
+ def parse(path, label):
10264
+ found = {}
10265
+ with open(path, encoding="utf-8", errors="surrogateescape") as fh:
10266
+ for n, raw in enumerate(fh, 1):
10267
+ line = raw.strip()
10268
+ if not line or line.startswith("#"):
10269
+ continue
10270
+ if line.startswith("export "):
10271
+ line = line[7:].lstrip()
10272
+ if "=" not in line:
10273
+ raise ValueError("%s line %d is not KEY=VALUE" % (label, n))
10274
+ k, v = line.split("=", 1)
10275
+ k, v = k.strip(), v.strip()
10276
+ if not KEY.match(k):
10277
+ raise ValueError("%s line %d has an invalid key name" % (label, n))
10278
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
10279
+ v = v[1:-1]
10280
+ elif " #" in v:
10281
+ v = v.split(" #", 1)[0].rstrip()
10282
+ if "\0" in v:
10283
+ raise ValueError("%s key %s has a NUL byte in its value" % (label, k))
10284
+ found[k] = v
10285
+ return found
10286
+ try:
10287
+ env = parse(sys.argv[1], "env file")
10288
+ if not env:
10289
+ raise ValueError("env file has no KEY=VALUE entries")
10290
+ if os.path.isfile(sys.argv[2]):
10291
+ missing = sorted(set(parse(sys.argv[2], ".env.example")) - set(env))
10292
+ if missing:
10293
+ raise ValueError("env file is missing key(s) listed in .env.example: " + ", ".join(missing))
10294
+ except (OSError, ValueError) as e:
10295
+ out("__LOKI_ENV_ERR__:" + (str(e) if isinstance(e, ValueError) else "cannot read the env file"))
10296
+ sys.exit(0)
10297
+ for k in sorted(env):
10298
+ out(k + "=" + env[k])
10299
+ out("__LOKI_ENV_OK__")
10300
+ PYEOF
10301
+ }
10302
+
10244
10303
  # _deploy_try_execute <dir> <type>
10245
10304
  # The ONLY place a deploy command is ever run. Evaluates the gate, and either
10246
10305
  # executes + records, or REFUSES naming every failed check. Returns the deploy
@@ -10290,6 +10349,52 @@ _deploy_try_execute() {
10290
10349
  esac
10291
10350
  fi
10292
10351
 
10352
+ # --env-file: values live ONLY in env_pairs and the provider argv. Stdout, the
10353
+ # 'Running:' line and the record carry names (K=***), never values.
10354
+ # Documented verbs only: Vercel '-e K=V' per key on the deploy itself; Fly
10355
+ # 'fly secrets set --stage K=V ...' before 'fly deploy'; Netlify
10356
+ # 'netlify env:import FILE' and Cloudflare 'wrangler secret bulk FILE' before
10357
+ # the deploy (the provider reads FILE, so no value enters argv).
10358
+ local env_file="${_LOKI_DEPLOY_ENV_FILE:-}" env_kind="" env_keys="" env_disp="" env_pre_disp="" _env_rec _env_ok=false _env_err=""
10359
+ local -a env_pairs=() env_pre_argv=()
10360
+ if [ -n "$env_file" ]; then
10361
+ case "$command" in
10362
+ "vercel --prod") env_kind="vercel" ;;
10363
+ "fly deploy") env_kind="fly" ;;
10364
+ "netlify deploy --prod") env_kind="netlify" ;;
10365
+ "wrangler deploy") env_kind="cloudflare" ;;
10366
+ *)
10367
+ echo -e "${RED}REFUSED: --env-file is not supported for: ${command}${NC}" >&2
10368
+ echo " No documented env verb is wired for this provider. Nothing was run." >&2
10369
+ return 1
10370
+ ;;
10371
+ esac
10372
+ while IFS= read -r -d '' _env_rec; do
10373
+ case "$_env_rec" in
10374
+ __LOKI_ENV_OK__) _env_ok=true ;;
10375
+ __LOKI_ENV_ERR__:*) _env_err="${_env_rec#__LOKI_ENV_ERR__:}" ;;
10376
+ *) env_pairs+=("$_env_rec") ;;
10377
+ esac
10378
+ done < <(_deploy_env_load "$env_file" "$dir/.env.example")
10379
+ if [ "$_env_ok" != true ]; then
10380
+ echo -e "${RED}REFUSED: --env-file: ${_env_err:-the env file could not be parsed}${NC}" >&2
10381
+ echo " Nothing was run." >&2
10382
+ return 1
10383
+ fi
10384
+ case "$env_kind" in
10385
+ fly) env_pre_argv=(fly secrets set --stage); env_pre_disp="fly secrets set --stage" ;;
10386
+ netlify) env_pre_argv=(netlify env:import "$env_file"); env_pre_disp="netlify env:import ${env_file}" ;;
10387
+ cloudflare) env_pre_argv=(wrangler secret bulk "$env_file"); env_pre_disp="wrangler secret bulk ${env_file}" ;;
10388
+ esac
10389
+ for _env_rec in "${env_pairs[@]}"; do
10390
+ env_keys="${env_keys:+${env_keys},}${_env_rec%%=*}"
10391
+ case "$env_kind" in
10392
+ vercel) env_disp="${env_disp} -e ${_env_rec%%=*}=***" ;;
10393
+ fly) env_pre_argv+=("$_env_rec"); env_pre_disp="${env_pre_disp} ${_env_rec%%=*}=***" ;;
10394
+ esac
10395
+ done
10396
+ fi
10397
+
10293
10398
  local reasons
10294
10399
  reasons="$(_deploy_gate "$dir" "$command")"
10295
10400
 
@@ -10346,17 +10451,18 @@ _deploy_try_execute() {
10346
10451
  else
10347
10452
  echo -e "${GREEN}Evidence gate PASSED.${NC} Receipt ${run_id:-unknown} VERIFIED this tree (${anchor:0:12})."
10348
10453
  fi
10349
- if [ "$canary" = true ]; then echo "Running (canary): ${exec_cmd}"; else echo "Running: ${command}"; fi
10454
+ [ -n "$env_pre_disp" ] && echo "Running (env): ${env_pre_disp}"
10455
+ if [ "$canary" = true ]; then echo "Running (canary): ${exec_cmd}${env_disp}"; else echo "Running: ${command}${env_disp}"; fi
10350
10456
  echo ""
10351
10457
 
10352
10458
  # Run in the project dir. No credential env var is read, set, or forwarded:
10353
10459
  # the command inherits the user's already-authenticated CLI session as-is.
10354
10460
  #
10355
- # eval is used because the canonical commands carry their own flags
10356
- # ("vercel --prod", "netlify deploy --prod --dir=<out>") and must word-split.
10357
- # The string is NOT user input: it comes from the hardcoded
10358
- # _deploy_options_for_type table, and the only path here has already passed
10359
- # the destructive-operation check in _deploy_gate.
10461
+ # The deploy runs as an argv array, never through eval: the canonical
10462
+ # command ("vercel --prod") is word-split from the hardcoded
10463
+ # _deploy_options_for_type table (already past the destructive-operation
10464
+ # check in _deploy_gate), and --env-file values are appended as single
10465
+ # array elements, so a value such as $(cmd) is never evaluated.
10360
10466
  # Resolve the rollback target BEFORE deploying: after it, the previous Fly
10361
10467
  # release is no longer the newest one and cannot be recovered.
10362
10468
  local rb_cmd="" rb_note=""
@@ -10377,6 +10483,24 @@ _deploy_try_execute() {
10377
10483
  fi
10378
10484
 
10379
10485
  local rc=0 started_at finished_at purl="" canary_out=""
10486
+ local -a deploy_argv=()
10487
+ read -r -a deploy_argv <<<"$exec_cmd"
10488
+ if [ "$env_kind" = vercel ]; then
10489
+ for _env_rec in "${env_pairs[@]}"; do deploy_argv+=(-e "$_env_rec"); done
10490
+ fi
10491
+ # What the record and messages name: the command with values masked as K=***.
10492
+ local exec_disp="${env_pre_disp:+${env_pre_disp}; }${exec_cmd}${env_disp}"
10493
+ if [ -n "$env_pre_disp" ]; then
10494
+ # The env step runs after the gate and before the deploy; if it fails the
10495
+ # deploy is NOT run (a deploy missing its env is not the one asked for).
10496
+ local env_rc=0
10497
+ ( cd "$dir" && "${env_pre_argv[@]}" ) || env_rc=$?
10498
+ if [ "$env_rc" -ne 0 ]; then
10499
+ echo -e "${RED}REFUSED: the env step exited ${env_rc}: ${env_pre_disp}${NC}" >&2
10500
+ echo " The deploy was NOT run." >&2
10501
+ return "$env_rc"
10502
+ fi
10503
+ fi
10380
10504
  started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
10381
10505
  local deployed_url="" health_src=""
10382
10506
  [ -n "$health_url" ] && health_src="flag"
@@ -10385,8 +10509,7 @@ _deploy_try_execute() {
10385
10509
  # Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
10386
10510
  # Nothing reaches production before the probe, so there is nothing to roll back.
10387
10511
  rb_cmd=""; auto_rollback=false
10388
- exec_cmd="vercel"
10389
- canary_out="$( cd "$dir" && eval "$exec_cmd" )" || rc=$?
10512
+ canary_out="$( cd "$dir" && "${deploy_argv[@]}" )" || rc=$?
10390
10513
  printf '%s\n' "$canary_out"
10391
10514
  purl="$(grep -Eo 'https?://[^[:space:]]+' <<<"$canary_out" | tail -n 1)"
10392
10515
  if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
@@ -10395,7 +10518,7 @@ _deploy_try_execute() {
10395
10518
  if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
10396
10519
  health_status="healthy"
10397
10520
  echo "Promoting: vercel promote ${purl}"
10398
- exec_cmd="vercel; vercel promote ${purl}"
10521
+ exec_disp="${exec_disp}; vercel promote ${purl}"
10399
10522
  ( cd "$dir" && vercel promote "$purl" ) || rc=$?
10400
10523
  else
10401
10524
  health_status="FAILED_HEALTH"
@@ -10418,7 +10541,7 @@ _deploy_try_execute() {
10418
10541
  fi
10419
10542
  case $- in *e*) dep_errexit=1 ;; esac
10420
10543
  set +e
10421
- ( cd "$dir" && eval "$exec_cmd" ) 2>&1 | tee "$dep_tmp"
10544
+ ( cd "$dir" && "${deploy_argv[@]}" ) 2>&1 | tee "$dep_tmp"
10422
10545
  rc=${PIPESTATUS[0]}
10423
10546
  [ "$dep_errexit" -eq 1 ] && set -e
10424
10547
  if [ "$rc" -eq 0 ] && [ "$dep_tmp" != /dev/null ]; then
@@ -10472,9 +10595,9 @@ _deploy_try_execute() {
10472
10595
  local rec rsha
10473
10596
  finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
10474
10597
  rsha="$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || printf '')"
10475
- rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
10598
+ rec="$(_deploy_record "$dir" "$exec_disp" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
10476
10599
  "$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
10477
- "$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest" "$deployed_url" "$health_src")"
10600
+ "$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest" "$deployed_url" "$health_src" "$env_keys" "$env_file")"
10478
10601
  echo ""
10479
10602
  if [ "$rc" -eq 0 ]; then
10480
10603
  echo -e "${GREEN}Deploy command exited 0.${NC}"
@@ -10623,6 +10746,7 @@ cmd_deploy() {
10623
10746
  local LOKI_DEPLOY_ALLOW_UNANCHORED=false
10624
10747
  # Per-invocation like --execute; local so an ambient env var cannot set them.
10625
10748
  local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false _LOKI_DEPLOY_DESTINATION=""
10749
+ local _LOKI_DEPLOY_ENV_FILE=""
10626
10750
 
10627
10751
  # Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
10628
10752
  while [ $# -gt 0 ]; do
@@ -10630,7 +10754,7 @@ cmd_deploy() {
10630
10754
  --help|-h|help)
10631
10755
  echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
10632
10756
  echo ""
10633
- echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--destination preview|staging|production] [--health-url URL [--health-match STR] [--auto-rollback]]"
10757
+ echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary] [--env-file FILE]] [--destination preview|staging|production] [--health-url URL [--health-match STR] [--auto-rollback]]"
10634
10758
  echo ""
10635
10759
  echo "Detects your project type and your installed cloud CLI (and any"
10636
10760
  echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
@@ -10720,6 +10844,16 @@ cmd_deploy() {
10720
10844
  echo " With --execute: preview/staging run a non-production deploy"
10721
10845
  echo " (Vercel only); production is refused unless a VERIFIED staging"
10722
10846
  echo " deploy record exists for the same SHA. Unset keeps today's behavior."
10847
+ echo " --env-file FILE"
10848
+ echo " With --execute: push KEY=VALUE lines from FILE to the provider"
10849
+ echo " (parsed, never shell-evaluated). Refuses when a key listed in"
10850
+ echo " .env.example is missing from FILE. Vercel: 'vercel --prod -e K=V'"
10851
+ echo " per key; Fly: 'fly secrets set --stage K=V ...' then 'fly deploy';"
10852
+ echo " Netlify: 'netlify env:import FILE'; Cloudflare: 'wrangler secret"
10853
+ echo " bulk FILE'. Any other provider refuses with nothing run. If the env"
10854
+ echo " step fails the deploy is not run. Values are masked as K=*** in"
10855
+ echo " output; the deploy record stores env_keys (names) and env_source,"
10856
+ echo " never values."
10723
10857
  echo " --allow-unanchored"
10724
10858
  echo " Proceed when the receipt records NO baseline (nothing"
10725
10859
  echo " about this tree is proven). Never bypasses a recorded"
@@ -10770,10 +10904,33 @@ cmd_deploy() {
10770
10904
  --no-clip)
10771
10905
  do_clip=false
10772
10906
  ;;
10907
+ --env-file)
10908
+ case "${2:-}" in
10909
+ -*|"") echo "loki deploy --env-file requires a file path" >&2; return 1 ;;
10910
+ *) _LOKI_DEPLOY_ENV_FILE="$2"; shift ;;
10911
+ esac
10912
+ ;;
10913
+ --env-file=*)
10914
+ _LOKI_DEPLOY_ENV_FILE="${1#--env-file=}"
10915
+ [ -n "$_LOKI_DEPLOY_ENV_FILE" ] || { echo "loki deploy --env-file requires a file path" >&2; return 1; }
10916
+ ;;
10773
10917
  esac
10774
10918
  shift
10775
10919
  done
10776
10920
 
10921
+ if [ -n "$_LOKI_DEPLOY_ENV_FILE" ]; then
10922
+ if [ "$do_execute" != true ]; then
10923
+ echo "loki deploy --env-file requires --execute (the advisory run pushes nothing)" >&2
10924
+ return 1
10925
+ fi
10926
+ if [ ! -f "$_LOKI_DEPLOY_ENV_FILE" ] || [ ! -r "$_LOKI_DEPLOY_ENV_FILE" ]; then
10927
+ echo "loki deploy --env-file: not a readable file: $_LOKI_DEPLOY_ENV_FILE" >&2
10928
+ return 1
10929
+ fi
10930
+ # Absolute, so a provider verb run from the project dir finds the same file.
10931
+ _LOKI_DEPLOY_ENV_FILE="$(cd "$(dirname -- "$_LOKI_DEPLOY_ENV_FILE")" && pwd -P)/$(basename -- "$_LOKI_DEPLOY_ENV_FILE")"
10932
+ fi
10933
+
10777
10934
  if [ "$_LOKI_DEPLOY_AUTO_ROLLBACK" = true ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
10778
10935
  echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
10779
10936
  return 1
package/completions/_loki CHANGED
@@ -178,6 +178,7 @@ function _loki {
178
178
  '--no-clip[Do not copy the command to the clipboard]' \
179
179
  '--execute[Run the deploy command if the receipt gate passes]' \
180
180
  '--canary[With --execute: staged rollout (Vercel promote, Fly canary)]' \
181
+ '--env-file[With --execute: push KEY=VALUE env via Vercel, Fly, Netlify or Cloudflare (names-only record)]:env file:_files' \
181
182
  '--allow-unanchored[Proceed when the receipt records no baseline]' \
182
183
  '--health-url[Health probe URL]:url:' \
183
184
  '--auto-rollback[Roll back on a failed health probe]' \
@@ -150,7 +150,7 @@ _loki_completion() {
150
150
 
151
151
  deploy)
152
152
  if [[ "$cur" == -* ]]; then
153
- COMPREPLY=( $(compgen -W "--execute --destination --canary --health-url --health-match --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
153
+ COMPREPLY=( $(compgen -W "--execute --destination --canary --env-file --health-url --health-match --auto-rollback --dir --no-clip --allow-unanchored --help" -- "$cur") )
154
154
  return 0
155
155
  fi
156
156
  ;;
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.17.34"
10
+ __version__ = "11.17.36"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.17.34. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.17.36. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.17.34
5
+ **Version:** v11.17.36
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8