loki-mode 11.10.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 +3 -3
- 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 +263 -58
- package/autonomy/quickstart.sh +40 -0
- 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 +16 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +7 -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 +140 -85
- package/docs/v10/CLI-MODERN.md +5 -4
- package/docs/v10/DECISIONS.md +1 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +3 -3
- package/docs/v10/MODERNIZE.md +34 -2
- package/docs/v10/PROGRESS.md +15 -0
- package/loki-ts/dist/loki.js +1225 -1139
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +1889 -1577
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/schemas/loki-yaml.schema.json +5 -0
- package/web-app/dist/assets/{AdminPage-D3PgG7SH.js → AdminPage-bZlvH4TL.js} +1 -1
- package/web-app/dist/assets/{Avatar-Ct77hvlG.js → Avatar-BZrBstQe.js} +1 -1
- package/web-app/dist/assets/{Badge-Cg_gZaJF.js → Badge-Q8l8r4Vq.js} +1 -1
- package/web-app/dist/assets/{Button-i9r2G84n.js → Button-B8Bt0A3S.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-C3UyLXyp.js → CockpitPage-CIYExrXa.js} +1 -1
- package/web-app/dist/assets/{ComparePage-B1dXHga_.js → ComparePage-CBOmHdam.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CQ_xREw0.js → ErrorBoundary-BKwDPSnK.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DwUxd_Xi.js → EvidenceReceiptPanel-KqzjphWl.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-C0jAnuAd.js → GitHubIssuesPanel-lF3GkHRv.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-B4pNXJMz.js → GitHubPRsPanel-BCzipPWD.js} +1 -1
- package/web-app/dist/assets/{HomePage-BC9UTS1l.js → HomePage-CIdnkzaN.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CnpdwK8F.js → LoginPage-D-178nTT.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Bj0lZuNp.js → MagicPage-BmHuzMT8.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-yyw3VnfX.js → MetricsPage-CPCiyDFn.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-LMxkJP15.js → NotFoundPage-B2T6n1gx.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-2x0MRkNu.js → ProjectPage-DQmpFhb0.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-A8J8o1gj.js → ProjectsPage-BmugzFEd.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-fwvOVcRb.js → SettingsPage-ZB-RrmgM.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-DqI4sUwk.js → ShowcasePage-BAhwHt4_.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-CyytfNo_.js → SystemSettingsPage-Bt7sf2Vu.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BTTe76W1.js → TeamsPage-B-v5j_8q.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-CIp3OTlL.js → TemplatesPage-bdqUL-LG.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-dX5L4ky-.js → TerminalOutput-CRHEXBwS.js} +1 -1
- package/web-app/dist/assets/{activity-uHXr2AaF.js → activity-C-ROkmXt.js} +1 -1
- package/web-app/dist/assets/{bell-D6RbxRMo.js → bell-DbiCBxQA.js} +1 -1
- package/web-app/dist/assets/{bot-D31w2Mu6.js → bot-DsIgE_qr.js} +1 -1
- package/web-app/dist/assets/{check-CoKJksbr.js → check-CkAdmHWP.js} +1 -1
- package/web-app/dist/assets/{chevron-left-Bjx7oX7s.js → chevron-left-CYTLivAo.js} +1 -1
- package/web-app/dist/assets/{circle-alert-DOAh8Jmi.js → circle-alert-BVIcAsFJ.js} +1 -1
- package/web-app/dist/assets/{clock-DgEXPNWH.js → clock-Dg3Rq0go.js} +1 -1
- package/web-app/dist/assets/{cloud-0dXJFzBv.js → cloud-CenrqHFl.js} +1 -1
- package/web-app/dist/assets/{code-xml-DytQ_bq7.js → code-xml-C2mvfoug.js} +1 -1
- package/web-app/dist/assets/{database-Qyjryb72.js → database-BKa0_eCb.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-BzhTVy1e.js → dollar-sign-DfelUK1S.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DTdSOALw.js → file-code-corner-CZKWoHVg.js} +1 -1
- package/web-app/dist/assets/{file-plus-VdjOG1RK.js → file-plus-CZ-m1zMX.js} +1 -1
- package/web-app/dist/assets/{globe-BhxNOfyM.js → globe-S0kv_GwC.js} +1 -1
- package/web-app/dist/assets/{hammer-MQzrw8ee.js → hammer-BW1MDdu1.js} +1 -1
- package/web-app/dist/assets/{index-DaxrMbHf.js → index-ACEYsTc2.js} +3 -3
- package/web-app/dist/assets/{layers-C7nusTP-.js → layers-BrXHYZ-L.js} +1 -1
- package/web-app/dist/assets/{loader-circle-B1qBkgOY.js → loader-circle-DOdhKFx0.js} +1 -1
- package/web-app/dist/assets/{lock-BdkOxK2O.js → lock-BcFTyg8J.js} +1 -1
- package/web-app/dist/assets/{package-RVyK96Zn.js → package-D34cMVQG.js} +1 -1
- package/web-app/dist/assets/{plus-ntmwFRrp.js → plus-CEaMeBxL.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-BJ39b_PP.js → refresh-cw-D2Rzxr5U.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-uPx_-3vz.js → rotate-ccw-C_-EjRLh.js} +1 -1
- package/web-app/dist/assets/{scroll-text-D9qvXkj-.js → scroll-text-BSSCMI_c.js} +1 -1
- package/web-app/dist/assets/{server-gs4xmD8Y.js → server-BKvLonWt.js} +1 -1
- package/web-app/dist/assets/{shield-alert-O2_YffGI.js → shield-alert-DbbQbPdW.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-B3RVBhQx.js → thumbs-up-NoBD1fUR.js} +1 -1
- package/web-app/dist/assets/{trash-2-DEUt7qhq.js → trash-2-BXz8rAyd.js} +1 -1
- package/web-app/dist/assets/{trending-down-oyTns4UZ.js → trending-down-D8u535Re.js} +1 -1
- package/web-app/dist/assets/{trending-up-CdpZK0Oc.js → trending-up-q8k3kgiy.js} +1 -1
- package/web-app/dist/assets/{upload-D9sbjjC8.js → upload-Cp7_zeBI.js} +1 -1
- package/web-app/dist/assets/{usePolling-6ognrnyV.js → usePolling-CTHVdcuX.js} +1 -1
- package/web-app/dist/assets/{user-CBKc8QoI.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"
|
|
@@ -1905,6 +1908,7 @@ show_help_all() {
|
|
|
1905
1908
|
echo " loki memory list # All learnings"
|
|
1906
1909
|
echo " loki memory search <query> # Search across learnings"
|
|
1907
1910
|
echo " loki memory consolidate # Run episodic-to-semantic pipeline"
|
|
1911
|
+
echo " loki memory learn --recent N # Learn from the newest N merged PRs"
|
|
1908
1912
|
echo ""
|
|
1909
1913
|
echo " # Config + dashboard"
|
|
1910
1914
|
echo " loki config set maxTier sonnet # Cap model cost"
|
|
@@ -2548,7 +2552,7 @@ cmd_start() {
|
|
|
2548
2552
|
# [--auto-rollback] chains the gated `loki deploy --execute` after a start
|
|
2549
2553
|
# that ended 0. cmd_deploy owns every gate: no VERIFIED HEAD-anchored
|
|
2550
2554
|
# 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
|
|
2555
|
+
local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_hmatch="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
|
|
2552
2556
|
while [ $# -gt 0 ]; do
|
|
2553
2557
|
case "$1" in
|
|
2554
2558
|
--workspace)
|
|
@@ -2560,12 +2564,16 @@ cmd_start() {
|
|
|
2560
2564
|
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-url requires a URL${NC}" >&2; exit 2; }
|
|
2561
2565
|
_ps_health="$2"; shift 2 ;;
|
|
2562
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 ;;
|
|
2563
2571
|
--auto-rollback) _ps_rb=1; shift ;;
|
|
2564
2572
|
*) _ps_rest+=("$1"); shift ;;
|
|
2565
2573
|
esac
|
|
2566
2574
|
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
|
|
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
|
|
2569
2577
|
exit 2
|
|
2570
2578
|
fi
|
|
2571
2579
|
if [ -n "$_ps_ws" ]; then
|
|
@@ -2602,6 +2610,7 @@ cmd_start() {
|
|
|
2602
2610
|
fi
|
|
2603
2611
|
local _ps_dargs=(--execute)
|
|
2604
2612
|
[ -n "$_ps_health" ] && _ps_dargs+=(--health-url "$_ps_health")
|
|
2613
|
+
[ -n "$_ps_hmatch" ] && _ps_dargs+=(--health-match "$_ps_hmatch")
|
|
2605
2614
|
[ "$_ps_rb" = 1 ] && _ps_dargs+=(--auto-rollback)
|
|
2606
2615
|
cmd_deploy "${_ps_dargs[@]}"
|
|
2607
2616
|
return $?
|
|
@@ -5060,11 +5069,14 @@ cmd_pause() {
|
|
|
5060
5069
|
}
|
|
5061
5070
|
|
|
5062
5071
|
# RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
|
|
5063
|
-
#
|
|
5064
|
-
#
|
|
5065
|
-
#
|
|
5066
|
-
#
|
|
5067
|
-
#
|
|
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.
|
|
5068
5080
|
cmd_steer() {
|
|
5069
5081
|
case "${1:-}" in
|
|
5070
5082
|
--help|-h|help|"")
|
|
@@ -5072,13 +5084,14 @@ cmd_steer() {
|
|
|
5072
5084
|
echo ""
|
|
5073
5085
|
echo "Usage: loki steer \"<guidance>\""
|
|
5074
5086
|
echo ""
|
|
5075
|
-
echo "Writes your guidance to .loki/HUMAN_INPUT.md
|
|
5076
|
-
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."
|
|
5077
5090
|
echo ""
|
|
5078
5091
|
echo " loki steer \"focus on the auth flow, skip the docs for now\""
|
|
5079
5092
|
echo ""
|
|
5080
|
-
echo "
|
|
5081
|
-
echo "
|
|
5093
|
+
echo "Opt out with LOKI_PROMPT_INJECTION=0. The legacy loop reads the note"
|
|
5094
|
+
echo "only when LOKI_PROMPT_INJECTION=1."
|
|
5082
5095
|
[ -z "${1:-}" ] && return 2
|
|
5083
5096
|
return 0
|
|
5084
5097
|
;;
|
|
@@ -5088,20 +5101,34 @@ cmd_steer() {
|
|
|
5088
5101
|
return 1
|
|
5089
5102
|
fi
|
|
5090
5103
|
local note="$*"
|
|
5091
|
-
|
|
5092
|
-
|
|
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.
|
|
5093
5116
|
{
|
|
5094
|
-
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"
|
|
5095
5118
|
printf '%s\n\n' "$note"
|
|
5096
5119
|
} >> "$LOKI_DIR/HUMAN_INPUT.md"
|
|
5097
5120
|
emit_event session cli steer "chars=${#note}" 2>/dev/null || true
|
|
5098
|
-
echo -e "${GREEN}Steering note written to .loki/HUMAN_INPUT.md${NC}"
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
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
|
|
5105
5132
|
return 0
|
|
5106
5133
|
}
|
|
5107
5134
|
|
|
@@ -9770,7 +9797,7 @@ _deploy_latest_receipt() {
|
|
|
9770
9797
|
return 0
|
|
9771
9798
|
}
|
|
9772
9799
|
|
|
9773
|
-
# _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]
|
|
9774
9801
|
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
|
|
9775
9802
|
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
|
|
9776
9803
|
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
|
|
@@ -9793,6 +9820,7 @@ _deploy_record() {
|
|
|
9793
9820
|
local health_status="${7:-unchecked}" health_url="${8:-}"
|
|
9794
9821
|
local rb_attempted="${9:-false}" rb_exit="${10:-}"
|
|
9795
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:-}"
|
|
9796
9824
|
local ddir="$dir/.loki/deploys"
|
|
9797
9825
|
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
9798
9826
|
mkdir -p "$ddir" 2>/dev/null || return 0
|
|
@@ -9800,7 +9828,7 @@ _deploy_record() {
|
|
|
9800
9828
|
DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
|
|
9801
9829
|
DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
|
|
9802
9830
|
DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
|
|
9803
|
-
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" \
|
|
9804
9832
|
python3 -c '
|
|
9805
9833
|
import json, os, datetime
|
|
9806
9834
|
state = os.environ.get("DR_STATE") or "anchored"
|
|
@@ -9826,6 +9854,10 @@ json.dump({
|
|
|
9826
9854
|
"health_url": os.environ.get("DR_HU") or "",
|
|
9827
9855
|
"rollback_attempted": os.environ.get("DR_RBA") == "true",
|
|
9828
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,
|
|
9829
9861
|
}, open(os.environ["DR_OUT"], "w"), indent=2)
|
|
9830
9862
|
' 2>/dev/null || return 0
|
|
9831
9863
|
# Sign through the one Bun implementation. A subshell, because the delegate execs.
|
|
@@ -9894,20 +9926,31 @@ except Exception:
|
|
|
9894
9926
|
return 0
|
|
9895
9927
|
}
|
|
9896
9928
|
|
|
9897
|
-
# _deploy_probe_health <url>
|
|
9898
|
-
# 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
|
|
9899
9933
|
# spacing are env-tunable (LOKI_DEPLOY_HEALTH_ATTEMPTS / _INTERVAL) for tests.
|
|
9900
9934
|
_deploy_probe_health() {
|
|
9901
|
-
local url="${1:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
|
|
9902
|
-
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
|
|
9903
9938
|
case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
|
|
9904
9939
|
case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
|
|
9905
9940
|
while [ "$i" -lt "$attempts" ]; do
|
|
9906
9941
|
i=$((i + 1))
|
|
9907
|
-
code="$(curl -s -o
|
|
9908
|
-
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
|
|
9909
9951
|
[ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
|
|
9910
9952
|
done
|
|
9953
|
+
[ "$body" = /dev/null ] || rm -f "$body"
|
|
9911
9954
|
printf '%s' "$code"
|
|
9912
9955
|
return 1
|
|
9913
9956
|
}
|
|
@@ -9973,6 +10016,7 @@ _deploy_try_execute() {
|
|
|
9973
10016
|
local dir="${1:-.}" type="${2:-}"
|
|
9974
10017
|
# Set by cmd_deploy (dynamic scope); empty/false when called any other way.
|
|
9975
10018
|
local health_url="${_LOKI_DEPLOY_HEALTH_URL:-}" auto_rollback="${_LOKI_DEPLOY_AUTO_ROLLBACK:-false}"
|
|
10019
|
+
local health_match="${_LOKI_DEPLOY_HEALTH_MATCH:-}"
|
|
9976
10020
|
|
|
9977
10021
|
local command
|
|
9978
10022
|
command="$(_deploy_first_installed_command "$dir" "$type")"
|
|
@@ -10085,7 +10129,7 @@ _deploy_try_execute() {
|
|
|
10085
10129
|
|
|
10086
10130
|
local rc=0 started_at finished_at purl="" canary_out=""
|
|
10087
10131
|
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
10088
|
-
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"
|
|
10089
10133
|
if [ "$canary_kind" = vercel ]; then
|
|
10090
10134
|
# Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
|
|
10091
10135
|
# Nothing reaches production before the probe, so there is nothing to roll back.
|
|
@@ -10097,7 +10141,7 @@ _deploy_try_execute() {
|
|
|
10097
10141
|
if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
|
|
10098
10142
|
health_url="$purl"
|
|
10099
10143
|
echo "Probing canary health: ${health_url}"
|
|
10100
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10144
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10101
10145
|
health_status="healthy"
|
|
10102
10146
|
echo "Promoting: vercel promote ${purl}"
|
|
10103
10147
|
exec_cmd="vercel; vercel promote ${purl}"
|
|
@@ -10118,7 +10162,7 @@ _deploy_try_execute() {
|
|
|
10118
10162
|
# failed deploy, status is "unchecked": never a claim the service is healthy.
|
|
10119
10163
|
if [ "$canary_kind" != vercel ] && [ "$rc" -eq 0 ] && [ -n "$health_url" ]; then
|
|
10120
10164
|
echo "Probing health: ${health_url}"
|
|
10121
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10165
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10122
10166
|
health_status="healthy"
|
|
10123
10167
|
else
|
|
10124
10168
|
health_status="FAILED_HEALTH"
|
|
@@ -10126,14 +10170,21 @@ _deploy_try_execute() {
|
|
|
10126
10170
|
fi
|
|
10127
10171
|
|
|
10128
10172
|
if [ "$health_status" = "FAILED_HEALTH" ]; then
|
|
10129
|
-
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
|
|
10130
10174
|
if [ "$auto_rollback" = true ] && [ -n "$rb_cmd" ] && ! _deploy_is_destructive "$rb_cmd"; then
|
|
10131
10175
|
echo "Rolling back: ${rb_cmd}" >&2
|
|
10132
10176
|
rb_attempted=true
|
|
10133
10177
|
rb_exit=0
|
|
10134
10178
|
( cd "$dir" && eval "$rb_cmd" ) || rb_exit=$?
|
|
10135
10179
|
if [ "$rb_exit" -eq 0 ]; then
|
|
10136
|
-
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
|
|
10137
10188
|
else
|
|
10138
10189
|
echo -e "${RED}Rollback command exited ${rb_exit}.${NC}" >&2
|
|
10139
10190
|
fi
|
|
@@ -10149,7 +10200,7 @@ _deploy_try_execute() {
|
|
|
10149
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 '')"
|
|
10150
10201
|
rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
|
|
10151
10202
|
"$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
|
|
10152
|
-
"$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)")"
|
|
10153
10204
|
echo ""
|
|
10154
10205
|
if [ "$rc" -eq 0 ]; then
|
|
10155
10206
|
echo -e "${GREEN}Deploy command exited 0.${NC}"
|
|
@@ -10176,6 +10227,8 @@ _deploy_try_execute() {
|
|
|
10176
10227
|
fi
|
|
10177
10228
|
# A deploy that exited 0 but failed its health probe is a failed deploy.
|
|
10178
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
|
|
10179
10232
|
return 4
|
|
10180
10233
|
fi
|
|
10181
10234
|
return "$rc"
|
|
@@ -10289,7 +10342,7 @@ cmd_deploy() {
|
|
|
10289
10342
|
# file alone weakens this gate.
|
|
10290
10343
|
local LOKI_DEPLOY_ALLOW_UNANCHORED=false
|
|
10291
10344
|
# Per-invocation like --execute; local so an ambient env var cannot set them.
|
|
10292
|
-
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
|
|
10293
10346
|
|
|
10294
10347
|
# Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
|
|
10295
10348
|
while [ $# -gt 0 ]; do
|
|
@@ -10297,7 +10350,7 @@ cmd_deploy() {
|
|
|
10297
10350
|
--help|-h|help)
|
|
10298
10351
|
echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
|
|
10299
10352
|
echo ""
|
|
10300
|
-
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]]"
|
|
10301
10354
|
echo ""
|
|
10302
10355
|
echo "Detects your project type and your installed cloud CLI (and any"
|
|
10303
10356
|
echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
|
|
@@ -10358,11 +10411,14 @@ cmd_deploy() {
|
|
|
10358
10411
|
echo " non-2xx marks the deploy FAILED_HEALTH and exits 4."
|
|
10359
10412
|
echo " Without a URL, health is recorded as 'unchecked',"
|
|
10360
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."
|
|
10361
10416
|
echo " --auto-rollback On FAILED_HEALTH run the provider's documented rollback:"
|
|
10362
10417
|
echo " 'vercel rollback' or 'fly releases rollback <version"
|
|
10363
10418
|
echo " captured before the deploy>'. Netlify and Cloudflare"
|
|
10364
10419
|
echo " print a manual note. Needs --health-url. The rolled-back"
|
|
10365
|
-
echo " service is
|
|
10420
|
+
echo " service is re-probed and recorded as post_rollback_health;"
|
|
10421
|
+
echo " still unhealthy exits 5."
|
|
10366
10422
|
echo ""
|
|
10367
10423
|
echo "Staged rollout (only with --execute):"
|
|
10368
10424
|
echo " --canary Vercel: deploy without --prod, health-check the preview"
|
|
@@ -10401,6 +10457,10 @@ cmd_deploy() {
|
|
|
10401
10457
|
*) echo "loki deploy --health-url requires an http(s) URL" >&2; return 1 ;;
|
|
10402
10458
|
esac
|
|
10403
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
|
+
;;
|
|
10404
10464
|
--auto-rollback)
|
|
10405
10465
|
_LOKI_DEPLOY_AUTO_ROLLBACK=true
|
|
10406
10466
|
;;
|
|
@@ -10428,6 +10488,10 @@ cmd_deploy() {
|
|
|
10428
10488
|
echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
|
|
10429
10489
|
return 1
|
|
10430
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
|
|
10431
10495
|
if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ "$do_execute" != true ]; then
|
|
10432
10496
|
echo "loki deploy --canary requires --execute" >&2
|
|
10433
10497
|
return 1
|
|
@@ -11491,7 +11555,7 @@ fi
|
|
|
11491
11555
|
# Post-completion: auto-merge if requested
|
|
11492
11556
|
if [[ "$LOKI_AUTO_MERGE" == "true" ]]; then
|
|
11493
11557
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11494
|
-
if
|
|
11558
|
+
if "$LOKI_CMD" merge watch "$branch_current" --require-verified --delete-branch; then
|
|
11495
11559
|
# A brief run carries a synthetic ref (b<epoch>), not a real issue: close only numeric refs.
|
|
11496
11560
|
if [[ "${LOKI_ISSUE_NUMBER:-}" =~ ^[0-9]+$ ]]; then
|
|
11497
11561
|
gh issue close "$LOKI_ISSUE_NUMBER" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -11709,7 +11773,8 @@ ${_gh_receipt}"
|
|
|
11709
11773
|
if command -v jq >/dev/null 2>&1; then
|
|
11710
11774
|
jq -n --arg url "$(printf '%s' "$_pr_out" | grep -Eo 'https://[^[:space:]]+' | head -1)" \
|
|
11711
11775
|
--arg branch "$branch_current" \
|
|
11712
|
-
'{state:"created", url:$url, branch:$branch
|
|
11776
|
+
'{state:"created", url:$url, branch:$branch,
|
|
11777
|
+
merge:{outcome:"waiting", gate:"human_review", checks:"not recorded"}}' \
|
|
11713
11778
|
> .loki/state/pr.json 2>/dev/null || true
|
|
11714
11779
|
fi
|
|
11715
11780
|
else
|
|
@@ -11743,7 +11808,7 @@ ${_gh_receipt}"
|
|
|
11743
11808
|
github|brief)
|
|
11744
11809
|
local branch_current
|
|
11745
11810
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11746
|
-
if
|
|
11811
|
+
if _merge_pr_watch "$branch_current" --require-verified --delete-branch; then
|
|
11747
11812
|
echo -e "${GREEN}PR merged and branch deleted${NC}"
|
|
11748
11813
|
if [[ "${number:-}" =~ ^[0-9]+$ ]]; then
|
|
11749
11814
|
gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -18472,6 +18537,7 @@ cmd_modernize() {
|
|
|
18472
18537
|
echo "Subcommands (grouped modernization surface):"
|
|
18473
18538
|
echo " heal <path> Legacy system healing in phases (was: loki heal)"
|
|
18474
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)"
|
|
18475
18541
|
echo ""
|
|
18476
18542
|
echo "Run 'loki modernize <subcommand> --help' for each. The old"
|
|
18477
18543
|
echo "top-level names still work; see 'loki help aliases'."
|
|
@@ -25881,6 +25947,142 @@ SHOW_EOF
|
|
|
25881
25947
|
esac
|
|
25882
25948
|
}
|
|
25883
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
|
+
|
|
25884
26086
|
# loki merge: serial PR merge queue. Queue lives in .loki/merge-queue (one PR
|
|
25885
26087
|
# number per line). A PR merges only when `gh pr checks` exits 0 (all green);
|
|
25886
26088
|
# after each merge the next PR is rebased onto the new base (gh pr update-branch
|
|
@@ -25915,8 +26117,7 @@ cmd_merge() {
|
|
|
25915
26117
|
[ "${1:-}" = "--dry-run" ] && dry=true
|
|
25916
26118
|
if [ ! -s "$qfile" ]; then echo "Merge queue is empty"; return 0; fi
|
|
25917
26119
|
command -v gh >/dev/null 2>&1 || { echo "gh CLI is required" >&2; return 1; }
|
|
25918
|
-
local
|
|
25919
|
-
local first=true n rc i remaining="" merged=0 blocked=0
|
|
26120
|
+
local first=true n rc remaining="" merged=0 blocked=0
|
|
25920
26121
|
while IFS= read -r n; do
|
|
25921
26122
|
[ -n "$n" ] || continue
|
|
25922
26123
|
if $dry; then
|
|
@@ -25935,29 +26136,33 @@ cmd_merge() {
|
|
|
25935
26136
|
|| { echo "#$n: rebase onto new base failed, left in queue"; remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue; }
|
|
25936
26137
|
echo "#$n: rebased onto new base"
|
|
25937
26138
|
fi
|
|
25938
|
-
|
|
25939
|
-
while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
|
|
25940
|
-
rc=0; gh pr checks "$n" >/dev/null 2>&1 || rc=$?
|
|
25941
|
-
[ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
|
|
25942
|
-
done
|
|
25943
|
-
if [ "$rc" != "0" ]; then
|
|
25944
|
-
echo "#$n: checks not green (rc=$rc), left in queue, not merged"
|
|
25945
|
-
remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue
|
|
25946
|
-
fi
|
|
25947
|
-
if gh pr merge "$n" --squash >/dev/null 2>&1; then
|
|
26139
|
+
if _merge_pr_gated "$n" 2>"$qfile.err"; then
|
|
25948
26140
|
echo "#$n: merged"; merged=$((merged+1)); first=false
|
|
25949
26141
|
else
|
|
25950
|
-
echo "#$n:
|
|
26142
|
+
sed 's/^/#/' "$qfile.err"; echo "#$n: left in queue"
|
|
25951
26143
|
remaining="$remaining$n"$'\n'; blocked=$((blocked+1))
|
|
25952
26144
|
fi
|
|
25953
26145
|
done < "$qfile"
|
|
25954
26146
|
if $dry; then echo "Dry run: nothing changed"; return 0; fi
|
|
26147
|
+
rm -f "$qfile.err"
|
|
25955
26148
|
printf '%s' "$remaining" > "$qfile"
|
|
25956
26149
|
echo "Merged: $merged, left in queue: $blocked"
|
|
25957
26150
|
[ "$blocked" -eq 0 ]
|
|
25958
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
|
+
;;
|
|
25959
26160
|
help|--help|-h|*)
|
|
25960
|
-
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)."
|
|
25961
26166
|
echo " add <pr>... Queue pull requests"
|
|
25962
26167
|
echo " list Show the queue"
|
|
25963
26168
|
echo " run [--dry-run] Merge one at a time, only when checks are green;"
|