loki-mode 11.3.9 → 11.5.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 +34 -18
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/agents/custom_agents.py +280 -0
- package/autonomy/council-v2.sh +22 -1
- package/autonomy/edit.sh +242 -0
- package/autonomy/lib/chat.sh +315 -0
- package/autonomy/lib/engine10-push.sh +19 -1
- package/autonomy/lib/help-core.sh +164 -0
- package/autonomy/lib/json-output.sh +38 -0
- package/autonomy/lib/proof-generator.py +23 -5
- package/autonomy/lib/proof-verify.py +607 -5
- package/autonomy/lib/run-summary.sh +112 -0
- package/autonomy/lib/setup-requirements.json +35 -0
- package/autonomy/loki +337 -46
- package/autonomy/provider-offer.sh +95 -47
- package/autonomy/route.sh +302 -0
- package/autonomy/run.sh +48 -14
- package/autonomy/setup-wizard.sh +170 -0
- package/autonomy/verify.sh +62 -3
- package/bin/loki +73 -18
- package/completions/_loki +58 -1
- package/completions/loki.bash +57 -5
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-CORE-VERBS.md +112 -0
- package/docs/CLI-REFERENCE.md +9 -1
- package/docs/COMPARE-OPENCODE.md +94 -0
- package/docs/COMPONENTS.md +1 -0
- package/docs/EVALUATING.md +27 -0
- package/docs/HARNESS-INTELLIGENCE-PLAN.md +20 -0
- package/docs/INSTALLATION.md +56 -12
- package/docs/KNOWN-ISSUES.md +8 -9
- package/docs/PROOF-PASSPORT.md +114 -0
- package/docs/QUICKSTART.md +108 -0
- package/docs/alternative-installations.md +103 -0
- package/docs/cost-controls.md +139 -0
- package/docs/environment-variables.md +45 -2
- package/docs/exit-codes.md +129 -12
- package/docs/spec-first-intent.md +29 -0
- package/docs/v10/CLI-MODERN.md +11 -5
- package/docs/v10/ENGINE.md +32 -0
- package/docs/v10/GUIDE.md +270 -44
- package/docs/v10/OPENCODE-PARITY-PLAN.md +84 -0
- package/loki-ts/data/model-pricing.json +1 -0
- package/loki-ts/dist/loki.js +1065 -978
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +1829 -715
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/plugins/loki-mode/README.md +41 -1
- package/plugins/loki-mode/commands/handoff.md +12 -0
- package/plugins/loki-mode/commands/loki-verify.md +9 -2
- package/plugins/loki-mode/commands/start.md +20 -0
- package/plugins/loki-mode/hooks/hooks.json +11 -0
- package/plugins/loki-mode/scripts/loki-stop-verify.sh +84 -0
- package/providers/claude.sh +16 -2
- package/providers/model_route_exec.sh +22 -0
- package/providers/model_routes.sh +188 -0
- package/skills/providers.md +13 -0
- package/templates/agents/planner.md +10 -0
- package/templates/agents/reviewer.md +10 -0
- package/templates/agents/tester.md +10 -0
- package/web-app/dist/assets/{AdminPage-QK-i_x-O.js → AdminPage-Cbu3pXL8.js} +1 -1
- package/web-app/dist/assets/{Avatar-DblkLNjB.js → Avatar-DGqGGcGx.js} +1 -1
- package/web-app/dist/assets/{Badge-DIV2ny5H.js → Badge-Bhlipt6v.js} +1 -1
- package/web-app/dist/assets/{Button--rePJOlQ.js → Button-CpN2sO61.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-BZlKxpGy.js → CockpitPage-BqeDJwdL.js} +1 -1
- package/web-app/dist/assets/{ComparePage-3_INSVux.js → ComparePage-DUieA2ba.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-DHG63CNr.js → ErrorBoundary-DTWsYrOm.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DFsBnLlO.js → EvidenceReceiptPanel-BU_B9X19.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-phPGah98.js → GitHubIssuesPanel-DEZoLOsP.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-T7KQqJmx.js → GitHubPRsPanel-BEsnrq7Y.js} +1 -1
- package/web-app/dist/assets/{HomePage-DFjGUF5I.js → HomePage-BoUVuq7h.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CorUid_5.js → LoginPage-CmOED7vb.js} +1 -1
- package/web-app/dist/assets/{MagicPage-DofbSxKD.js → MagicPage-DATZitrZ.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-qzrhhfLC.js → MetricsPage-DGL1v0I-.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BAZR0mMb.js → NotFoundPage-C2WgOjZd.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BOSUzG9J.js → ProjectPage-BMi1tepj.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-Ba0YLtzD.js → ProjectsPage-vc3y5YPh.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Cr10Ue2K.js → SettingsPage-kbq0Ftvi.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Czn655fe.js → ShowcasePage-Bn6Q8Kbg.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Dwjnw6Ob.js → SystemSettingsPage-Basmhs_b.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-D8nc69Be.js → TeamsPage-BTqOc4TR.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DHsaCDGv.js → TemplatesPage-2P9002VL.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DBejni2a.js → TerminalOutput-CAoSPBrg.js} +1 -1
- package/web-app/dist/assets/{activity-B2VsBl1C.js → activity-BcWVWCqU.js} +1 -1
- package/web-app/dist/assets/{bell-CT77Fq2H.js → bell-CYRqqgZ6.js} +1 -1
- package/web-app/dist/assets/{bot-DfaWgF4H.js → bot-CU7gdn3g.js} +1 -1
- package/web-app/dist/assets/{check-D1yp7Ndv.js → check-DLlCpz9V.js} +1 -1
- package/web-app/dist/assets/{chevron-left-B6Pa8IOX.js → chevron-left-CE9Cb30C.js} +1 -1
- package/web-app/dist/assets/{circle-alert-D5jrL_Y7.js → circle-alert-uou4bjua.js} +1 -1
- package/web-app/dist/assets/{clock-DIsY_fa_.js → clock-C_dSJcDX.js} +1 -1
- package/web-app/dist/assets/{cloud-eGwLh1As.js → cloud-DZ22ITF6.js} +1 -1
- package/web-app/dist/assets/{code-xml-CGDIcZ3m.js → code-xml-BrJS0hhS.js} +1 -1
- package/web-app/dist/assets/{database-CWpdaykI.js → database-D2HOmxtl.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DZEuTCs6.js → dollar-sign-CQ1vK_6q.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-BHBW6m9v.js → file-code-corner-w2vpGm5d.js} +1 -1
- package/web-app/dist/assets/{file-plus-D4h1jZSR.js → file-plus-DlF6wjkD.js} +1 -1
- package/web-app/dist/assets/{globe-CXdqKgwd.js → globe-Dy3lBJg1.js} +1 -1
- package/web-app/dist/assets/{hammer-B3YuuYI_.js → hammer-C3xIiRXN.js} +1 -1
- package/web-app/dist/assets/{index-BLEnboSv.js → index-4t-_IW2y.js} +3 -3
- package/web-app/dist/assets/{layers-Dvp5nFtw.js → layers-Y8Fs8qXI.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DodDmIZ3.js → loader-circle-CxgN1v4m.js} +1 -1
- package/web-app/dist/assets/{lock-EiuaH1Ep.js → lock-DFFNbXDs.js} +1 -1
- package/web-app/dist/assets/{package-Cx4m2hFg.js → package-5e6qCTUg.js} +1 -1
- package/web-app/dist/assets/{plus-D-Sd5rpH.js → plus-C0Go_ytj.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-C0w1cdmF.js → refresh-cw-D4i-NW3o.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-C_qk6MTT.js → rotate-ccw-YlKkiUW6.js} +1 -1
- package/web-app/dist/assets/{scroll-text-DSxiFtkc.js → scroll-text-4qXOHTap.js} +1 -1
- package/web-app/dist/assets/{server-NTJt7I8M.js → server-BB6zXNp9.js} +1 -1
- package/web-app/dist/assets/{shield-alert-HBxMQOvH.js → shield-alert-D4tzjB1H.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-DEgsHzSD.js → thumbs-up-DZiRwdFj.js} +1 -1
- package/web-app/dist/assets/{trash-2-px46FnTD.js → trash-2-Dv_LkRIK.js} +1 -1
- package/web-app/dist/assets/{trending-down-CBosVGKa.js → trending-down-BJYp9PQe.js} +1 -1
- package/web-app/dist/assets/{trending-up-Dbd7zrQj.js → trending-up-BTcXLklK.js} +1 -1
- package/web-app/dist/assets/{upload-nhdFKAs1.js → upload-fcHqwOxr.js} +1 -1
- package/web-app/dist/assets/{usePolling-BgaGdinh.js → usePolling-BeJYw_4L.js} +1 -1
- package/web-app/dist/assets/{user-CzIihUy9.js → user-BcDwUCsO.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/edit.sh
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# loki edit: the lightest path. One provider pass on the current repo.
|
|
3
|
+
# No PRD, no plan stage, no council, no PR, no receipt (unless asked), no branch switching.
|
|
4
|
+
# Functions-only file, sourced by autonomy/loki. Claude Code is the default provider.
|
|
5
|
+
# Exit codes: 0 done, 1 provider or --verify failure, 2 usage or setup error.
|
|
6
|
+
|
|
7
|
+
_loki_edit_usage() {
|
|
8
|
+
echo "Usage: loki edit [options] [path ...] \"<change>\""
|
|
9
|
+
echo ""
|
|
10
|
+
echo "One provider pass on the current repo. No PRD, plan, council, PR or branch switch."
|
|
11
|
+
echo "Prints git diff --stat, the diff (paged on a TTY), then cost and elapsed time."
|
|
12
|
+
echo ""
|
|
13
|
+
echo "Examples:"
|
|
14
|
+
echo " loki edit \"rename fooBar to foo_bar in the config loader\""
|
|
15
|
+
echo " loki edit src/foo.ts \"rename X to Y\""
|
|
16
|
+
echo " loki edit --verify --commit src/a.ts src/b.ts \"add null checks\""
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Options:"
|
|
19
|
+
echo " --verify Run the fast verify (loki verify --fast) after the edit"
|
|
20
|
+
echo " --receipt Write an unsigned edit receipt to .loki/edit-receipts/"
|
|
21
|
+
echo " --commit Commit the result with a generated message (stages only touched files)"
|
|
22
|
+
echo " --dry-run Plan only: read-only pass, no files are changed"
|
|
23
|
+
echo " --quiet, -q Print only the run summary block"
|
|
24
|
+
echo " --json Print only the run summary as one JSON object"
|
|
25
|
+
echo " --help, -h Show this help"
|
|
26
|
+
echo ""
|
|
27
|
+
echo "Leading arguments that are existing files or directories scope the edit to them."
|
|
28
|
+
echo "Environment: LOKI_EDIT_MODEL overrides the model."
|
|
29
|
+
echo "Exit codes: 0 done, 1 provider or verify failure, 2 usage or setup error."
|
|
30
|
+
echo ""
|
|
31
|
+
echo "edit = one pass, no loop. quick = small task with the verify loop (loki quick)."
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Run the edit, then print the unified run summary (autonomy/lib/run-summary.sh).
|
|
35
|
+
# --quiet hides the diff and per-step lines; --json prints only the JSON summary.
|
|
36
|
+
cmd_edit() {
|
|
37
|
+
local _q=false _j=false _a _t0 _sha _rc _lib _out _cost
|
|
38
|
+
for _a in "$@"; do
|
|
39
|
+
case "$_a" in --quiet|-q) _q=true ;; --json) _j=true ;; --) break ;; esac
|
|
40
|
+
done
|
|
41
|
+
case " $* " in *" --help "*|*" -h "*) _cmd_edit_run "$@"; return $? ;; esac
|
|
42
|
+
_t0="$(date +%s)"
|
|
43
|
+
_sha="$(git rev-parse HEAD 2>/dev/null || true)"
|
|
44
|
+
_LOKI_EDIT_COST_FILE="$(mktemp 2>/dev/null || true)"
|
|
45
|
+
export _LOKI_EDIT_COST_FILE
|
|
46
|
+
if [ "$_q" = true ] || [ "$_j" = true ]; then
|
|
47
|
+
_out="$(_cmd_edit_run "$@" 2>&1)"; _rc=$?
|
|
48
|
+
# Failures and usage errors stay visible even when quiet.
|
|
49
|
+
if [ "$_rc" -ne 0 ]; then printf '%s\n' "$_out" >&2; fi
|
|
50
|
+
else
|
|
51
|
+
_cmd_edit_run "$@"; _rc=$?
|
|
52
|
+
fi
|
|
53
|
+
_cost=""
|
|
54
|
+
if [ -n "${_LOKI_EDIT_COST_FILE:-}" ]; then
|
|
55
|
+
[ -s "$_LOKI_EDIT_COST_FILE" ] && _cost="$(head -n1 "$_LOKI_EDIT_COST_FILE")"
|
|
56
|
+
rm -f "$_LOKI_EDIT_COST_FILE"
|
|
57
|
+
fi
|
|
58
|
+
unset _LOKI_EDIT_COST_FILE
|
|
59
|
+
# Usage and setup errors (exit 2) never ran a pass: no summary.
|
|
60
|
+
if [ "$_rc" -ne 2 ]; then
|
|
61
|
+
_lib="${_LOKI_SCRIPT_DIR:-$(dirname "${BASH_SOURCE[0]}")}/lib/run-summary.sh"
|
|
62
|
+
if [ -f "$_lib" ]; then
|
|
63
|
+
# shellcheck source=lib/run-summary.sh
|
|
64
|
+
source "$_lib" 2>/dev/null || true
|
|
65
|
+
if command -v loki_run_summary >/dev/null 2>&1; then
|
|
66
|
+
_LOKI_SUMMARY_PRINTED=0
|
|
67
|
+
LOKI_SUMMARY_WORKTREE=1 LOKI_SUMMARY_COST="$_cost" SESSION_START_EPOCH="$_t0" \
|
|
68
|
+
LOKI_SUMMARY_JSON="$([ "$_j" = true ] && echo 1 || echo 0)" \
|
|
69
|
+
TARGET_DIR="." loki_run_summary "$_rc" "$_sha" || true
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
return "$_rc"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_cmd_edit_run() {
|
|
77
|
+
local do_verify=false do_receipt=false do_commit=false dry=false
|
|
78
|
+
local paths=() change=""
|
|
79
|
+
local a
|
|
80
|
+
while [ $# -gt 0 ]; do
|
|
81
|
+
a="$1"
|
|
82
|
+
case "$a" in
|
|
83
|
+
--help|-h|help) _loki_edit_usage; return 0 ;;
|
|
84
|
+
--verify) do_verify=true ;;
|
|
85
|
+
--receipt) do_receipt=true ;;
|
|
86
|
+
--commit) do_commit=true ;;
|
|
87
|
+
--dry-run) dry=true ;;
|
|
88
|
+
--quiet|-q|--json) ;;
|
|
89
|
+
--) shift; change="$*"; break ;;
|
|
90
|
+
-*) echo "loki edit: unknown option: $a" >&2; _loki_edit_usage >&2; return 2 ;;
|
|
91
|
+
*)
|
|
92
|
+
if [ $# -gt 1 ] && [ -e "$a" ]; then
|
|
93
|
+
paths+=("$a")
|
|
94
|
+
else
|
|
95
|
+
change="$*"
|
|
96
|
+
break
|
|
97
|
+
fi
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
shift
|
|
101
|
+
done
|
|
102
|
+
if [ -z "$change" ]; then
|
|
103
|
+
echo "loki edit: no change description given" >&2
|
|
104
|
+
_loki_edit_usage >&2
|
|
105
|
+
return 2
|
|
106
|
+
fi
|
|
107
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
108
|
+
echo "loki edit: not inside a git repository" >&2
|
|
109
|
+
return 2
|
|
110
|
+
fi
|
|
111
|
+
local prov="${LOKI_PROVIDER:-claude}"
|
|
112
|
+
if [ "$prov" != "claude" ]; then
|
|
113
|
+
echo "loki edit: provider '$prov' is not supported by the one-pass path; use loki quick, or unset LOKI_PROVIDER" >&2
|
|
114
|
+
return 2
|
|
115
|
+
fi
|
|
116
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
117
|
+
echo "loki edit: claude CLI not found (run loki doctor)" >&2
|
|
118
|
+
return 2
|
|
119
|
+
fi
|
|
120
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
121
|
+
echo "loki edit: python3 is required" >&2
|
|
122
|
+
return 2
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
local before_head before_status
|
|
126
|
+
before_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
127
|
+
before_status="$(git status --porcelain)"
|
|
128
|
+
|
|
129
|
+
local prompt scope=""
|
|
130
|
+
if [ "${#paths[@]}" -gt 0 ]; then
|
|
131
|
+
scope=" Limit changes to these paths: ${paths[*]}."
|
|
132
|
+
fi
|
|
133
|
+
if [ "$dry" = true ]; then
|
|
134
|
+
prompt="Plan only, do not modify any file. Describe the exact edits (file, location, change) needed for: ${change}.${scope}"
|
|
135
|
+
else
|
|
136
|
+
prompt="Make this change in the current repository with the smallest correct edit: ${change}.${scope} Do not create branches, commits, plans or documents. Reply with one short sentence."
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
local args=(-p "$prompt" --output-format json)
|
|
140
|
+
if [ "$dry" = true ]; then
|
|
141
|
+
args+=(--permission-mode plan)
|
|
142
|
+
else
|
|
143
|
+
args+=(--permission-mode acceptEdits --allowedTools "Read Edit Write Glob Grep")
|
|
144
|
+
fi
|
|
145
|
+
[ -n "${LOKI_EDIT_MODEL:-}" ] && args+=(--model "$LOKI_EDIT_MODEL")
|
|
146
|
+
|
|
147
|
+
local start_s out rc
|
|
148
|
+
start_s="$(date +%s)"
|
|
149
|
+
out="$(LOKI_NO_BROWSER=1 claude "${args[@]}" 2>&1)"
|
|
150
|
+
rc=$?
|
|
151
|
+
local elapsed=$(( $(date +%s) - start_s ))
|
|
152
|
+
|
|
153
|
+
local parsed
|
|
154
|
+
parsed="$(printf '%s' "$out" | python3 -c '
|
|
155
|
+
import json,sys
|
|
156
|
+
raw=sys.stdin.read()
|
|
157
|
+
try:
|
|
158
|
+
d=json.loads(raw)
|
|
159
|
+
except Exception:
|
|
160
|
+
print("0|1|"+raw.strip()[:400].replace("\n"," ")); sys.exit(0)
|
|
161
|
+
c=d.get("total_cost_usd",d.get("cost_usd",0)) or 0
|
|
162
|
+
err=1 if d.get("is_error") else 0
|
|
163
|
+
print("%s|%s|%s" % (c,err,(d.get("result") or "").strip().replace("\n"," ")[:400]))
|
|
164
|
+
' 2>/dev/null)"
|
|
165
|
+
local cost="0" iserr="1" msg="" rest=""
|
|
166
|
+
case "$parsed" in
|
|
167
|
+
*'|'*'|'*) cost="${parsed%%|*}"; rest="${parsed#*|}"; iserr="${rest%%|*}"; msg="${rest#*|}" ;;
|
|
168
|
+
*) msg="$parsed" ;;
|
|
169
|
+
esac
|
|
170
|
+
|
|
171
|
+
[ -n "${_LOKI_EDIT_COST_FILE:-}" ] && printf '%s\n' "$cost" > "$_LOKI_EDIT_COST_FILE" 2>/dev/null
|
|
172
|
+
if [ "$rc" -ne 0 ] || [ "$iserr" = "1" ]; then
|
|
173
|
+
echo "loki edit: provider pass failed (exit $rc)" >&2
|
|
174
|
+
[ -n "$msg" ] && echo "$msg" >&2
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
if [ "$dry" = true ]; then
|
|
179
|
+
[ -n "$msg" ] && echo "$msg"
|
|
180
|
+
printf 'dry-run: no files changed | cost $%s | %ss\n' "$cost" "$elapsed"
|
|
181
|
+
return 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Files touched by this pass: status delta against the pre-run snapshot.
|
|
185
|
+
local touched
|
|
186
|
+
touched="$(git status --porcelain | grep -vxF -f <(printf '%s\n' "$before_status") 2>/dev/null | sed 's/^...//' || true)"
|
|
187
|
+
[ -n "$touched" ] || touched="$(git diff --name-only)"
|
|
188
|
+
|
|
189
|
+
if [ -z "$touched" ]; then
|
|
190
|
+
echo "No changes made. ${msg}"
|
|
191
|
+
printf 'cost $%s | %ss\n' "$cost" "$elapsed"
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
git --no-pager diff --stat
|
|
196
|
+
if [ -t 1 ]; then git diff; else git --no-pager diff; fi
|
|
197
|
+
[ -n "$msg" ] && echo "$msg"
|
|
198
|
+
|
|
199
|
+
local vrc=0
|
|
200
|
+
if [ "$do_verify" = true ]; then
|
|
201
|
+
local fv="${_LOKI_SCRIPT_DIR:-$(dirname "${BASH_SOURCE[0]}")}/lib/fast_verify.py"
|
|
202
|
+
if [ -f "$fv" ]; then
|
|
203
|
+
python3 "$fv" || vrc=$?
|
|
204
|
+
else
|
|
205
|
+
echo "loki edit: fast_verify not found at $fv" >&2
|
|
206
|
+
vrc=2
|
|
207
|
+
fi
|
|
208
|
+
if [ "$vrc" -eq 0 ]; then echo "verify: pass"; else echo "verify: FAILED (exit $vrc)" >&2; fi
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
if [ "$do_receipt" = true ]; then
|
|
212
|
+
local rdir=".loki/edit-receipts" ts rf
|
|
213
|
+
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
214
|
+
mkdir -p "$rdir"
|
|
215
|
+
rf="$rdir/edit-$ts.json"
|
|
216
|
+
git diff | python3 -c '
|
|
217
|
+
import hashlib,json,sys
|
|
218
|
+
d=sys.stdin.buffer.read()
|
|
219
|
+
json.dump({"kind":"edit-receipt","signed":False,"task":sys.argv[1],"base":sys.argv[2],"diff_sha256":hashlib.sha256(d).hexdigest(),"cost_usd":float(sys.argv[3] or 0),"elapsed_s":int(sys.argv[4]),"verify_exit":int(sys.argv[5])},open(sys.argv[6],"w"),indent=2)
|
|
220
|
+
' "$change" "$before_head" "$cost" "$elapsed" "$vrc" "$rf"
|
|
221
|
+
echo "receipt: $rf (unsigned)"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
if [ "$do_commit" = true ]; then
|
|
225
|
+
if [ "$vrc" -ne 0 ]; then
|
|
226
|
+
echo "loki edit: not committing because verify failed" >&2
|
|
227
|
+
else
|
|
228
|
+
local subj f
|
|
229
|
+
subj="$(printf '%s' "$change" | tr '\n' ' ' | cut -c1-68)"
|
|
230
|
+
while IFS= read -r f; do
|
|
231
|
+
[ -n "$f" ] && git add -- "$f"
|
|
232
|
+
done <<< "$touched"
|
|
233
|
+
if git commit -q -m "edit: $subj" -m "Generated by loki edit."; then
|
|
234
|
+
echo "committed: $(git rev-parse --short HEAD)"
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
printf 'cost $%s | %ss\n' "$cost" "$elapsed"
|
|
240
|
+
[ "$vrc" -eq 0 ] || return 1
|
|
241
|
+
return 0
|
|
242
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# loki chat: interactive Plan/Build REPL. Claude Code is the default provider.
|
|
3
|
+
# Sourced by autonomy/loki; defines cmd_chat. Free text goes to the provider
|
|
4
|
+
# in the current mode; slash commands delegate to existing loki commands.
|
|
5
|
+
|
|
6
|
+
_LOKI_CHAT_SYSTEM_PROMPT="You are running inside Loki Mode chat. Be concise. Prefer small, verifiable changes. Loki commands available to the user: loki start, loki quick, loki verify, loki receipt, loki undo."
|
|
7
|
+
|
|
8
|
+
_chat_loki_bin() {
|
|
9
|
+
local d="${_LOKI_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
10
|
+
if [ -x "$d/../bin/loki" ]; then echo "$d/../bin/loki"
|
|
11
|
+
elif command -v loki >/dev/null 2>&1; then command -v loki
|
|
12
|
+
else echo "$d/loki"; fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_chat_help() {
|
|
16
|
+
cat <<'HLP'
|
|
17
|
+
Loki chat commands:
|
|
18
|
+
/plan read-only mode (no edits)
|
|
19
|
+
/build edit mode
|
|
20
|
+
/run <prd|brief> loki start with this session's model and provider (same run as the shell)
|
|
21
|
+
/attach attach to the current or most recent loki start run in this repo
|
|
22
|
+
/quick <task> loki quick
|
|
23
|
+
/edit <change> apply one change (build mode for this turn)
|
|
24
|
+
/verify loki verify
|
|
25
|
+
/receipt loki receipt
|
|
26
|
+
/model <id> set model for this session
|
|
27
|
+
/provider <name> switch provider (claude, codex, cline, aider, opencode)
|
|
28
|
+
/cost loki cost
|
|
29
|
+
/diff git diff --stat plus diff
|
|
30
|
+
/undo [args] loki undo
|
|
31
|
+
/help, /exit
|
|
32
|
+
Attached mode (loki chat --attach or /attach): /status /steer <msg> /diff /verify /receipt /stop
|
|
33
|
+
/detach returns to normal chat. Free text asks the provider about the run (state and log tail as context).
|
|
34
|
+
Anything else is sent to the current provider in the current mode.
|
|
35
|
+
HLP
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Run a delegate without letting a non-zero exit end the session (autonomy/loki runs under set -e).
|
|
40
|
+
_chat_do() { local _rc=0; "$@" || _rc=$?; [ "$_rc" -eq 0 ] || echo "(exit $_rc)"; return 0; }
|
|
41
|
+
|
|
42
|
+
# Newest loki start run in this repo: engine10 run dir first, then legacy session state.
|
|
43
|
+
_chat_run_dir() {
|
|
44
|
+
local root="${LOKI_DIR:-.loki}" d newest=""
|
|
45
|
+
for d in "$root"/runs/e10-*/; do [ -d "$d" ] && newest="${d%/}"; done
|
|
46
|
+
echo "$newest"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# True when the pid file at $1 holds a live pid (plain number or run.pid JSON).
|
|
50
|
+
_chat_pidfile_live() {
|
|
51
|
+
local f="$1" pid
|
|
52
|
+
[ -f "$f" ] || return 1
|
|
53
|
+
pid="$(head -n1 "$f" 2>/dev/null | tr -d '[:space:]')"
|
|
54
|
+
case "$pid" in
|
|
55
|
+
'{'*) pid="$(printf '%s' "$pid" | sed -n 's/.*"pid":\([0-9][0-9]*\).*/\1/p')" ;;
|
|
56
|
+
esac
|
|
57
|
+
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
|
58
|
+
kill -0 "$pid" 2>/dev/null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Same sources loki status reads (list_running_sessions: loki.pid, sessions/*/loki.pid,
|
|
62
|
+
# run-*.pid; PAUSE/STOP signals; session.json) plus the engine10 run.pid the supervisor
|
|
63
|
+
# writes in the run dir and removes on every exit path. No events.jsonl guessing.
|
|
64
|
+
_chat_run_state() {
|
|
65
|
+
local root="${LOKI_DIR:-.loki}" rd="$1" f st="" live=0
|
|
66
|
+
if [ -n "$rd" ] && _chat_pidfile_live "$rd/run.pid"; then live=1; fi
|
|
67
|
+
if _chat_pidfile_live "$root/loki.pid"; then live=1; fi
|
|
68
|
+
for f in "$root"/sessions/*/loki.pid "$root"/run-*.pid; do
|
|
69
|
+
if _chat_pidfile_live "$f"; then live=1; fi
|
|
70
|
+
done
|
|
71
|
+
if [ "$live" = "1" ]; then
|
|
72
|
+
if [ -f "$root/PAUSE" ]; then echo paused; else echo running; fi
|
|
73
|
+
return
|
|
74
|
+
fi
|
|
75
|
+
if [ -f "$root/STOP" ] || [ -f "$root/STOPPED" ]; then echo stopped; return; fi
|
|
76
|
+
if [ -f "$root/session.json" ]; then
|
|
77
|
+
st="$(sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$root/session.json" 2>/dev/null | head -n1)"
|
|
78
|
+
case "$st" in running|paused|stopped) echo "$st (no live process)"; return ;; esac
|
|
79
|
+
fi
|
|
80
|
+
echo finished
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Context block for free-text questions in attached mode: state plus the log tail.
|
|
84
|
+
_chat_attach_context() {
|
|
85
|
+
local root="${LOKI_DIR:-.loki}" rd lf
|
|
86
|
+
rd="$(_chat_run_dir)"
|
|
87
|
+
printf 'Run state: %s\n' "$(_chat_run_state "$rd")"
|
|
88
|
+
[ -n "$rd" ] && printf 'Run dir: %s\n' "$rd"
|
|
89
|
+
[ -f "$root/state/provider" ] && printf 'Provider: %s\n' "$(head -n1 "$root/state/provider" 2>/dev/null)"
|
|
90
|
+
if [ -n "$rd" ] && [ -f "$rd/events.jsonl" ]; then
|
|
91
|
+
printf 'Last events:\n'; tail -n 20 "$rd/events.jsonl" 2>/dev/null | cut -c1-300
|
|
92
|
+
fi
|
|
93
|
+
# shellcheck disable=SC2012
|
|
94
|
+
lf="$(ls -t "$root"/logs/*.log 2>/dev/null | head -n1 || true)"
|
|
95
|
+
if [ -n "$lf" ]; then printf 'Log tail (%s):\n' "$lf"; tail -n 40 "$lf" 2>/dev/null | cut -c1-300; fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_chat_attach_banner() {
|
|
99
|
+
local rd; rd="$(_chat_run_dir)"
|
|
100
|
+
if [ ! -d "${LOKI_DIR:-.loki}" ]; then
|
|
101
|
+
echo "No .loki state here. Start a run with: loki start <prd|brief>"; return 1
|
|
102
|
+
fi
|
|
103
|
+
echo "Attached to ${rd:+$(basename "$rd")}${rd:-the current session} ($(_chat_run_state "$rd")). /status /steer /diff /verify /receipt /stop /detach"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# _chat_send <mode> <text>: one turn against the active provider.
|
|
107
|
+
_chat_send() {
|
|
108
|
+
local mode="$1" text="$2"
|
|
109
|
+
if [ "$_CHAT_PROVIDER" = "claude" ]; then
|
|
110
|
+
command -v claude >/dev/null 2>&1 || { echo "loki chat: claude CLI not found. Fix (same as loki doctor):" >&2; _loki_req_fix_line claude >&2; return 1; }
|
|
111
|
+
local -a args=(-p "$text" --append-system-prompt "$_LOKI_CHAT_SYSTEM_PROMPT")
|
|
112
|
+
if [ "$mode" = "plan" ]; then args+=(--permission-mode plan); else args+=(--permission-mode acceptEdits); fi
|
|
113
|
+
[ -n "$_CHAT_MODEL" ] && args+=(--model "$_CHAT_MODEL")
|
|
114
|
+
[ "$_CHAT_STARTED" = "1" ] && args+=(--continue)
|
|
115
|
+
claude "${args[@]}" || return $?
|
|
116
|
+
_CHAT_STARTED=1
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
local prompt="$text"
|
|
120
|
+
if [ "$mode" = "plan" ]; then
|
|
121
|
+
prompt="PLAN MODE: analyze and propose only. Do NOT modify any files. Request: $text"
|
|
122
|
+
fi
|
|
123
|
+
if [ "$mode" != "plan" ]; then
|
|
124
|
+
(
|
|
125
|
+
# shellcheck disable=SC1091
|
|
126
|
+
source "${_LOKI_SCRIPT_DIR:-.}/../providers/loader.sh" 2>/dev/null || exit 1
|
|
127
|
+
load_provider "$_CHAT_PROVIDER" >/dev/null 2>&1 || { echo "loki chat: cannot load provider $_CHAT_PROVIDER" >&2; exit 1; }
|
|
128
|
+
provider_invoke "$prompt"
|
|
129
|
+
)
|
|
130
|
+
return $?
|
|
131
|
+
fi
|
|
132
|
+
# Plan mode: read-only via the provider's native flags, then a post-turn guard
|
|
133
|
+
# that reverts any change the provider made anyway.
|
|
134
|
+
local rc=0
|
|
135
|
+
_chat_plan_snapshot
|
|
136
|
+
(
|
|
137
|
+
# shellcheck disable=SC1091
|
|
138
|
+
source "${_LOKI_SCRIPT_DIR:-.}/../providers/loader.sh" 2>/dev/null || exit 1
|
|
139
|
+
load_provider "$_CHAT_PROVIDER" >/dev/null 2>&1 || { echo "loki chat: cannot load provider $_CHAT_PROVIDER" >&2; exit 1; }
|
|
140
|
+
case "$_CHAT_PROVIDER" in
|
|
141
|
+
codex) codex exec --sandbox read-only --skip-git-repo-check "$prompt" < /dev/null ;;
|
|
142
|
+
opencode) opencode run --agent plan --model "$PROVIDER_MODEL_DEVELOPMENT" "$prompt" < /dev/null ;;
|
|
143
|
+
aider) aider --message "$prompt" --yes-always --no-auto-commits --dry-run --model "$AIDER_DEFAULT_MODEL" < /dev/null 2>&1 ;;
|
|
144
|
+
cline) cline --plan "$prompt" < /dev/null ;;
|
|
145
|
+
*) provider_invoke "$prompt" ;;
|
|
146
|
+
esac
|
|
147
|
+
) || rc=$?
|
|
148
|
+
_chat_plan_guard
|
|
149
|
+
return "$rc"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Plan-mode snapshot: tracked state as a tree-ish commit (git stash create does not
|
|
153
|
+
# touch the stash stack; empty output means clean, so HEAD is the base) plus the
|
|
154
|
+
# list of untracked files.
|
|
155
|
+
_CHAT_PLAN_BASE=""
|
|
156
|
+
_CHAT_PLAN_UNTRACKED=""
|
|
157
|
+
_CHAT_PLAN_ARMED=0
|
|
158
|
+
_chat_plan_snapshot() {
|
|
159
|
+
_CHAT_PLAN_ARMED=0
|
|
160
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
|
|
161
|
+
_CHAT_PLAN_BASE="$(git stash create 2>/dev/null || true)"
|
|
162
|
+
[ -n "$_CHAT_PLAN_BASE" ] || _CHAT_PLAN_BASE="$(git rev-parse HEAD 2>/dev/null || true)"
|
|
163
|
+
[ -n "$_CHAT_PLAN_BASE" ] || return 0
|
|
164
|
+
_CHAT_PLAN_UNTRACKED="$(git ls-files -o --exclude-standard 2>/dev/null | sort)"
|
|
165
|
+
_CHAT_PLAN_ARMED=1
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_chat_plan_guard() {
|
|
169
|
+
[ "$_CHAT_PLAN_ARMED" = "1" ] || return 0
|
|
170
|
+
local f changed=0 now
|
|
171
|
+
while IFS= read -r f; do
|
|
172
|
+
[ -n "$f" ] || continue
|
|
173
|
+
changed=1
|
|
174
|
+
if git cat-file -e "${_CHAT_PLAN_BASE}:${f}" 2>/dev/null; then
|
|
175
|
+
git checkout "$_CHAT_PLAN_BASE" -- "$f" 2>/dev/null || true
|
|
176
|
+
else
|
|
177
|
+
git rm -f -q --cached -- "$f" 2>/dev/null || true
|
|
178
|
+
rm -f -- "$f"
|
|
179
|
+
fi
|
|
180
|
+
done < <(git diff --name-only "$_CHAT_PLAN_BASE" 2>/dev/null)
|
|
181
|
+
now="$(git ls-files -o --exclude-standard 2>/dev/null | sort)"
|
|
182
|
+
while IFS= read -r f; do
|
|
183
|
+
[ -n "$f" ] || continue
|
|
184
|
+
printf '%s\n' "$_CHAT_PLAN_UNTRACKED" | grep -Fxq -- "$f" && continue
|
|
185
|
+
changed=1
|
|
186
|
+
rm -f -- "$f"
|
|
187
|
+
done <<EOF
|
|
188
|
+
$now
|
|
189
|
+
EOF
|
|
190
|
+
_CHAT_PLAN_ARMED=0
|
|
191
|
+
if [ "$changed" = "1" ]; then
|
|
192
|
+
echo "loki chat: plan mode is read-only; the provider changed files, and those changes were reverted. Use /build to allow edits."
|
|
193
|
+
fi
|
|
194
|
+
return 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# /run: the exact run the shell would give: loki start with this session's provider and model.
|
|
198
|
+
# The summary prints at the end of the run; chat stays open afterwards.
|
|
199
|
+
_chat_run_start() {
|
|
200
|
+
local loki="$1" input="$2" rc=0 t0=$SECONDS
|
|
201
|
+
local -a args=("start" "$input" --provider "$_CHAT_PROVIDER")
|
|
202
|
+
[ -n "$_CHAT_MODEL" ] && args+=(--model "$_CHAT_MODEL")
|
|
203
|
+
"$loki" "${args[@]}" || rc=$?
|
|
204
|
+
# HANDOFF: same file loki start wrote, so chat and the shell show one review view
|
|
205
|
+
# (only when written during this run; a stale file from an earlier run is never shown)
|
|
206
|
+
if [ -f ".loki/review/HANDOFF.md" ] && [ -n "$(find ".loki/review/HANDOFF.md" -mmin "-$(( (SECONDS - t0) / 60 + 1 ))" 2>/dev/null)" ]; then cat ".loki/review/HANDOFF.md"; fi
|
|
207
|
+
echo "(run finished, exit $rc; chat is still open)"
|
|
208
|
+
return 0
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
cmd_chat() {
|
|
212
|
+
local tui=0 attach=0
|
|
213
|
+
while [ $# -gt 0 ]; do
|
|
214
|
+
case "$1" in
|
|
215
|
+
--tui) tui=1 ;;
|
|
216
|
+
--attach) attach=1 ;;
|
|
217
|
+
--help|-h)
|
|
218
|
+
echo "Usage: loki chat [--tui] [--attach]"
|
|
219
|
+
echo " Interactive Plan/Build session. --tui execs the claude CLI with Loki context."
|
|
220
|
+
echo " --attach joins the current or most recent 'loki start' run in this repo."
|
|
221
|
+
echo " 'loki start' is the primary path; /run inside chat runs the same thing."
|
|
222
|
+
_chat_help
|
|
223
|
+
return 0 ;;
|
|
224
|
+
*) echo "loki chat: unknown option: $1" >&2; return 2 ;;
|
|
225
|
+
esac
|
|
226
|
+
shift
|
|
227
|
+
done
|
|
228
|
+
|
|
229
|
+
if [ "$tui" = "1" ]; then
|
|
230
|
+
command -v claude >/dev/null 2>&1 || { echo "loki chat: claude CLI not found. Fix (same as loki doctor):" >&2; _loki_req_fix_line claude >&2; return 1; }
|
|
231
|
+
local -a targs=(--append-system-prompt "$_LOKI_CHAT_SYSTEM_PROMPT")
|
|
232
|
+
[ -n "${LOKI_MODEL:-}" ] && targs+=(--model "$LOKI_MODEL")
|
|
233
|
+
exec claude "${targs[@]}"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [ ! -t 0 ]; then
|
|
237
|
+
echo "loki chat needs an interactive terminal (use loki quick for one-shot tasks)" >&2
|
|
238
|
+
return 2
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
local loki; loki="$(_chat_loki_bin)"
|
|
242
|
+
local mode="plan" line cmd rest
|
|
243
|
+
_CHAT_PROVIDER="${LOKI_PROVIDER:-claude}"
|
|
244
|
+
_CHAT_MODEL="${LOKI_MODEL:-}"
|
|
245
|
+
_CHAT_STARTED=0
|
|
246
|
+
local attached=0
|
|
247
|
+
echo "Loki chat (provider: $_CHAT_PROVIDER). /help for commands, /exit to quit."
|
|
248
|
+
if [ "$attach" = "1" ]; then _chat_attach_banner && attached=1; fi
|
|
249
|
+
while true; do
|
|
250
|
+
local plabel="$mode"; [ "$attached" = "1" ] && plabel="attached"
|
|
251
|
+
if ! IFS= read -r -e -p "loki[$plabel]> " line; then echo; break; fi
|
|
252
|
+
[ -n "$line" ] || continue
|
|
253
|
+
case "$line" in
|
|
254
|
+
/*) cmd="${line%% *}"; rest=""; [ "$cmd" != "$line" ] && rest="${line#* }" ;;
|
|
255
|
+
*)
|
|
256
|
+
if [ "$attached" = "1" ]; then
|
|
257
|
+
_chat_send plan "You are answering questions about a running or finished Loki run. Read-only: do not modify files. Context:
|
|
258
|
+
$(_chat_attach_context)
|
|
259
|
+
|
|
260
|
+
Question: $line" || true
|
|
261
|
+
else
|
|
262
|
+
_chat_send "$mode" "$line" || true
|
|
263
|
+
fi
|
|
264
|
+
continue ;;
|
|
265
|
+
esac
|
|
266
|
+
if [ "$attached" = "1" ]; then
|
|
267
|
+
case "$cmd" in
|
|
268
|
+
/status) _chat_do "$loki" status; continue ;;
|
|
269
|
+
/steer)
|
|
270
|
+
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
271
|
+
_chat_do "$loki" steer "$rest"; continue ;;
|
|
272
|
+
/stop) _chat_do "$loki" stop; continue ;;
|
|
273
|
+
/detach) attached=0; echo "Detached."; continue ;;
|
|
274
|
+
esac
|
|
275
|
+
fi
|
|
276
|
+
case "$cmd" in
|
|
277
|
+
/exit|/quit) break ;;
|
|
278
|
+
/help) _chat_help ;;
|
|
279
|
+
/plan) mode="plan"; echo "Plan mode: read-only." ;;
|
|
280
|
+
/build) mode="build"; echo "Build mode: edits allowed." ;;
|
|
281
|
+
/run)
|
|
282
|
+
[ -n "$rest" ] || { echo "usage: /run <prd|brief>"; continue; }
|
|
283
|
+
_chat_run_start "$loki" "$rest"
|
|
284
|
+
_chat_attach_banner || true ;;
|
|
285
|
+
/attach) _chat_attach_banner && attached=1 ;;
|
|
286
|
+
/status) _chat_do "$loki" status ;;
|
|
287
|
+
/steer)
|
|
288
|
+
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
289
|
+
_chat_do "$loki" steer "$rest" ;;
|
|
290
|
+
/stop) _chat_do "$loki" stop ;;
|
|
291
|
+
/detach) echo "Not attached." ;;
|
|
292
|
+
/quick)
|
|
293
|
+
[ -n "$rest" ] || { echo "usage: /quick <task>"; continue; }
|
|
294
|
+
_chat_do "$loki" quick "$rest" ;;
|
|
295
|
+
/edit)
|
|
296
|
+
[ -n "$rest" ] || { echo "usage: /edit <change>"; continue; }
|
|
297
|
+
_chat_send build "$rest" ;;
|
|
298
|
+
/verify) _chat_do "$loki" verify ;;
|
|
299
|
+
/receipt) _chat_do "$loki" receipt ;;
|
|
300
|
+
/cost) _chat_do "$loki" cost ;;
|
|
301
|
+
/undo) # shellcheck disable=SC2086
|
|
302
|
+
_chat_do "$loki" undo $rest ;;
|
|
303
|
+
/diff) _chat_do git diff --stat; _chat_do git diff ;;
|
|
304
|
+
/model)
|
|
305
|
+
if [ -n "$rest" ]; then _CHAT_MODEL="$rest"; echo "model: $rest"; else echo "model: ${_CHAT_MODEL:-default}"; fi ;;
|
|
306
|
+
/provider)
|
|
307
|
+
case "$rest" in
|
|
308
|
+
claude|codex|cline|aider|opencode) _CHAT_PROVIDER="$rest"; _CHAT_STARTED=0; echo "provider: $rest" ;;
|
|
309
|
+
*) echo "usage: /provider claude|codex|cline|aider|opencode (now: $_CHAT_PROVIDER)" ;;
|
|
310
|
+
esac ;;
|
|
311
|
+
*) echo "unknown command: $cmd (try /help)" ;;
|
|
312
|
+
esac
|
|
313
|
+
done
|
|
314
|
+
return 0
|
|
315
|
+
}
|
|
@@ -95,6 +95,14 @@ _e10_local_push() {
|
|
|
95
95
|
log_warn "Not pushing branch '$branch': it is (or may be) the default branch of $origin."
|
|
96
96
|
return 2
|
|
97
97
|
fi
|
|
98
|
+
# RECOVERY: remote already at the local commit means a prior attempt landed; push nothing.
|
|
99
|
+
local want have
|
|
100
|
+
want="$(_e10_git -C "$dir" rev-parse -q --verify "refs/heads/$branch" 2>/dev/null)" || want=""
|
|
101
|
+
have="$(_e10_git ls-remote "$origin" "refs/heads/$branch" 2>/dev/null | awk '{print $1; exit}')" || have=""
|
|
102
|
+
if [ -n "$want" ] && [ "$want" = "$have" ]; then
|
|
103
|
+
log_info "push skipped: remote already has $branch at ${want:0:12}"
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
98
106
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/loki-push.XXXXXX")" || return 1
|
|
99
107
|
if _e10_git init -q --template= "$tmp" >/dev/null 2>&1 \
|
|
100
108
|
&& _e10_git -C "$tmp" fetch -q --no-tags --update-shallow "$dir" \
|
|
@@ -138,7 +146,17 @@ case "$mode" in
|
|
|
138
146
|
printf 'local://%s#%s\n' "$_LOKI_PINNED_ORIGIN" "$branch"
|
|
139
147
|
exit 0
|
|
140
148
|
fi
|
|
141
|
-
|
|
149
|
+
# RECOVERY: a resumed publish skips the push when the remote branch is already at the local commit.
|
|
150
|
+
_e10_want="$(_e10_git -C "$dir" rev-parse -q --verify "refs/heads/$branch" 2>/dev/null)" || _e10_want=""
|
|
151
|
+
_e10_have=""
|
|
152
|
+
case "$branch" in main | master | HEAD) ;; *)
|
|
153
|
+
_e10_have="$(_e10_gh api "repos/$_e10_repo/git/ref/heads/$branch" --jq .object.sha 2>/dev/null)" || _e10_have="" ;;
|
|
154
|
+
esac
|
|
155
|
+
if [ -n "$_e10_want" ] && [ "$_e10_want" = "$_e10_have" ]; then
|
|
156
|
+
log_info "push skipped: remote already has $branch at ${_e10_want:0:12}"
|
|
157
|
+
else
|
|
158
|
+
_loki_trusted_push _loki_with_github_tokens "$dir" "$branch" || die "push refused or failed (rc=$?)"
|
|
159
|
+
fi
|
|
142
160
|
url="$(_e10_gh pr list --repo "$_e10_repo" --head "$branch" --state open --json url --jq '.[0].url')" \
|
|
143
161
|
|| die "gh pr list failed"
|
|
144
162
|
if [ -z "$url" ] || [ "$url" = "null" ]; then
|