loki-mode 11.6.0 → 11.7.1

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 (86) hide show
  1. package/README.md +19 -2
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +11 -2
  5. package/autonomy/lib/chat.sh +57 -1
  6. package/autonomy/lib/loki_yaml.py +8 -2
  7. package/autonomy/lib/workspace.py +267 -11
  8. package/autonomy/loki +315 -21
  9. package/autonomy/trigger-server.py +185 -2
  10. package/autonomy/verify.sh +4 -0
  11. package/completions/_loki +4 -0
  12. package/completions/loki.bash +20 -2
  13. package/dashboard/__init__.py +1 -1
  14. package/docs/CLI-REFERENCE.md +2 -1
  15. package/docs/DEPLOY-PLAN.md +17 -8
  16. package/docs/EVALUATING.md +8 -0
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/WORKSPACES.md +15 -2
  19. package/docs/dev/architecture-reference.md +8 -0
  20. package/docs/sentry-intake.md +11 -0
  21. package/docs/v10/CLI-MODERN.md +1 -1
  22. package/loki-ts/dist/loki.js +1009 -975
  23. package/mcp/__init__.py +1 -1
  24. package/package.json +2 -2
  25. package/packages/control-plane/dist/server.js +618 -469
  26. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  27. package/schemas/loki-yaml.schema.json +5 -2
  28. package/src/policies/engine.js +41 -2
  29. package/src/policies/types.js +49 -0
  30. package/web-app/dist/assets/{AdminPage-DhXdJNQk.js → AdminPage-S3BYeapm.js} +1 -1
  31. package/web-app/dist/assets/{Avatar-ixVj76fM.js → Avatar-C1rHwLgn.js} +1 -1
  32. package/web-app/dist/assets/{Badge-BGMSp22s.js → Badge-CzlnN5aQ.js} +1 -1
  33. package/web-app/dist/assets/{Button-pYJc32tG.js → Button-B1w5AeGC.js} +1 -1
  34. package/web-app/dist/assets/{CockpitPage-C1JBBouA.js → CockpitPage-Dyeqjfkc.js} +1 -1
  35. package/web-app/dist/assets/{ComparePage-qD0NV4RK.js → ComparePage-C6y2GkGR.js} +1 -1
  36. package/web-app/dist/assets/{ErrorBoundary-C-yxO58j.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
  37. package/web-app/dist/assets/{EvidenceReceiptPanel-ByPzuE1S.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
  38. package/web-app/dist/assets/{GitHubIssuesPanel-BNv_2BEJ.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
  39. package/web-app/dist/assets/{GitHubPRsPanel-CuLZQ9CS.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
  40. package/web-app/dist/assets/{HomePage-CS_FW8Je.js → HomePage-BKnv-oYw.js} +2 -2
  41. package/web-app/dist/assets/{LoginPage-DwiNf1Wm.js → LoginPage-BGksmFRd.js} +1 -1
  42. package/web-app/dist/assets/{MagicPage-CmQybLPZ.js → MagicPage-OCvhp16M.js} +1 -1
  43. package/web-app/dist/assets/{MetricsPage-D9ERdpv8.js → MetricsPage-CJOJpXWC.js} +1 -1
  44. package/web-app/dist/assets/{NotFoundPage-D9ccgdbF.js → NotFoundPage-C04p3QvN.js} +1 -1
  45. package/web-app/dist/assets/{ProjectPage-B9Jslmgv.js → ProjectPage-BRB8txsY.js} +3 -3
  46. package/web-app/dist/assets/{ProjectsPage-DnU99Go2.js → ProjectsPage-CoUYeQEh.js} +1 -1
  47. package/web-app/dist/assets/{SettingsPage-DcTyx6wo.js → SettingsPage-BkjmJGfM.js} +1 -1
  48. package/web-app/dist/assets/{ShowcasePage-jEY9ghSr.js → ShowcasePage-B4vU9TjQ.js} +1 -1
  49. package/web-app/dist/assets/{SystemSettingsPage-CSGltW-8.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
  50. package/web-app/dist/assets/{TeamsPage-gUuJXb9l.js → TeamsPage-Ulm8SISq.js} +1 -1
  51. package/web-app/dist/assets/{TemplatesPage-37Zhe--v.js → TemplatesPage-C_7MM6Ey.js} +1 -1
  52. package/web-app/dist/assets/{TerminalOutput-B09hRwaR.js → TerminalOutput-Cumd7Tgs.js} +1 -1
  53. package/web-app/dist/assets/{activity-q_4MY9J0.js → activity-CZAUkbiY.js} +1 -1
  54. package/web-app/dist/assets/{bell-X8OC_PFP.js → bell-BOvSXpEr.js} +1 -1
  55. package/web-app/dist/assets/{bot-C-N1PUOD.js → bot-DOTZxBYj.js} +1 -1
  56. package/web-app/dist/assets/{check-B2c76Jmp.js → check-BxpE0B5u.js} +1 -1
  57. package/web-app/dist/assets/{chevron-left-BF76YLNw.js → chevron-left-D6KPvBOA.js} +1 -1
  58. package/web-app/dist/assets/{circle-alert-AeFKBD8h.js → circle-alert-Bajq86oe.js} +1 -1
  59. package/web-app/dist/assets/{clock-CiXRf-FH.js → clock-CnlHAVJc.js} +1 -1
  60. package/web-app/dist/assets/{cloud-DKQ8SRJ5.js → cloud-DjaymjwM.js} +1 -1
  61. package/web-app/dist/assets/{code-xml-DEb6jXWP.js → code-xml-BADEw2Ld.js} +1 -1
  62. package/web-app/dist/assets/{database-6W7e-E9c.js → database-B4aKb5Cq.js} +1 -1
  63. package/web-app/dist/assets/{dollar-sign-Ds2GOuyW.js → dollar-sign-DrldXpja.js} +1 -1
  64. package/web-app/dist/assets/{file-code-corner-CZU0jg52.js → file-code-corner-DVebDVR-.js} +1 -1
  65. package/web-app/dist/assets/{file-plus-DTVYnXCN.js → file-plus-BD-N_-Tl.js} +1 -1
  66. package/web-app/dist/assets/{globe-0mUzKYDe.js → globe-C7ieInC-.js} +1 -1
  67. package/web-app/dist/assets/{hammer-BMHZg3YX.js → hammer-BSugKIji.js} +1 -1
  68. package/web-app/dist/assets/{index-BvOTvjax.js → index-nLQgfvOn.js} +3 -3
  69. package/web-app/dist/assets/{layers-Df7TdcWB.js → layers-CHXxU2on.js} +1 -1
  70. package/web-app/dist/assets/{loader-circle-kPsVfrSY.js → loader-circle-BEEz0LbD.js} +1 -1
  71. package/web-app/dist/assets/{lock-CXYcbEc5.js → lock-CzQGfcnE.js} +1 -1
  72. package/web-app/dist/assets/{package-Cqhua1ID.js → package-CcHYQLGd.js} +1 -1
  73. package/web-app/dist/assets/{plus-lseXhsn-.js → plus-DdzgFEXn.js} +1 -1
  74. package/web-app/dist/assets/{refresh-cw-OZriI6gD.js → refresh-cw-7x0HbH9Y.js} +1 -1
  75. package/web-app/dist/assets/{rotate-ccw-CKCfjPg8.js → rotate-ccw-DBahX1Sp.js} +1 -1
  76. package/web-app/dist/assets/{scroll-text-DoVyyfKb.js → scroll-text-BB8y8a9Z.js} +1 -1
  77. package/web-app/dist/assets/{server-DGy7TflW.js → server-CfkbW2MH.js} +1 -1
  78. package/web-app/dist/assets/{shield-alert-BCzRcMXi.js → shield-alert-DUgD8rBF.js} +1 -1
  79. package/web-app/dist/assets/{thumbs-up-C_znJd5g.js → thumbs-up-DGoyY6QC.js} +1 -1
  80. package/web-app/dist/assets/{trash-2-BosCbeeA.js → trash-2-B6xrSUMr.js} +1 -1
  81. package/web-app/dist/assets/{trending-down-CkR5zDkV.js → trending-down-B6RGKTOx.js} +1 -1
  82. package/web-app/dist/assets/{trending-up-DpUIfOH4.js → trending-up-B44DCrt1.js} +1 -1
  83. package/web-app/dist/assets/{upload-CH9aKc8L.js → upload-4kWm33ZY.js} +1 -1
  84. package/web-app/dist/assets/{usePolling-pzxrHJeg.js → usePolling-zkUtRQG6.js} +1 -1
  85. package/web-app/dist/assets/{user-B7OHa-VQ.js → user-04cEVIe2.js} +1 -1
  86. package/web-app/dist/index.html +1 -1
package/autonomy/loki CHANGED
@@ -1785,7 +1785,8 @@ show_help_all() {
1785
1785
  echo " start [SPEC] Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
1786
1786
  echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
1787
1787
  echo " backlog <owner/repo> --all|--label X|--issues N,N Run every matching open issue, N in parallel"
1788
- echo " workspace list | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=0 disables)"
1788
+ echo " workspace list | infer <name> | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=0 disables)"
1789
+ echo " start --workspace <name> \"<brief>\" Run a workspace; start --deploy [--health-url U] deploys after a VERIFIED run"
1789
1790
  echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
1790
1791
  echo ""
1791
1792
  echo "Session:"
@@ -2542,6 +2543,71 @@ cmd_start() {
2542
2543
  # version all return 0 without output), so no guard is needed here.
2543
2544
  maybe_print_update_hint || true
2544
2545
 
2546
+ # PATH slice: --workspace NAME runs a multi-repo workspace through the one
2547
+ # workspace runner (no new logic here), and --deploy [--health-url U]
2548
+ # [--auto-rollback] chains the gated `loki deploy --execute` after a start
2549
+ # that ended 0. cmd_deploy owns every gate: no VERIFIED HEAD-anchored
2550
+ # receipt means a refusal, never a deploy.
2551
+ local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
2552
+ while [ $# -gt 0 ]; do
2553
+ case "$1" in
2554
+ --workspace)
2555
+ [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--workspace requires a workspace name${NC}" >&2; exit 2; }
2556
+ _ps_ws="$2"; shift 2 ;;
2557
+ --workspace=*) _ps_ws="${1#*=}"; shift ;;
2558
+ --deploy) _ps_deploy=1; shift ;;
2559
+ --health-url)
2560
+ [ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-url requires a URL${NC}" >&2; exit 2; }
2561
+ _ps_health="$2"; shift 2 ;;
2562
+ --health-url=*) _ps_health="${1#*=}"; shift ;;
2563
+ --auto-rollback) _ps_rb=1; shift ;;
2564
+ *) _ps_rest+=("$1"); shift ;;
2565
+ esac
2566
+ done
2567
+ if { [ -n "$_ps_health" ] || [ "$_ps_rb" = 1 ]; } && [ "$_ps_deploy" != 1 ]; then
2568
+ echo -e "${RED}--health-url and --auto-rollback apply only with --deploy${NC}" >&2
2569
+ exit 2
2570
+ fi
2571
+ if [ -n "$_ps_ws" ]; then
2572
+ if [ "$_ps_deploy" = 1 ]; then
2573
+ echo -e "${RED}--workspace and --deploy cannot be combined: deploy each repo with loki deploy${NC}" >&2
2574
+ exit 2
2575
+ fi
2576
+ local _ps_skip=0
2577
+ for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
2578
+ if [ "$_ps_skip" = 1 ]; then _ps_skip=0; continue; fi
2579
+ if [ "$_ps_skip" = 2 ]; then _ps_skip=0; _ps_brief+=("$_ps_a"); continue; fi
2580
+ case "$_ps_a" in
2581
+ --brief=*) _ps_brief+=("${_ps_a#*=}") ;;
2582
+ --brief) _ps_skip=2 ;;
2583
+ # Every value-taking cmd_start flag: its value is never brief text.
2584
+ --provider|--budget|--prd|--issue|--model|--max-cost|--isolation|--aider-model|--aider-flags|--cline-model|--compliance|--max-duration|--log-level|--bmad-project|--openspec|--mirofish-rounds|--mirofish-timeout|--output|--config|--vars|--env-file|--remote|--attempts|--max-wait) _ps_skip=1 ;;
2585
+ -*) ;;
2586
+ *) _ps_brief+=("$_ps_a") ;;
2587
+ esac
2588
+ done
2589
+ if [ "${#_ps_brief[@]}" -eq 0 ]; then
2590
+ echo -e "${RED}loki start --workspace $_ps_ws needs a brief or an issue ref${NC}" >&2
2591
+ exit 2
2592
+ fi
2593
+ cmd_workspace run "$_ps_ws" "${_ps_brief[*]}"
2594
+ return $?
2595
+ fi
2596
+ if [ "$_ps_deploy" = 1 ]; then
2597
+ local _ps_rc=0
2598
+ ( cmd_start ${_ps_rest[@]+"${_ps_rest[@]}"} ) || _ps_rc=$?
2599
+ if [ "$_ps_rc" -ne 0 ]; then
2600
+ echo -e "${RED}start exited $_ps_rc: not deploying${NC}" >&2
2601
+ return "$_ps_rc"
2602
+ fi
2603
+ local _ps_dargs=(--execute)
2604
+ [ -n "$_ps_health" ] && _ps_dargs+=(--health-url "$_ps_health")
2605
+ [ "$_ps_rb" = 1 ] && _ps_dargs+=(--auto-rollback)
2606
+ cmd_deploy "${_ps_dargs[@]}"
2607
+ return $?
2608
+ fi
2609
+ set -- ${_ps_rest[@]+"${_ps_rest[@]}"}
2610
+
2545
2611
  local args=()
2546
2612
  local prd_file=""
2547
2613
  local provider=""
@@ -9238,33 +9304,36 @@ _deploy_detect_type() {
9238
9304
  # paraphrase). Project-dependent dirs stay as `<...>` placeholders the user fills.
9239
9305
  # Note: the Fly CLI binary is `flyctl` but the canonical deploy verb is
9240
9306
  # `fly deploy` -- "fly" not "flyctl" here is CORRECT, intentional, not a typo.
9307
+ # Row format: provider|cli|command|docs|rollback. rollback is a documented verb
9308
+ # (vercel rollback, fly releases rollback <captured-version>) or `manual:<note>`
9309
+ # when the provider documents no CLI rollback.
9241
9310
  _deploy_options_for_type() {
9242
9311
  local type="${1:-}"
9243
9312
  case "$type" in
9244
9313
  nextjs)
9245
- printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli"
9246
- printf '%s\n' "Netlify|netlify|netlify deploy --prod|https://docs.netlify.com/cli/get-started/"
9314
+ printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
9315
+ printf '%s\n' "Netlify|netlify|netlify deploy --prod|https://docs.netlify.com/cli/get-started/|manual:Netlify documents no CLI rollback; restore the previous deploy from the Netlify dashboard"
9247
9316
  # fly not flyctl - correct: binary is flyctl, deploy verb is `fly deploy`.
9248
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9317
+ printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
9249
9318
  ;;
9250
9319
  static)
9251
- printf '%s\n' "Netlify|netlify|netlify deploy --prod --dir=<build-output>|https://docs.netlify.com/cli/get-started/"
9252
- printf '%s\n' "Cloudflare Pages|wrangler|wrangler pages deploy <build-output>|https://developers.cloudflare.com/workers/wrangler/install-and-update/"
9253
- printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli"
9320
+ printf '%s\n' "Netlify|netlify|netlify deploy --prod --dir=<build-output>|https://docs.netlify.com/cli/get-started/|manual:Netlify documents no CLI rollback; restore the previous deploy from the Netlify dashboard"
9321
+ printf '%s\n' "Cloudflare Pages|wrangler|wrangler pages deploy <build-output>|https://developers.cloudflare.com/workers/wrangler/install-and-update/|manual:Cloudflare rollback is not automated here; roll back from the dashboard or with the provider CLI yourself"
9322
+ printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
9254
9323
  ;;
9255
9324
  docker)
9256
9325
  # fly not flyctl - correct (see note above).
9257
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9258
- printf '%s\n' "Cloudflare|wrangler|wrangler deploy|https://developers.cloudflare.com/workers/wrangler/install-and-update/"
9326
+ printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
9327
+ printf '%s\n' "Cloudflare|wrangler|wrangler deploy|https://developers.cloudflare.com/workers/wrangler/install-and-update/|manual:Cloudflare rollback is not automated here; roll back from the dashboard or with the provider CLI yourself"
9259
9328
  ;;
9260
9329
  node)
9261
9330
  # fly not flyctl - correct (see note above).
9262
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9263
- printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli"
9331
+ printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
9332
+ printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli|vercel rollback"
9264
9333
  ;;
9265
9334
  python)
9266
9335
  # fly not flyctl - correct (see note above).
9267
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9336
+ printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/|fly releases rollback <captured-version>"
9268
9337
  ;;
9269
9338
  esac
9270
9339
  return 0
@@ -9339,9 +9408,9 @@ _deploy_print_install_hint() {
9339
9408
  echo "Loki never accesses your cloud account or runs deploy for you -- you run"
9340
9409
  echo "the printed command. Install one of the following, then re-run 'loki deploy':"
9341
9410
  echo ""
9342
- local options provider cli command docs
9411
+ local options provider cli command docs _rb
9343
9412
  options="$(_deploy_options_for_type "$type")"
9344
- while IFS='|' read -r provider cli command docs; do
9413
+ while IFS='|' read -r provider cli command docs _rb; do
9345
9414
  [ -n "$provider" ] || continue
9346
9415
  case "$cli" in
9347
9416
  vercel) echo " Vercel: brew install vercel | ${docs}" ;;
@@ -9373,8 +9442,8 @@ _deploy_print_cloud_options() {
9373
9442
 
9374
9443
  local printed=false
9375
9444
  local first_cmd=""
9376
- local provider cli command docs
9377
- while IFS='|' read -r provider cli command docs; do
9445
+ local provider cli command docs _rb
9446
+ while IFS='|' read -r provider cli command docs _rb; do
9378
9447
  [ -n "$cli" ] || continue
9379
9448
  if command -v "$cli" >/dev/null 2>&1; then
9380
9449
  if [ "$printed" != true ]; then
@@ -9636,6 +9705,9 @@ PYANCHOR
9636
9705
  # verified tree, never tearing an environment down. Word-boundary matched so
9637
9706
  # "wrangler pages deploy dist" is not caught by "destroy" appearing in a path.
9638
9707
  _deploy_is_destructive() {
9708
+ # Allowlisted rollback verbs (the only ones auto-rollback may run) are never
9709
+ # destructive: they restore a prior release rather than tear anything down.
9710
+ printf '%s' "${1:-}" | grep -Eq '^(vercel rollback|fly releases rollback [0-9]+)$' && return 1
9639
9711
  printf '%s' "${1:-}" | grep -Eqi '(^|[^[:alnum:]_-])(delete|destroy|teardown|down|drop|rm|remove|purge|prune|wipe|scale-down)([^[:alnum:]_-]|$)'
9640
9712
  }
9641
9713
 
@@ -9694,12 +9766,16 @@ _deploy_record() {
9694
9766
  # "refused" needs no value: a refusal never reaches this function, so the
9695
9767
  # existence of a record already means one of the two above.
9696
9768
  local anchor_state="${6:-anchored}"
9769
+ # Post-deploy health and rollback facts. "unchecked" never implies healthy.
9770
+ local health_status="${7:-unchecked}" health_url="${8:-}"
9771
+ local rb_attempted="${9:-false}" rb_exit="${10:-}"
9697
9772
  local ddir="$dir/.loki/deploys"
9698
9773
  [ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
9699
9774
  mkdir -p "$ddir" 2>/dev/null || return 0
9700
9775
  local f="$ddir/$(date -u +%Y%m%dT%H%M%SZ)-$$.json"
9701
9776
  DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
9702
- DR_STATE="$anchor_state" \
9777
+ DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
9778
+ DR_RBA="$rb_attempted" DR_RBX="$rb_exit" \
9703
9779
  python3 -c '
9704
9780
  import json, os, datetime
9705
9781
  state = os.environ.get("DR_STATE") or "anchored"
@@ -9713,6 +9789,10 @@ json.dump({
9713
9789
  # Spelled out so a reader months later does not have to know the vocabulary
9714
9790
  # to judge how strong the authorization was.
9715
9791
  "anchor_proven": state == "anchored",
9792
+ "health_status": os.environ.get("DR_HS") or "unchecked",
9793
+ "health_url": os.environ.get("DR_HU") or "",
9794
+ "rollback_attempted": os.environ.get("DR_RBA") == "true",
9795
+ "rollback_exit": int(os.environ["DR_RBX"]) if os.environ.get("DR_RBX", "") != "" else None,
9716
9796
  }, open(os.environ["DR_OUT"], "w"), indent=2)
9717
9797
  ' 2>/dev/null || return 0
9718
9798
  printf '%s' "$f"
@@ -9726,9 +9806,9 @@ json.dump({
9726
9806
  # differs from the one the advisory showed. `command -v` only; no CLI is run.
9727
9807
  _deploy_first_installed_command() {
9728
9808
  local dir="${1:-.}" type="${2:-}"
9729
- local options provider cli command docs
9809
+ local options provider cli command docs _rb
9730
9810
  options="$(_deploy_options_for_type "$type")"
9731
- while IFS='|' read -r provider cli command docs; do
9811
+ while IFS='|' read -r provider cli command docs _rb; do
9732
9812
  [ -n "$cli" ] || continue
9733
9813
  if command -v "$cli" >/dev/null 2>&1; then
9734
9814
  printf '%s' "$command"
@@ -9741,6 +9821,110 @@ EOF
9741
9821
  return 0
9742
9822
  }
9743
9823
 
9824
+ # _deploy_rollback_for <type> <command>
9825
+ # Echoes the rollback column of the first option row whose deploy command is
9826
+ # <command>, or "" when none. Pure table lookup; runs nothing.
9827
+ _deploy_rollback_for() {
9828
+ local type="${1:-}" want="${2:-}" provider cli command docs rb
9829
+ while IFS='|' read -r provider cli command docs rb; do
9830
+ [ -n "$cli" ] || continue
9831
+ if [ "$command" = "$want" ]; then
9832
+ printf '%s' "$rb"
9833
+ return 0
9834
+ fi
9835
+ done <<EOF
9836
+ $(_deploy_options_for_type "$type")
9837
+ EOF
9838
+ printf '%s' ""
9839
+ return 0
9840
+ }
9841
+
9842
+ # _deploy_fly_capture_version <dir>
9843
+ # Echoes the newest Fly release version BEFORE a deploy so a rollback has a real
9844
+ # target, or "" when it cannot be read (then no rollback is attempted).
9845
+ _deploy_fly_capture_version() {
9846
+ local dir="${1:-.}" out
9847
+ out="$( cd "$dir" && fly releases --json 2>/dev/null )" || { printf '%s' ""; return 0; }
9848
+ printf '%s' "$out" | python3 -c '
9849
+ import json, sys
9850
+ try:
9851
+ rel = json.load(sys.stdin)
9852
+ v = rel[0].get("Version") if rel else None
9853
+ print(int(v) if v is not None else "", end="")
9854
+ except Exception:
9855
+ print("", end="")
9856
+ ' 2>/dev/null || printf '%s' ""
9857
+ return 0
9858
+ }
9859
+
9860
+ # _deploy_probe_health <url>
9861
+ # Bounded poll: echoes the last HTTP status, returns 0 on any 2xx. Attempts and
9862
+ # spacing are env-tunable (LOKI_DEPLOY_HEALTH_ATTEMPTS / _INTERVAL) for tests.
9863
+ _deploy_probe_health() {
9864
+ local url="${1:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
9865
+ local i=0 code="000"
9866
+ case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
9867
+ case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
9868
+ while [ "$i" -lt "$attempts" ]; do
9869
+ i=$((i + 1))
9870
+ code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$url" 2>/dev/null)" || code="000"
9871
+ case "$code" in 2??) printf '%s' "$code"; return 0 ;; esac
9872
+ [ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
9873
+ done
9874
+ printf '%s' "$code"
9875
+ return 1
9876
+ }
9877
+
9878
+ # _deploy_policy_refusal <dir>
9879
+ # Echoes a refusal reason when the policy file does not permit a deploy, else "".
9880
+ # No policy file: "" (unchanged behaviour). File present and node or check.js
9881
+ # unavailable: refuse (fail-closed, mirrors run.sh check_policy). Exit 0 allows;
9882
+ # 2 (approval needed) and anything else refuse. Scoped env, no secrets forwarded.
9883
+ _deploy_policy_refusal() {
9884
+ local dir="${1:-.}" gdir="${LOKI_DATA_DIR:-$HOME/.loki}"
9885
+ if [ ! -f "$dir/.loki/policies.json" ] && [ ! -f "$dir/.loki/policies.yaml" ] \
9886
+ && [ ! -f "$gdir/policies.json" ] && [ ! -f "$gdir/policies.yaml" ]; then
9887
+ printf '%s' ""
9888
+ return 0
9889
+ fi
9890
+ local checkjs="${SKILL_DIR:-}/src/policies/check.js"
9891
+ [ -f "$checkjs" ] || checkjs="${PROJECT_DIR:-.}/src/policies/check.js"
9892
+ if ! command -v node >/dev/null 2>&1; then
9893
+ printf '%s' "policy file present but node is unavailable: cannot evaluate policy (fail-closed)"
9894
+ return 0
9895
+ fi
9896
+ if [ ! -f "$checkjs" ]; then
9897
+ printf '%s' "policy file present but check.js is missing (fail-closed)"
9898
+ return 0
9899
+ fi
9900
+ local out rc=0
9901
+ out="$(env -i PATH="$PATH" HOME="$HOME" LOKI_PROJECT_DIR="$dir" \
9902
+ ${LOKI_DATA_DIR:+LOKI_DATA_DIR="$LOKI_DATA_DIR"} \
9903
+ node "$checkjs" pre_deployment '{"action":"deploy"}' 2>/dev/null)" || rc=$?
9904
+ if [ "$rc" -eq 0 ]; then
9905
+ # Exit 0 also means "nothing to evaluate". Deploy needs explicit permission:
9906
+ # the policy must set autonomy or actions.deploy, else refuse.
9907
+ local setsperm=""
9908
+ setsperm="$(env -i PATH="$PATH" HOME="$HOME" LOKI_PROJECT_DIR="$dir" \
9909
+ ${LOKI_DATA_DIR:+LOKI_DATA_DIR="$LOKI_DATA_DIR"} \
9910
+ node -e 'var E=require(process.argv[1].replace(/check\.js$/,"engine.js")).PolicyEngine;var e=new E(process.env.LOKI_PROJECT_DIR);var p=e._policies;p=p&&(p.policies||p);var a=p&&p.actions&&typeof p.actions==="object"?p.actions.deploy:undefined;process.stdout.write(p&&(p.autonomy!==undefined||a!==undefined)?"yes":"");try{e.destroy()}catch(x){}process.exit(0)' "$checkjs" 2>/dev/null)" || setsperm=""
9911
+ if [ "$setsperm" != "yes" ]; then
9912
+ printf '%s' "policy file sets no autonomy/actions.deploy for deploy (no policy permission)"
9913
+ else
9914
+ printf '%s' ""
9915
+ fi
9916
+ return 0
9917
+ fi
9918
+ local why
9919
+ why="$(printf '%s' "$out" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("reason",""),end="")' 2>/dev/null || printf '')"
9920
+ if [ "$rc" -eq 2 ]; then
9921
+ printf '%s' "policy requires approval for deploy: ${why:-no reason given}"
9922
+ else
9923
+ printf '%s' "policy denied deploy: ${why:-policy check failed (fail-closed)}"
9924
+ fi
9925
+ return 0
9926
+ }
9927
+
9744
9928
  # _deploy_try_execute <dir> <type>
9745
9929
  # The ONLY place a deploy command is ever run. Evaluates the gate, and either
9746
9930
  # executes + records, or REFUSES naming every failed check. Returns the deploy
@@ -9750,6 +9934,8 @@ EOF
9750
9934
  # refusal still leaves the user with exactly the output they get today.
9751
9935
  _deploy_try_execute() {
9752
9936
  local dir="${1:-.}" type="${2:-}"
9937
+ # Set by cmd_deploy (dynamic scope); empty/false when called any other way.
9938
+ local health_url="${_LOKI_DEPLOY_HEALTH_URL:-}" auto_rollback="${_LOKI_DEPLOY_AUTO_ROLLBACK:-false}"
9753
9939
 
9754
9940
  local command
9755
9941
  command="$(_deploy_first_installed_command "$dir" "$type")"
@@ -9827,18 +10013,73 @@ _deploy_try_execute() {
9827
10013
  # The string is NOT user input: it comes from the hardcoded
9828
10014
  # _deploy_options_for_type table, and the only path here has already passed
9829
10015
  # the destructive-operation check in _deploy_gate.
10016
+ # Resolve the rollback target BEFORE deploying: after it, the previous Fly
10017
+ # release is no longer the newest one and cannot be recovered.
10018
+ local rb_cmd="" rb_note=""
10019
+ if [ -n "$health_url" ]; then
10020
+ local rb_raw
10021
+ rb_raw="$(_deploy_rollback_for "$type" "$command")"
10022
+ case "$rb_raw" in
10023
+ manual:*) rb_note="${rb_raw#manual:}" ;;
10024
+ *"<captured-version>"*)
10025
+ local fv
10026
+ fv="$(_deploy_fly_capture_version "$dir")"
10027
+ if [ -n "$fv" ]; then rb_cmd="${rb_raw//<captured-version>/$fv}"
10028
+ else rb_note="could not read the current Fly release before deploying, so no rollback target is known"; fi
10029
+ ;;
10030
+ "") rb_note="no rollback verb is documented for this command" ;;
10031
+ *) rb_cmd="$rb_raw" ;;
10032
+ esac
10033
+ fi
10034
+
9830
10035
  local rc=0
9831
10036
  ( cd "$dir" && eval "$command" ) || rc=$?
9832
10037
 
10038
+ # Health probe only after a deploy that exited 0. Without a URL, or after a
10039
+ # failed deploy, status is "unchecked": never a claim the service is healthy.
10040
+ local health_status="unchecked" rb_attempted=false rb_exit="" hcode=""
10041
+ if [ "$rc" -eq 0 ] && [ -n "$health_url" ]; then
10042
+ echo "Probing health: ${health_url}"
10043
+ if hcode="$(_deploy_probe_health "$health_url")"; then
10044
+ health_status="healthy"
10045
+ else
10046
+ health_status="FAILED_HEALTH"
10047
+ fi
10048
+ fi
10049
+
10050
+ if [ "$health_status" = "FAILED_HEALTH" ]; then
10051
+ echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx (last status ${hcode:-000}).${NC}" >&2
10052
+ if [ "$auto_rollback" = true ] && [ -n "$rb_cmd" ] && ! _deploy_is_destructive "$rb_cmd"; then
10053
+ echo "Rolling back: ${rb_cmd}" >&2
10054
+ rb_attempted=true
10055
+ rb_exit=0
10056
+ ( cd "$dir" && eval "$rb_cmd" ) || rb_exit=$?
10057
+ if [ "$rb_exit" -eq 0 ]; then
10058
+ echo "Rollback command exited 0. Health of the rolled-back service was NOT re-checked." >&2
10059
+ else
10060
+ echo -e "${RED}Rollback command exited ${rb_exit}.${NC}" >&2
10061
+ fi
10062
+ elif [ "$auto_rollback" = true ]; then
10063
+ echo " No automatic rollback ran: ${rb_note:-no allowlisted rollback command}." >&2
10064
+ elif [ -n "$rb_cmd" ]; then
10065
+ echo " Re-run with --auto-rollback to roll back automatically, or run: ${rb_cmd}" >&2
10066
+ fi
10067
+ fi
10068
+
9833
10069
  local rec
9834
10070
  rec="$(_deploy_record "$dir" "$command" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
9835
- "$anchor_state")"
10071
+ "$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit")"
9836
10072
  echo ""
9837
10073
  if [ "$rc" -eq 0 ]; then
9838
10074
  echo -e "${GREEN}Deploy command exited 0.${NC}"
9839
10075
  else
9840
10076
  echo -e "${RED}Deploy command exited ${rc}.${NC}" >&2
9841
10077
  fi
10078
+ if [ "$health_status" = "healthy" ]; then
10079
+ echo -e "${GREEN}Health check passed${NC} (HTTP ${hcode})."
10080
+ elif [ "$health_status" = "unchecked" ]; then
10081
+ echo "Health: unchecked (no --health-url, or the deploy did not exit 0). Not a claim the service is healthy."
10082
+ fi
9842
10083
  if [ -n "$rec" ]; then
9843
10084
  echo "Deploy record: ${rec}"
9844
10085
  else
@@ -9852,6 +10093,10 @@ _deploy_try_execute() {
9852
10093
  echo " Authorizing receipt run_id: ${run_id:-unknown}; anchor: ${anchor:-unknown}" >&2
9853
10094
  echo " Record this yourself: a deploy with no record of itself is unevidenced." >&2
9854
10095
  fi
10096
+ # A deploy that exited 0 but failed its health probe is a failed deploy.
10097
+ if [ "$rc" -eq 0 ] && [ "$health_status" = "FAILED_HEALTH" ]; then
10098
+ return 4
10099
+ fi
9855
10100
  return "$rc"
9856
10101
  }
9857
10102
 
@@ -9937,6 +10182,11 @@ _deploy_gate() {
9937
10182
  [ -n "$dirty" ] && reasons="${reasons}${dirty}
9938
10183
  "
9939
10184
 
10185
+ local pol
10186
+ pol="$(_deploy_policy_refusal "$dir")"
10187
+ [ -n "$pol" ] && reasons="${reasons}${pol}
10188
+ "
10189
+
9940
10190
  printf '%s' "$reasons"
9941
10191
  return 0
9942
10192
  }
@@ -9957,6 +10207,8 @@ cmd_deploy() {
9957
10207
  # below sets it. Same rule --execute already follows -- no env var or config
9958
10208
  # file alone weakens this gate.
9959
10209
  local LOKI_DEPLOY_ALLOW_UNANCHORED=false
10210
+ # Per-invocation like --execute; local so an ambient env var cannot set them.
10211
+ local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_AUTO_ROLLBACK=false
9960
10212
 
9961
10213
  # Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
9962
10214
  while [ $# -gt 0 ]; do
@@ -9964,7 +10216,7 @@ cmd_deploy() {
9964
10216
  --help|-h|help)
9965
10217
  echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
9966
10218
  echo ""
9967
- echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute]"
10219
+ echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute] [--health-url URL [--auto-rollback]]"
9968
10220
  echo ""
9969
10221
  echo "Detects your project type and your installed cloud CLI (and any"
9970
10222
  echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
@@ -9997,6 +10249,10 @@ cmd_deploy() {
9997
10249
  echo " --allow-unanchored does NOT bypass it."
9998
10250
  echo " 4. You passed --execute on THIS invocation. No env var or config"
9999
10251
  echo " file alone can opt in."
10252
+ echo " 5. Policy permits it. If .loki/policies.json (or the global one) exists,"
10253
+ echo " deploy needs autonomy 'operate' or actions.deploy 'allow'. An"
10254
+ echo " approval-required, denied, unevaluable or permission-less (neither key set) policy REFUSES. No policy"
10255
+ echo " file leaves the gate unchanged."
10000
10256
  echo ""
10001
10257
  echo "It FAILS CLOSED: if a check cannot be evaluated at all (no receipt,"
10002
10258
  echo "signing key or keys unavailable, anchor unresolvable, not a git repo), that is a"
@@ -10016,6 +10272,17 @@ cmd_deploy() {
10016
10272
  echo "resolved anchor, and anchor_state -- so a reader can tell later"
10017
10273
  echo "whether the anchor was proven or explicitly overridden."
10018
10274
  echo ""
10275
+ echo "Health check and rollback (only with --execute):"
10276
+ echo " --health-url URL After a deploy that exits 0, poll URL (bounded). Any"
10277
+ echo " non-2xx marks the deploy FAILED_HEALTH and exits 4."
10278
+ echo " Without a URL, health is recorded as 'unchecked',"
10279
+ echo " never as healthy."
10280
+ echo " --auto-rollback On FAILED_HEALTH run the provider's documented rollback:"
10281
+ echo " 'vercel rollback' or 'fly releases rollback <version"
10282
+ echo " captured before the deploy>'. Netlify and Cloudflare"
10283
+ echo " print a manual note. Needs --health-url. The rolled-back"
10284
+ echo " service is not re-probed."
10285
+ echo ""
10019
10286
  echo "Options:"
10020
10287
  echo " --dir <path> Project directory to scan (default: current dir)"
10021
10288
  echo " --no-clip Do not copy the idiomatic command to the clipboard"
@@ -10035,6 +10302,15 @@ cmd_deploy() {
10035
10302
  # never persisted and never settable by config alone.
10036
10303
  LOKI_DEPLOY_ALLOW_UNANCHORED=true
10037
10304
  ;;
10305
+ --health-url)
10306
+ case "${2:-}" in
10307
+ http://*|https://*) _LOKI_DEPLOY_HEALTH_URL="$2"; shift ;;
10308
+ *) echo "loki deploy --health-url requires an http(s) URL" >&2; return 1 ;;
10309
+ esac
10310
+ ;;
10311
+ --auto-rollback)
10312
+ _LOKI_DEPLOY_AUTO_ROLLBACK=true
10313
+ ;;
10038
10314
  --dir)
10039
10315
  # Guard against a missing OR flag-shaped value: an unguarded
10040
10316
  # shift when --dir is the last arg underflows under set -e, and a
@@ -10052,6 +10328,14 @@ cmd_deploy() {
10052
10328
  shift
10053
10329
  done
10054
10330
 
10331
+ if [ "$_LOKI_DEPLOY_AUTO_ROLLBACK" = true ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
10332
+ echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
10333
+ return 1
10334
+ fi
10335
+ if [ "$do_execute" != true ] && [ -n "$_LOKI_DEPLOY_HEALTH_URL" ]; then
10336
+ echo "Note: --health-url and --auto-rollback only apply with --execute; ignored on this advisory run." >&2
10337
+ fi
10338
+
10055
10339
  # Resolve to '.' if the chosen dir does not exist (honest, no crash).
10056
10340
  [ -d "$dir" ] || dir="."
10057
10341
 
@@ -17846,6 +18130,15 @@ cmd_verify() {
17846
18130
  --pubkey|--pubkey=*) exec "$_LOKI_SCRIPT_DIR/../bin/loki" verify "$@" ;;
17847
18131
  esac
17848
18132
  done
18133
+ # PATH slice: a workspace composite (composite.json, or --seal-composite RUN_DIR) is
18134
+ # verified by the engine10 verifier directly; bin/loki sends a bare verify back here.
18135
+ for _pk_arg in "$@"; do
18136
+ case "$_pk_arg" in
18137
+ composite.json|*/composite.json|--seal-composite|--seal-composite=*)
18138
+ _loki_bun_delegate engine10 verify "$@"
18139
+ return $? ;;
18140
+ esac
18141
+ done
17849
18142
  # --fast: deterministic checks only, in milliseconds. Measured on this repo
17850
18143
  # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
17851
18144
  # an 11,040 ms shell-based baseline.
@@ -24459,6 +24752,7 @@ except Exception as e:
24459
24752
  echo " lessons [--json] PR review lessons with their uses and outcomes"
24460
24753
  echo " learn <o/r#PR> Learn lessons from a merged PR's review comments"
24461
24754
  echo " forget <prl-id> Delete one PR lesson and print its text"
24755
+ echo " brief [--task T] Print the lessons and learnings block injected into a run"
24462
24756
  echo ""
24463
24757
  echo "Memory System Commands:"
24464
24758
  echo " index [rebuild] Show or rebuild the memory index layer"