loki-mode 11.5.1 → 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 (116) 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 +327 -222
  9. package/autonomy/notification-checker.py +1 -1
  10. package/autonomy/trigger-server.py +185 -2
  11. package/autonomy/verify.sh +4 -0
  12. package/completions/_loki +4 -0
  13. package/completions/loki.bash +20 -2
  14. package/dashboard/__init__.py +3 -10
  15. package/dashboard/control.py +5 -435
  16. package/docs/ARCHITECTURE.md +1 -2
  17. package/docs/CLI-CORE-VERBS.md +2 -6
  18. package/docs/CLI-REFERENCE.md +2 -1
  19. package/docs/COMPONENTS.md +0 -2
  20. package/docs/DEPLOY-PLAN.md +17 -8
  21. package/docs/EVALUATING.md +8 -0
  22. package/docs/INSTALLATION.md +1 -1
  23. package/docs/WORKSPACES.md +15 -2
  24. package/docs/authentication.md +19 -1
  25. package/docs/dev/architecture-reference.md +8 -0
  26. package/docs/dev/release-checklist.md +9 -8
  27. package/docs/enterprise/security.md +3 -234
  28. package/docs/sentry-intake.md +11 -0
  29. package/docs/v10/ARCHITECT-CUTS.md +304 -0
  30. package/docs/v10/CLI-MODERN.md +1 -1
  31. package/docs/v10/DECISIONS.md +5 -5
  32. package/docs/v10/ENGINE.md +735 -299
  33. package/docs/v10/GUARDS.md +16 -14
  34. package/docs/v10/GUIDE.md +5 -7
  35. package/docs/v10/PROGRESS.md +12 -1
  36. package/docs/v11/MARKET-SLICES.md +1 -5
  37. package/integrations/openclaw/README.md +20 -0
  38. package/integrations/vibe-kanban.md +12 -50
  39. package/loki-ts/dist/loki.js +1213 -1178
  40. package/mcp/__init__.py +1 -1
  41. package/mcp/_sdk_loader.py +1 -1
  42. package/package.json +2 -2
  43. package/packages/control-plane/dist/server.js +1090 -924
  44. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  45. package/references/quality-control.md +1 -116
  46. package/schemas/loki-yaml.schema.json +5 -2
  47. package/skills/00-index.md +2 -2
  48. package/skills/artifacts.md +0 -22
  49. package/skills/production.md +2 -38
  50. package/src/policies/engine.js +41 -2
  51. package/src/policies/types.js +49 -0
  52. package/web-app/dist/assets/{AdminPage-Cbu3pXL8.js → AdminPage-S3BYeapm.js} +1 -1
  53. package/web-app/dist/assets/{Avatar-DGqGGcGx.js → Avatar-C1rHwLgn.js} +1 -1
  54. package/web-app/dist/assets/{Badge-Bhlipt6v.js → Badge-CzlnN5aQ.js} +1 -1
  55. package/web-app/dist/assets/{Button-CpN2sO61.js → Button-B1w5AeGC.js} +1 -1
  56. package/web-app/dist/assets/{CockpitPage-BqeDJwdL.js → CockpitPage-Dyeqjfkc.js} +1 -1
  57. package/web-app/dist/assets/{ComparePage-DUieA2ba.js → ComparePage-C6y2GkGR.js} +1 -1
  58. package/web-app/dist/assets/{ErrorBoundary-DTWsYrOm.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
  59. package/web-app/dist/assets/{EvidenceReceiptPanel-BU_B9X19.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
  60. package/web-app/dist/assets/{GitHubIssuesPanel-DEZoLOsP.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
  61. package/web-app/dist/assets/{GitHubPRsPanel-BEsnrq7Y.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
  62. package/web-app/dist/assets/{HomePage-BoUVuq7h.js → HomePage-BKnv-oYw.js} +2 -2
  63. package/web-app/dist/assets/{LoginPage-CmOED7vb.js → LoginPage-BGksmFRd.js} +1 -1
  64. package/web-app/dist/assets/{MagicPage-DATZitrZ.js → MagicPage-OCvhp16M.js} +1 -1
  65. package/web-app/dist/assets/{MetricsPage-DGL1v0I-.js → MetricsPage-CJOJpXWC.js} +1 -1
  66. package/web-app/dist/assets/{NotFoundPage-C2WgOjZd.js → NotFoundPage-C04p3QvN.js} +1 -1
  67. package/web-app/dist/assets/{ProjectPage-BMi1tepj.js → ProjectPage-BRB8txsY.js} +3 -3
  68. package/web-app/dist/assets/{ProjectsPage-vc3y5YPh.js → ProjectsPage-CoUYeQEh.js} +1 -1
  69. package/web-app/dist/assets/{SettingsPage-kbq0Ftvi.js → SettingsPage-BkjmJGfM.js} +1 -1
  70. package/web-app/dist/assets/{ShowcasePage-Bn6Q8Kbg.js → ShowcasePage-B4vU9TjQ.js} +1 -1
  71. package/web-app/dist/assets/{SystemSettingsPage-Basmhs_b.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
  72. package/web-app/dist/assets/{TeamsPage-BTqOc4TR.js → TeamsPage-Ulm8SISq.js} +1 -1
  73. package/web-app/dist/assets/{TemplatesPage-2P9002VL.js → TemplatesPage-C_7MM6Ey.js} +1 -1
  74. package/web-app/dist/assets/{TerminalOutput-CAoSPBrg.js → TerminalOutput-Cumd7Tgs.js} +1 -1
  75. package/web-app/dist/assets/{activity-BcWVWCqU.js → activity-CZAUkbiY.js} +1 -1
  76. package/web-app/dist/assets/{bell-CYRqqgZ6.js → bell-BOvSXpEr.js} +1 -1
  77. package/web-app/dist/assets/{bot-CU7gdn3g.js → bot-DOTZxBYj.js} +1 -1
  78. package/web-app/dist/assets/{check-DLlCpz9V.js → check-BxpE0B5u.js} +1 -1
  79. package/web-app/dist/assets/{chevron-left-CE9Cb30C.js → chevron-left-D6KPvBOA.js} +1 -1
  80. package/web-app/dist/assets/{circle-alert-uou4bjua.js → circle-alert-Bajq86oe.js} +1 -1
  81. package/web-app/dist/assets/{clock-C_dSJcDX.js → clock-CnlHAVJc.js} +1 -1
  82. package/web-app/dist/assets/{cloud-DZ22ITF6.js → cloud-DjaymjwM.js} +1 -1
  83. package/web-app/dist/assets/{code-xml-BrJS0hhS.js → code-xml-BADEw2Ld.js} +1 -1
  84. package/web-app/dist/assets/{database-D2HOmxtl.js → database-B4aKb5Cq.js} +1 -1
  85. package/web-app/dist/assets/{dollar-sign-CQ1vK_6q.js → dollar-sign-DrldXpja.js} +1 -1
  86. package/web-app/dist/assets/{file-code-corner-w2vpGm5d.js → file-code-corner-DVebDVR-.js} +1 -1
  87. package/web-app/dist/assets/{file-plus-DlF6wjkD.js → file-plus-BD-N_-Tl.js} +1 -1
  88. package/web-app/dist/assets/{globe-Dy3lBJg1.js → globe-C7ieInC-.js} +1 -1
  89. package/web-app/dist/assets/{hammer-C3xIiRXN.js → hammer-BSugKIji.js} +1 -1
  90. package/web-app/dist/assets/{index-4t-_IW2y.js → index-nLQgfvOn.js} +3 -3
  91. package/web-app/dist/assets/{layers-Y8Fs8qXI.js → layers-CHXxU2on.js} +1 -1
  92. package/web-app/dist/assets/{loader-circle-CxgN1v4m.js → loader-circle-BEEz0LbD.js} +1 -1
  93. package/web-app/dist/assets/{lock-DFFNbXDs.js → lock-CzQGfcnE.js} +1 -1
  94. package/web-app/dist/assets/{package-5e6qCTUg.js → package-CcHYQLGd.js} +1 -1
  95. package/web-app/dist/assets/{plus-C0Go_ytj.js → plus-DdzgFEXn.js} +1 -1
  96. package/web-app/dist/assets/{refresh-cw-D4i-NW3o.js → refresh-cw-7x0HbH9Y.js} +1 -1
  97. package/web-app/dist/assets/{rotate-ccw-YlKkiUW6.js → rotate-ccw-DBahX1Sp.js} +1 -1
  98. package/web-app/dist/assets/{scroll-text-4qXOHTap.js → scroll-text-BB8y8a9Z.js} +1 -1
  99. package/web-app/dist/assets/{server-BB6zXNp9.js → server-CfkbW2MH.js} +1 -1
  100. package/web-app/dist/assets/{shield-alert-D4tzjB1H.js → shield-alert-DUgD8rBF.js} +1 -1
  101. package/web-app/dist/assets/{thumbs-up-DZiRwdFj.js → thumbs-up-DGoyY6QC.js} +1 -1
  102. package/web-app/dist/assets/{trash-2-Dv_LkRIK.js → trash-2-B6xrSUMr.js} +1 -1
  103. package/web-app/dist/assets/{trending-down-BJYp9PQe.js → trending-down-B6RGKTOx.js} +1 -1
  104. package/web-app/dist/assets/{trending-up-BTcXLklK.js → trending-up-B44DCrt1.js} +1 -1
  105. package/web-app/dist/assets/{upload-fcHqwOxr.js → upload-4kWm33ZY.js} +1 -1
  106. package/web-app/dist/assets/{usePolling-BeJYw_4L.js → usePolling-zkUtRQG6.js} +1 -1
  107. package/web-app/dist/assets/{user-BcDwUCsO.js → user-04cEVIe2.js} +1 -1
  108. package/web-app/dist/index.html +1 -1
  109. package/dashboard/run.py +0 -63
  110. package/docs/openclaw-integration.md +0 -52
  111. package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
  112. package/docs/v10/MORNING-BRIEF-1003.md +0 -25
  113. package/docs/v10/MORNING-BRIEF.md +0 -52
  114. package/docs/v10/TODO.md +0 -35
  115. package/mcp/resources.py +0 -351
  116. package/mcp/tools.py +0 -200
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=""
@@ -8293,7 +8359,7 @@ PURPLE_LAB_STATE_DIR="${HOME}/.loki/purple-lab"
8293
8359
  PURPLE_LAB_PID_FILE="${PURPLE_LAB_STATE_DIR}/purple-lab.pid"
8294
8360
 
8295
8361
  # Is this pid positively identified as THIS installation's Purple Lab server
8296
- # (cmd_web_start launches exactly `$DASHBOARD_PYTHON $SKILL_DIR/web-app/server.py`
8362
+ # (a server started before the web deprecation launched exactly `$DASHBOARD_PYTHON $SKILL_DIR/web-app/server.py`
8297
8363
  # at :8078), rather than a recycled pid, a decoy that merely mentions the path
8298
8364
  # in an argument, or another installation's server.py at a different SKILL_DIR?
8299
8365
  #
@@ -8506,205 +8572,6 @@ cmd_web_help() {
8506
8572
  echo " loki web stop Stop a standalone web server started the old way"
8507
8573
  }
8508
8574
 
8509
- cmd_web_start() {
8510
- local open_browser=true
8511
- local port="${PURPLE_LAB_DEFAULT_PORT}"
8512
- local prd_file=""
8513
-
8514
- # Parse arguments
8515
- while [[ $# -gt 0 ]]; do
8516
- case "$1" in
8517
- --no-open)
8518
- open_browser=false
8519
- shift
8520
- ;;
8521
- --port)
8522
- if [[ -z "${2:-}" ]]; then
8523
- echo -e "${RED}--port requires a port number${NC}"
8524
- exit 1
8525
- fi
8526
- port="$2"
8527
- shift 2
8528
- ;;
8529
- --prd)
8530
- if [[ -z "${2:-}" ]]; then
8531
- echo -e "${RED}--prd requires a file path${NC}"
8532
- exit 1
8533
- fi
8534
- prd_file="$2"
8535
- shift 2
8536
- ;;
8537
- --help|-h)
8538
- cmd_web_help
8539
- exit 0
8540
- ;;
8541
- *)
8542
- echo -e "${RED}Unknown option: $1${NC}"
8543
- echo "Run 'loki web --help' for usage."
8544
- exit 1
8545
- ;;
8546
- esac
8547
- done
8548
-
8549
- # Validate port: must be numeric and in valid range
8550
- if [[ ! "$port" =~ ^[0-9]+$ ]]; then
8551
- echo -e "${RED}Error: Port must be a number, got '$port'${NC}"
8552
- exit 1
8553
- fi
8554
- if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
8555
- echo -e "${RED}Error: Port must be between 1 and 65535, got $port${NC}"
8556
- exit 1
8557
- fi
8558
- # Port validation moved below (after PID file check) so we can offer to kill orphans
8559
-
8560
- # Validate --prd file if provided
8561
- if [ -n "$prd_file" ]; then
8562
- if [ ! -f "$prd_file" ]; then
8563
- echo -e "${RED}Error: PRD file not found: $prd_file${NC}"
8564
- exit 1
8565
- fi
8566
- export PURPLE_LAB_PRD
8567
- PURPLE_LAB_PRD=$(cat "$prd_file")
8568
- fi
8569
-
8570
- # Check that web-app dist exists
8571
- local web_dist="${SKILL_DIR}/web-app/dist"
8572
- if [ ! -d "$web_dist" ] || [ ! -f "$web_dist/index.html" ]; then
8573
- echo -e "${RED}Error: Web app not built${NC}"
8574
- echo "Expected files at: $web_dist"
8575
- echo ""
8576
- echo "Build the web app first:"
8577
- echo " cd web-app && npm install && npm run build"
8578
- exit 1
8579
- fi
8580
-
8581
- # Check that server.py exists
8582
- local server_py="${SKILL_DIR}/web-app/server.py"
8583
- if [ ! -f "$server_py" ]; then
8584
- echo -e "${RED}Error: Purple Lab server not found at $server_py${NC}"
8585
- exit 1
8586
- fi
8587
-
8588
- # Ensure Python venv with FastAPI is available
8589
- if ! ensure_dashboard_venv; then
8590
- echo -e "${RED}Error: Failed to set up Python environment${NC}"
8591
- exit 1
8592
- fi
8593
-
8594
- # Check if already running
8595
- mkdir -p "${LOKI_DIR}/purple-lab"
8596
- if [ -f "$PURPLE_LAB_PID_FILE" ]; then
8597
- local existing_pid
8598
- existing_pid=$(cat "$PURPLE_LAB_PID_FILE" 2>/dev/null)
8599
- if [ -n "$existing_pid" ] && kill -0 "$existing_pid" 2>/dev/null; then
8600
- echo -e "${GREEN}Purple Lab already running (PID: $existing_pid)${NC}"
8601
- local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/"
8602
- echo -e "Open: ${CYAN}$url${NC}"
8603
- if [ "$open_browser" = true ]; then
8604
- loki_open_url "$url" 2>/dev/null || true
8605
- fi
8606
- return 0
8607
- fi
8608
- fi
8609
-
8610
- # Check if port is already in use -- try to clean up orphaned processes.
8611
- # Only kill it if it is positively identified as THIS installation's own
8612
- # Purple Lab server (never a bare "it's listening on our port" guess): an
8613
- # unrelated process bound to the same port, or another installation's
8614
- # server, must never be signaled.
8615
- if lsof -i:"$port" -sTCP:LISTEN &> /dev/null; then
8616
- local blocking_pid
8617
- blocking_pid=$(lsof -ti:"$port" -sTCP:LISTEN 2>/dev/null | head -1)
8618
- if [ -n "$blocking_pid" ]; then
8619
- if _loki_pid_looks_like_purplelab "$blocking_pid"; then
8620
- echo -e "${YELLOW}Port $port in use by PID $blocking_pid -- stopping it...${NC}"
8621
- kill "$blocking_pid" 2>/dev/null
8622
- sleep 1
8623
- if kill -0 "$blocking_pid" 2>/dev/null; then
8624
- kill -9 "$blocking_pid" 2>/dev/null
8625
- sleep 1
8626
- fi
8627
- # Verify port is now free
8628
- if lsof -i:"$port" -sTCP:LISTEN &> /dev/null; then
8629
- echo -e "${RED}Error: Could not free port $port${NC}"
8630
- echo "Use --port to specify a different port, or manually kill the process."
8631
- exit 1
8632
- fi
8633
- echo -e "${GREEN}Port $port freed.${NC}"
8634
- else
8635
- echo -e "${RED}Error: Port $port is in use by PID $blocking_pid, which is not this installation's Purple Lab server.${NC}"
8636
- echo "Use --port to specify a different port, or manually stop that process."
8637
- exit 1
8638
- fi
8639
- fi
8640
- fi
8641
-
8642
- # Start the server
8643
- local log_dir="${LOKI_DIR}/purple-lab/logs"
8644
- mkdir -p "$log_dir"
8645
- local log_file="$log_dir/purple-lab.log"
8646
-
8647
- echo -e "${CYAN}Starting Purple Lab...${NC}"
8648
-
8649
- PURPLE_LAB_PORT="$port" PURPLE_LAB_HOST="${PURPLE_LAB_DEFAULT_HOST}" \
8650
- LOKI_DIR="$LOKI_DIR" LOKI_SKILL_DIR="$SKILL_DIR" PYTHONPATH="$SKILL_DIR" \
8651
- nohup "$DASHBOARD_PYTHON" "$server_py" > "$log_file" 2>&1 &
8652
- local pid=$!
8653
-
8654
- mkdir -p "$PURPLE_LAB_STATE_DIR"
8655
- echo "$pid" > "$PURPLE_LAB_PID_FILE"
8656
- echo "$port" > "${PURPLE_LAB_STATE_DIR}/port"
8657
- # Also write to local .loki for backward compat
8658
- mkdir -p "${LOKI_DIR}/purple-lab"
8659
- echo "$pid" > "${LOKI_DIR}/purple-lab/purple-lab.pid"
8660
- echo "$port" > "${LOKI_DIR}/purple-lab/port"
8661
-
8662
- # Wait for server to be ready
8663
- local retries=0
8664
- local server_ready=false
8665
- while [ $retries -lt 15 ]; do
8666
- if curl -s "http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/api/session/status" > /dev/null 2>&1; then
8667
- server_ready=true
8668
- break
8669
- fi
8670
- sleep 0.5
8671
- retries=$((retries + 1))
8672
- done
8673
-
8674
- if ! kill -0 "$pid" 2>/dev/null; then
8675
- echo -e "${RED}Error: Purple Lab failed to start${NC}"
8676
- echo "Check logs at: $log_file"
8677
- rm -f "$PURPLE_LAB_PID_FILE"
8678
- exit 1
8679
- fi
8680
-
8681
- local url="http://${PURPLE_LAB_DEFAULT_HOST}:${port}/lab/"
8682
-
8683
- if [ "$server_ready" = true ]; then
8684
- echo -e "${GREEN}Purple Lab (project web UI) running at: $url${NC} (PID: $pid)"
8685
- else
8686
- echo -e "${YELLOW}Purple Lab (project web UI) starting at: $url${NC} (PID: $pid)"
8687
- echo "Server may still be loading. Refresh the browser if it does not load immediately."
8688
- fi
8689
- echo -e "Logs: $log_file"
8690
- # v7.28.x UX #5: distinguish the two browser UIs so the ops monitor and the
8691
- # spec-input web UI are never confused for each other.
8692
- echo -e "${DIM}Looking for the live run monitor? loki dashboard (:${DASHBOARD_DEFAULT_PORT:-57374})${NC}"
8693
- echo -e "Stop with: ${CYAN}loki web stop${NC}"
8694
-
8695
- # Open browser only after server is confirmed ready
8696
- if [ "$open_browser" = true ]; then
8697
- if [ "$server_ready" = true ]; then
8698
- echo ""
8699
- loki_open_url "$url" 2>/dev/null || echo "Please open in browser: $url"
8700
- else
8701
- echo ""
8702
- echo -e "${YELLOW}Browser not opened because server is not ready yet.${NC}"
8703
- echo "Open manually when ready: $url"
8704
- fi
8705
- fi
8706
- }
8707
-
8708
8575
  cmd_web_stop() {
8709
8576
  local stopped=false
8710
8577
  local port
@@ -9437,33 +9304,36 @@ _deploy_detect_type() {
9437
9304
  # paraphrase). Project-dependent dirs stay as `<...>` placeholders the user fills.
9438
9305
  # Note: the Fly CLI binary is `flyctl` but the canonical deploy verb is
9439
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.
9440
9310
  _deploy_options_for_type() {
9441
9311
  local type="${1:-}"
9442
9312
  case "$type" in
9443
9313
  nextjs)
9444
- printf '%s\n' "Vercel|vercel|vercel --prod|https://vercel.com/docs/cli"
9445
- 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"
9446
9316
  # fly not flyctl - correct: binary is flyctl, deploy verb is `fly deploy`.
9447
- 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>"
9448
9318
  ;;
9449
9319
  static)
9450
- printf '%s\n' "Netlify|netlify|netlify deploy --prod --dir=<build-output>|https://docs.netlify.com/cli/get-started/"
9451
- printf '%s\n' "Cloudflare Pages|wrangler|wrangler pages deploy <build-output>|https://developers.cloudflare.com/workers/wrangler/install-and-update/"
9452
- 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"
9453
9323
  ;;
9454
9324
  docker)
9455
9325
  # fly not flyctl - correct (see note above).
9456
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9457
- 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"
9458
9328
  ;;
9459
9329
  node)
9460
9330
  # fly not flyctl - correct (see note above).
9461
- printf '%s\n' "Fly.io|flyctl|fly deploy|https://fly.io/docs/flyctl/install/"
9462
- 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"
9463
9333
  ;;
9464
9334
  python)
9465
9335
  # fly not flyctl - correct (see note above).
9466
- 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>"
9467
9337
  ;;
9468
9338
  esac
9469
9339
  return 0
@@ -9538,9 +9408,9 @@ _deploy_print_install_hint() {
9538
9408
  echo "Loki never accesses your cloud account or runs deploy for you -- you run"
9539
9409
  echo "the printed command. Install one of the following, then re-run 'loki deploy':"
9540
9410
  echo ""
9541
- local options provider cli command docs
9411
+ local options provider cli command docs _rb
9542
9412
  options="$(_deploy_options_for_type "$type")"
9543
- while IFS='|' read -r provider cli command docs; do
9413
+ while IFS='|' read -r provider cli command docs _rb; do
9544
9414
  [ -n "$provider" ] || continue
9545
9415
  case "$cli" in
9546
9416
  vercel) echo " Vercel: brew install vercel | ${docs}" ;;
@@ -9572,8 +9442,8 @@ _deploy_print_cloud_options() {
9572
9442
 
9573
9443
  local printed=false
9574
9444
  local first_cmd=""
9575
- local provider cli command docs
9576
- 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
9577
9447
  [ -n "$cli" ] || continue
9578
9448
  if command -v "$cli" >/dev/null 2>&1; then
9579
9449
  if [ "$printed" != true ]; then
@@ -9835,6 +9705,9 @@ PYANCHOR
9835
9705
  # verified tree, never tearing an environment down. Word-boundary matched so
9836
9706
  # "wrangler pages deploy dist" is not caught by "destroy" appearing in a path.
9837
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
9838
9711
  printf '%s' "${1:-}" | grep -Eqi '(^|[^[:alnum:]_-])(delete|destroy|teardown|down|drop|rm|remove|purge|prune|wipe|scale-down)([^[:alnum:]_-]|$)'
9839
9712
  }
9840
9713
 
@@ -9893,12 +9766,16 @@ _deploy_record() {
9893
9766
  # "refused" needs no value: a refusal never reaches this function, so the
9894
9767
  # existence of a record already means one of the two above.
9895
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:-}"
9896
9772
  local ddir="$dir/.loki/deploys"
9897
9773
  [ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
9898
9774
  mkdir -p "$ddir" 2>/dev/null || return 0
9899
9775
  local f="$ddir/$(date -u +%Y%m%dT%H%M%SZ)-$$.json"
9900
9776
  DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
9901
- 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" \
9902
9779
  python3 -c '
9903
9780
  import json, os, datetime
9904
9781
  state = os.environ.get("DR_STATE") or "anchored"
@@ -9912,6 +9789,10 @@ json.dump({
9912
9789
  # Spelled out so a reader months later does not have to know the vocabulary
9913
9790
  # to judge how strong the authorization was.
9914
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,
9915
9796
  }, open(os.environ["DR_OUT"], "w"), indent=2)
9916
9797
  ' 2>/dev/null || return 0
9917
9798
  printf '%s' "$f"
@@ -9925,9 +9806,9 @@ json.dump({
9925
9806
  # differs from the one the advisory showed. `command -v` only; no CLI is run.
9926
9807
  _deploy_first_installed_command() {
9927
9808
  local dir="${1:-.}" type="${2:-}"
9928
- local options provider cli command docs
9809
+ local options provider cli command docs _rb
9929
9810
  options="$(_deploy_options_for_type "$type")"
9930
- while IFS='|' read -r provider cli command docs; do
9811
+ while IFS='|' read -r provider cli command docs _rb; do
9931
9812
  [ -n "$cli" ] || continue
9932
9813
  if command -v "$cli" >/dev/null 2>&1; then
9933
9814
  printf '%s' "$command"
@@ -9940,6 +9821,110 @@ EOF
9940
9821
  return 0
9941
9822
  }
9942
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
+
9943
9928
  # _deploy_try_execute <dir> <type>
9944
9929
  # The ONLY place a deploy command is ever run. Evaluates the gate, and either
9945
9930
  # executes + records, or REFUSES naming every failed check. Returns the deploy
@@ -9949,6 +9934,8 @@ EOF
9949
9934
  # refusal still leaves the user with exactly the output they get today.
9950
9935
  _deploy_try_execute() {
9951
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}"
9952
9939
 
9953
9940
  local command
9954
9941
  command="$(_deploy_first_installed_command "$dir" "$type")"
@@ -10026,18 +10013,73 @@ _deploy_try_execute() {
10026
10013
  # The string is NOT user input: it comes from the hardcoded
10027
10014
  # _deploy_options_for_type table, and the only path here has already passed
10028
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
+
10029
10035
  local rc=0
10030
10036
  ( cd "$dir" && eval "$command" ) || rc=$?
10031
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
+
10032
10069
  local rec
10033
10070
  rec="$(_deploy_record "$dir" "$command" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
10034
- "$anchor_state")"
10071
+ "$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit")"
10035
10072
  echo ""
10036
10073
  if [ "$rc" -eq 0 ]; then
10037
10074
  echo -e "${GREEN}Deploy command exited 0.${NC}"
10038
10075
  else
10039
10076
  echo -e "${RED}Deploy command exited ${rc}.${NC}" >&2
10040
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
10041
10083
  if [ -n "$rec" ]; then
10042
10084
  echo "Deploy record: ${rec}"
10043
10085
  else
@@ -10051,6 +10093,10 @@ _deploy_try_execute() {
10051
10093
  echo " Authorizing receipt run_id: ${run_id:-unknown}; anchor: ${anchor:-unknown}" >&2
10052
10094
  echo " Record this yourself: a deploy with no record of itself is unevidenced." >&2
10053
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
10054
10100
  return "$rc"
10055
10101
  }
10056
10102
 
@@ -10136,6 +10182,11 @@ _deploy_gate() {
10136
10182
  [ -n "$dirty" ] && reasons="${reasons}${dirty}
10137
10183
  "
10138
10184
 
10185
+ local pol
10186
+ pol="$(_deploy_policy_refusal "$dir")"
10187
+ [ -n "$pol" ] && reasons="${reasons}${pol}
10188
+ "
10189
+
10139
10190
  printf '%s' "$reasons"
10140
10191
  return 0
10141
10192
  }
@@ -10156,6 +10207,8 @@ cmd_deploy() {
10156
10207
  # below sets it. Same rule --execute already follows -- no env var or config
10157
10208
  # file alone weakens this gate.
10158
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
10159
10212
 
10160
10213
  # Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
10161
10214
  while [ $# -gt 0 ]; do
@@ -10163,7 +10216,7 @@ cmd_deploy() {
10163
10216
  --help|-h|help)
10164
10217
  echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
10165
10218
  echo ""
10166
- echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute]"
10219
+ echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute] [--health-url URL [--auto-rollback]]"
10167
10220
  echo ""
10168
10221
  echo "Detects your project type and your installed cloud CLI (and any"
10169
10222
  echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
@@ -10196,6 +10249,10 @@ cmd_deploy() {
10196
10249
  echo " --allow-unanchored does NOT bypass it."
10197
10250
  echo " 4. You passed --execute on THIS invocation. No env var or config"
10198
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."
10199
10256
  echo ""
10200
10257
  echo "It FAILS CLOSED: if a check cannot be evaluated at all (no receipt,"
10201
10258
  echo "signing key or keys unavailable, anchor unresolvable, not a git repo), that is a"
@@ -10215,6 +10272,17 @@ cmd_deploy() {
10215
10272
  echo "resolved anchor, and anchor_state -- so a reader can tell later"
10216
10273
  echo "whether the anchor was proven or explicitly overridden."
10217
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 ""
10218
10286
  echo "Options:"
10219
10287
  echo " --dir <path> Project directory to scan (default: current dir)"
10220
10288
  echo " --no-clip Do not copy the idiomatic command to the clipboard"
@@ -10234,6 +10302,15 @@ cmd_deploy() {
10234
10302
  # never persisted and never settable by config alone.
10235
10303
  LOKI_DEPLOY_ALLOW_UNANCHORED=true
10236
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
+ ;;
10237
10314
  --dir)
10238
10315
  # Guard against a missing OR flag-shaped value: an unguarded
10239
10316
  # shift when --dir is the last arg underflows under set -e, and a
@@ -10251,6 +10328,14 @@ cmd_deploy() {
10251
10328
  shift
10252
10329
  done
10253
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
+
10254
10339
  # Resolve to '.' if the chosen dir does not exist (honest, no crash).
10255
10340
  [ -d "$dir" ] || dir="."
10256
10341
 
@@ -12451,6 +12536,7 @@ cmd_export() {
12451
12536
  echo " markdown Human-readable session summary"
12452
12537
  echo " csv Task queue as CSV"
12453
12538
  echo " timeline Iteration timeline as JSON"
12539
+ echo " vibe-kanban Sync .loki/queue tasks into the Vibe Kanban SQLite DB"
12454
12540
  echo ""
12455
12541
  echo "Flags:"
12456
12542
  echo " --sarif [run-id] Write .loki/runs/<id>/findings.sarif (SARIF 2.1.0; needs bun)"
@@ -12461,6 +12547,7 @@ cmd_export() {
12461
12547
  echo " loki export markdown # Readable summary"
12462
12548
  echo " loki export csv tasks.csv # Task queue CSV"
12463
12549
  echo " loki export timeline # Iteration timeline"
12550
+ echo " loki export vibe-kanban # Sync tasks to Vibe Kanban"
12464
12551
  exit 0
12465
12552
  ;;
12466
12553
  json)
@@ -12477,13 +12564,21 @@ cmd_export() {
12477
12564
  require_jq || return 1
12478
12565
  _export_timeline "$output_path"
12479
12566
  ;;
12567
+ vibe-kanban)
12568
+ local vk_script="${SKILL_DIR:-}/scripts/sync-to-vibe-kanban.sh"
12569
+ if [ ! -f "$vk_script" ]; then
12570
+ echo -e "${RED}Error: sync-to-vibe-kanban.sh not found at $vk_script${NC}" >&2
12571
+ return 1
12572
+ fi
12573
+ bash "$vk_script"
12574
+ ;;
12480
12575
  --sarif)
12481
12576
  echo -e "${RED}loki export --sarif needs bun (https://bun.sh).${NC}" >&2
12482
12577
  return 1
12483
12578
  ;;
12484
12579
  *)
12485
12580
  echo -e "${RED}Unknown format: $format${NC}"
12486
- echo "Supported: json, markdown, csv, timeline"
12581
+ echo "Supported: json, markdown, csv, timeline, vibe-kanban"
12487
12582
  exit 1
12488
12583
  ;;
12489
12584
  esac
@@ -18035,6 +18130,15 @@ cmd_verify() {
18035
18130
  --pubkey|--pubkey=*) exec "$_LOKI_SCRIPT_DIR/../bin/loki" verify "$@" ;;
18036
18131
  esac
18037
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
18038
18142
  # --fast: deterministic checks only, in milliseconds. Measured on this repo
18039
18143
  # (1,932 tracked files): 298 ms cold, 87 ms warm, 19 ms diff-scoped, against
18040
18144
  # an 11,040 ms shell-based baseline.
@@ -24648,6 +24752,7 @@ except Exception as e:
24648
24752
  echo " lessons [--json] PR review lessons with their uses and outcomes"
24649
24753
  echo " learn <o/r#PR> Learn lessons from a merged PR's review comments"
24650
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"
24651
24756
  echo ""
24652
24757
  echo "Memory System Commands:"
24653
24758
  echo " index [rebuild] Show or rebuild the memory index layer"