loki-mode 11.12.1 → 11.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +18 -3
- package/autonomy/lib/chat.sh +6 -0
- package/autonomy/lib/engine10-push.sh +26 -2
- package/autonomy/lib/proof-generator.py +16 -0
- package/autonomy/lib/trust_trajectory.py +130 -0
- package/autonomy/lib/workspace.py +2 -3
- package/autonomy/loki +262 -58
- package/autonomy/run.sh +39 -15
- package/autonomy/trigger-server.py +452 -29
- package/bin/loki +4 -1
- package/completions/_loki +1 -1
- package/completions/loki.bash +8 -0
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +5 -4
- package/docs/DEPLOYMENT.md +12 -5
- package/docs/EVALUATING.md +13 -0
- package/docs/INSTALLATION.md +2 -2
- package/docs/SIGNED-RECEIPTS.md +7 -0
- package/docs/WORKSPACES.md +2 -2
- package/docs/cost-controls.md +26 -0
- package/docs/merge-queue.md +19 -0
- package/docs/sentry-intake.md +4 -0
- package/docs/slack.md +2 -1
- package/docs/trackers.md +12 -1
- package/docs/v10/BOARD.md +123 -123
- package/docs/v10/CLI-MODERN.md +2 -2
- package/docs/v10/DECISIONS.md +1 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/MODERNIZE.md +32 -1
- package/docs/v10/PROGRESS.md +3 -0
- package/loki-ts/dist/loki.js +1221 -1148
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +2486 -2190
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/schemas/loki-yaml.schema.json +5 -0
- package/web-app/dist/assets/{AdminPage-DuT74pf5.js → AdminPage-bZlvH4TL.js} +1 -1
- package/web-app/dist/assets/{Avatar-BtgidR5a.js → Avatar-BZrBstQe.js} +1 -1
- package/web-app/dist/assets/{Badge-D2nt8G_I.js → Badge-Q8l8r4Vq.js} +1 -1
- package/web-app/dist/assets/{Button-BKl2jOyb.js → Button-B8Bt0A3S.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DaWr0EUX.js → CockpitPage-CIYExrXa.js} +1 -1
- package/web-app/dist/assets/{ComparePage-aIiNhxLr.js → ComparePage-CBOmHdam.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C1Q7rV23.js → ErrorBoundary-BKwDPSnK.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C-C0-tOx.js → EvidenceReceiptPanel-KqzjphWl.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-l3IaXDag.js → GitHubIssuesPanel-lF3GkHRv.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DweLBgr5.js → GitHubPRsPanel-BCzipPWD.js} +1 -1
- package/web-app/dist/assets/{HomePage-B_KK7AvY.js → HomePage-CIdnkzaN.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CKS-KLdH.js → LoginPage-D-178nTT.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Cl6CEp8J.js → MagicPage-BmHuzMT8.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-n9CPUaX0.js → MetricsPage-CPCiyDFn.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D85eWn70.js → NotFoundPage-B2T6n1gx.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CDN7LpSo.js → ProjectPage-DQmpFhb0.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D4SDmU4-.js → ProjectsPage-BmugzFEd.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Dby6GnKm.js → SettingsPage-ZB-RrmgM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CgHxeqoU.js → ShowcasePage-BAhwHt4_.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BaewGHlc.js → SystemSettingsPage-Bt7sf2Vu.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-905MJyke.js → TeamsPage-B-v5j_8q.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DMrUHcvF.js → TemplatesPage-bdqUL-LG.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BsbEbBYX.js → TerminalOutput-CRHEXBwS.js} +1 -1
- package/web-app/dist/assets/{activity-BuaXhBsC.js → activity-C-ROkmXt.js} +1 -1
- package/web-app/dist/assets/{bell-BfEQ71OG.js → bell-DbiCBxQA.js} +1 -1
- package/web-app/dist/assets/{bot-DOGeYewg.js → bot-DsIgE_qr.js} +1 -1
- package/web-app/dist/assets/{check-zbjPpb3E.js → check-CkAdmHWP.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DWTOgfFt.js → chevron-left-CYTLivAo.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BEPKfRzR.js → circle-alert-BVIcAsFJ.js} +1 -1
- package/web-app/dist/assets/{clock-C_fX3OHo.js → clock-Dg3Rq0go.js} +1 -1
- package/web-app/dist/assets/{cloud-DUXHygXQ.js → cloud-CenrqHFl.js} +1 -1
- package/web-app/dist/assets/{code-xml-BEVl_9Hw.js → code-xml-C2mvfoug.js} +1 -1
- package/web-app/dist/assets/{database-_a22uh7l.js → database-BKa0_eCb.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-l2en7xfg.js → dollar-sign-DfelUK1S.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DItKl6H4.js → file-code-corner-CZKWoHVg.js} +1 -1
- package/web-app/dist/assets/{file-plus-BSk7qFMA.js → file-plus-CZ-m1zMX.js} +1 -1
- package/web-app/dist/assets/{globe-2TveFE5l.js → globe-S0kv_GwC.js} +1 -1
- package/web-app/dist/assets/{hammer-BU6OJlQj.js → hammer-BW1MDdu1.js} +1 -1
- package/web-app/dist/assets/{index-DGnZXUYk.js → index-ACEYsTc2.js} +3 -3
- package/web-app/dist/assets/{layers-DKw7MNuq.js → layers-BrXHYZ-L.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CoYfykzD.js → loader-circle-DOdhKFx0.js} +1 -1
- package/web-app/dist/assets/{lock-D9JzrXDL.js → lock-BcFTyg8J.js} +1 -1
- package/web-app/dist/assets/{package-BZtqTIqH.js → package-D34cMVQG.js} +1 -1
- package/web-app/dist/assets/{plus-BGJfo8bV.js → plus-CEaMeBxL.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-F6lowBgR.js → refresh-cw-D2Rzxr5U.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DHOK8dM4.js → rotate-ccw-C_-EjRLh.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CKcROoMV.js → scroll-text-BSSCMI_c.js} +1 -1
- package/web-app/dist/assets/{server-DbG8MDv6.js → server-BKvLonWt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-RNKNXdJT.js → shield-alert-DbbQbPdW.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Yxp-UBxS.js → thumbs-up-NoBD1fUR.js} +1 -1
- package/web-app/dist/assets/{trash-2-DY-eba1n.js → trash-2-BXz8rAyd.js} +1 -1
- package/web-app/dist/assets/{trending-down-CARiDB3t.js → trending-down-D8u535Re.js} +1 -1
- package/web-app/dist/assets/{trending-up-brQqWbA3.js → trending-up-q8k3kgiy.js} +1 -1
- package/web-app/dist/assets/{upload-D6wRjKhz.js → upload-Cp7_zeBI.js} +1 -1
- package/web-app/dist/assets/{usePolling-BMrqNbAI.js → usePolling-CTHVdcuX.js} +1 -1
- package/web-app/dist/assets/{user-CK5-EXIj.js → user-DF7bfSf_.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/autonomy/lib/doctor-fix.py +0 -279
package/autonomy/loki
CHANGED
|
@@ -1682,7 +1682,7 @@ PYEOF
|
|
|
1682
1682
|
# `loki help answer`: `loki answer` is a v10 (loki-ts) command with no bash
|
|
1683
1683
|
# implementation, so its help lives here rather than behind delegation.
|
|
1684
1684
|
_help_answer() {
|
|
1685
|
-
echo -e "${BOLD}loki answer${NC} -
|
|
1685
|
+
echo -e "${BOLD}loki answer${NC} - Start a follow-up run from a BLOCKED run with your answer"
|
|
1686
1686
|
echo ""
|
|
1687
1687
|
echo "Usage: loki answer [<run-id>] [--text \"...\"]"
|
|
1688
1688
|
echo ""
|
|
@@ -1711,6 +1711,8 @@ _help_verify_pr() {
|
|
|
1711
1711
|
echo -e "${BOLD}loki verify-pr${NC} - Verify a PR against its linked issue (experimental, LOKI_VERIFY_PR=1)"
|
|
1712
1712
|
echo ""
|
|
1713
1713
|
echo "Usage: loki verify-pr <https://github.com/o/r/pull/N | o/r#N> [--out DIR]"
|
|
1714
|
+
echo " loki verify-pr respond <o/r#N> fix open review comments (one commit each, on the PR"
|
|
1715
|
+
echo " branch), push, reply with the sha; never resolves threads"
|
|
1714
1716
|
echo ""
|
|
1715
1717
|
echo "Derives a check from the PR's linked issue (Check: \`cmd\` or a loki-check fence)."
|
|
1716
1718
|
echo "VERIFIED only if the check fails on the PR base, passes on its head, and the"
|
|
@@ -1799,7 +1801,8 @@ show_help_all() {
|
|
|
1799
1801
|
echo ""
|
|
1800
1802
|
echo "Verify / trust:"
|
|
1801
1803
|
echo " verify [base] Deterministic PR verification (CI-gate exit codes)"
|
|
1802
|
-
echo "
|
|
1804
|
+
echo " verify-pr <pr> Verify a PR against its linked issue; 'verify-pr respond <pr>' fixes review comments"
|
|
1805
|
+
echo " keys export Print the public receipt-signing key (JWK) for third-party verify --pubkey"
|
|
1803
1806
|
echo " review [opts] Standalone code review with quality gates"
|
|
1804
1807
|
echo " ultracode \"task\" Run a task as a native Claude Code Dynamic Workflow (opt-in, Claude-only)"
|
|
1805
1808
|
echo " trust [cmd] Visible trust trajectory; 'trust detail' for metrics"
|
|
@@ -2549,7 +2552,7 @@ cmd_start() {
|
|
|
2549
2552
|
# [--auto-rollback] chains the gated `loki deploy --execute` after a start
|
|
2550
2553
|
# that ended 0. cmd_deploy owns every gate: no VERIFIED HEAD-anchored
|
|
2551
2554
|
# receipt means a refusal, never a deploy.
|
|
2552
|
-
local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
|
|
2555
|
+
local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_hmatch="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
|
|
2553
2556
|
while [ $# -gt 0 ]; do
|
|
2554
2557
|
case "$1" in
|
|
2555
2558
|
--workspace)
|
|
@@ -2561,12 +2564,16 @@ cmd_start() {
|
|
|
2561
2564
|
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-url requires a URL${NC}" >&2; exit 2; }
|
|
2562
2565
|
_ps_health="$2"; shift 2 ;;
|
|
2563
2566
|
--health-url=*) _ps_health="${1#*=}"; shift ;;
|
|
2567
|
+
--health-match)
|
|
2568
|
+
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-match requires a string${NC}" >&2; exit 2; }
|
|
2569
|
+
_ps_hmatch="$2"; shift 2 ;;
|
|
2570
|
+
--health-match=*) _ps_hmatch="${1#*=}"; shift ;;
|
|
2564
2571
|
--auto-rollback) _ps_rb=1; shift ;;
|
|
2565
2572
|
*) _ps_rest+=("$1"); shift ;;
|
|
2566
2573
|
esac
|
|
2567
2574
|
done
|
|
2568
|
-
if { [ -n "$_ps_health" ] || [ "$_ps_rb" = 1 ]; } && [ "$_ps_deploy" != 1 ]; then
|
|
2569
|
-
echo -e "${RED}--health-url and --auto-rollback apply only with --deploy${NC}" >&2
|
|
2575
|
+
if { [ -n "$_ps_health" ] || [ -n "$_ps_hmatch" ] || [ "$_ps_rb" = 1 ]; } && [ "$_ps_deploy" != 1 ]; then
|
|
2576
|
+
echo -e "${RED}--health-url, --health-match and --auto-rollback apply only with --deploy${NC}" >&2
|
|
2570
2577
|
exit 2
|
|
2571
2578
|
fi
|
|
2572
2579
|
if [ -n "$_ps_ws" ]; then
|
|
@@ -2603,6 +2610,7 @@ cmd_start() {
|
|
|
2603
2610
|
fi
|
|
2604
2611
|
local _ps_dargs=(--execute)
|
|
2605
2612
|
[ -n "$_ps_health" ] && _ps_dargs+=(--health-url "$_ps_health")
|
|
2613
|
+
[ -n "$_ps_hmatch" ] && _ps_dargs+=(--health-match "$_ps_hmatch")
|
|
2606
2614
|
[ "$_ps_rb" = 1 ] && _ps_dargs+=(--auto-rollback)
|
|
2607
2615
|
cmd_deploy "${_ps_dargs[@]}"
|
|
2608
2616
|
return $?
|
|
@@ -5061,11 +5069,14 @@ cmd_pause() {
|
|
|
5061
5069
|
}
|
|
5062
5070
|
|
|
5063
5071
|
# RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
|
|
5064
|
-
#
|
|
5065
|
-
#
|
|
5066
|
-
#
|
|
5067
|
-
#
|
|
5068
|
-
#
|
|
5072
|
+
# to .loki/HUMAN_INPUT.md. engine10 reads it before the next plan or implement
|
|
5073
|
+
# session (default on; only LOKI_PROMPT_INJECTION=0/false opts out, steer.ts).
|
|
5074
|
+
# The legacy loop injects it only when LOKI_PROMPT_INJECTION=1.
|
|
5075
|
+
# Each steer is a numbered revision: `## Operator directive rev N (ts) actor=A source=S`.
|
|
5076
|
+
# engine10 (util/steer.ts) archives the file and appends the revisions to
|
|
5077
|
+
# .loki/intent-revisions.jsonl. Actor and source come from LOKI_STEER_ACTOR
|
|
5078
|
+
# (default: git user.name, then $USER) and LOKI_STEER_SOURCE (cli, chat or an
|
|
5079
|
+
# https comment URL; default cli). Both are reduced to a safe character set.
|
|
5069
5080
|
cmd_steer() {
|
|
5070
5081
|
case "${1:-}" in
|
|
5071
5082
|
--help|-h|help|"")
|
|
@@ -5073,13 +5084,14 @@ cmd_steer() {
|
|
|
5073
5084
|
echo ""
|
|
5074
5085
|
echo "Usage: loki steer \"<guidance>\""
|
|
5075
5086
|
echo ""
|
|
5076
|
-
echo "Writes your guidance to .loki/HUMAN_INPUT.md
|
|
5077
|
-
echo "
|
|
5087
|
+
echo "Writes your guidance to .loki/HUMAN_INPUT.md as a numbered revision"
|
|
5088
|
+
echo "(rev N, actor, source). The Loki 10 engine applies it once, before the"
|
|
5089
|
+
echo "next plan or implement session, and records it in .loki/intent-revisions.jsonl."
|
|
5078
5090
|
echo ""
|
|
5079
5091
|
echo " loki steer \"focus on the auth flow, skip the docs for now\""
|
|
5080
5092
|
echo ""
|
|
5081
|
-
echo "
|
|
5082
|
-
echo "
|
|
5093
|
+
echo "Opt out with LOKI_PROMPT_INJECTION=0. The legacy loop reads the note"
|
|
5094
|
+
echo "only when LOKI_PROMPT_INJECTION=1."
|
|
5083
5095
|
[ -z "${1:-}" ] && return 2
|
|
5084
5096
|
return 0
|
|
5085
5097
|
;;
|
|
@@ -5089,20 +5101,34 @@ cmd_steer() {
|
|
|
5089
5101
|
return 1
|
|
5090
5102
|
fi
|
|
5091
5103
|
local note="$*"
|
|
5092
|
-
|
|
5093
|
-
|
|
5104
|
+
local actor source rev pending archived
|
|
5105
|
+
actor="${LOKI_STEER_ACTOR:-$(git config user.name 2>/dev/null || true)}"
|
|
5106
|
+
actor="${actor:-${USER:-unknown}}"
|
|
5107
|
+
actor="$(printf '%s' "$actor" | tr -c 'A-Za-z0-9._@-' '-' | cut -c1-64)"
|
|
5108
|
+
source="$(printf '%s' "${LOKI_STEER_SOURCE:-cli}" | tr -c 'A-Za-z0-9:/._#?=&%-' '-' | cut -c1-300)"
|
|
5109
|
+
# rev = consumed revisions + steers still pending in the buffer + 1
|
|
5110
|
+
archived=0; pending=0
|
|
5111
|
+
[ -f "$LOKI_DIR/intent-revisions.jsonl" ] && archived="$(wc -l < "$LOKI_DIR/intent-revisions.jsonl" | tr -d ' ')"
|
|
5112
|
+
[ -f "$LOKI_DIR/HUMAN_INPUT.md" ] && pending="$(grep -c '^## Operator directive rev ' "$LOKI_DIR/HUMAN_INPUT.md" 2>/dev/null || true)"
|
|
5113
|
+
rev=$(( ${archived:-0} + ${pending:-0} + 1 ))
|
|
5114
|
+
# Append so multiple steers accumulate; the reader consumes and clears the
|
|
5115
|
+
# file, so this is the pending-directive buffer.
|
|
5094
5116
|
{
|
|
5095
|
-
printf '## Operator directive (%s)\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
5117
|
+
printf '## Operator directive rev %s (%s) actor=%s source=%s\n' "$rev" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$actor" "$source"
|
|
5096
5118
|
printf '%s\n\n' "$note"
|
|
5097
5119
|
} >> "$LOKI_DIR/HUMAN_INPUT.md"
|
|
5098
5120
|
emit_event session cli steer "chars=${#note}" 2>/dev/null || true
|
|
5099
|
-
echo -e "${GREEN}Steering note written to .loki/HUMAN_INPUT.md${NC}"
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5121
|
+
echo -e "${GREEN}Steering note (rev $rev) written to .loki/HUMAN_INPUT.md${NC}"
|
|
5122
|
+
case "${LOKI_PROMPT_INJECTION:-}" in
|
|
5123
|
+
0|false|FALSE|False)
|
|
5124
|
+
echo -e "${YELLOW}Note: LOKI_PROMPT_INJECTION is off, so the run will NOT read this.${NC}"
|
|
5125
|
+
;;
|
|
5126
|
+
*)
|
|
5127
|
+
echo -e "${DIM}The Loki 10 engine applies it before the next plan or implement session.${NC}"
|
|
5128
|
+
# Same text the issue comment ack posts (trigger-server.py steer_ack_text).
|
|
5129
|
+
echo "Steer rev $rev received: applies at next plan/implement stage."
|
|
5130
|
+
;;
|
|
5131
|
+
esac
|
|
5106
5132
|
return 0
|
|
5107
5133
|
}
|
|
5108
5134
|
|
|
@@ -9771,7 +9797,7 @@ _deploy_latest_receipt() {
|
|
|
9771
9797
|
return 0
|
|
9772
9798
|
}
|
|
9773
9799
|
|
|
9774
|
-
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary]
|
|
9800
|
+
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha]
|
|
9775
9801
|
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
|
|
9776
9802
|
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
|
|
9777
9803
|
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
|
|
@@ -9794,6 +9820,7 @@ _deploy_record() {
|
|
|
9794
9820
|
local health_status="${7:-unchecked}" health_url="${8:-}"
|
|
9795
9821
|
local rb_attempted="${9:-false}" rb_exit="${10:-}"
|
|
9796
9822
|
local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
|
|
9823
|
+
local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}"
|
|
9797
9824
|
local ddir="$dir/.loki/deploys"
|
|
9798
9825
|
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
9799
9826
|
mkdir -p "$ddir" 2>/dev/null || return 0
|
|
@@ -9801,7 +9828,7 @@ _deploy_record() {
|
|
|
9801
9828
|
DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
|
|
9802
9829
|
DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
|
|
9803
9830
|
DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
|
|
9804
|
-
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" \
|
|
9831
|
+
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" \
|
|
9805
9832
|
python3 -c '
|
|
9806
9833
|
import json, os, datetime
|
|
9807
9834
|
state = os.environ.get("DR_STATE") or "anchored"
|
|
@@ -9827,6 +9854,10 @@ json.dump({
|
|
|
9827
9854
|
"health_url": os.environ.get("DR_HU") or "",
|
|
9828
9855
|
"rollback_attempted": os.environ.get("DR_RBA") == "true",
|
|
9829
9856
|
"rollback_exit": int(os.environ["DR_RBX"]) if os.environ.get("DR_RBX", "") != "" else None,
|
|
9857
|
+
"post_rollback_health": os.environ.get("DR_PRB") or "unchecked",
|
|
9858
|
+
"health_match": os.environ.get("DR_HM", ""),
|
|
9859
|
+
"tree_sha": os.environ.get("DR_TREE", ""),
|
|
9860
|
+
"health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
|
|
9830
9861
|
}, open(os.environ["DR_OUT"], "w"), indent=2)
|
|
9831
9862
|
' 2>/dev/null || return 0
|
|
9832
9863
|
# Sign through the one Bun implementation. A subshell, because the delegate execs.
|
|
@@ -9895,20 +9926,31 @@ except Exception:
|
|
|
9895
9926
|
return 0
|
|
9896
9927
|
}
|
|
9897
9928
|
|
|
9898
|
-
# _deploy_probe_health <url>
|
|
9899
|
-
# Bounded poll: echoes the last HTTP status, returns 0 on any 2xx.
|
|
9929
|
+
# _deploy_probe_health <url> [match]
|
|
9930
|
+
# Bounded poll: echoes the last HTTP status, returns 0 on any 2xx. With a match
|
|
9931
|
+
# string the 2xx body must also contain it (fixed-string), else the attempt fails
|
|
9932
|
+
# and the status is still echoed. Attempts and
|
|
9900
9933
|
# spacing are env-tunable (LOKI_DEPLOY_HEALTH_ATTEMPTS / _INTERVAL) for tests.
|
|
9901
9934
|
_deploy_probe_health() {
|
|
9902
|
-
local url="${1:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
|
|
9903
|
-
local i=0 code="000"
|
|
9935
|
+
local url="${1:-}" match="${2:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
|
|
9936
|
+
local i=0 code="000" body="/dev/null"
|
|
9937
|
+
if [ -n "$match" ]; then body="$(mktemp 2>/dev/null)" || body="/dev/null"; fi
|
|
9904
9938
|
case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
|
|
9905
9939
|
case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
|
|
9906
9940
|
while [ "$i" -lt "$attempts" ]; do
|
|
9907
9941
|
i=$((i + 1))
|
|
9908
|
-
code="$(curl -s -o
|
|
9909
|
-
case "$code" in
|
|
9942
|
+
code="$(curl -s -o "$body" -w '%{http_code}' --max-time 10 "$url" 2>/dev/null)" || code="000"
|
|
9943
|
+
case "$code" in
|
|
9944
|
+
2??)
|
|
9945
|
+
if [ -z "$match" ] || grep -qF -- "$match" "$body" 2>/dev/null; then
|
|
9946
|
+
[ "$body" = /dev/null ] || rm -f "$body"
|
|
9947
|
+
printf '%s' "$code"; return 0
|
|
9948
|
+
fi
|
|
9949
|
+
;;
|
|
9950
|
+
esac
|
|
9910
9951
|
[ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
|
|
9911
9952
|
done
|
|
9953
|
+
[ "$body" = /dev/null ] || rm -f "$body"
|
|
9912
9954
|
printf '%s' "$code"
|
|
9913
9955
|
return 1
|
|
9914
9956
|
}
|
|
@@ -9974,6 +10016,7 @@ _deploy_try_execute() {
|
|
|
9974
10016
|
local dir="${1:-.}" type="${2:-}"
|
|
9975
10017
|
# Set by cmd_deploy (dynamic scope); empty/false when called any other way.
|
|
9976
10018
|
local health_url="${_LOKI_DEPLOY_HEALTH_URL:-}" auto_rollback="${_LOKI_DEPLOY_AUTO_ROLLBACK:-false}"
|
|
10019
|
+
local health_match="${_LOKI_DEPLOY_HEALTH_MATCH:-}"
|
|
9977
10020
|
|
|
9978
10021
|
local command
|
|
9979
10022
|
command="$(_deploy_first_installed_command "$dir" "$type")"
|
|
@@ -10086,7 +10129,7 @@ _deploy_try_execute() {
|
|
|
10086
10129
|
|
|
10087
10130
|
local rc=0 started_at finished_at purl="" canary_out=""
|
|
10088
10131
|
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
10089
|
-
local health_status="unchecked" rb_attempted=false rb_exit="" hcode=""
|
|
10132
|
+
local health_status="unchecked" rb_attempted=false rb_exit="" hcode="" rb_hcode="" post_rb="unchecked"
|
|
10090
10133
|
if [ "$canary_kind" = vercel ]; then
|
|
10091
10134
|
# Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
|
|
10092
10135
|
# Nothing reaches production before the probe, so there is nothing to roll back.
|
|
@@ -10098,7 +10141,7 @@ _deploy_try_execute() {
|
|
|
10098
10141
|
if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
|
|
10099
10142
|
health_url="$purl"
|
|
10100
10143
|
echo "Probing canary health: ${health_url}"
|
|
10101
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10144
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10102
10145
|
health_status="healthy"
|
|
10103
10146
|
echo "Promoting: vercel promote ${purl}"
|
|
10104
10147
|
exec_cmd="vercel; vercel promote ${purl}"
|
|
@@ -10119,7 +10162,7 @@ _deploy_try_execute() {
|
|
|
10119
10162
|
# failed deploy, status is "unchecked": never a claim the service is healthy.
|
|
10120
10163
|
if [ "$canary_kind" != vercel ] && [ "$rc" -eq 0 ] && [ -n "$health_url" ]; then
|
|
10121
10164
|
echo "Probing health: ${health_url}"
|
|
10122
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10165
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10123
10166
|
health_status="healthy"
|
|
10124
10167
|
else
|
|
10125
10168
|
health_status="FAILED_HEALTH"
|
|
@@ -10127,14 +10170,21 @@ _deploy_try_execute() {
|
|
|
10127
10170
|
fi
|
|
10128
10171
|
|
|
10129
10172
|
if [ "$health_status" = "FAILED_HEALTH" ]; then
|
|
10130
|
-
echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx (last status ${hcode:-000}).${NC}" >&2
|
|
10173
|
+
echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx${health_match:+ containing \"${health_match}\"} (last status ${hcode:-000}).${NC}" >&2
|
|
10131
10174
|
if [ "$auto_rollback" = true ] && [ -n "$rb_cmd" ] && ! _deploy_is_destructive "$rb_cmd"; then
|
|
10132
10175
|
echo "Rolling back: ${rb_cmd}" >&2
|
|
10133
10176
|
rb_attempted=true
|
|
10134
10177
|
rb_exit=0
|
|
10135
10178
|
( cd "$dir" && eval "$rb_cmd" ) || rb_exit=$?
|
|
10136
10179
|
if [ "$rb_exit" -eq 0 ]; then
|
|
10137
|
-
echo "Rollback command exited 0.
|
|
10180
|
+
echo "Rollback command exited 0. Re-checking health: ${health_url}" >&2
|
|
10181
|
+
if rb_hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10182
|
+
post_rb="healthy"
|
|
10183
|
+
echo -e "${GREEN}Rolled-back service is healthy${NC} (HTTP ${rb_hcode})." >&2
|
|
10184
|
+
else
|
|
10185
|
+
post_rb="FAILED_HEALTH"
|
|
10186
|
+
echo -e "${RED}Rolled-back service is STILL unhealthy (last status ${rb_hcode:-000}).${NC}" >&2
|
|
10187
|
+
fi
|
|
10138
10188
|
else
|
|
10139
10189
|
echo -e "${RED}Rollback command exited ${rb_exit}.${NC}" >&2
|
|
10140
10190
|
fi
|
|
@@ -10150,7 +10200,7 @@ _deploy_try_execute() {
|
|
|
10150
10200
|
rsha="$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || printf '')"
|
|
10151
10201
|
rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
|
|
10152
10202
|
"$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
|
|
10153
|
-
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary")"
|
|
10203
|
+
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)")"
|
|
10154
10204
|
echo ""
|
|
10155
10205
|
if [ "$rc" -eq 0 ]; then
|
|
10156
10206
|
echo -e "${GREEN}Deploy command exited 0.${NC}"
|
|
@@ -10177,6 +10227,8 @@ _deploy_try_execute() {
|
|
|
10177
10227
|
fi
|
|
10178
10228
|
# A deploy that exited 0 but failed its health probe is a failed deploy.
|
|
10179
10229
|
if [ "$rc" -eq 0 ] && [ "$health_status" = "FAILED_HEALTH" ]; then
|
|
10230
|
+
# 5: the rollback ran and the service is still down.
|
|
10231
|
+
[ "$post_rb" = "FAILED_HEALTH" ] && return 5
|
|
10180
10232
|
return 4
|
|
10181
10233
|
fi
|
|
10182
10234
|
return "$rc"
|
|
@@ -10290,7 +10342,7 @@ cmd_deploy() {
|
|
|
10290
10342
|
# file alone weakens this gate.
|
|
10291
10343
|
local LOKI_DEPLOY_ALLOW_UNANCHORED=false
|
|
10292
10344
|
# Per-invocation like --execute; local so an ambient env var cannot set them.
|
|
10293
|
-
local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false
|
|
10345
|
+
local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false
|
|
10294
10346
|
|
|
10295
10347
|
# Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
|
|
10296
10348
|
while [ $# -gt 0 ]; do
|
|
@@ -10298,7 +10350,7 @@ cmd_deploy() {
|
|
|
10298
10350
|
--help|-h|help)
|
|
10299
10351
|
echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
|
|
10300
10352
|
echo ""
|
|
10301
|
-
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--health-url URL [--auto-rollback]]"
|
|
10353
|
+
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--health-url URL [--health-match STR] [--auto-rollback]]"
|
|
10302
10354
|
echo ""
|
|
10303
10355
|
echo "Detects your project type and your installed cloud CLI (and any"
|
|
10304
10356
|
echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
|
|
@@ -10359,11 +10411,14 @@ cmd_deploy() {
|
|
|
10359
10411
|
echo " non-2xx marks the deploy FAILED_HEALTH and exits 4."
|
|
10360
10412
|
echo " Without a URL, health is recorded as 'unchecked',"
|
|
10361
10413
|
echo " never as healthy."
|
|
10414
|
+
echo " --health-match S The 2xx body must also contain S (for example the HEAD sha"
|
|
10415
|
+
echo " or version), else FAILED_HEALTH. Needs --health-url."
|
|
10362
10416
|
echo " --auto-rollback On FAILED_HEALTH run the provider's documented rollback:"
|
|
10363
10417
|
echo " 'vercel rollback' or 'fly releases rollback <version"
|
|
10364
10418
|
echo " captured before the deploy>'. Netlify and Cloudflare"
|
|
10365
10419
|
echo " print a manual note. Needs --health-url. The rolled-back"
|
|
10366
|
-
echo " service is
|
|
10420
|
+
echo " service is re-probed and recorded as post_rollback_health;"
|
|
10421
|
+
echo " still unhealthy exits 5."
|
|
10367
10422
|
echo ""
|
|
10368
10423
|
echo "Staged rollout (only with --execute):"
|
|
10369
10424
|
echo " --canary Vercel: deploy without --prod, health-check the preview"
|
|
@@ -10402,6 +10457,10 @@ cmd_deploy() {
|
|
|
10402
10457
|
*) echo "loki deploy --health-url requires an http(s) URL" >&2; return 1 ;;
|
|
10403
10458
|
esac
|
|
10404
10459
|
;;
|
|
10460
|
+
--health-match)
|
|
10461
|
+
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo "loki deploy --health-match requires a string" >&2; return 1; }
|
|
10462
|
+
_LOKI_DEPLOY_HEALTH_MATCH="$2"; shift
|
|
10463
|
+
;;
|
|
10405
10464
|
--auto-rollback)
|
|
10406
10465
|
_LOKI_DEPLOY_AUTO_ROLLBACK=true
|
|
10407
10466
|
;;
|
|
@@ -10429,6 +10488,10 @@ cmd_deploy() {
|
|
|
10429
10488
|
echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
|
|
10430
10489
|
return 1
|
|
10431
10490
|
fi
|
|
10491
|
+
if [ -n "$_LOKI_DEPLOY_HEALTH_MATCH" ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
|
|
10492
|
+
echo "loki deploy --health-match requires --health-url" >&2
|
|
10493
|
+
return 1
|
|
10494
|
+
fi
|
|
10432
10495
|
if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ "$do_execute" != true ]; then
|
|
10433
10496
|
echo "loki deploy --canary requires --execute" >&2
|
|
10434
10497
|
return 1
|
|
@@ -11492,7 +11555,7 @@ fi
|
|
|
11492
11555
|
# Post-completion: auto-merge if requested
|
|
11493
11556
|
if [[ "$LOKI_AUTO_MERGE" == "true" ]]; then
|
|
11494
11557
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11495
|
-
if
|
|
11558
|
+
if "$LOKI_CMD" merge watch "$branch_current" --require-verified --delete-branch; then
|
|
11496
11559
|
# A brief run carries a synthetic ref (b<epoch>), not a real issue: close only numeric refs.
|
|
11497
11560
|
if [[ "${LOKI_ISSUE_NUMBER:-}" =~ ^[0-9]+$ ]]; then
|
|
11498
11561
|
gh issue close "$LOKI_ISSUE_NUMBER" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -11710,7 +11773,8 @@ ${_gh_receipt}"
|
|
|
11710
11773
|
if command -v jq >/dev/null 2>&1; then
|
|
11711
11774
|
jq -n --arg url "$(printf '%s' "$_pr_out" | grep -Eo 'https://[^[:space:]]+' | head -1)" \
|
|
11712
11775
|
--arg branch "$branch_current" \
|
|
11713
|
-
'{state:"created", url:$url, branch:$branch
|
|
11776
|
+
'{state:"created", url:$url, branch:$branch,
|
|
11777
|
+
merge:{outcome:"waiting", gate:"human_review", checks:"not recorded"}}' \
|
|
11714
11778
|
> .loki/state/pr.json 2>/dev/null || true
|
|
11715
11779
|
fi
|
|
11716
11780
|
else
|
|
@@ -11744,7 +11808,7 @@ ${_gh_receipt}"
|
|
|
11744
11808
|
github|brief)
|
|
11745
11809
|
local branch_current
|
|
11746
11810
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11747
|
-
if
|
|
11811
|
+
if _merge_pr_watch "$branch_current" --require-verified --delete-branch; then
|
|
11748
11812
|
echo -e "${GREEN}PR merged and branch deleted${NC}"
|
|
11749
11813
|
if [[ "${number:-}" =~ ^[0-9]+$ ]]; then
|
|
11750
11814
|
gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -18473,6 +18537,7 @@ cmd_modernize() {
|
|
|
18473
18537
|
echo "Subcommands (grouped modernization surface):"
|
|
18474
18538
|
echo " heal <path> Legacy system healing in phases (was: loki heal)"
|
|
18475
18539
|
echo " migrate <path> Codebase migration in phases (was: loki migrate)"
|
|
18540
|
+
echo " unit --repo <owner/name> --ref <ref> --unit <id> Run one unit (remote unit job worker)"
|
|
18476
18541
|
echo ""
|
|
18477
18542
|
echo "Run 'loki modernize <subcommand> --help' for each. The old"
|
|
18478
18543
|
echo "top-level names still work; see 'loki help aliases'."
|
|
@@ -25882,6 +25947,142 @@ SHOW_EOF
|
|
|
25882
25947
|
esac
|
|
25883
25948
|
}
|
|
25884
25949
|
|
|
25950
|
+
# Poll `gh pr checks` while pending (rc=8); returns the final rc (0 = green).
|
|
25951
|
+
_pr_checks_wait() {
|
|
25952
|
+
local pr="$1" poll="${LOKI_MERGE_POLL_S:-20}" tries="${LOKI_MERGE_MAX_POLLS:-90}" rc=8 i=0
|
|
25953
|
+
while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
|
|
25954
|
+
rc=0; gh pr checks "$pr" >/dev/null 2>&1 || rc=$?
|
|
25955
|
+
[ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
|
|
25956
|
+
done
|
|
25957
|
+
return "$rc"
|
|
25958
|
+
}
|
|
25959
|
+
|
|
25960
|
+
# CI repair loop. Waits for checks; on red, hands the failed-run log tail to one
|
|
25961
|
+
# bounded repair session on the PR branch (LOKI_PR_REPAIR_CMD, default: a
|
|
25962
|
+
# `loki start` of a generated brief; it receives the evidence file path as its last argument), pushes,
|
|
25963
|
+
# and re-checks, up to LOKI_PR_REPAIR_MAX attempts (default 2). Green goes through
|
|
25964
|
+
# _merge_pr_gated (extra args pass through). Exhausted: the PR is returned to
|
|
25965
|
+
# draft, nothing merges, and the last failure evidence is printed.
|
|
25966
|
+
# Usage: _merge_pr_watch <pr> [--require-verified] [--delete-branch]
|
|
25967
|
+
_merge_pr_watch() {
|
|
25968
|
+
local pr="$1" max="${LOKI_PR_REPAIR_MAX:-2}" attempt=0 rc branch cur rid evid tmp
|
|
25969
|
+
shift
|
|
25970
|
+
case "$max" in ''|*[!0-9]*) max=2 ;; esac
|
|
25971
|
+
command -v gh >/dev/null 2>&1 || { echo "$pr: not merged, gate: gh (gh CLI is required)" >&2; return 1; }
|
|
25972
|
+
branch="$(gh pr view "$pr" --json headRefName -q .headRefName 2>/dev/null)" || branch=""
|
|
25973
|
+
tmp="$(mktemp -d)" || return 1
|
|
25974
|
+
evid="$tmp/ci-failure.md"
|
|
25975
|
+
while true; do
|
|
25976
|
+
rc=0; _pr_checks_wait "$pr" || rc=$?
|
|
25977
|
+
if [ "$rc" = "0" ]; then
|
|
25978
|
+
rm -rf "$tmp"
|
|
25979
|
+
_merge_pr_gated "$pr" "$@"
|
|
25980
|
+
return $?
|
|
25981
|
+
fi
|
|
25982
|
+
rid="$(gh run list --branch "$branch" --status failure --limit 1 --json databaseId -q '.[0].databaseId' 2>/dev/null)" || rid=""
|
|
25983
|
+
{
|
|
25984
|
+
echo "# CI failed for PR $pr (branch ${branch:-unknown}); fix the failing checks, keep the change minimal"
|
|
25985
|
+
[ -n "$rid" ] && gh run view "$rid" --log-failed 2>/dev/null | tail -n 120
|
|
25986
|
+
} > "$evid"
|
|
25987
|
+
if [ "$attempt" -ge "$max" ]; then
|
|
25988
|
+
gh pr ready "$pr" --undo >/dev/null 2>&1 || true
|
|
25989
|
+
echo "$pr: not merged, gate: checks (gh pr checks rc=$rc after $attempt repair attempts); PR left as draft" >&2
|
|
25990
|
+
_pr_record_merge "$pr" "blocked:checks" "checks" "failing after $attempt repair attempts"
|
|
25991
|
+
echo "Last failure evidence:" >&2; sed 's/^/ /' "$evid" >&2
|
|
25992
|
+
rm -rf "$tmp"
|
|
25993
|
+
return 1
|
|
25994
|
+
fi
|
|
25995
|
+
attempt=$((attempt+1))
|
|
25996
|
+
cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
25997
|
+
if [ -z "$branch" ] || [ "$cur" != "$branch" ]; then
|
|
25998
|
+
echo "$pr: not merged, gate: repair (checkout is '${cur:-none}', repair must run on PR branch '${branch:-unknown}'); PR left open" >&2
|
|
25999
|
+
rm -rf "$tmp"; return 1
|
|
26000
|
+
fi
|
|
26001
|
+
echo "$pr: checks red, repair attempt $attempt of $max"
|
|
26002
|
+
if [ -n "${LOKI_PR_REPAIR_CMD:-}" ]; then
|
|
26003
|
+
bash -c "$LOKI_PR_REPAIR_CMD \"\$1\"" _ "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
|
|
26004
|
+
else
|
|
26005
|
+
"$(command -v loki || echo "$0")" start "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
|
|
26006
|
+
fi
|
|
26007
|
+
git push origin "HEAD:refs/heads/$branch" >/dev/null 2>&1 || echo "$pr: push of repair failed" >&2
|
|
26008
|
+
done
|
|
26009
|
+
}
|
|
26010
|
+
|
|
26011
|
+
# Record the merge gate's own result in .loki/state/pr.json (merged,
|
|
26012
|
+
# blocked:<gate> or waiting) so the receipt and handoff read the same outcome
|
|
26013
|
+
# the gate produced. Only updates an existing "created" record that describes
|
|
26014
|
+
# the same PR (pr arg matches .branch or the number at the end of .url); any
|
|
26015
|
+
# other record is left untouched. Never invents one. File mode is preserved.
|
|
26016
|
+
_pr_record_merge() {
|
|
26017
|
+
local pr="$1" outcome="$2" gate="${3:-}" checks="${4:-not recorded}" f=".loki/state/pr.json" t n
|
|
26018
|
+
n="${pr#\#}"
|
|
26019
|
+
case "$n" in */pull/*) n="${n%/}"; n="${n##*/}" ;; esac
|
|
26020
|
+
[ -f "$f" ] && command -v jq >/dev/null 2>&1 || return 0
|
|
26021
|
+
t="$(mktemp "$f.XXXXXX" 2>/dev/null)" || return 0
|
|
26022
|
+
cp -p "$f" "$t" 2>/dev/null || { rm -f "$t"; return 0; }
|
|
26023
|
+
if jq --arg p "$pr" --arg n "$n" --arg o "$outcome" --arg g "$gate" --arg c "$checks" \
|
|
26024
|
+
'select(.state=="created" and (.branch==$p or ((.url // "") | (endswith("/pull/" + $n) or endswith("/pull/" + $n + "/"))))) | .merge={outcome:$o, gate:(if $g=="" then null else $g end), checks:$c}' "$f" > "$t" 2>/dev/null && [ -s "$t" ]; then
|
|
26025
|
+
mv "$t" "$f"
|
|
26026
|
+
else
|
|
26027
|
+
rm -f "$t"
|
|
26028
|
+
fi
|
|
26029
|
+
return 0
|
|
26030
|
+
}
|
|
26031
|
+
|
|
26032
|
+
# Shared merge gate. One PR (number or branch) merges only when it is not a
|
|
26033
|
+
# draft, `gh pr checks` exits 0 after polling pending (rc=8), and, with
|
|
26034
|
+
# --require-verified, this run's proof headline is VERIFIED (missing proof or
|
|
26035
|
+
# an unreadable headline is never VERIFIED). On refusal it prints the exact
|
|
26036
|
+
# blocking gate ("gate: draft|verdict|checks") and leaves the PR open.
|
|
26037
|
+
# Usage: _merge_pr_gated <pr> [--require-verified] [--delete-branch]
|
|
26038
|
+
_merge_pr_gated() {
|
|
26039
|
+
local pr="$1" need_verified=false margs=(--squash) rc d
|
|
26040
|
+
shift
|
|
26041
|
+
while [ $# -gt 0 ]; do
|
|
26042
|
+
case "$1" in
|
|
26043
|
+
--require-verified) need_verified=true ;;
|
|
26044
|
+
--delete-branch) margs+=(--delete-branch) ;;
|
|
26045
|
+
esac
|
|
26046
|
+
shift
|
|
26047
|
+
done
|
|
26048
|
+
command -v gh >/dev/null 2>&1 || { echo "$pr: not merged, gate: gh (gh CLI is required)" >&2; _pr_record_merge "$pr" "blocked:gh" "gh"; return 1; }
|
|
26049
|
+
d="$(gh pr view "$pr" --json isDraft -q .isDraft 2>/dev/null)" || d=""
|
|
26050
|
+
if [ "$d" != "false" ]; then
|
|
26051
|
+
echo "$pr: not merged, gate: draft (PR is a draft or its state is unreadable); PR left open" >&2
|
|
26052
|
+
_pr_record_merge "$pr" "blocked:draft" "draft"
|
|
26053
|
+
return 1
|
|
26054
|
+
fi
|
|
26055
|
+
if $need_verified; then
|
|
26056
|
+
local pid="" headline=""
|
|
26057
|
+
[ -f .loki/state/last-proof-id.txt ] && pid="$(cat .loki/state/last-proof-id.txt 2>/dev/null)"
|
|
26058
|
+
if [ -n "$pid" ] && [ -f ".loki/proofs/${pid}/proof.json" ] && command -v jq >/dev/null 2>&1; then
|
|
26059
|
+
headline="$(jq -r '.honesty.headline // empty' ".loki/proofs/${pid}/proof.json" 2>/dev/null)"
|
|
26060
|
+
fi
|
|
26061
|
+
if [ "$headline" != "VERIFIED" ]; then
|
|
26062
|
+
echo "$pr: not merged, gate: verdict (run verdict is '${headline:-missing}', need VERIFIED); PR left open" >&2
|
|
26063
|
+
_pr_record_merge "$pr" "blocked:verdict" "verdict"
|
|
26064
|
+
return 1
|
|
26065
|
+
fi
|
|
26066
|
+
fi
|
|
26067
|
+
rc=0; _pr_checks_wait "$pr" || rc=$?
|
|
26068
|
+
if [ "$rc" != "0" ]; then
|
|
26069
|
+
echo "$pr: not merged, gate: checks (gh pr checks rc=$rc); PR left open" >&2
|
|
26070
|
+
case "$rc" in
|
|
26071
|
+
8) _pr_record_merge "$pr" "waiting" "checks" "pending" ;;
|
|
26072
|
+
1) _pr_record_merge "$pr" "blocked:checks" "checks" "failing" ;;
|
|
26073
|
+
*) _pr_record_merge "$pr" "blocked:checks" "checks" "unknown" ;;
|
|
26074
|
+
esac
|
|
26075
|
+
return 1
|
|
26076
|
+
fi
|
|
26077
|
+
if ! gh pr merge "$pr" "${margs[@]}" >/dev/null 2>&1; then
|
|
26078
|
+
echo "$pr: not merged, gate: merge (gh pr merge failed); PR left open" >&2
|
|
26079
|
+
_pr_record_merge "$pr" "blocked:merge" "merge" "passing"
|
|
26080
|
+
return 1
|
|
26081
|
+
fi
|
|
26082
|
+
_pr_record_merge "$pr" "merged" "" "all passing"
|
|
26083
|
+
return 0
|
|
26084
|
+
}
|
|
26085
|
+
|
|
25885
26086
|
# loki merge: serial PR merge queue. Queue lives in .loki/merge-queue (one PR
|
|
25886
26087
|
# number per line). A PR merges only when `gh pr checks` exits 0 (all green);
|
|
25887
26088
|
# after each merge the next PR is rebased onto the new base (gh pr update-branch
|
|
@@ -25916,8 +26117,7 @@ cmd_merge() {
|
|
|
25916
26117
|
[ "${1:-}" = "--dry-run" ] && dry=true
|
|
25917
26118
|
if [ ! -s "$qfile" ]; then echo "Merge queue is empty"; return 0; fi
|
|
25918
26119
|
command -v gh >/dev/null 2>&1 || { echo "gh CLI is required" >&2; return 1; }
|
|
25919
|
-
local
|
|
25920
|
-
local first=true n rc i remaining="" merged=0 blocked=0
|
|
26120
|
+
local first=true n rc remaining="" merged=0 blocked=0
|
|
25921
26121
|
while IFS= read -r n; do
|
|
25922
26122
|
[ -n "$n" ] || continue
|
|
25923
26123
|
if $dry; then
|
|
@@ -25936,29 +26136,33 @@ cmd_merge() {
|
|
|
25936
26136
|
|| { echo "#$n: rebase onto new base failed, left in queue"; remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue; }
|
|
25937
26137
|
echo "#$n: rebased onto new base"
|
|
25938
26138
|
fi
|
|
25939
|
-
|
|
25940
|
-
while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
|
|
25941
|
-
rc=0; gh pr checks "$n" >/dev/null 2>&1 || rc=$?
|
|
25942
|
-
[ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
|
|
25943
|
-
done
|
|
25944
|
-
if [ "$rc" != "0" ]; then
|
|
25945
|
-
echo "#$n: checks not green (rc=$rc), left in queue, not merged"
|
|
25946
|
-
remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue
|
|
25947
|
-
fi
|
|
25948
|
-
if gh pr merge "$n" --squash >/dev/null 2>&1; then
|
|
26139
|
+
if _merge_pr_gated "$n" 2>"$qfile.err"; then
|
|
25949
26140
|
echo "#$n: merged"; merged=$((merged+1)); first=false
|
|
25950
26141
|
else
|
|
25951
|
-
echo "#$n:
|
|
26142
|
+
sed 's/^/#/' "$qfile.err"; echo "#$n: left in queue"
|
|
25952
26143
|
remaining="$remaining$n"$'\n'; blocked=$((blocked+1))
|
|
25953
26144
|
fi
|
|
25954
26145
|
done < "$qfile"
|
|
25955
26146
|
if $dry; then echo "Dry run: nothing changed"; return 0; fi
|
|
26147
|
+
rm -f "$qfile.err"
|
|
25956
26148
|
printf '%s' "$remaining" > "$qfile"
|
|
25957
26149
|
echo "Merged: $merged, left in queue: $blocked"
|
|
25958
26150
|
[ "$blocked" -eq 0 ]
|
|
25959
26151
|
;;
|
|
26152
|
+
watch)
|
|
26153
|
+
[ $# -gt 0 ] || { echo "Usage: loki merge watch <pr> [--require-verified] [--delete-branch]" >&2; return 2; }
|
|
26154
|
+
_merge_pr_watch "$@" && echo "$1: merged"
|
|
26155
|
+
;;
|
|
26156
|
+
pr)
|
|
26157
|
+
[ $# -gt 0 ] || { echo "Usage: loki merge pr <pr-or-branch> [--require-verified] [--delete-branch]" >&2; return 2; }
|
|
26158
|
+
_merge_pr_gated "$@" && echo "$1: merged"
|
|
26159
|
+
;;
|
|
25960
26160
|
help|--help|-h|*)
|
|
25961
|
-
echo "Usage: loki merge <add|list|run|clear>"
|
|
26161
|
+
echo "Usage: loki merge <add|list|run|pr|watch|clear>"
|
|
26162
|
+
echo " watch <pr> Wait for checks; on red run up to LOKI_PR_REPAIR_MAX (2) bounded"
|
|
26163
|
+
echo " repair sessions on the PR branch, push, re-check, merge when green."
|
|
26164
|
+
echo " pr <pr|branch> Merge one PR through the same gate (not draft, checks green;"
|
|
26165
|
+
echo " --require-verified also needs a VERIFIED run verdict)."
|
|
25962
26166
|
echo " add <pr>... Queue pull requests"
|
|
25963
26167
|
echo " list Show the queue"
|
|
25964
26168
|
echo " run [--dry-run] Merge one at a time, only when checks are green;"
|