loki-mode 11.12.1 → 11.17.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 +2 -2
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/grill.sh +3 -1
- package/autonomy/hooks/validate-bash.sh +51 -13
- package/autonomy/lib/backlog.py +18 -3
- package/autonomy/lib/chat.sh +117 -7
- package/autonomy/lib/claude-flags.sh +20 -0
- package/autonomy/lib/claude_permissions.py +40 -0
- package/autonomy/lib/done-recognition.sh +8 -4
- package/autonomy/lib/engine10-push.sh +56 -7
- package/autonomy/lib/prd-enrich.sh +4 -2
- package/autonomy/lib/proof-generator.py +21 -0
- package/autonomy/lib/proof-verify.py +109 -1
- package/autonomy/lib/trust_trajectory.py +139 -0
- package/autonomy/lib/voter-agents.sh +3 -1
- package/autonomy/lib/workspace.py +2 -3
- package/autonomy/loki +427 -74
- package/autonomy/quickstart.sh +21 -2
- package/autonomy/run.sh +55 -21
- package/autonomy/trigger-server.py +461 -32
- package/bin/loki +4 -1
- package/completions/_loki +1 -1
- package/completions/loki.bash +10 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +2 -2
- package/dashboard/build_supervisor.py +2 -0
- package/docs/CLI-REFERENCE.md +6 -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 +148 -123
- package/docs/v10/CLI-MODERN.md +5 -5
- package/docs/v10/DECISIONS.md +2 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +18 -0
- package/docs/v10/MODERNIZE.md +32 -1
- package/docs/v10/PROGRESS.md +7 -0
- package/loki-ts/data/finding-schema.json +2 -1
- package/loki-ts/dist/loki.js +1222 -1128
- package/magic/core/debate.py +27 -1
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +5895 -5292
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +6 -3
- package/providers/model_catalog.json +6 -0
- package/schemas/loki-yaml.schema.json +5 -0
- package/src/policies/claude_permissions.js +114 -0
- package/web-app/dist/assets/{AdminPage-DuT74pf5.js → AdminPage-OVzN6eLm.js} +1 -1
- package/web-app/dist/assets/{Avatar-BtgidR5a.js → Avatar-hCCrxrCZ.js} +1 -1
- package/web-app/dist/assets/{Badge-D2nt8G_I.js → Badge-CDcpkhbJ.js} +1 -1
- package/web-app/dist/assets/{Button-BKl2jOyb.js → Button-8mwWlhEu.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DaWr0EUX.js → CockpitPage-D8cf3u22.js} +1 -1
- package/web-app/dist/assets/{ComparePage-aIiNhxLr.js → ComparePage-CIgkbDRY.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C1Q7rV23.js → ErrorBoundary-99kJH9g2.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C-C0-tOx.js → EvidenceReceiptPanel-C9fj32eS.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-l3IaXDag.js → GitHubIssuesPanel-De43YQ_C.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DweLBgr5.js → GitHubPRsPanel-7g7ZTnkx.js} +1 -1
- package/web-app/dist/assets/{HomePage-B_KK7AvY.js → HomePage-CcNA24ny.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CKS-KLdH.js → LoginPage-BYjlb-IK.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Cl6CEp8J.js → MagicPage-C0Dwt9Na.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-n9CPUaX0.js → MetricsPage-jDGmdcgg.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D85eWn70.js → NotFoundPage-C_ZO8tLE.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CDN7LpSo.js → ProjectPage-B5wnurfQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D4SDmU4-.js → ProjectsPage--EyKt5-D.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Dby6GnKm.js → SettingsPage-cB0lnfti.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CgHxeqoU.js → ShowcasePage-Dl16FDkK.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BaewGHlc.js → SystemSettingsPage-BmAXd4V2.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-905MJyke.js → TeamsPage-BFkWVOZe.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DMrUHcvF.js → TemplatesPage-Bf2kmEUM.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BsbEbBYX.js → TerminalOutput-BItv9Wa6.js} +1 -1
- package/web-app/dist/assets/{activity-BuaXhBsC.js → activity-Df2Awg6I.js} +1 -1
- package/web-app/dist/assets/{bell-BfEQ71OG.js → bell-D-1ztTsJ.js} +1 -1
- package/web-app/dist/assets/{bot-DOGeYewg.js → bot-A4qgZsKi.js} +1 -1
- package/web-app/dist/assets/{check-zbjPpb3E.js → check-BPfFpF0c.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DWTOgfFt.js → chevron-left-CfSJvmDi.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BEPKfRzR.js → circle-alert-wx2d1dzb.js} +1 -1
- package/web-app/dist/assets/{clock-C_fX3OHo.js → clock-pQMai6l-.js} +1 -1
- package/web-app/dist/assets/{cloud-DUXHygXQ.js → cloud-DWL470r6.js} +1 -1
- package/web-app/dist/assets/{code-xml-BEVl_9Hw.js → code-xml-BaEQpQLZ.js} +1 -1
- package/web-app/dist/assets/{database-_a22uh7l.js → database-h-N7WkQN.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-l2en7xfg.js → dollar-sign-B5oYiD27.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DItKl6H4.js → file-code-corner-fbTtTy0o.js} +1 -1
- package/web-app/dist/assets/{file-plus-BSk7qFMA.js → file-plus-PaF6HMo8.js} +1 -1
- package/web-app/dist/assets/{globe-2TveFE5l.js → globe-Ct-GlWID.js} +1 -1
- package/web-app/dist/assets/{hammer-BU6OJlQj.js → hammer-C1oZFX51.js} +1 -1
- package/web-app/dist/assets/{index-DGnZXUYk.js → index-C2egrRhz.js} +3 -3
- package/web-app/dist/assets/{layers-DKw7MNuq.js → layers-Dt5ZK4v2.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CoYfykzD.js → loader-circle-DrpMziic.js} +1 -1
- package/web-app/dist/assets/{lock-D9JzrXDL.js → lock-DOez8MmK.js} +1 -1
- package/web-app/dist/assets/{package-BZtqTIqH.js → package-ClGIkerr.js} +1 -1
- package/web-app/dist/assets/{plus-BGJfo8bV.js → plus-C004i9ua.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-F6lowBgR.js → refresh-cw-Ddfk-7Sy.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DHOK8dM4.js → rotate-ccw-Bu1KgDjS.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CKcROoMV.js → scroll-text-3q1lW5pm.js} +1 -1
- package/web-app/dist/assets/{server-DbG8MDv6.js → server-pd6x4HW1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-RNKNXdJT.js → shield-alert-B9jGJepT.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Yxp-UBxS.js → thumbs-up-Cdcc9nQf.js} +1 -1
- package/web-app/dist/assets/{trash-2-DY-eba1n.js → trash-2-foSojXHK.js} +1 -1
- package/web-app/dist/assets/{trending-down-CARiDB3t.js → trending-down-CTTiqlJ1.js} +1 -1
- package/web-app/dist/assets/{trending-up-brQqWbA3.js → trending-up-B_MgmRAx.js} +1 -1
- package/web-app/dist/assets/{upload-D6wRjKhz.js → upload-g6BfALzG.js} +1 -1
- package/web-app/dist/assets/{usePolling-BMrqNbAI.js → usePolling-DrHwvzuh.js} +1 -1
- package/web-app/dist/assets/{user-CK5-EXIj.js → user-COp75m7l.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/autonomy/lib/doctor-fix.py +0 -279
package/autonomy/loki
CHANGED
|
@@ -132,6 +132,34 @@ _loki_caveman_narration_level() {
|
|
|
132
132
|
fi
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
# P12-26b: permission flags for the migrate/doc/heal claude launches. Lazily sources
|
|
136
|
+
# claude-flags.sh (like the caveman wrapper above) and fills _LOKI_PERM_ARGV; unset
|
|
137
|
+
# LOKI_PERMISSION_POLICY keeps --dangerously-skip-permissions byte-identically.
|
|
138
|
+
_loki_perm_argv_for() {
|
|
139
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
140
|
+
# Unset policy: return before sourcing anything (claude-flags.sh exports caveman vars).
|
|
141
|
+
[ "${LOKI_PERMISSION_POLICY:-}" = "compiled" ] || return 0
|
|
142
|
+
_LOKI_PERM_ARGV=(--permission-mode plan)
|
|
143
|
+
local _pa_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" _pa_out _pa_a
|
|
144
|
+
if [ ! -f "$_pa_lib" ]; then
|
|
145
|
+
echo "[loki] WARNING: LOKI_PERMISSION_POLICY=compiled: helper missing; using most restrictive mode (plan)" >&2
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
# Subshell so the helper's top-level exports never leak into this shell.
|
|
149
|
+
_pa_out="$(
|
|
150
|
+
LOKI_PROJECT_DIR="${1:-${LOKI_PROJECT_DIR:-$PWD}}"
|
|
151
|
+
# shellcheck source=autonomy/lib/claude-flags.sh
|
|
152
|
+
. "$_pa_lib" >/dev/null 2>&1 || exit 1
|
|
153
|
+
declare -F _loki_claude_permission_argv >/dev/null 2>&1 || exit 1
|
|
154
|
+
_loki_claude_permission_argv
|
|
155
|
+
printf '%s\n' "${_LOKI_PERM_ARGV[@]}"
|
|
156
|
+
)" || return 0
|
|
157
|
+
[ -n "$_pa_out" ] || return 0
|
|
158
|
+
_LOKI_PERM_ARGV=()
|
|
159
|
+
while IFS= read -r _pa_a; do _LOKI_PERM_ARGV+=("$_pa_a"); done <<< "$_pa_out"
|
|
160
|
+
return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
# loki edit: one-pass small change fast path (functions-only, self-guarded).
|
|
136
164
|
if [ -f "$_LOKI_SCRIPT_DIR/edit.sh" ]; then
|
|
137
165
|
# shellcheck source=edit.sh
|
|
@@ -1682,7 +1710,7 @@ PYEOF
|
|
|
1682
1710
|
# `loki help answer`: `loki answer` is a v10 (loki-ts) command with no bash
|
|
1683
1711
|
# implementation, so its help lives here rather than behind delegation.
|
|
1684
1712
|
_help_answer() {
|
|
1685
|
-
echo -e "${BOLD}loki answer${NC} -
|
|
1713
|
+
echo -e "${BOLD}loki answer${NC} - Start a follow-up run from a BLOCKED run with your answer"
|
|
1686
1714
|
echo ""
|
|
1687
1715
|
echo "Usage: loki answer [<run-id>] [--text \"...\"]"
|
|
1688
1716
|
echo ""
|
|
@@ -1711,6 +1739,8 @@ _help_verify_pr() {
|
|
|
1711
1739
|
echo -e "${BOLD}loki verify-pr${NC} - Verify a PR against its linked issue (experimental, LOKI_VERIFY_PR=1)"
|
|
1712
1740
|
echo ""
|
|
1713
1741
|
echo "Usage: loki verify-pr <https://github.com/o/r/pull/N | o/r#N> [--out DIR]"
|
|
1742
|
+
echo " loki verify-pr respond <o/r#N> fix open review comments (one commit each, on the PR"
|
|
1743
|
+
echo " branch), push, reply with the sha; never resolves threads"
|
|
1714
1744
|
echo ""
|
|
1715
1745
|
echo "Derives a check from the PR's linked issue (Check: \`cmd\` or a loki-check fence)."
|
|
1716
1746
|
echo "VERIFIED only if the check fails on the PR base, passes on its head, and the"
|
|
@@ -1795,11 +1825,12 @@ show_help_all() {
|
|
|
1795
1825
|
echo " stop Stop execution immediately"
|
|
1796
1826
|
echo " pause Pause after current session"
|
|
1797
1827
|
echo " resume Resume a paused or interrupted run (after Ctrl-C or a crash)"
|
|
1798
|
-
echo " steer \"<note>\" Nudge a running build (needs LOKI_PROMPT_INJECTION=
|
|
1828
|
+
echo " steer \"<note>\" Nudge a running build (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)"
|
|
1799
1829
|
echo ""
|
|
1800
1830
|
echo "Verify / trust:"
|
|
1801
1831
|
echo " verify [base] Deterministic PR verification (CI-gate exit codes)"
|
|
1802
|
-
echo "
|
|
1832
|
+
echo " verify-pr <pr> Verify a PR against its linked issue; 'verify-pr respond <pr>' fixes review comments"
|
|
1833
|
+
echo " keys export Print the public receipt-signing key (JWK) for third-party verify --pubkey"
|
|
1803
1834
|
echo " review [opts] Standalone code review with quality gates"
|
|
1804
1835
|
echo " ultracode \"task\" Run a task as a native Claude Code Dynamic Workflow (opt-in, Claude-only)"
|
|
1805
1836
|
echo " trust [cmd] Visible trust trajectory; 'trust detail' for metrics"
|
|
@@ -1839,7 +1870,7 @@ show_help_all() {
|
|
|
1839
1870
|
echo ""
|
|
1840
1871
|
echo "Options for 'start':"
|
|
1841
1872
|
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
1842
|
-
echo "
|
|
1873
|
+
echo " (model route: set LOKI_MODEL_ROUTE, e.g. ollama/qwen3-coder; there is no --model flag)"
|
|
1843
1874
|
echo " --simple Force simple complexity tier (3 phases)"
|
|
1844
1875
|
echo " --complex Force complex complexity tier (8 phases)"
|
|
1845
1876
|
echo " --no-dashboard Disable web dashboard"
|
|
@@ -2516,6 +2547,37 @@ _loki_start_handoff() {
|
|
|
2516
2547
|
}
|
|
2517
2548
|
|
|
2518
2549
|
# Start Loki Mode
|
|
2550
|
+
# Every value-taking cmd_start flag (its value is never brief text or a run id).
|
|
2551
|
+
# The one list: the --workspace brief scan and the --resume scan both use it.
|
|
2552
|
+
# It also names value flags that are not cmd_start's own, so a scan never
|
|
2553
|
+
# mistakes their value for a brief or run id. --model is honored nowhere (model
|
|
2554
|
+
# routing is LOKI_MODEL_ROUTE); --max-cost only via the engine10 rewrite.
|
|
2555
|
+
_loki_start_flag_takes_value() {
|
|
2556
|
+
case "$1" in
|
|
2557
|
+
--provider|--brief|--budget|--prd|--issue|--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|--mirofish-docker|--spec|--model|--max-cost|--max-wait|--base-wait|--max-iterations|--max-retries|--budget-limit|--session-model|--completion-promise) return 0 ;;
|
|
2558
|
+
esac
|
|
2559
|
+
return 1
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
# Optional-value cmd_start flags (--mirofish [URL]). The one decision on whether
|
|
2563
|
+
# the NEXT token is the flag's value: non-empty and not another --flag. The
|
|
2564
|
+
# cmd_start --mirofish arm and both scans use it, so they cannot disagree.
|
|
2565
|
+
_loki_start_flag_optional_value() { [ "$1" = "--mirofish" ]; }
|
|
2566
|
+
_loki_start_optval_consumes() { [[ -n "${1:-}" && "${1:-}" != --* ]]; }
|
|
2567
|
+
|
|
2568
|
+
# One exit-code mapping for an engine10 resume, shared by cmd_resume and
|
|
2569
|
+
# `loki start --resume`. 0 and 4 (phase reported) -> 0, 5 (publish failed) -> 1.
|
|
2570
|
+
# Sets _LOKI_E10_RC and returns 0 when handled; any other code returns 1 with
|
|
2571
|
+
# _LOKI_E10_RC set to the raw code (cmd_resume falls through, start returns it).
|
|
2572
|
+
_loki_e10_resume_map() {
|
|
2573
|
+
case "$1" in
|
|
2574
|
+
0 | 4) _LOKI_E10_RC=0 ;;
|
|
2575
|
+
5) _LOKI_E10_RC=1 ;;
|
|
2576
|
+
*) _LOKI_E10_RC="$1"; return 1 ;;
|
|
2577
|
+
esac
|
|
2578
|
+
return 0
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2519
2581
|
cmd_start() {
|
|
2520
2582
|
# First-run welcome (once): open the branded welcome page on the very
|
|
2521
2583
|
# first start. Writes ~/.loki/.welcomed and never repeats; non-blocking,
|
|
@@ -2549,7 +2611,7 @@ cmd_start() {
|
|
|
2549
2611
|
# [--auto-rollback] chains the gated `loki deploy --execute` after a start
|
|
2550
2612
|
# that ended 0. cmd_deploy owns every gate: no VERIFIED HEAD-anchored
|
|
2551
2613
|
# 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
|
|
2614
|
+
local _ps_ws="" _ps_deploy=0 _ps_health="" _ps_hmatch="" _ps_rb=0 _ps_rest=() _ps_brief=() _ps_a
|
|
2553
2615
|
while [ $# -gt 0 ]; do
|
|
2554
2616
|
case "$1" in
|
|
2555
2617
|
--workspace)
|
|
@@ -2561,12 +2623,16 @@ cmd_start() {
|
|
|
2561
2623
|
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-url requires a URL${NC}" >&2; exit 2; }
|
|
2562
2624
|
_ps_health="$2"; shift 2 ;;
|
|
2563
2625
|
--health-url=*) _ps_health="${1#*=}"; shift ;;
|
|
2626
|
+
--health-match)
|
|
2627
|
+
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo -e "${RED}--health-match requires a string${NC}" >&2; exit 2; }
|
|
2628
|
+
_ps_hmatch="$2"; shift 2 ;;
|
|
2629
|
+
--health-match=*) _ps_hmatch="${1#*=}"; shift ;;
|
|
2564
2630
|
--auto-rollback) _ps_rb=1; shift ;;
|
|
2565
2631
|
*) _ps_rest+=("$1"); shift ;;
|
|
2566
2632
|
esac
|
|
2567
2633
|
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
|
|
2634
|
+
if { [ -n "$_ps_health" ] || [ -n "$_ps_hmatch" ] || [ "$_ps_rb" = 1 ]; } && [ "$_ps_deploy" != 1 ]; then
|
|
2635
|
+
echo -e "${RED}--health-url, --health-match and --auto-rollback apply only with --deploy${NC}" >&2
|
|
2570
2636
|
exit 2
|
|
2571
2637
|
fi
|
|
2572
2638
|
if [ -n "$_ps_ws" ]; then
|
|
@@ -2577,13 +2643,13 @@ cmd_start() {
|
|
|
2577
2643
|
local _ps_skip=0
|
|
2578
2644
|
for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
|
|
2579
2645
|
if [ "$_ps_skip" = 1 ]; then _ps_skip=0; continue; fi
|
|
2646
|
+
if [ "$_ps_skip" = 3 ]; then _ps_skip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
|
|
2580
2647
|
if [ "$_ps_skip" = 2 ]; then _ps_skip=0; _ps_brief+=("$_ps_a"); continue; fi
|
|
2581
2648
|
case "$_ps_a" in
|
|
2582
2649
|
--brief=*) _ps_brief+=("${_ps_a#*=}") ;;
|
|
2583
2650
|
--brief) _ps_skip=2 ;;
|
|
2584
2651
|
# Every value-taking cmd_start flag: its value is never brief text.
|
|
2585
|
-
|
|
2586
|
-
-*) ;;
|
|
2652
|
+
-*) if _loki_start_flag_takes_value "$_ps_a"; then _ps_skip=1; elif _loki_start_flag_optional_value "$_ps_a"; then _ps_skip=3; fi ;;
|
|
2587
2653
|
*) _ps_brief+=("$_ps_a") ;;
|
|
2588
2654
|
esac
|
|
2589
2655
|
done
|
|
@@ -2603,10 +2669,39 @@ cmd_start() {
|
|
|
2603
2669
|
fi
|
|
2604
2670
|
local _ps_dargs=(--execute)
|
|
2605
2671
|
[ -n "$_ps_health" ] && _ps_dargs+=(--health-url "$_ps_health")
|
|
2672
|
+
[ -n "$_ps_hmatch" ] && _ps_dargs+=(--health-match "$_ps_hmatch")
|
|
2606
2673
|
[ "$_ps_rb" = 1 ] && _ps_dargs+=(--auto-rollback)
|
|
2607
2674
|
cmd_deploy "${_ps_dargs[@]}"
|
|
2608
2675
|
return $?
|
|
2609
2676
|
fi
|
|
2677
|
+
# P12-07b: a bare `loki start --resume [run-id]` goes to the same engine10
|
|
2678
|
+
# resume path as `loki resume` (util/run_outputs.ts resumeMain), never into a
|
|
2679
|
+
# new build. Exit codes go through _loki_e10_resume_map (0 and 4 -> 0, 5 -> 1); any other code, such as 3 for nothing to resume, is returned as is.
|
|
2680
|
+
local _ps_resume=0 _ps_resume_args=() _ps_rskip=0
|
|
2681
|
+
for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
|
|
2682
|
+
if [ "$_ps_rskip" = 1 ]; then _ps_rskip=0; continue; fi
|
|
2683
|
+
if [ "$_ps_rskip" = 3 ]; then _ps_rskip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
|
|
2684
|
+
case "$_ps_a" in
|
|
2685
|
+
--resume) _ps_resume=1 ;;
|
|
2686
|
+
--*=*) ;;
|
|
2687
|
+
-*) if _loki_start_flag_takes_value "$_ps_a"; then _ps_rskip=1; elif _loki_start_flag_optional_value "$_ps_a"; then _ps_rskip=3; fi ;;
|
|
2688
|
+
*) _ps_resume_args+=("$_ps_a") ;;
|
|
2689
|
+
esac
|
|
2690
|
+
done
|
|
2691
|
+
if [ "$_ps_resume" = 1 ]; then
|
|
2692
|
+
local _ps_rrc=0
|
|
2693
|
+
for _ps_a in ${_ps_resume_args[@]+"${_ps_resume_args[@]}"}; do
|
|
2694
|
+
case "$_ps_a" in
|
|
2695
|
+
e10-[A-Za-z0-9-]*) ;;
|
|
2696
|
+
*)
|
|
2697
|
+
echo -e "${RED}--resume resumes a sealed run; it takes an optional run id, not a task. Run \`loki start <task>\` for a new run.${NC}" >&2
|
|
2698
|
+
return 2 ;;
|
|
2699
|
+
esac
|
|
2700
|
+
done
|
|
2701
|
+
( _loki_bun_delegate engine10 resume ${_ps_resume_args[@]+"${_ps_resume_args[@]}"} ) || _ps_rrc=$?
|
|
2702
|
+
_loki_e10_resume_map "$_ps_rrc" || true
|
|
2703
|
+
return "$_LOKI_E10_RC"
|
|
2704
|
+
fi
|
|
2610
2705
|
set -- ${_ps_rest[@]+"${_ps_rest[@]}"}
|
|
2611
2706
|
|
|
2612
2707
|
local args=()
|
|
@@ -2667,7 +2762,7 @@ cmd_start() {
|
|
|
2667
2762
|
echo " Token from LOKI_REMOTE_TOKEN / LOKI_REMOTE_TOKEN_FILE"
|
|
2668
2763
|
echo " (never an argument). Env: LOKI_REMOTE_URL."
|
|
2669
2764
|
echo " --provider NAME AI provider: claude (default), codex, cline, aider, opencode"
|
|
2670
|
-
echo "
|
|
2765
|
+
echo " (model route: set LOKI_MODEL_ROUTE, for example ollama/qwen3-coder; no --model flag)"
|
|
2671
2766
|
echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
|
|
2672
2767
|
echo " --simple Force simple complexity tier (3 phases)"
|
|
2673
2768
|
echo " --complex Force complex complexity tier (8 phases)"
|
|
@@ -2712,6 +2807,7 @@ cmd_start() {
|
|
|
2712
2807
|
echo " --pr Worktree + auto-create PR"
|
|
2713
2808
|
echo " --ship Worktree + PR + auto-merge"
|
|
2714
2809
|
echo " --detach, -d Run in background (implies --worktree)"
|
|
2810
|
+
echo " --resume [RUN_ID] Resume an interrupted run at the stage it stopped in"
|
|
2715
2811
|
echo ""
|
|
2716
2812
|
echo "Preview without spending (any input mode):"
|
|
2717
2813
|
echo " loki plan <PRD> Analyze a spec WITHOUT executing: complexity,"
|
|
@@ -3016,7 +3112,7 @@ cmd_start() {
|
|
|
3016
3112
|
shift
|
|
3017
3113
|
;;
|
|
3018
3114
|
--mirofish)
|
|
3019
|
-
if
|
|
3115
|
+
if _loki_start_optval_consumes "${2:-}"; then
|
|
3020
3116
|
mirofish_url="$2"
|
|
3021
3117
|
shift 2
|
|
3022
3118
|
else
|
|
@@ -5061,25 +5157,35 @@ cmd_pause() {
|
|
|
5061
5157
|
}
|
|
5062
5158
|
|
|
5063
5159
|
# RUN-25 iter 22 (Wave D #3): `loki steer "<note>"` writes an operator directive
|
|
5064
|
-
#
|
|
5065
|
-
#
|
|
5066
|
-
#
|
|
5067
|
-
#
|
|
5068
|
-
#
|
|
5160
|
+
# to .loki/HUMAN_INPUT.md. engine10 reads it before the next plan or implement
|
|
5161
|
+
# session (default on; only LOKI_PROMPT_INJECTION=0/false opts out, steer.ts).
|
|
5162
|
+
# The legacy loop injects it only when LOKI_PROMPT_INJECTION=true.
|
|
5163
|
+
# Each steer is a numbered revision: `## Operator directive rev N (ts) actor=A source=S`.
|
|
5164
|
+
# engine10 (util/steer.ts) archives the file and appends the revisions to
|
|
5165
|
+
# .loki/intent-revisions.jsonl. Actor and source come from LOKI_STEER_ACTOR
|
|
5166
|
+
# (default: git user.name, then $USER) and LOKI_STEER_SOURCE (cli, chat or an
|
|
5167
|
+
# https comment URL; default cli). Both are reduced to a safe character set.
|
|
5069
5168
|
cmd_steer() {
|
|
5070
|
-
|
|
5071
|
-
|
|
5169
|
+
# "--" ends options so guidance beginning with "-" is data, not a flag
|
|
5170
|
+
local _eoo=0
|
|
5171
|
+
if [ "${1:-}" = "--" ]; then _eoo=1; shift; fi
|
|
5172
|
+
case "$_eoo:${1:-}" in
|
|
5173
|
+
1:?*) ;;
|
|
5174
|
+
*:--help|*:-h|*:help|*:"")
|
|
5072
5175
|
echo -e "${BOLD}Loki Mode -- steer the running build${NC}"
|
|
5073
5176
|
echo ""
|
|
5074
5177
|
echo "Usage: loki steer \"<guidance>\""
|
|
5075
5178
|
echo ""
|
|
5076
|
-
echo "Writes your guidance to .loki/HUMAN_INPUT.md
|
|
5077
|
-
echo "
|
|
5179
|
+
echo "Writes your guidance to .loki/HUMAN_INPUT.md as a numbered revision"
|
|
5180
|
+
echo "(rev N, actor, source). The Loki 10 engine applies it once, before the"
|
|
5181
|
+
echo "next plan or implement session, and records it in .loki/intent-revisions.jsonl."
|
|
5182
|
+
echo "A steer that arrives after the Wall (the frozen acceptance tests) was authored"
|
|
5183
|
+
echo "makes Seal refuse the run; rerun the task with the new intent included."
|
|
5078
5184
|
echo ""
|
|
5079
5185
|
echo " loki steer \"focus on the auth flow, skip the docs for now\""
|
|
5080
5186
|
echo ""
|
|
5081
|
-
echo "
|
|
5082
|
-
echo "
|
|
5187
|
+
echo "Opt out with LOKI_PROMPT_INJECTION=0. The legacy loop reads the note"
|
|
5188
|
+
echo "only when LOKI_PROMPT_INJECTION=true."
|
|
5083
5189
|
[ -z "${1:-}" ] && return 2
|
|
5084
5190
|
return 0
|
|
5085
5191
|
;;
|
|
@@ -5089,20 +5195,35 @@ cmd_steer() {
|
|
|
5089
5195
|
return 1
|
|
5090
5196
|
fi
|
|
5091
5197
|
local note="$*"
|
|
5092
|
-
|
|
5093
|
-
|
|
5198
|
+
local actor source rev pending archived
|
|
5199
|
+
actor="${LOKI_STEER_ACTOR:-$(git config user.name 2>/dev/null || true)}"
|
|
5200
|
+
actor="${actor:-${USER:-unknown}}"
|
|
5201
|
+
actor="$(printf '%s' "$actor" | tr -c 'A-Za-z0-9._@-' '-' | cut -c1-64)"
|
|
5202
|
+
source="$(printf '%s' "${LOKI_STEER_SOURCE:-cli}" | tr -c 'A-Za-z0-9:/._#?=&%-' '-' | cut -c1-300)"
|
|
5203
|
+
# rev = consumed revisions + steers still pending in the buffer + 1
|
|
5204
|
+
archived=0; pending=0
|
|
5205
|
+
[ -f "$LOKI_DIR/intent-revisions.jsonl" ] && archived="$(wc -l < "$LOKI_DIR/intent-revisions.jsonl" | tr -d ' ')"
|
|
5206
|
+
[ -f "$LOKI_DIR/HUMAN_INPUT.md" ] && pending="$(grep -c '^## Operator directive rev ' "$LOKI_DIR/HUMAN_INPUT.md" 2>/dev/null || true)"
|
|
5207
|
+
rev=$(( ${archived:-0} + ${pending:-0} + 1 ))
|
|
5208
|
+
# Append so multiple steers accumulate; the reader consumes and clears the
|
|
5209
|
+
# file, so this is the pending-directive buffer.
|
|
5094
5210
|
{
|
|
5095
|
-
printf '## Operator directive (%s)\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
5211
|
+
printf '## Operator directive rev %s (%s) actor=%s source=%s\n' "$rev" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$actor" "$source"
|
|
5096
5212
|
printf '%s\n\n' "$note"
|
|
5097
5213
|
} >> "$LOKI_DIR/HUMAN_INPUT.md"
|
|
5098
5214
|
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
|
-
|
|
5215
|
+
echo -e "${GREEN}Steering note (rev $rev) written to .loki/HUMAN_INPUT.md${NC}"
|
|
5216
|
+
case "${LOKI_PROMPT_INJECTION:-}" in
|
|
5217
|
+
0|false|FALSE|False)
|
|
5218
|
+
echo -e "${YELLOW}Note: LOKI_PROMPT_INJECTION is off, so the run will NOT read this.${NC}"
|
|
5219
|
+
;;
|
|
5220
|
+
*)
|
|
5221
|
+
echo -e "${DIM}The Loki 10 engine applies it before the next plan or implement session.${NC}"
|
|
5222
|
+
# Same text the issue comment ack posts (trigger-server.py steer_ack_text).
|
|
5223
|
+
echo "Steer rev $rev received: applies at next plan/implement stage."
|
|
5224
|
+
echo "If the Wall is already frozen, Seal will refuse this run: rerun with the new intent."
|
|
5225
|
+
;;
|
|
5226
|
+
esac
|
|
5106
5227
|
return 0
|
|
5107
5228
|
}
|
|
5108
5229
|
|
|
@@ -5263,10 +5384,7 @@ cmd_resume() {
|
|
|
5263
5384
|
if ls "$LOKI_DIR"/runs/e10-*/events.jsonl >/dev/null 2>&1; then
|
|
5264
5385
|
local _e10_rc=0
|
|
5265
5386
|
( _loki_bun_delegate engine10 resume "$@" ) || _e10_rc=$?
|
|
5266
|
-
|
|
5267
|
-
0 | 4) return 0 ;;
|
|
5268
|
-
5) return 1 ;;
|
|
5269
|
-
esac
|
|
5387
|
+
if _loki_e10_resume_map "$_e10_rc"; then return "$_LOKI_E10_RC"; fi
|
|
5270
5388
|
fi
|
|
5271
5389
|
|
|
5272
5390
|
# Check if there is anything to resume before touching signal files
|
|
@@ -6264,7 +6382,7 @@ if _unc and os.path.exists(_unc):
|
|
|
6264
6382
|
except Exception:
|
|
6265
6383
|
pass
|
|
6266
6384
|
print(" Unstick it : give the loop a concrete next step:")
|
|
6267
|
-
print(" loki steer \"<what to try next>\" (needs LOKI_PROMPT_INJECTION=
|
|
6385
|
+
print(" loki steer \"<what to try next>\" (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)")
|
|
6268
6386
|
print(" or narrow the spec / fix the blocker, then loki resume.")
|
|
6269
6387
|
|
|
6270
6388
|
# Surface the structured per-iteration error record, if one exists at
|
|
@@ -9771,7 +9889,7 @@ _deploy_latest_receipt() {
|
|
|
9771
9889
|
return 0
|
|
9772
9890
|
}
|
|
9773
9891
|
|
|
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]
|
|
9892
|
+
# _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
9893
|
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
|
|
9776
9894
|
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
|
|
9777
9895
|
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
|
|
@@ -9794,6 +9912,7 @@ _deploy_record() {
|
|
|
9794
9912
|
local health_status="${7:-unchecked}" health_url="${8:-}"
|
|
9795
9913
|
local rb_attempted="${9:-false}" rb_exit="${10:-}"
|
|
9796
9914
|
local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
|
|
9915
|
+
local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}"
|
|
9797
9916
|
local ddir="$dir/.loki/deploys"
|
|
9798
9917
|
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
9799
9918
|
mkdir -p "$ddir" 2>/dev/null || return 0
|
|
@@ -9801,7 +9920,7 @@ _deploy_record() {
|
|
|
9801
9920
|
DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
|
|
9802
9921
|
DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
|
|
9803
9922
|
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" \
|
|
9923
|
+
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
9924
|
python3 -c '
|
|
9806
9925
|
import json, os, datetime
|
|
9807
9926
|
state = os.environ.get("DR_STATE") or "anchored"
|
|
@@ -9827,6 +9946,10 @@ json.dump({
|
|
|
9827
9946
|
"health_url": os.environ.get("DR_HU") or "",
|
|
9828
9947
|
"rollback_attempted": os.environ.get("DR_RBA") == "true",
|
|
9829
9948
|
"rollback_exit": int(os.environ["DR_RBX"]) if os.environ.get("DR_RBX", "") != "" else None,
|
|
9949
|
+
"post_rollback_health": os.environ.get("DR_PRB") or "unchecked",
|
|
9950
|
+
"health_match": os.environ.get("DR_HM", ""),
|
|
9951
|
+
"tree_sha": os.environ.get("DR_TREE", ""),
|
|
9952
|
+
"health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
|
|
9830
9953
|
}, open(os.environ["DR_OUT"], "w"), indent=2)
|
|
9831
9954
|
' 2>/dev/null || return 0
|
|
9832
9955
|
# Sign through the one Bun implementation. A subshell, because the delegate execs.
|
|
@@ -9895,20 +10018,31 @@ except Exception:
|
|
|
9895
10018
|
return 0
|
|
9896
10019
|
}
|
|
9897
10020
|
|
|
9898
|
-
# _deploy_probe_health <url>
|
|
9899
|
-
# Bounded poll: echoes the last HTTP status, returns 0 on any 2xx.
|
|
10021
|
+
# _deploy_probe_health <url> [match]
|
|
10022
|
+
# Bounded poll: echoes the last HTTP status, returns 0 on any 2xx. With a match
|
|
10023
|
+
# string the 2xx body must also contain it (fixed-string), else the attempt fails
|
|
10024
|
+
# and the status is still echoed. Attempts and
|
|
9900
10025
|
# spacing are env-tunable (LOKI_DEPLOY_HEALTH_ATTEMPTS / _INTERVAL) for tests.
|
|
9901
10026
|
_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"
|
|
10027
|
+
local url="${1:-}" match="${2:-}" attempts="${LOKI_DEPLOY_HEALTH_ATTEMPTS:-5}" interval="${LOKI_DEPLOY_HEALTH_INTERVAL:-3}"
|
|
10028
|
+
local i=0 code="000" body="/dev/null"
|
|
10029
|
+
if [ -n "$match" ]; then body="$(mktemp 2>/dev/null)" || body="/dev/null"; fi
|
|
9904
10030
|
case "$attempts" in ''|*[!0-9]*) attempts=5 ;; esac
|
|
9905
10031
|
case "$interval" in ''|*[!0-9]*) interval=3 ;; esac
|
|
9906
10032
|
while [ "$i" -lt "$attempts" ]; do
|
|
9907
10033
|
i=$((i + 1))
|
|
9908
|
-
code="$(curl -s -o
|
|
9909
|
-
case "$code" in
|
|
10034
|
+
code="$(curl -s -o "$body" -w '%{http_code}' --max-time 10 "$url" 2>/dev/null)" || code="000"
|
|
10035
|
+
case "$code" in
|
|
10036
|
+
2??)
|
|
10037
|
+
if [ -z "$match" ] || grep -qF -- "$match" "$body" 2>/dev/null; then
|
|
10038
|
+
[ "$body" = /dev/null ] || rm -f "$body"
|
|
10039
|
+
printf '%s' "$code"; return 0
|
|
10040
|
+
fi
|
|
10041
|
+
;;
|
|
10042
|
+
esac
|
|
9910
10043
|
[ "$i" -lt "$attempts" ] && [ "$interval" -gt 0 ] && sleep "$interval"
|
|
9911
10044
|
done
|
|
10045
|
+
[ "$body" = /dev/null ] || rm -f "$body"
|
|
9912
10046
|
printf '%s' "$code"
|
|
9913
10047
|
return 1
|
|
9914
10048
|
}
|
|
@@ -9974,6 +10108,7 @@ _deploy_try_execute() {
|
|
|
9974
10108
|
local dir="${1:-.}" type="${2:-}"
|
|
9975
10109
|
# Set by cmd_deploy (dynamic scope); empty/false when called any other way.
|
|
9976
10110
|
local health_url="${_LOKI_DEPLOY_HEALTH_URL:-}" auto_rollback="${_LOKI_DEPLOY_AUTO_ROLLBACK:-false}"
|
|
10111
|
+
local health_match="${_LOKI_DEPLOY_HEALTH_MATCH:-}"
|
|
9977
10112
|
|
|
9978
10113
|
local command
|
|
9979
10114
|
command="$(_deploy_first_installed_command "$dir" "$type")"
|
|
@@ -10086,7 +10221,7 @@ _deploy_try_execute() {
|
|
|
10086
10221
|
|
|
10087
10222
|
local rc=0 started_at finished_at purl="" canary_out=""
|
|
10088
10223
|
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
10089
|
-
local health_status="unchecked" rb_attempted=false rb_exit="" hcode=""
|
|
10224
|
+
local health_status="unchecked" rb_attempted=false rb_exit="" hcode="" rb_hcode="" post_rb="unchecked"
|
|
10090
10225
|
if [ "$canary_kind" = vercel ]; then
|
|
10091
10226
|
# Staged: a preview deploy (no --prod), probe the preview URL, promote only if healthy.
|
|
10092
10227
|
# Nothing reaches production before the probe, so there is nothing to roll back.
|
|
@@ -10098,7 +10233,7 @@ _deploy_try_execute() {
|
|
|
10098
10233
|
if [ "$rc" -eq 0 ] && [ -n "$purl" ]; then
|
|
10099
10234
|
health_url="$purl"
|
|
10100
10235
|
echo "Probing canary health: ${health_url}"
|
|
10101
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10236
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10102
10237
|
health_status="healthy"
|
|
10103
10238
|
echo "Promoting: vercel promote ${purl}"
|
|
10104
10239
|
exec_cmd="vercel; vercel promote ${purl}"
|
|
@@ -10119,7 +10254,7 @@ _deploy_try_execute() {
|
|
|
10119
10254
|
# failed deploy, status is "unchecked": never a claim the service is healthy.
|
|
10120
10255
|
if [ "$canary_kind" != vercel ] && [ "$rc" -eq 0 ] && [ -n "$health_url" ]; then
|
|
10121
10256
|
echo "Probing health: ${health_url}"
|
|
10122
|
-
if hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10257
|
+
if hcode="$(_deploy_probe_health "$health_url" "$health_match")"; then
|
|
10123
10258
|
health_status="healthy"
|
|
10124
10259
|
else
|
|
10125
10260
|
health_status="FAILED_HEALTH"
|
|
@@ -10127,14 +10262,21 @@ _deploy_try_execute() {
|
|
|
10127
10262
|
fi
|
|
10128
10263
|
|
|
10129
10264
|
if [ "$health_status" = "FAILED_HEALTH" ]; then
|
|
10130
|
-
echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx (last status ${hcode:-000}).${NC}" >&2
|
|
10265
|
+
echo -e "${RED}FAILED_HEALTH: ${health_url} did not return 2xx${health_match:+ containing \"${health_match}\"} (last status ${hcode:-000}).${NC}" >&2
|
|
10131
10266
|
if [ "$auto_rollback" = true ] && [ -n "$rb_cmd" ] && ! _deploy_is_destructive "$rb_cmd"; then
|
|
10132
10267
|
echo "Rolling back: ${rb_cmd}" >&2
|
|
10133
10268
|
rb_attempted=true
|
|
10134
10269
|
rb_exit=0
|
|
10135
10270
|
( cd "$dir" && eval "$rb_cmd" ) || rb_exit=$?
|
|
10136
10271
|
if [ "$rb_exit" -eq 0 ]; then
|
|
10137
|
-
echo "Rollback command exited 0.
|
|
10272
|
+
echo "Rollback command exited 0. Re-checking health: ${health_url}" >&2
|
|
10273
|
+
if rb_hcode="$(_deploy_probe_health "$health_url")"; then
|
|
10274
|
+
post_rb="healthy"
|
|
10275
|
+
echo -e "${GREEN}Rolled-back service is healthy${NC} (HTTP ${rb_hcode})." >&2
|
|
10276
|
+
else
|
|
10277
|
+
post_rb="FAILED_HEALTH"
|
|
10278
|
+
echo -e "${RED}Rolled-back service is STILL unhealthy (last status ${rb_hcode:-000}).${NC}" >&2
|
|
10279
|
+
fi
|
|
10138
10280
|
else
|
|
10139
10281
|
echo -e "${RED}Rollback command exited ${rb_exit}.${NC}" >&2
|
|
10140
10282
|
fi
|
|
@@ -10150,7 +10292,7 @@ _deploy_try_execute() {
|
|
|
10150
10292
|
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
10293
|
rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
|
|
10152
10294
|
"$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
|
|
10153
|
-
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary")"
|
|
10295
|
+
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)")"
|
|
10154
10296
|
echo ""
|
|
10155
10297
|
if [ "$rc" -eq 0 ]; then
|
|
10156
10298
|
echo -e "${GREEN}Deploy command exited 0.${NC}"
|
|
@@ -10177,6 +10319,8 @@ _deploy_try_execute() {
|
|
|
10177
10319
|
fi
|
|
10178
10320
|
# A deploy that exited 0 but failed its health probe is a failed deploy.
|
|
10179
10321
|
if [ "$rc" -eq 0 ] && [ "$health_status" = "FAILED_HEALTH" ]; then
|
|
10322
|
+
# 5: the rollback ran and the service is still down.
|
|
10323
|
+
[ "$post_rb" = "FAILED_HEALTH" ] && return 5
|
|
10180
10324
|
return 4
|
|
10181
10325
|
fi
|
|
10182
10326
|
return "$rc"
|
|
@@ -10290,7 +10434,7 @@ cmd_deploy() {
|
|
|
10290
10434
|
# file alone weakens this gate.
|
|
10291
10435
|
local LOKI_DEPLOY_ALLOW_UNANCHORED=false
|
|
10292
10436
|
# 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
|
|
10437
|
+
local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false
|
|
10294
10438
|
|
|
10295
10439
|
# Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
|
|
10296
10440
|
while [ $# -gt 0 ]; do
|
|
@@ -10298,7 +10442,7 @@ cmd_deploy() {
|
|
|
10298
10442
|
--help|-h|help)
|
|
10299
10443
|
echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
|
|
10300
10444
|
echo ""
|
|
10301
|
-
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--health-url URL [--auto-rollback]]"
|
|
10445
|
+
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--health-url URL [--health-match STR] [--auto-rollback]]"
|
|
10302
10446
|
echo ""
|
|
10303
10447
|
echo "Detects your project type and your installed cloud CLI (and any"
|
|
10304
10448
|
echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
|
|
@@ -10359,11 +10503,14 @@ cmd_deploy() {
|
|
|
10359
10503
|
echo " non-2xx marks the deploy FAILED_HEALTH and exits 4."
|
|
10360
10504
|
echo " Without a URL, health is recorded as 'unchecked',"
|
|
10361
10505
|
echo " never as healthy."
|
|
10506
|
+
echo " --health-match S The 2xx body must also contain S (for example the HEAD sha"
|
|
10507
|
+
echo " or version), else FAILED_HEALTH. Needs --health-url."
|
|
10362
10508
|
echo " --auto-rollback On FAILED_HEALTH run the provider's documented rollback:"
|
|
10363
10509
|
echo " 'vercel rollback' or 'fly releases rollback <version"
|
|
10364
10510
|
echo " captured before the deploy>'. Netlify and Cloudflare"
|
|
10365
10511
|
echo " print a manual note. Needs --health-url. The rolled-back"
|
|
10366
|
-
echo " service is
|
|
10512
|
+
echo " service is re-probed and recorded as post_rollback_health;"
|
|
10513
|
+
echo " still unhealthy exits 5."
|
|
10367
10514
|
echo ""
|
|
10368
10515
|
echo "Staged rollout (only with --execute):"
|
|
10369
10516
|
echo " --canary Vercel: deploy without --prod, health-check the preview"
|
|
@@ -10402,6 +10549,10 @@ cmd_deploy() {
|
|
|
10402
10549
|
*) echo "loki deploy --health-url requires an http(s) URL" >&2; return 1 ;;
|
|
10403
10550
|
esac
|
|
10404
10551
|
;;
|
|
10552
|
+
--health-match)
|
|
10553
|
+
[ -n "${2:-}" ] && [ "${2#-}" = "$2" ] || { echo "loki deploy --health-match requires a string" >&2; return 1; }
|
|
10554
|
+
_LOKI_DEPLOY_HEALTH_MATCH="$2"; shift
|
|
10555
|
+
;;
|
|
10405
10556
|
--auto-rollback)
|
|
10406
10557
|
_LOKI_DEPLOY_AUTO_ROLLBACK=true
|
|
10407
10558
|
;;
|
|
@@ -10429,6 +10580,10 @@ cmd_deploy() {
|
|
|
10429
10580
|
echo "loki deploy --auto-rollback requires --health-url (no health signal, no rollback)" >&2
|
|
10430
10581
|
return 1
|
|
10431
10582
|
fi
|
|
10583
|
+
if [ -n "$_LOKI_DEPLOY_HEALTH_MATCH" ] && [ -z "$_LOKI_DEPLOY_HEALTH_URL" ]; then
|
|
10584
|
+
echo "loki deploy --health-match requires --health-url" >&2
|
|
10585
|
+
return 1
|
|
10586
|
+
fi
|
|
10432
10587
|
if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ "$do_execute" != true ]; then
|
|
10433
10588
|
echo "loki deploy --canary requires --execute" >&2
|
|
10434
10589
|
return 1
|
|
@@ -11492,7 +11647,7 @@ fi
|
|
|
11492
11647
|
# Post-completion: auto-merge if requested
|
|
11493
11648
|
if [[ "$LOKI_AUTO_MERGE" == "true" ]]; then
|
|
11494
11649
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11495
|
-
if
|
|
11650
|
+
if "$LOKI_CMD" merge watch "$branch_current" --require-verified --delete-branch; then
|
|
11496
11651
|
# A brief run carries a synthetic ref (b<epoch>), not a real issue: close only numeric refs.
|
|
11497
11652
|
if [[ "${LOKI_ISSUE_NUMBER:-}" =~ ^[0-9]+$ ]]; then
|
|
11498
11653
|
gh issue close "$LOKI_ISSUE_NUMBER" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -11710,7 +11865,8 @@ ${_gh_receipt}"
|
|
|
11710
11865
|
if command -v jq >/dev/null 2>&1; then
|
|
11711
11866
|
jq -n --arg url "$(printf '%s' "$_pr_out" | grep -Eo 'https://[^[:space:]]+' | head -1)" \
|
|
11712
11867
|
--arg branch "$branch_current" \
|
|
11713
|
-
'{state:"created", url:$url, branch:$branch
|
|
11868
|
+
'{state:"created", url:$url, branch:$branch,
|
|
11869
|
+
merge:{outcome:"waiting", gate:"human_review", checks:"not recorded"}}' \
|
|
11714
11870
|
> .loki/state/pr.json 2>/dev/null || true
|
|
11715
11871
|
fi
|
|
11716
11872
|
else
|
|
@@ -11744,7 +11900,7 @@ ${_gh_receipt}"
|
|
|
11744
11900
|
github|brief)
|
|
11745
11901
|
local branch_current
|
|
11746
11902
|
branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
11747
|
-
if
|
|
11903
|
+
if _merge_pr_watch "$branch_current" --require-verified --delete-branch; then
|
|
11748
11904
|
echo -e "${GREEN}PR merged and branch deleted${NC}"
|
|
11749
11905
|
if [[ "${number:-}" =~ ^[0-9]+$ ]]; then
|
|
11750
11906
|
gh issue close "$number" --comment "Resolved by Loki Mode" 2>/dev/null || true
|
|
@@ -17950,9 +18106,10 @@ Tasks:
|
|
|
17950
18106
|
# pre-caveman. No-op when caveman is absent.
|
|
17951
18107
|
local _loki_mig_cm=""
|
|
17952
18108
|
_loki_mig_cm="$(_loki_caveman_narration_level)"
|
|
18109
|
+
_loki_perm_argv_for "$codebase_path"
|
|
17953
18110
|
case "$provider_name" in
|
|
17954
18111
|
claude)
|
|
17955
|
-
{ (cd "$codebase_path" && if [ -n "$_loki_mig_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_mig_cm" claude
|
|
18112
|
+
{ (cd "$codebase_path" && if [ -n "$_loki_mig_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_mig_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; else claude "${_LOKI_PERM_ARGV[@]}" -p "$phase_prompt" --output-format stream-json --verbose 2>&1; fi) | \
|
|
17956
18113
|
while IFS= read -r line; do
|
|
17957
18114
|
# Extract text from stream-json
|
|
17958
18115
|
if echo "$line" | python3 -c "
|
|
@@ -18127,9 +18284,10 @@ IMPORTANT RULES:
|
|
|
18127
18284
|
# branch, byte-identical to pre-caveman (empty mode is NOT inert). No-op absent.
|
|
18128
18285
|
local _loki_doc_cm=""
|
|
18129
18286
|
_loki_doc_cm="$(_loki_caveman_narration_level)"
|
|
18287
|
+
_loki_perm_argv_for "$codebase_path"
|
|
18130
18288
|
case "$provider_name" in
|
|
18131
18289
|
claude)
|
|
18132
|
-
{ (cd "$codebase_path" && if [ -n "$_loki_doc_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_doc_cm" claude
|
|
18290
|
+
{ (cd "$codebase_path" && if [ -n "$_loki_doc_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_doc_cm" claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; else claude "${_LOKI_PERM_ARGV[@]}" -p "$doc_prompt" --output-format stream-json --verbose 2>&1; fi) | \
|
|
18133
18291
|
while IFS= read -r line; do
|
|
18134
18292
|
if echo "$line" | python3 -c "
|
|
18135
18293
|
import sys, json
|
|
@@ -18473,6 +18631,7 @@ cmd_modernize() {
|
|
|
18473
18631
|
echo "Subcommands (grouped modernization surface):"
|
|
18474
18632
|
echo " heal <path> Legacy system healing in phases (was: loki heal)"
|
|
18475
18633
|
echo " migrate <path> Codebase migration in phases (was: loki migrate)"
|
|
18634
|
+
echo " unit --repo <owner/name> --ref <ref> --unit <id> Run one unit (remote unit job worker)"
|
|
18476
18635
|
echo ""
|
|
18477
18636
|
echo "Run 'loki modernize <subcommand> --help' for each. The old"
|
|
18478
18637
|
echo "top-level names still work; see 'loki help aliases'."
|
|
@@ -19191,7 +19350,8 @@ Begin healing now. Follow the RARV cycle for each action."
|
|
|
19191
19350
|
_loki_heal_cm="$(_loki_caveman_narration_level)"
|
|
19192
19351
|
case "$provider" in
|
|
19193
19352
|
claude)
|
|
19194
|
-
|
|
19353
|
+
_loki_perm_argv_for "$codebase_path"
|
|
19354
|
+
local run_args=("${_LOKI_PERM_ARGV[@]}" -p "$heal_prompt" --output-format stream-json --verbose)
|
|
19195
19355
|
(cd "$codebase_path" && if [ -n "$_loki_heal_cm" ]; then CAVEMAN_DEFAULT_MODE="$_loki_heal_cm" claude "${run_args[@]}" 2>&1; else claude "${run_args[@]}" 2>&1; fi) | \
|
|
19196
19356
|
while IFS= read -r line; do
|
|
19197
19357
|
if echo "$line" | python3 -c "
|
|
@@ -25882,6 +26042,196 @@ SHOW_EOF
|
|
|
25882
26042
|
esac
|
|
25883
26043
|
}
|
|
25884
26044
|
|
|
26045
|
+
# Poll `gh pr checks` while pending (rc=8); returns the final rc (0 = green).
|
|
26046
|
+
_pr_checks_wait() {
|
|
26047
|
+
local pr="$1" poll="${LOKI_MERGE_POLL_S:-20}" tries="${LOKI_MERGE_MAX_POLLS:-90}" rc=8 i=0
|
|
26048
|
+
while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
|
|
26049
|
+
rc=0; gh pr checks "$pr" >/dev/null 2>&1 || rc=$?
|
|
26050
|
+
[ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
|
|
26051
|
+
done
|
|
26052
|
+
return "$rc"
|
|
26053
|
+
}
|
|
26054
|
+
|
|
26055
|
+
# CI repair loop. Waits for checks; on red, hands the failed-run log tail to one
|
|
26056
|
+
# bounded repair session on the PR branch (LOKI_PR_REPAIR_CMD, default: a
|
|
26057
|
+
# `loki start` of a generated brief; it receives the evidence file path as its last argument), pushes,
|
|
26058
|
+
# and re-checks, up to LOKI_PR_REPAIR_MAX attempts (default 2). Green goes through
|
|
26059
|
+
# _merge_pr_gated (extra args pass through). Exhausted: the PR is returned to
|
|
26060
|
+
# draft, nothing merges, and the last failure evidence is printed.
|
|
26061
|
+
# Usage: _merge_pr_watch <pr> [--require-verified] [--delete-branch]
|
|
26062
|
+
_merge_pr_watch() {
|
|
26063
|
+
local pr="$1" max="${LOKI_PR_REPAIR_MAX:-2}" attempt=0 rc branch cur rid evid tmp
|
|
26064
|
+
shift
|
|
26065
|
+
case "$max" in ''|*[!0-9]*) max=2 ;; esac
|
|
26066
|
+
command -v gh >/dev/null 2>&1 || { echo "$pr: not merged, gate: gh (gh CLI is required)" >&2; return 1; }
|
|
26067
|
+
branch="$(gh pr view "$pr" --json headRefName -q .headRefName 2>/dev/null)" || branch=""
|
|
26068
|
+
tmp="$(mktemp -d)" || return 1
|
|
26069
|
+
evid="$tmp/ci-failure.md"
|
|
26070
|
+
while true; do
|
|
26071
|
+
rc=0; _pr_checks_wait "$pr" || rc=$?
|
|
26072
|
+
if [ "$rc" = "0" ]; then
|
|
26073
|
+
rm -rf "$tmp"
|
|
26074
|
+
_merge_pr_gated "$pr" "$@"
|
|
26075
|
+
return $?
|
|
26076
|
+
fi
|
|
26077
|
+
rid="$(gh run list --branch "$branch" --status failure --limit 1 --json databaseId -q '.[0].databaseId' 2>/dev/null)" || rid=""
|
|
26078
|
+
{
|
|
26079
|
+
echo "# CI failed for PR $pr (branch ${branch:-unknown}); fix the failing checks, keep the change minimal"
|
|
26080
|
+
[ -n "$rid" ] && gh run view "$rid" --log-failed 2>/dev/null | tail -n 120
|
|
26081
|
+
} > "$evid"
|
|
26082
|
+
if [ "$attempt" -ge "$max" ]; then
|
|
26083
|
+
gh pr ready "$pr" --undo >/dev/null 2>&1 || true
|
|
26084
|
+
echo "$pr: not merged, gate: checks (gh pr checks rc=$rc after $attempt repair attempts); PR left as draft" >&2
|
|
26085
|
+
_pr_record_merge "$pr" "blocked:checks" "checks" "failing after $attempt repair attempts"
|
|
26086
|
+
echo "Last failure evidence:" >&2; sed 's/^/ /' "$evid" >&2
|
|
26087
|
+
rm -rf "$tmp"
|
|
26088
|
+
return 1
|
|
26089
|
+
fi
|
|
26090
|
+
attempt=$((attempt+1))
|
|
26091
|
+
cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
26092
|
+
if [ -z "$branch" ] || [ "$cur" != "$branch" ]; then
|
|
26093
|
+
echo "$pr: not merged, gate: repair (checkout is '${cur:-none}', repair must run on PR branch '${branch:-unknown}'); PR left open" >&2
|
|
26094
|
+
rm -rf "$tmp"; return 1
|
|
26095
|
+
fi
|
|
26096
|
+
echo "$pr: checks red, repair attempt $attempt of $max"
|
|
26097
|
+
if [ -n "${LOKI_PR_REPAIR_CMD:-}" ]; then
|
|
26098
|
+
bash -c "$LOKI_PR_REPAIR_CMD \"\$1\"" _ "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
|
|
26099
|
+
else
|
|
26100
|
+
"$(command -v loki || echo "$0")" start "$evid" || { echo "$pr: repair attempt $attempt failed, not pushing"; continue; }
|
|
26101
|
+
fi
|
|
26102
|
+
git push origin "HEAD:refs/heads/$branch" >/dev/null 2>&1 || echo "$pr: push of repair failed" >&2
|
|
26103
|
+
done
|
|
26104
|
+
}
|
|
26105
|
+
|
|
26106
|
+
# Record the merge gate's own result in .loki/state/pr.json (merged,
|
|
26107
|
+
# blocked:<gate> or waiting) so the receipt and handoff read the same outcome
|
|
26108
|
+
# the gate produced. Only updates an existing "created" record that describes
|
|
26109
|
+
# the same PR (pr arg matches .branch or the number at the end of .url); any
|
|
26110
|
+
# other record is left untouched. Never invents one. File mode is preserved.
|
|
26111
|
+
_pr_record_merge() {
|
|
26112
|
+
local pr="$1" outcome="$2" gate="${3:-}" checks="${4:-not recorded}" f=".loki/state/pr.json" t n
|
|
26113
|
+
n="${pr#\#}"
|
|
26114
|
+
case "$n" in */pull/*) n="${n%/}"; n="${n##*/}" ;; esac
|
|
26115
|
+
[ -f "$f" ] && command -v jq >/dev/null 2>&1 || return 0
|
|
26116
|
+
t="$(mktemp "$f.XXXXXX" 2>/dev/null)" || return 0
|
|
26117
|
+
cp -p "$f" "$t" 2>/dev/null || { rm -f "$t"; return 0; }
|
|
26118
|
+
if jq --arg p "$pr" --arg n "$n" --arg o "$outcome" --arg g "$gate" --arg c "$checks" \
|
|
26119
|
+
'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
|
|
26120
|
+
mv "$t" "$f"
|
|
26121
|
+
else
|
|
26122
|
+
rm -f "$t"
|
|
26123
|
+
fi
|
|
26124
|
+
return 0
|
|
26125
|
+
}
|
|
26126
|
+
|
|
26127
|
+
# Shared merge gate. One PR (number or branch) merges only when it is not a
|
|
26128
|
+
# draft, `gh pr checks` exits 0 after polling pending (rc=8), and, with
|
|
26129
|
+
# --require-verified, every clean proof recorded for the PR's head commit
|
|
26130
|
+
# (facts.git.head_sha == headRefOid, dirty-tree proofs ignored) says VERIFIED; a stale or missing proof
|
|
26131
|
+
# refuses ("gate: head"). The merge passes --match-head-commit so a push after
|
|
26132
|
+
# the gate cannot land unjudged. On refusal it prints the exact blocking gate
|
|
26133
|
+
# ("gate: draft|head|verdict|checks") and leaves the PR open.
|
|
26134
|
+
# Usage: _merge_pr_gated <pr> [--require-verified] [--delete-branch]
|
|
26135
|
+
_merge_pr_gated() {
|
|
26136
|
+
local pr="$1" need_verified=false margs=(--squash) rc d
|
|
26137
|
+
shift
|
|
26138
|
+
while [ $# -gt 0 ]; do
|
|
26139
|
+
case "$1" in
|
|
26140
|
+
--require-verified) need_verified=true ;;
|
|
26141
|
+
--delete-branch) margs+=(--delete-branch) ;;
|
|
26142
|
+
esac
|
|
26143
|
+
shift
|
|
26144
|
+
done
|
|
26145
|
+
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; }
|
|
26146
|
+
d="$(gh pr view "$pr" --json isDraft -q .isDraft 2>/dev/null)" || d=""
|
|
26147
|
+
if [ "$d" != "false" ]; then
|
|
26148
|
+
echo "$pr: not merged, gate: draft (PR is a draft or its state is unreadable); PR left open" >&2
|
|
26149
|
+
_pr_record_merge "$pr" "blocked:draft" "draft"
|
|
26150
|
+
return 1
|
|
26151
|
+
fi
|
|
26152
|
+
# Pin the merge to the exact head the checks and proof are about to judge.
|
|
26153
|
+
local head=""
|
|
26154
|
+
head="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head=""
|
|
26155
|
+
case "$head" in
|
|
26156
|
+
*[!0-9a-f]*) head="" ;;
|
|
26157
|
+
esac
|
|
26158
|
+
if [ "${#head}" != "40" ] && [ "${#head}" != "64" ]; then
|
|
26159
|
+
echo "$pr: not merged, gate: head (PR head commit is unreadable); PR left open" >&2
|
|
26160
|
+
_pr_record_merge "$pr" "blocked:head" "head"
|
|
26161
|
+
return 1
|
|
26162
|
+
fi
|
|
26163
|
+
if $need_verified; then
|
|
26164
|
+
local pid="" headline="" pj="" ph="" cands=() dirty_n=0 unknown_n=0
|
|
26165
|
+
# Resolve the proof recorded for THIS head: the run pointer first, then
|
|
26166
|
+
# any proof in .loki/proofs whose facts.git.head_sha equals the PR head.
|
|
26167
|
+
[ -f .loki/state/last-proof-id.txt ] && pid="$(cat .loki/state/last-proof-id.txt 2>/dev/null)"
|
|
26168
|
+
[ -z "$pid" ] || cands+=(".loki/proofs/${pid}/proof.json")
|
|
26169
|
+
cands+=(.loki/proofs/*/proof.json)
|
|
26170
|
+
if command -v jq >/dev/null 2>&1; then
|
|
26171
|
+
# Every clean proof for this head must agree: an older VERIFIED
|
|
26172
|
+
# never overrides a newer NOT_PROVEN. A proof taken on a dirty tree
|
|
26173
|
+
# (facts.git.dirty anything but literal false, incl. missing/null; .loki/ is excluded from that flag) judged
|
|
26174
|
+
# bytes other than the commit and is ignored.
|
|
26175
|
+
local hl="" disagree=false
|
|
26176
|
+
for pj in "${cands[@]}"; do
|
|
26177
|
+
[ -f "$pj" ] || continue
|
|
26178
|
+
ph="$(jq -r '.facts.git.head_sha // empty' "$pj" 2>/dev/null)"
|
|
26179
|
+
[ "$ph" = "$head" ] || continue
|
|
26180
|
+
case "$(jq -r 'if .facts.git.dirty == false then "clean" elif .facts.git.dirty == true then "dirty" else "unknown" end' "$pj" 2>/dev/null)" in
|
|
26181
|
+
clean) ;;
|
|
26182
|
+
dirty) dirty_n=$((dirty_n + 1)); continue ;;
|
|
26183
|
+
*) unknown_n=$((unknown_n + 1)); continue ;;
|
|
26184
|
+
esac
|
|
26185
|
+
hl="$(jq -r '.honesty.headline // empty' "$pj" 2>/dev/null)"
|
|
26186
|
+
[ -n "$hl" ] || hl="UNREADABLE"
|
|
26187
|
+
if [ -z "$headline" ]; then headline="$hl"; elif [ "$headline" != "$hl" ]; then disagree=true; fi
|
|
26188
|
+
done
|
|
26189
|
+
if $disagree; then
|
|
26190
|
+
echo "$pr: not merged, gate: verdict (proofs for PR head ${head:0:12} disagree); PR left open" >&2
|
|
26191
|
+
_pr_record_merge "$pr" "blocked:verdict" "verdict"
|
|
26192
|
+
return 1
|
|
26193
|
+
fi
|
|
26194
|
+
fi
|
|
26195
|
+
if [ -z "$headline" ]; then
|
|
26196
|
+
echo "$pr: not merged, gate: head (no clean proof recorded for PR head ${head:0:12}; proof is stale, dirty ($dirty_n), dirty-unknown ($unknown_n) or missing); PR left open" >&2
|
|
26197
|
+
if [ "$unknown_n" -gt 0 ]; then
|
|
26198
|
+
echo "$pr: $unknown_n proof(s) for this head have an unknown dirty state (the proof predates the dirty flag, or git could not read the tree). Commit or stash local changes, then re-run 'loki start ...' on a clean tree for this head to record a clean proof." >&2
|
|
26199
|
+
fi
|
|
26200
|
+
_pr_record_merge "$pr" "blocked:head" "head"
|
|
26201
|
+
return 1
|
|
26202
|
+
fi
|
|
26203
|
+
if [ "$headline" != "VERIFIED" ]; then
|
|
26204
|
+
echo "$pr: not merged, gate: verdict (run verdict is '${headline}', need VERIFIED); PR left open" >&2
|
|
26205
|
+
_pr_record_merge "$pr" "blocked:verdict" "verdict"
|
|
26206
|
+
return 1
|
|
26207
|
+
fi
|
|
26208
|
+
fi
|
|
26209
|
+
rc=0; _pr_checks_wait "$pr" || rc=$?
|
|
26210
|
+
if [ "$rc" != "0" ]; then
|
|
26211
|
+
echo "$pr: not merged, gate: checks (gh pr checks rc=$rc); PR left open" >&2
|
|
26212
|
+
case "$rc" in
|
|
26213
|
+
8) _pr_record_merge "$pr" "waiting" "checks" "pending" ;;
|
|
26214
|
+
1) _pr_record_merge "$pr" "blocked:checks" "checks" "failing" ;;
|
|
26215
|
+
*) _pr_record_merge "$pr" "blocked:checks" "checks" "unknown" ;;
|
|
26216
|
+
esac
|
|
26217
|
+
return 1
|
|
26218
|
+
fi
|
|
26219
|
+
if ! gh pr merge "$pr" "${margs[@]}" --match-head-commit "$head" >/dev/null 2>&1; then
|
|
26220
|
+
local head_now=""
|
|
26221
|
+
head_now="$(gh pr view "$pr" --json headRefOid -q .headRefOid 2>/dev/null)" || head_now=""
|
|
26222
|
+
if [ -n "$head_now" ] && [ "$head_now" != "$head" ]; then
|
|
26223
|
+
echo "$pr: not merged, gate: head (PR head moved from ${head:0:12} to ${head_now:0:12} during checks); PR left open" >&2
|
|
26224
|
+
_pr_record_merge "$pr" "blocked:head" "head" "passing"
|
|
26225
|
+
return 1
|
|
26226
|
+
fi
|
|
26227
|
+
echo "$pr: not merged, gate: merge (gh pr merge failed); PR left open" >&2
|
|
26228
|
+
_pr_record_merge "$pr" "blocked:merge" "merge" "passing"
|
|
26229
|
+
return 1
|
|
26230
|
+
fi
|
|
26231
|
+
_pr_record_merge "$pr" "merged" "" "all passing"
|
|
26232
|
+
return 0
|
|
26233
|
+
}
|
|
26234
|
+
|
|
25885
26235
|
# loki merge: serial PR merge queue. Queue lives in .loki/merge-queue (one PR
|
|
25886
26236
|
# number per line). A PR merges only when `gh pr checks` exits 0 (all green);
|
|
25887
26237
|
# after each merge the next PR is rebased onto the new base (gh pr update-branch
|
|
@@ -25916,8 +26266,7 @@ cmd_merge() {
|
|
|
25916
26266
|
[ "${1:-}" = "--dry-run" ] && dry=true
|
|
25917
26267
|
if [ ! -s "$qfile" ]; then echo "Merge queue is empty"; return 0; fi
|
|
25918
26268
|
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
|
|
26269
|
+
local first=true n rc remaining="" merged=0 blocked=0
|
|
25921
26270
|
while IFS= read -r n; do
|
|
25922
26271
|
[ -n "$n" ] || continue
|
|
25923
26272
|
if $dry; then
|
|
@@ -25936,29 +26285,33 @@ cmd_merge() {
|
|
|
25936
26285
|
|| { echo "#$n: rebase onto new base failed, left in queue"; remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue; }
|
|
25937
26286
|
echo "#$n: rebased onto new base"
|
|
25938
26287
|
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
|
|
26288
|
+
if _merge_pr_gated "$n" 2>"$qfile.err"; then
|
|
25949
26289
|
echo "#$n: merged"; merged=$((merged+1)); first=false
|
|
25950
26290
|
else
|
|
25951
|
-
echo "#$n:
|
|
26291
|
+
sed 's/^/#/' "$qfile.err"; echo "#$n: left in queue"
|
|
25952
26292
|
remaining="$remaining$n"$'\n'; blocked=$((blocked+1))
|
|
25953
26293
|
fi
|
|
25954
26294
|
done < "$qfile"
|
|
25955
26295
|
if $dry; then echo "Dry run: nothing changed"; return 0; fi
|
|
26296
|
+
rm -f "$qfile.err"
|
|
25956
26297
|
printf '%s' "$remaining" > "$qfile"
|
|
25957
26298
|
echo "Merged: $merged, left in queue: $blocked"
|
|
25958
26299
|
[ "$blocked" -eq 0 ]
|
|
25959
26300
|
;;
|
|
26301
|
+
watch)
|
|
26302
|
+
[ $# -gt 0 ] || { echo "Usage: loki merge watch <pr> [--require-verified] [--delete-branch]" >&2; return 2; }
|
|
26303
|
+
_merge_pr_watch "$@" && echo "$1: merged"
|
|
26304
|
+
;;
|
|
26305
|
+
pr)
|
|
26306
|
+
[ $# -gt 0 ] || { echo "Usage: loki merge pr <pr-or-branch> [--require-verified] [--delete-branch]" >&2; return 2; }
|
|
26307
|
+
_merge_pr_gated "$@" && echo "$1: merged"
|
|
26308
|
+
;;
|
|
25960
26309
|
help|--help|-h|*)
|
|
25961
|
-
echo "Usage: loki merge <add|list|run|clear>"
|
|
26310
|
+
echo "Usage: loki merge <add|list|run|pr|watch|clear>"
|
|
26311
|
+
echo " watch <pr> Wait for checks; on red run up to LOKI_PR_REPAIR_MAX (2) bounded"
|
|
26312
|
+
echo " repair sessions on the PR branch, push, re-check, merge when green."
|
|
26313
|
+
echo " pr <pr|branch> Merge one PR through the same gate (not draft, checks green;"
|
|
26314
|
+
echo " --require-verified also needs a VERIFIED run verdict)."
|
|
25962
26315
|
echo " add <pr>... Queue pull requests"
|
|
25963
26316
|
echo " list Show the queue"
|
|
25964
26317
|
echo " run [--dry-run] Merge one at a time, only when checks are green;"
|