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.
- package/README.md +19 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +11 -2
- package/autonomy/lib/chat.sh +57 -1
- package/autonomy/lib/loki_yaml.py +8 -2
- package/autonomy/lib/workspace.py +267 -11
- package/autonomy/loki +327 -222
- package/autonomy/notification-checker.py +1 -1
- package/autonomy/trigger-server.py +185 -2
- package/autonomy/verify.sh +4 -0
- package/completions/_loki +4 -0
- package/completions/loki.bash +20 -2
- package/dashboard/__init__.py +3 -10
- package/dashboard/control.py +5 -435
- package/docs/ARCHITECTURE.md +1 -2
- package/docs/CLI-CORE-VERBS.md +2 -6
- package/docs/CLI-REFERENCE.md +2 -1
- package/docs/COMPONENTS.md +0 -2
- package/docs/DEPLOY-PLAN.md +17 -8
- package/docs/EVALUATING.md +8 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/WORKSPACES.md +15 -2
- package/docs/authentication.md +19 -1
- package/docs/dev/architecture-reference.md +8 -0
- package/docs/dev/release-checklist.md +9 -8
- package/docs/enterprise/security.md +3 -234
- package/docs/sentry-intake.md +11 -0
- package/docs/v10/ARCHITECT-CUTS.md +304 -0
- package/docs/v10/CLI-MODERN.md +1 -1
- package/docs/v10/DECISIONS.md +5 -5
- package/docs/v10/ENGINE.md +735 -299
- package/docs/v10/GUARDS.md +16 -14
- package/docs/v10/GUIDE.md +5 -7
- package/docs/v10/PROGRESS.md +12 -1
- package/docs/v11/MARKET-SLICES.md +1 -5
- package/integrations/openclaw/README.md +20 -0
- package/integrations/vibe-kanban.md +12 -50
- package/loki-ts/dist/loki.js +1213 -1178
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +1090 -924
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/quality-control.md +1 -116
- package/schemas/loki-yaml.schema.json +5 -2
- package/skills/00-index.md +2 -2
- package/skills/artifacts.md +0 -22
- package/skills/production.md +2 -38
- package/src/policies/engine.js +41 -2
- package/src/policies/types.js +49 -0
- package/web-app/dist/assets/{AdminPage-Cbu3pXL8.js → AdminPage-S3BYeapm.js} +1 -1
- package/web-app/dist/assets/{Avatar-DGqGGcGx.js → Avatar-C1rHwLgn.js} +1 -1
- package/web-app/dist/assets/{Badge-Bhlipt6v.js → Badge-CzlnN5aQ.js} +1 -1
- package/web-app/dist/assets/{Button-CpN2sO61.js → Button-B1w5AeGC.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-BqeDJwdL.js → CockpitPage-Dyeqjfkc.js} +1 -1
- package/web-app/dist/assets/{ComparePage-DUieA2ba.js → ComparePage-C6y2GkGR.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-DTWsYrOm.js → ErrorBoundary-Dg2fcGkR.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-BU_B9X19.js → EvidenceReceiptPanel-CxbyjvTj.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DEZoLOsP.js → GitHubIssuesPanel-Dpys1xDb.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BEsnrq7Y.js → GitHubPRsPanel-CRVjWoFl.js} +1 -1
- package/web-app/dist/assets/{HomePage-BoUVuq7h.js → HomePage-BKnv-oYw.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CmOED7vb.js → LoginPage-BGksmFRd.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DATZitrZ.js → MagicPage-OCvhp16M.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-DGL1v0I-.js → MetricsPage-CJOJpXWC.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C2WgOjZd.js → NotFoundPage-C04p3QvN.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BMi1tepj.js → ProjectPage-BRB8txsY.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-vc3y5YPh.js → ProjectsPage-CoUYeQEh.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-kbq0Ftvi.js → SettingsPage-BkjmJGfM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Bn6Q8Kbg.js → ShowcasePage-B4vU9TjQ.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Basmhs_b.js → SystemSettingsPage-Z2jiVsni.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BTqOc4TR.js → TeamsPage-Ulm8SISq.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-2P9002VL.js → TemplatesPage-C_7MM6Ey.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CAoSPBrg.js → TerminalOutput-Cumd7Tgs.js} +1 -1
- package/web-app/dist/assets/{activity-BcWVWCqU.js → activity-CZAUkbiY.js} +1 -1
- package/web-app/dist/assets/{bell-CYRqqgZ6.js → bell-BOvSXpEr.js} +1 -1
- package/web-app/dist/assets/{bot-CU7gdn3g.js → bot-DOTZxBYj.js} +1 -1
- package/web-app/dist/assets/{check-DLlCpz9V.js → check-BxpE0B5u.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CE9Cb30C.js → chevron-left-D6KPvBOA.js} +1 -1
- package/web-app/dist/assets/{circle-alert-uou4bjua.js → circle-alert-Bajq86oe.js} +1 -1
- package/web-app/dist/assets/{clock-C_dSJcDX.js → clock-CnlHAVJc.js} +1 -1
- package/web-app/dist/assets/{cloud-DZ22ITF6.js → cloud-DjaymjwM.js} +1 -1
- package/web-app/dist/assets/{code-xml-BrJS0hhS.js → code-xml-BADEw2Ld.js} +1 -1
- package/web-app/dist/assets/{database-D2HOmxtl.js → database-B4aKb5Cq.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-CQ1vK_6q.js → dollar-sign-DrldXpja.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-w2vpGm5d.js → file-code-corner-DVebDVR-.js} +1 -1
- package/web-app/dist/assets/{file-plus-DlF6wjkD.js → file-plus-BD-N_-Tl.js} +1 -1
- package/web-app/dist/assets/{globe-Dy3lBJg1.js → globe-C7ieInC-.js} +1 -1
- package/web-app/dist/assets/{hammer-C3xIiRXN.js → hammer-BSugKIji.js} +1 -1
- package/web-app/dist/assets/{index-4t-_IW2y.js → index-nLQgfvOn.js} +3 -3
- package/web-app/dist/assets/{layers-Y8Fs8qXI.js → layers-CHXxU2on.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CxgN1v4m.js → loader-circle-BEEz0LbD.js} +1 -1
- package/web-app/dist/assets/{lock-DFFNbXDs.js → lock-CzQGfcnE.js} +1 -1
- package/web-app/dist/assets/{package-5e6qCTUg.js → package-CcHYQLGd.js} +1 -1
- package/web-app/dist/assets/{plus-C0Go_ytj.js → plus-DdzgFEXn.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D4i-NW3o.js → refresh-cw-7x0HbH9Y.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-YlKkiUW6.js → rotate-ccw-DBahX1Sp.js} +1 -1
- package/web-app/dist/assets/{scroll-text-4qXOHTap.js → scroll-text-BB8y8a9Z.js} +1 -1
- package/web-app/dist/assets/{server-BB6zXNp9.js → server-CfkbW2MH.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D4tzjB1H.js → shield-alert-DUgD8rBF.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DZiRwdFj.js → thumbs-up-DGoyY6QC.js} +1 -1
- package/web-app/dist/assets/{trash-2-Dv_LkRIK.js → trash-2-B6xrSUMr.js} +1 -1
- package/web-app/dist/assets/{trending-down-BJYp9PQe.js → trending-down-B6RGKTOx.js} +1 -1
- package/web-app/dist/assets/{trending-up-BTcXLklK.js → trending-up-B44DCrt1.js} +1 -1
- package/web-app/dist/assets/{upload-fcHqwOxr.js → upload-4kWm33ZY.js} +1 -1
- package/web-app/dist/assets/{usePolling-BeJYw_4L.js → usePolling-zkUtRQG6.js} +1 -1
- package/web-app/dist/assets/{user-BcDwUCsO.js → user-04cEVIe2.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/dashboard/run.py +0 -63
- package/docs/openclaw-integration.md +0 -52
- package/docs/v10/HANDOFF-NEXT-WEEK.md +0 -40
- package/docs/v10/MORNING-BRIEF-1003.md +0 -25
- package/docs/v10/MORNING-BRIEF.md +0 -52
- package/docs/v10/TODO.md +0 -35
- package/mcp/resources.py +0 -351
- 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
|
-
# (
|
|
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"
|