loki-mode 11.3.8 → 11.4.2

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.
Files changed (103) hide show
  1. package/README.md +34 -18
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/agents/custom_agents.py +280 -0
  5. package/autonomy/council-v2.sh +22 -1
  6. package/autonomy/edit.sh +242 -0
  7. package/autonomy/lib/chat.sh +312 -0
  8. package/autonomy/lib/help-core.sh +164 -0
  9. package/autonomy/lib/json-output.sh +38 -0
  10. package/autonomy/lib/run-summary.sh +96 -0
  11. package/autonomy/loki +105 -10
  12. package/autonomy/route.sh +270 -0
  13. package/autonomy/run.sh +6 -0
  14. package/autonomy/setup-wizard.sh +161 -0
  15. package/autonomy/verify.sh +22 -2
  16. package/bin/loki +73 -18
  17. package/completions/_loki +55 -0
  18. package/completions/loki.bash +49 -3
  19. package/dashboard/__init__.py +1 -1
  20. package/docs/CLI-CORE-VERBS.md +112 -0
  21. package/docs/CLI-REFERENCE.md +7 -1
  22. package/docs/COMPARE-OPENCODE.md +94 -0
  23. package/docs/COMPONENTS.md +1 -0
  24. package/docs/INSTALLATION.md +56 -12
  25. package/docs/KNOWN-ISSUES.md +2 -12
  26. package/docs/QUICKSTART.md +108 -0
  27. package/docs/environment-variables.md +9 -2
  28. package/docs/exit-codes.md +29 -12
  29. package/docs/v10/CLI-MODERN.md +10 -4
  30. package/docs/v10/FAILURE-CLASSES.md +2 -2
  31. package/docs/v10/GUIDE.md +270 -44
  32. package/docs/v10/METRICS.md +6 -0
  33. package/docs/v10/OPENCODE-PARITY-PLAN.md +84 -0
  34. package/docs/v10/PROGRESS.md +7 -0
  35. package/loki-ts/dist/loki.js +968 -933
  36. package/mcp/__init__.py +1 -1
  37. package/package.json +2 -2
  38. package/packages/control-plane/dist/server.js +198 -86
  39. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  40. package/providers/claude.sh +16 -2
  41. package/providers/model_route_exec.sh +22 -0
  42. package/providers/model_routes.sh +188 -0
  43. package/skills/providers.md +13 -0
  44. package/templates/agents/planner.md +10 -0
  45. package/templates/agents/reviewer.md +10 -0
  46. package/templates/agents/tester.md +10 -0
  47. package/web-app/dist/assets/{AdminPage-D1jlQGeE.js → AdminPage-CYJ2BWRC.js} +1 -1
  48. package/web-app/dist/assets/{Avatar-QUX_aJfQ.js → Avatar-DdK4wynI.js} +1 -1
  49. package/web-app/dist/assets/{Badge-pysXUR6J.js → Badge-CrX3MI7J.js} +1 -1
  50. package/web-app/dist/assets/{Button-CWzlTw9P.js → Button-709-wwGM.js} +1 -1
  51. package/web-app/dist/assets/{CockpitPage-DLCoLdyF.js → CockpitPage-BapdTInO.js} +1 -1
  52. package/web-app/dist/assets/{ComparePage-CCENGIti.js → ComparePage-DT99pGvA.js} +1 -1
  53. package/web-app/dist/assets/{ErrorBoundary-BssCcbgv.js → ErrorBoundary-B0uJSys8.js} +1 -1
  54. package/web-app/dist/assets/{EvidenceReceiptPanel-CwmAqJ90.js → EvidenceReceiptPanel-BtMFgElP.js} +1 -1
  55. package/web-app/dist/assets/{GitHubIssuesPanel-DuSw8shb.js → GitHubIssuesPanel-DjJuU941.js} +1 -1
  56. package/web-app/dist/assets/{GitHubPRsPanel-CGvLbDjh.js → GitHubPRsPanel-HXfEX1BC.js} +1 -1
  57. package/web-app/dist/assets/{HomePage-BrBX5Gn2.js → HomePage-Cqyves-S.js} +2 -2
  58. package/web-app/dist/assets/{LoginPage-DPjr7QHU.js → LoginPage-jUbtC7_Y.js} +1 -1
  59. package/web-app/dist/assets/{MagicPage-Dxd7FWF2.js → MagicPage-DHRsqjgR.js} +1 -1
  60. package/web-app/dist/assets/{MetricsPage-BP47Urni.js → MetricsPage-B-pED05R.js} +1 -1
  61. package/web-app/dist/assets/{NotFoundPage-D7_07YoB.js → NotFoundPage-CsbUs21R.js} +1 -1
  62. package/web-app/dist/assets/{ProjectPage-SQ183efy.js → ProjectPage-BtlLEQwB.js} +3 -3
  63. package/web-app/dist/assets/{ProjectsPage-Cgr1K2Ae.js → ProjectsPage-DaRbRjSp.js} +1 -1
  64. package/web-app/dist/assets/{SettingsPage-kyYOSDt0.js → SettingsPage-DALrtP3F.js} +1 -1
  65. package/web-app/dist/assets/{ShowcasePage-CRd-H1A4.js → ShowcasePage-CoPHSNP0.js} +1 -1
  66. package/web-app/dist/assets/{SystemSettingsPage-BHBdGXod.js → SystemSettingsPage-kOuSN50O.js} +1 -1
  67. package/web-app/dist/assets/{TeamsPage-CI3Wdbjw.js → TeamsPage-DGWK0LMp.js} +1 -1
  68. package/web-app/dist/assets/{TemplatesPage-DfhCgP6i.js → TemplatesPage-C-_10CwA.js} +1 -1
  69. package/web-app/dist/assets/{TerminalOutput-Cs87YXLJ.js → TerminalOutput-Cz_vPbXL.js} +1 -1
  70. package/web-app/dist/assets/{activity-CrQx5SbK.js → activity-icIsXJVa.js} +1 -1
  71. package/web-app/dist/assets/{bell-Dde5IbnI.js → bell-DbPR2_4r.js} +1 -1
  72. package/web-app/dist/assets/{bot-trRuIJT6.js → bot-DvjP3Yh5.js} +1 -1
  73. package/web-app/dist/assets/{check-DmsthR-3.js → check-D1OBxHeU.js} +1 -1
  74. package/web-app/dist/assets/{chevron-left-C5wZb7mg.js → chevron-left-D09jDZ29.js} +1 -1
  75. package/web-app/dist/assets/{circle-alert-BCb173fK.js → circle-alert-DyZMLOcC.js} +1 -1
  76. package/web-app/dist/assets/{clock-CvcqL3Ma.js → clock-CdpdJTId.js} +1 -1
  77. package/web-app/dist/assets/{cloud-CW1r3-AN.js → cloud-CoLTlmoj.js} +1 -1
  78. package/web-app/dist/assets/{code-xml-uxBTXAfZ.js → code-xml-DrkE2ZZY.js} +1 -1
  79. package/web-app/dist/assets/{database-DBrvW0o1.js → database-CMQQuM2R.js} +1 -1
  80. package/web-app/dist/assets/{dollar-sign-WrOyoDcN.js → dollar-sign-CuioDaOG.js} +1 -1
  81. package/web-app/dist/assets/{file-code-corner-8JC8TI2_.js → file-code-corner-Cx9j9K1S.js} +1 -1
  82. package/web-app/dist/assets/{file-plus-CgUdOr2u.js → file-plus-CYD2zaaI.js} +1 -1
  83. package/web-app/dist/assets/{globe-w-0x46nt.js → globe-6w-hnQkL.js} +1 -1
  84. package/web-app/dist/assets/{hammer-Civ6_wQq.js → hammer-BpqtautL.js} +1 -1
  85. package/web-app/dist/assets/{index-wvGJotyF.js → index-Dzs-9EgJ.js} +3 -3
  86. package/web-app/dist/assets/{layers-Cc3wLG39.js → layers-D5hd7jb7.js} +1 -1
  87. package/web-app/dist/assets/{loader-circle-BedHqfS4.js → loader-circle-C6Y9x_7H.js} +1 -1
  88. package/web-app/dist/assets/{lock-CRyzk3QG.js → lock-D51L7rYh.js} +1 -1
  89. package/web-app/dist/assets/{package-DCT1v7W2.js → package-BJF8hkP8.js} +1 -1
  90. package/web-app/dist/assets/{plus-C7eerOb_.js → plus-D5kUQ79n.js} +1 -1
  91. package/web-app/dist/assets/{refresh-cw-DO753Y_B.js → refresh-cw-BKdlCa7F.js} +1 -1
  92. package/web-app/dist/assets/{rotate-ccw-ptlyGV4I.js → rotate-ccw-_bd_YI1e.js} +1 -1
  93. package/web-app/dist/assets/{scroll-text-CqPwk4yI.js → scroll-text-DbkegIxF.js} +1 -1
  94. package/web-app/dist/assets/{server-BP_VvY9m.js → server-DAwqFIsq.js} +1 -1
  95. package/web-app/dist/assets/{shield-alert-Bf0waB0a.js → shield-alert-BuR1tPHu.js} +1 -1
  96. package/web-app/dist/assets/{thumbs-up-p1roSIsw.js → thumbs-up-BF6p3mRd.js} +1 -1
  97. package/web-app/dist/assets/{trash-2-BrMaXAxl.js → trash-2-BVpnkqqz.js} +1 -1
  98. package/web-app/dist/assets/{trending-down-DaGcCn2W.js → trending-down-DpsDvRwW.js} +1 -1
  99. package/web-app/dist/assets/{trending-up-CKPupOSE.js → trending-up-mFogTjt3.js} +1 -1
  100. package/web-app/dist/assets/{upload-CR0gzibW.js → upload-4T8zpBsi.js} +1 -1
  101. package/web-app/dist/assets/{usePolling-kKzJRjFw.js → usePolling-DPXQv8WO.js} +1 -1
  102. package/web-app/dist/assets/{user-CPOXPTtD.js → user-BYGIFaTm.js} +1 -1
  103. package/web-app/dist/index.html +1 -1
@@ -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,312 @@
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" >&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
201
+ local -a args=("start" "$input" --provider "$_CHAT_PROVIDER")
202
+ [ -n "$_CHAT_MODEL" ] && args+=(--model "$_CHAT_MODEL")
203
+ "$loki" "${args[@]}" || rc=$?
204
+ echo "(run finished, exit $rc; chat is still open)"
205
+ return 0
206
+ }
207
+
208
+ cmd_chat() {
209
+ local tui=0 attach=0
210
+ while [ $# -gt 0 ]; do
211
+ case "$1" in
212
+ --tui) tui=1 ;;
213
+ --attach) attach=1 ;;
214
+ --help|-h)
215
+ echo "Usage: loki chat [--tui] [--attach]"
216
+ echo " Interactive Plan/Build session. --tui execs the claude CLI with Loki context."
217
+ echo " --attach joins the current or most recent 'loki start' run in this repo."
218
+ echo " 'loki start' is the primary path; /run inside chat runs the same thing."
219
+ _chat_help
220
+ return 0 ;;
221
+ *) echo "loki chat: unknown option: $1" >&2; return 2 ;;
222
+ esac
223
+ shift
224
+ done
225
+
226
+ if [ "$tui" = "1" ]; then
227
+ command -v claude >/dev/null 2>&1 || { echo "loki chat: claude CLI not found" >&2; return 1; }
228
+ local -a targs=(--append-system-prompt "$_LOKI_CHAT_SYSTEM_PROMPT")
229
+ [ -n "${LOKI_MODEL:-}" ] && targs+=(--model "$LOKI_MODEL")
230
+ exec claude "${targs[@]}"
231
+ fi
232
+
233
+ if [ ! -t 0 ]; then
234
+ echo "loki chat needs an interactive terminal (use loki quick for one-shot tasks)" >&2
235
+ return 2
236
+ fi
237
+
238
+ local loki; loki="$(_chat_loki_bin)"
239
+ local mode="plan" line cmd rest
240
+ _CHAT_PROVIDER="${LOKI_PROVIDER:-claude}"
241
+ _CHAT_MODEL="${LOKI_MODEL:-}"
242
+ _CHAT_STARTED=0
243
+ local attached=0
244
+ echo "Loki chat (provider: $_CHAT_PROVIDER). /help for commands, /exit to quit."
245
+ if [ "$attach" = "1" ]; then _chat_attach_banner && attached=1; fi
246
+ while true; do
247
+ local plabel="$mode"; [ "$attached" = "1" ] && plabel="attached"
248
+ if ! IFS= read -r -e -p "loki[$plabel]> " line; then echo; break; fi
249
+ [ -n "$line" ] || continue
250
+ case "$line" in
251
+ /*) cmd="${line%% *}"; rest=""; [ "$cmd" != "$line" ] && rest="${line#* }" ;;
252
+ *)
253
+ if [ "$attached" = "1" ]; then
254
+ _chat_send plan "You are answering questions about a running or finished Loki run. Read-only: do not modify files. Context:
255
+ $(_chat_attach_context)
256
+
257
+ Question: $line" || true
258
+ else
259
+ _chat_send "$mode" "$line" || true
260
+ fi
261
+ continue ;;
262
+ esac
263
+ if [ "$attached" = "1" ]; then
264
+ case "$cmd" in
265
+ /status) _chat_do "$loki" status; continue ;;
266
+ /steer)
267
+ [ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
268
+ _chat_do "$loki" steer "$rest"; continue ;;
269
+ /stop) _chat_do "$loki" stop; continue ;;
270
+ /detach) attached=0; echo "Detached."; continue ;;
271
+ esac
272
+ fi
273
+ case "$cmd" in
274
+ /exit|/quit) break ;;
275
+ /help) _chat_help ;;
276
+ /plan) mode="plan"; echo "Plan mode: read-only." ;;
277
+ /build) mode="build"; echo "Build mode: edits allowed." ;;
278
+ /run)
279
+ [ -n "$rest" ] || { echo "usage: /run <prd|brief>"; continue; }
280
+ _chat_run_start "$loki" "$rest"
281
+ _chat_attach_banner || true ;;
282
+ /attach) _chat_attach_banner && attached=1 ;;
283
+ /status) _chat_do "$loki" status ;;
284
+ /steer)
285
+ [ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
286
+ _chat_do "$loki" steer "$rest" ;;
287
+ /stop) _chat_do "$loki" stop ;;
288
+ /detach) echo "Not attached." ;;
289
+ /quick)
290
+ [ -n "$rest" ] || { echo "usage: /quick <task>"; continue; }
291
+ _chat_do "$loki" quick "$rest" ;;
292
+ /edit)
293
+ [ -n "$rest" ] || { echo "usage: /edit <change>"; continue; }
294
+ _chat_send build "$rest" ;;
295
+ /verify) _chat_do "$loki" verify ;;
296
+ /receipt) _chat_do "$loki" receipt ;;
297
+ /cost) _chat_do "$loki" cost ;;
298
+ /undo) # shellcheck disable=SC2086
299
+ _chat_do "$loki" undo $rest ;;
300
+ /diff) _chat_do git diff --stat; _chat_do git diff ;;
301
+ /model)
302
+ if [ -n "$rest" ]; then _CHAT_MODEL="$rest"; echo "model: $rest"; else echo "model: ${_CHAT_MODEL:-default}"; fi ;;
303
+ /provider)
304
+ case "$rest" in
305
+ claude|codex|cline|aider|opencode) _CHAT_PROVIDER="$rest"; _CHAT_STARTED=0; echo "provider: $rest" ;;
306
+ *) echo "usage: /provider claude|codex|cline|aider|opencode (now: $_CHAT_PROVIDER)" ;;
307
+ esac ;;
308
+ *) echo "unknown command: $cmd (try /help)" ;;
309
+ esac
310
+ done
311
+ return 0
312
+ }