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
|
@@ -80,6 +80,60 @@ detect_any_provider() {
|
|
|
80
80
|
return 1
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
# sdk_binary_present: true (0) when an executable Claude Agent SDK platform binary exists
|
|
84
|
+
# in an installed node_modules. Shared by detect_bundled_sdk_provider and
|
|
85
|
+
# loki_requirements_json.
|
|
86
|
+
sdk_binary_present() {
|
|
87
|
+
# Search the node_modules trees Loki is actually installed into. The root is
|
|
88
|
+
# derived from THIS FILE's location (self-containment contract, see header):
|
|
89
|
+
# SKILL_DIR belongs to autonomy/loki and is NOT exported, so it would be
|
|
90
|
+
# empty when doctor.ts spawns this script standalone -- which would silently
|
|
91
|
+
# answer "no SDK" on the Bun route while the bash route said yes, exactly the
|
|
92
|
+
# kind of route split the parity gate exists to prevent.
|
|
93
|
+
# LOKI_SDK_NODE_MODULES is a TEST-ONLY seam (never set in production) so the
|
|
94
|
+
# shell tests can point at a fixture tree without mutating real node_modules.
|
|
95
|
+
local _po_root
|
|
96
|
+
_po_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)" || _po_root=""
|
|
97
|
+
# Array, not a word-split string: install roots can contain spaces.
|
|
98
|
+
local _po_search=()
|
|
99
|
+
if [ -n "${LOKI_SDK_NODE_MODULES:-}" ]; then
|
|
100
|
+
# When the test seam is set it is AUTHORITATIVE (not merely first): the
|
|
101
|
+
# real tree must not be consulted as a fallback, or a fixture asserting
|
|
102
|
+
# "no usable SDK" would silently pass by finding the repo's own binary.
|
|
103
|
+
_po_search=("$LOKI_SDK_NODE_MODULES")
|
|
104
|
+
elif [ -n "$_po_root" ]; then
|
|
105
|
+
# Two layouts, both real:
|
|
106
|
+
# repo clone -> <root>/node_modules, <root>/loki-ts/node_modules
|
|
107
|
+
# npm install -> npm HOISTS the SDK out of node_modules/loki-mode/
|
|
108
|
+
# up to the installing tree's top-level
|
|
109
|
+
# node_modules/ (verified by packing this repo and
|
|
110
|
+
# installing the tarball). Without the hoisted
|
|
111
|
+
# candidates the predicate silently returns 1 for
|
|
112
|
+
# every npm-installed user, i.e. the feature would
|
|
113
|
+
# be dead for exactly the audience it targets.
|
|
114
|
+
# <root> is .../node_modules/loki-mode, so ../ is the @scope-less
|
|
115
|
+
# top-level node_modules and ../../ covers a scoped install.
|
|
116
|
+
_po_search=(
|
|
117
|
+
"$_po_root/node_modules"
|
|
118
|
+
"$_po_root/loki-ts/node_modules"
|
|
119
|
+
"$_po_root/.."
|
|
120
|
+
"$_po_root/../.."
|
|
121
|
+
)
|
|
122
|
+
fi
|
|
123
|
+
local _nm
|
|
124
|
+
for _nm in ${_po_search+"${_po_search[@]}"}; do
|
|
125
|
+
[ -n "$_nm" ] || continue
|
|
126
|
+
set -- "$_nm"/@anthropic-ai/claude-agent-sdk-*/claude
|
|
127
|
+
# Unmatched globs stay literal in bash, so test the expansion for real.
|
|
128
|
+
while [ "$#" -gt 0 ]; do
|
|
129
|
+
[ -x "$1" ] && return 0
|
|
130
|
+
shift
|
|
131
|
+
done
|
|
132
|
+
done
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
83
137
|
# detect_bundled_sdk_provider: true (0) only when the bundled Claude Agent SDK
|
|
84
138
|
# can ACTUALLY run this machine's main loop with no separate CLI install.
|
|
85
139
|
#
|
|
@@ -130,53 +184,7 @@ detect_bundled_sdk_provider() {
|
|
|
130
184
|
return 1
|
|
131
185
|
fi
|
|
132
186
|
|
|
133
|
-
|
|
134
|
-
# derived from THIS FILE's location (self-containment contract, see header):
|
|
135
|
-
# SKILL_DIR belongs to autonomy/loki and is NOT exported, so it would be
|
|
136
|
-
# empty when doctor.ts spawns this script standalone -- which would silently
|
|
137
|
-
# answer "no SDK" on the Bun route while the bash route said yes, exactly the
|
|
138
|
-
# kind of route split the parity gate exists to prevent.
|
|
139
|
-
# LOKI_SDK_NODE_MODULES is a TEST-ONLY seam (never set in production) so the
|
|
140
|
-
# shell tests can point at a fixture tree without mutating real node_modules.
|
|
141
|
-
local _po_root
|
|
142
|
-
_po_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)" || _po_root=""
|
|
143
|
-
# Array, not a word-split string: install roots can contain spaces.
|
|
144
|
-
local _po_search=()
|
|
145
|
-
if [ -n "${LOKI_SDK_NODE_MODULES:-}" ]; then
|
|
146
|
-
# When the test seam is set it is AUTHORITATIVE (not merely first): the
|
|
147
|
-
# real tree must not be consulted as a fallback, or a fixture asserting
|
|
148
|
-
# "no usable SDK" would silently pass by finding the repo's own binary.
|
|
149
|
-
_po_search=("$LOKI_SDK_NODE_MODULES")
|
|
150
|
-
elif [ -n "$_po_root" ]; then
|
|
151
|
-
# Two layouts, both real:
|
|
152
|
-
# repo clone -> <root>/node_modules, <root>/loki-ts/node_modules
|
|
153
|
-
# npm install -> npm HOISTS the SDK out of node_modules/loki-mode/
|
|
154
|
-
# up to the installing tree's top-level
|
|
155
|
-
# node_modules/ (verified by packing this repo and
|
|
156
|
-
# installing the tarball). Without the hoisted
|
|
157
|
-
# candidates the predicate silently returns 1 for
|
|
158
|
-
# every npm-installed user, i.e. the feature would
|
|
159
|
-
# be dead for exactly the audience it targets.
|
|
160
|
-
# <root> is .../node_modules/loki-mode, so ../ is the @scope-less
|
|
161
|
-
# top-level node_modules and ../../ covers a scoped install.
|
|
162
|
-
_po_search=(
|
|
163
|
-
"$_po_root/node_modules"
|
|
164
|
-
"$_po_root/loki-ts/node_modules"
|
|
165
|
-
"$_po_root/.."
|
|
166
|
-
"$_po_root/../.."
|
|
167
|
-
)
|
|
168
|
-
fi
|
|
169
|
-
local _nm
|
|
170
|
-
for _nm in ${_po_search+"${_po_search[@]}"}; do
|
|
171
|
-
[ -n "$_nm" ] || continue
|
|
172
|
-
set -- "$_nm"/@anthropic-ai/claude-agent-sdk-*/claude
|
|
173
|
-
# Unmatched globs stay literal in bash, so test the expansion for real.
|
|
174
|
-
while [ "$#" -gt 0 ]; do
|
|
175
|
-
[ -x "$1" ] && return 0
|
|
176
|
-
shift
|
|
177
|
-
done
|
|
178
|
-
done
|
|
179
|
-
return 1
|
|
187
|
+
sdk_binary_present
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
# _po_assume_yes: true when the user has opted into unattended confirmation.
|
|
@@ -545,6 +553,46 @@ print(json.dumps({"selected": selected, "providers": rows}, indent=2))
|
|
|
545
553
|
) || return 1
|
|
546
554
|
}
|
|
547
555
|
|
|
556
|
+
# loki_requirements_json: print the setup requirements report (shared table
|
|
557
|
+
# autonomy/lib/setup-requirements.json) as one JSON object, matching the "requirements"
|
|
558
|
+
# key of the Bun doctor (loki-ts/src/commands/doctor.ts). Used by the minimal
|
|
559
|
+
# `loki doctor --json` fallback when the Bun doctor cannot run. Presence only: unlike the
|
|
560
|
+
# Bun evaluator it does not compare versions.
|
|
561
|
+
loki_requirements_json() {
|
|
562
|
+
local table statuses="" id kind target values st v
|
|
563
|
+
table="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)/lib/setup-requirements.json"
|
|
564
|
+
if [ ! -f "$table" ] || ! command -v jq >/dev/null 2>&1; then
|
|
565
|
+
printf 'null\n'
|
|
566
|
+
return 0
|
|
567
|
+
fi
|
|
568
|
+
while IFS=$'\t' read -r id kind target values; do
|
|
569
|
+
st=missing
|
|
570
|
+
case "$kind" in
|
|
571
|
+
command) command -v "$target" >/dev/null 2>&1 && st=present ;;
|
|
572
|
+
env_any)
|
|
573
|
+
for v in ${target//,/ }; do [ -n "${!v:-}" ] && st=present; done ;;
|
|
574
|
+
env_value)
|
|
575
|
+
for v in ${target//,/ }; do
|
|
576
|
+
case ",${values}," in *",${!v:-},"*) [ -n "${!v:-}" ] && st=present ;; esac
|
|
577
|
+
done ;;
|
|
578
|
+
sdk_binary) sdk_binary_present && st=present ;;
|
|
579
|
+
esac
|
|
580
|
+
statuses="${statuses}${id} ${st}"$'\n'
|
|
581
|
+
done < <(jq -r '.requirements[] | [.id, .kind, (.target | if type == "array" then join(",") else . end), ((.values // []) | join(","))] | @tsv' "$table")
|
|
582
|
+
|
|
583
|
+
jq -n --rawfile st <(printf '%s' "$statuses") --slurpfile t "$table" '
|
|
584
|
+
($st | split("\n") | map(select(length > 0) | split(" ") | {(.[0]): .[1]}) | add) as $s
|
|
585
|
+
| $t[0] as $tab
|
|
586
|
+
| {paths: [ $tab.paths[] as $p
|
|
587
|
+
| ([ $tab.requirements[] | select(.path == $p.id)
|
|
588
|
+
| {id, label, level, status: $s[.id], detail: (if $s[.id] == "present" then null else "not found by presence check" end), fix} ]) as $items
|
|
589
|
+
| ([ $items[] | select(.level == "required") ]) as $req
|
|
590
|
+
| {id: $p.id, title: $p.title,
|
|
591
|
+
ready: (if ($req | length) > 0 then ($req | all(.status == "present")) else ($items | any(.status == "present")) end),
|
|
592
|
+
items: $items} ]}
|
|
593
|
+
| . + {first_task_ready: ((.paths[] | select(.id == "common") | .ready) and ([.paths[] | select(.id != "common") | .ready] | any))}'
|
|
594
|
+
}
|
|
595
|
+
|
|
548
596
|
# Executed directly (doctor.ts child_process bridge, or manual): run the offer.
|
|
549
597
|
# When sourced by autonomy/loki, this block does not run.
|
|
550
598
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# One predictable execution path (OC-ROUTE).
|
|
3
|
+
# Sourced by bin/loki. Normalizes argv BEFORE the engine decision so argument
|
|
4
|
+
# order and input form never change the engine:
|
|
5
|
+
# engine10 is the default for a PRD file, an issue ref, a multi-word task and a
|
|
6
|
+
# one-word brief, whether the flags come first or last.
|
|
7
|
+
# The legacy engine runs only on --engine legacy / LOKI_ENGINE=legacy, or, for
|
|
8
|
+
# one specific flag engine10 cannot honor, for that call only (with a notice).
|
|
9
|
+
# Pure functions: no exec, no side effects except R_* globals and the printers.
|
|
10
|
+
# Bash 3.2 compatible (no associative arrays, no ${var,,}).
|
|
11
|
+
|
|
12
|
+
# Flags that take a separate value token.
|
|
13
|
+
_ROUTE_VALUE_FLAGS=" --provider --budget --budget-limit --max-cost --max-iterations --max-retries --session-model --completion-promise --base-wait --max-wait --prd --brief --attempts --spec --aider-model --aider-flags --cline-model --isolation --output --engine --mirofish-rounds --mirofish-timeout "
|
|
14
|
+
# Flags engine10 honors as-is.
|
|
15
|
+
_ROUTE_E10_FLAGS=" --no-pr --deep --json --verbose --provider --max-cost --spec --attempts "
|
|
16
|
+
# Accepted and dropped (no-ops on a non-interactive run).
|
|
17
|
+
_ROUTE_NOOP_FLAGS=" --yes -y --no-plan --no-mirofish --no-dashboard "
|
|
18
|
+
|
|
19
|
+
_route_has() { case "$1" in *" $2 "*) return 0 ;; esac; return 1; }
|
|
20
|
+
|
|
21
|
+
_route_json_str() {
|
|
22
|
+
printf '%s' "$1" | tr '\n\t\r' ' ' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_route_classify() {
|
|
26
|
+
# $1 = single positional. Echoes: issue | prd | task | brief | missing-prd
|
|
27
|
+
local p="$1"
|
|
28
|
+
case "$p" in
|
|
29
|
+
*/*\#[0-9]*|http*://*/issues/*|http*://*/browse/*|jira:*|linear:*|https://linear.app/*/issue/*) echo issue; return 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
if [ -f "$p" ]; then echo prd; return 0; fi
|
|
32
|
+
case "$p" in
|
|
33
|
+
*" "*) echo task; return 0 ;;
|
|
34
|
+
*.md|*.markdown|*.txt|*.json|*.yaml|*.yml) echo missing-prd; return 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
echo brief
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# loki_route_resolve VERB ARGS... VERB = start | quick | bare
|
|
40
|
+
# Sets: R_DECIDED R_E10 R_HARD R_ENGINE R_REASON R_INPUT_TYPE R_INPUT R_PROVIDER
|
|
41
|
+
# R_MODEL R_NOTICE R_ERR R_EXPLAIN R_JSON R_E10_ARGS[] R_CLEAN[] R_SESSION_MODEL R_VERB
|
|
42
|
+
# R_* are the result interface read by the sourcing caller.
|
|
43
|
+
# shellcheck disable=SC2034
|
|
44
|
+
loki_route_resolve() {
|
|
45
|
+
local verb="$1"; shift
|
|
46
|
+
R_DECIDED=0; R_E10=0; R_HARD=0; R_ENGINE=""; R_REASON=""; R_INPUT_TYPE=""; R_INPUT=""
|
|
47
|
+
R_PROVIDER=""; R_MODEL=""; R_NOTICE=""; R_ERR=""; R_EXPLAIN=0; R_JSON=0; R_SESSION_MODEL=""; R_VERB=""
|
|
48
|
+
R_E10_ARGS=(); R_CLEAN=(); R_ADAPTER=""; R_CAPDIFF=()
|
|
49
|
+
|
|
50
|
+
local a engine_req="" help=0 legacy_flag="" session_model="" provider_flag=""
|
|
51
|
+
local -a positionals=() passthru=()
|
|
52
|
+
local i=0 n=$#
|
|
53
|
+
local -a argv=("$@")
|
|
54
|
+
while [ "$i" -lt "$n" ]; do
|
|
55
|
+
a="${argv[$i]}"
|
|
56
|
+
i=$((i + 1))
|
|
57
|
+
case "$a" in
|
|
58
|
+
--engine) engine_req="${argv[$i]:-}"; i=$((i + 1)); continue ;;
|
|
59
|
+
--engine=*) engine_req="${a#--engine=}"; continue ;;
|
|
60
|
+
--explain-route) R_EXPLAIN=1; continue ;;
|
|
61
|
+
--help|-h) help=1; R_CLEAN+=("$a"); continue ;;
|
|
62
|
+
--) R_CLEAN+=("$a"); while [ "$i" -lt "$n" ]; do positionals+=("${argv[$i]}"); R_CLEAN+=("${argv[$i]}"); i=$((i + 1)); done; break ;;
|
|
63
|
+
esac
|
|
64
|
+
R_CLEAN+=("$a")
|
|
65
|
+
case "$a" in
|
|
66
|
+
-*)
|
|
67
|
+
local name="${a%%=*}" val=""
|
|
68
|
+
if _route_has "$_ROUTE_VALUE_FLAGS" "$name"; then
|
|
69
|
+
if [ "$name" = "$a" ]; then
|
|
70
|
+
val="${argv[$i]:-}"; i=$((i + 1)); R_CLEAN+=("$val")
|
|
71
|
+
else
|
|
72
|
+
val="${a#*=}"
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
case "$name" in
|
|
76
|
+
--prd) positionals+=("$val"); continue ;;
|
|
77
|
+
--brief) positionals+=("$val"); continue ;;
|
|
78
|
+
--provider) provider_flag="$val"; passthru+=(--provider "$val"); continue ;;
|
|
79
|
+
--session-model) session_model="$val"; continue ;;
|
|
80
|
+
--budget|--budget-limit|--max-cost) passthru+=(--max-cost "$val"); continue ;;
|
|
81
|
+
--json) R_JSON=1; passthru+=(--json); continue ;;
|
|
82
|
+
-v) passthru+=(--verbose); continue ;;
|
|
83
|
+
--attempts) passthru+=(--attempts "$val"); continue ;;
|
|
84
|
+
--spec) passthru+=(--spec "$val"); continue ;;
|
|
85
|
+
esac
|
|
86
|
+
if _route_has "$_ROUTE_NOOP_FLAGS" "$name"; then continue; fi
|
|
87
|
+
if _route_has "$_ROUTE_E10_FLAGS" "$name"; then passthru+=("$name"); continue; fi
|
|
88
|
+
# Everything else is a flag engine10 does not honor.
|
|
89
|
+
if [ -z "$legacy_flag" ]; then legacy_flag="$name"; fi
|
|
90
|
+
;;
|
|
91
|
+
*) positionals+=("$a") ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [ "$help" = "1" ]; then return 0; fi
|
|
96
|
+
|
|
97
|
+
# Engine request: flag wins over env.
|
|
98
|
+
if [ -z "$engine_req" ]; then engine_req="${LOKI_ENGINE:-}"; fi
|
|
99
|
+
engine_req=$(printf '%s' "$engine_req" | tr '[:upper:]' '[:lower:]')
|
|
100
|
+
case "$engine_req" in
|
|
101
|
+
""|legacy) ;;
|
|
102
|
+
engine10|e10|v10|10) engine_req=engine10 ;;
|
|
103
|
+
*) R_ERR="loki: unknown engine '$engine_req' (use --engine legacy or --engine engine10)"; R_DECIDED=1; return 0 ;;
|
|
104
|
+
esac
|
|
105
|
+
|
|
106
|
+
# Input.
|
|
107
|
+
local npos=${#positionals[@]}
|
|
108
|
+
if [ "$npos" -eq 0 ]; then
|
|
109
|
+
R_INPUT_TYPE=none
|
|
110
|
+
local k=0 sp=0
|
|
111
|
+
while [ "$k" -lt "${#passthru[@]}" ]; do
|
|
112
|
+
if [ "${passthru[$k]}" = "--spec" ]; then sp=1; fi
|
|
113
|
+
k=$((k + 1))
|
|
114
|
+
done
|
|
115
|
+
# No positional and no --spec: not ours (bare `loki start` auto-detects a PRD).
|
|
116
|
+
if [ "$sp" = "0" ]; then R_INPUT_TYPE=""; return 0; fi
|
|
117
|
+
R_INPUT_TYPE=spec
|
|
118
|
+
R_INPUT="(from --spec)"
|
|
119
|
+
elif [ "$npos" -eq 1 ]; then
|
|
120
|
+
R_INPUT_TYPE=$(_route_classify "${positionals[0]}")
|
|
121
|
+
R_INPUT="${positionals[0]}"
|
|
122
|
+
else
|
|
123
|
+
R_INPUT_TYPE=task
|
|
124
|
+
R_INPUT="${positionals[*]}"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Bare flag-first form takes over only multi-word tasks and issue refs (a one-word
|
|
128
|
+
# first argument is the command namespace) and only when no unknown flag is present.
|
|
129
|
+
if [ "$verb" = "bare" ]; then
|
|
130
|
+
case "$R_INPUT_TYPE" in
|
|
131
|
+
task|issue) ;;
|
|
132
|
+
*) R_INPUT_TYPE=""; return 0 ;;
|
|
133
|
+
esac
|
|
134
|
+
if [ -n "$legacy_flag" ] && [ -z "$engine_req" ]; then R_INPUT_TYPE=""; return 0; fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
R_DECIDED=1
|
|
138
|
+
if [ "$R_INPUT_TYPE" = "missing-prd" ]; then
|
|
139
|
+
R_ERR="loki: PRD file not found: ${positionals[0]}"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Provider and model (display only; engine10 resolves its own at run time).
|
|
144
|
+
R_PROVIDER="$provider_flag"
|
|
145
|
+
if [ -z "$R_PROVIDER" ]; then R_PROVIDER="${LOKI_PROVIDER:-}"; fi
|
|
146
|
+
if [ -z "$R_PROVIDER" ] && [ -f "${PWD}/.loki/state/provider" ]; then
|
|
147
|
+
R_PROVIDER="$(sed -n '1p' "${PWD}/.loki/state/provider" 2>/dev/null || true)"
|
|
148
|
+
fi
|
|
149
|
+
if [ -z "$R_PROVIDER" ]; then R_PROVIDER=claude; fi
|
|
150
|
+
if [ -n "$session_model" ]; then
|
|
151
|
+
R_MODEL="$session_model"
|
|
152
|
+
elif [ -n "${LOKI_SESSION_MODEL:-}" ]; then
|
|
153
|
+
R_MODEL="$LOKI_SESSION_MODEL"
|
|
154
|
+
elif [ -n "${LOKI_MODEL_ROUTE:-}" ]; then
|
|
155
|
+
R_MODEL="route: $LOKI_MODEL_ROUTE"
|
|
156
|
+
elif [ -n "${LOKI_MODEL_OVERRIDE:-}" ]; then
|
|
157
|
+
R_MODEL="$LOKI_MODEL_OVERRIDE"
|
|
158
|
+
elif [ "$R_PROVIDER" = "opencode" ] && [ -n "${LOKI_OPENCODE_MODEL:-}" ]; then
|
|
159
|
+
R_MODEL="$LOKI_OPENCODE_MODEL"
|
|
160
|
+
elif [ "$R_PROVIDER" = "claude" ]; then
|
|
161
|
+
R_MODEL="${LOKI_CLAUDE_MODEL_DEVELOPMENT:-${LOKI_MODEL_DEVELOPMENT:-claude (provider default)}}"
|
|
162
|
+
else
|
|
163
|
+
R_MODEL="$R_PROVIDER (model not recorded)"
|
|
164
|
+
fi
|
|
165
|
+
R_SESSION_MODEL="$session_model"
|
|
166
|
+
|
|
167
|
+
# Task text for engine10: a PRD file is read, anything else is the words.
|
|
168
|
+
local task
|
|
169
|
+
if [ "$R_INPUT_TYPE" = "prd" ]; then
|
|
170
|
+
task=$(cat "${positionals[0]}" 2>/dev/null || true)
|
|
171
|
+
elif [ "$R_INPUT_TYPE" = "spec" ]; then
|
|
172
|
+
task=""
|
|
173
|
+
else
|
|
174
|
+
task="${positionals[*]}"
|
|
175
|
+
fi
|
|
176
|
+
if [ "$verb" = "quick" ]; then R_E10_ARGS+=(--no-pr); fi
|
|
177
|
+
if [ -n "$task" ]; then R_E10_ARGS+=("$task"); fi
|
|
178
|
+
R_E10_ARGS+=("${passthru[@]+"${passthru[@]}"}")
|
|
179
|
+
|
|
180
|
+
local why=""
|
|
181
|
+
if [ "$engine_req" = "legacy" ]; then
|
|
182
|
+
R_ENGINE=legacy; R_REASON="explicit: --engine legacy or LOKI_ENGINE=legacy"
|
|
183
|
+
elif [ "$engine_req" = "engine10" ]; then
|
|
184
|
+
if [ -n "$legacy_flag" ]; then
|
|
185
|
+
R_ERR="loki: flag $legacy_flag is not supported by the engine10 engine; drop it or use --engine legacy"
|
|
186
|
+
return 0
|
|
187
|
+
fi
|
|
188
|
+
R_ENGINE=engine10; R_E10=1; R_HARD=1; R_REASON="explicit: --engine engine10"
|
|
189
|
+
elif [ -n "$legacy_flag" ]; then
|
|
190
|
+
R_ENGINE=legacy
|
|
191
|
+
R_REASON="flag $legacy_flag is not supported by engine10"
|
|
192
|
+
R_NOTICE="loki: routing this call to the legacy engine because of $legacy_flag (engine10 does not support it). Drop the flag to use engine10, or pass --engine legacy to silence this."
|
|
193
|
+
else
|
|
194
|
+
case "$R_PROVIDER" in
|
|
195
|
+
claude|codex|cline|aider|opencode) ;;
|
|
196
|
+
*) why="provider $R_PROVIDER has no engine10 invoker (use claude, codex, cline, aider or opencode)" ;;
|
|
197
|
+
esac
|
|
198
|
+
if [ -z "$why" ] && [ "$R_INPUT_TYPE" = "prd" ]; then
|
|
199
|
+
local sz
|
|
200
|
+
sz=$(wc -c < "${positionals[0]}" 2>/dev/null | tr -d ' ' || echo 0)
|
|
201
|
+
if [ "${sz:-0}" -gt 100000 ]; then why="PRD is larger than 100000 bytes (engine10 takes the task as one argument)"; fi
|
|
202
|
+
fi
|
|
203
|
+
if [ -n "$why" ]; then
|
|
204
|
+
R_ENGINE=legacy; R_REASON="$why"
|
|
205
|
+
R_NOTICE="loki: routing this call to the legacy engine: $why."
|
|
206
|
+
else
|
|
207
|
+
R_ENGINE=engine10; R_E10=1
|
|
208
|
+
R_REASON="default: $R_INPUT_TYPE input runs on engine10 regardless of flag order"
|
|
209
|
+
fi
|
|
210
|
+
fi
|
|
211
|
+
_route_adapter_and_diffs "$legacy_flag"
|
|
212
|
+
return 0
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Sets R_ADAPTER and R_CAPDIFF[] from R_ENGINE / R_PROVIDER. Empty R_CAPDIFF means the
|
|
216
|
+
# full Claude Code path on the Loki 10 engine. Statements mirror providers/*.sh
|
|
217
|
+
# (PROVIDER_HAS_TASK_TOOL=false for every non-claude provider).
|
|
218
|
+
_route_adapter_and_diffs() {
|
|
219
|
+
local legacy_flag="$1"
|
|
220
|
+
R_CAPDIFF=()
|
|
221
|
+
if [ "$R_ENGINE" = "legacy" ]; then
|
|
222
|
+
R_ADAPTER="legacy-bash:$R_PROVIDER"
|
|
223
|
+
R_CAPDIFF+=("runs the previous bash loop (autonomy/run.sh) instead of the Loki 10 supervisor")
|
|
224
|
+
if [ -n "$legacy_flag" ]; then
|
|
225
|
+
R_CAPDIFF+=("honors $legacy_flag, which engine10 does not support")
|
|
226
|
+
fi
|
|
227
|
+
else
|
|
228
|
+
R_ADAPTER="engine10:$R_PROVIDER"
|
|
229
|
+
fi
|
|
230
|
+
case "$R_PROVIDER" in
|
|
231
|
+
claude) ;;
|
|
232
|
+
codex|cline|aider|opencode)
|
|
233
|
+
R_CAPDIFF+=("provider $R_PROVIDER has no Claude Code Task-tool subagents (providers/$R_PROVIDER.sh)") ;;
|
|
234
|
+
*)
|
|
235
|
+
R_CAPDIFF+=("provider $R_PROVIDER has no engine10 invoker") ;;
|
|
236
|
+
esac
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Print the route (human or --json). Returns 0.
|
|
240
|
+
loki_route_print() {
|
|
241
|
+
local shown="$R_INPUT"
|
|
242
|
+
if [ "${#shown}" -gt 72 ]; then shown="$(printf '%s' "$shown" | cut -c1-69)..."; fi
|
|
243
|
+
if [ "$R_JSON" = "1" ]; then
|
|
244
|
+
local diffs="" d
|
|
245
|
+
for d in ${R_CAPDIFF[@]+"${R_CAPDIFF[@]}"}; do
|
|
246
|
+
diffs="${diffs:+$diffs,}\"$(_route_json_str "$d")\""
|
|
247
|
+
done
|
|
248
|
+
printf '{"engine":"%s","adapter":"%s","provider":"%s","model":"%s","input_type":"%s","input":"%s","reason":"%s","capability_differences":[%s]}\n' \
|
|
249
|
+
"$(_route_json_str "$R_ENGINE")" "$(_route_json_str "$R_ADAPTER")" "$(_route_json_str "$R_PROVIDER")" "$(_route_json_str "$R_MODEL")" \
|
|
250
|
+
"$(_route_json_str "$R_INPUT_TYPE")" "$(_route_json_str "$shown")" "$(_route_json_str "$R_REASON")" "$diffs"
|
|
251
|
+
else
|
|
252
|
+
printf 'engine: %s\nadapter: %s\nprovider: %s\nmodel: %s\ninput: %s (%s)\nreason: %s\n' \
|
|
253
|
+
"$R_ENGINE" "$R_ADAPTER" "$R_PROVIDER" "$R_MODEL" "$R_INPUT_TYPE" "$shown" "$R_REASON"
|
|
254
|
+
local d
|
|
255
|
+
for d in ${R_CAPDIFF[@]+"${R_CAPDIFF[@]}"}; do printf 'differs: %s\n' "$d"; done
|
|
256
|
+
fi
|
|
257
|
+
return 0
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Entry used by bin/loki. Args: the full original argv. Sets R_* and returns:
|
|
261
|
+
# 0 = handled (caller reads R_DECIDED); 7 = `loki route` / --explain-route printed (caller exits 0)
|
|
262
|
+
# 8 = usage or input error already printed (caller exits 2)
|
|
263
|
+
# shellcheck disable=SC2034
|
|
264
|
+
loki_route_dispatch() {
|
|
265
|
+
R_DECIDED=0
|
|
266
|
+
local verb="" first="${1:-}"
|
|
267
|
+
case "$first" in
|
|
268
|
+
route)
|
|
269
|
+
shift
|
|
270
|
+
case "${1:-}" in start|quick) verb="$1"; shift ;; *) verb=start ;; esac
|
|
271
|
+
loki_route_resolve "$verb" "$@"
|
|
272
|
+
if [ -n "$R_ERR" ]; then echo "$R_ERR" >&2; return 8; fi
|
|
273
|
+
if [ "$R_DECIDED" != "1" ]; then
|
|
274
|
+
echo "loki: usage: loki route [start|quick] <task | prd.md | owner/repo#N> [flags] [--json]" >&2; return 8
|
|
275
|
+
fi
|
|
276
|
+
loki_route_print; return 7
|
|
277
|
+
;;
|
|
278
|
+
start|quick)
|
|
279
|
+
verb="$first"; shift
|
|
280
|
+
loki_route_resolve "$verb" "$@"
|
|
281
|
+
;;
|
|
282
|
+
-*)
|
|
283
|
+
loki_route_resolve bare "$@"
|
|
284
|
+
verb=bare
|
|
285
|
+
;;
|
|
286
|
+
*)
|
|
287
|
+
# Positional-first call: only LOKI_ENGINE needs the resolver here.
|
|
288
|
+
if [ -n "${LOKI_ENGINE:-}" ]; then
|
|
289
|
+
case "$first" in
|
|
290
|
+
*" "*|*/*\#[0-9]*|http*://*/issues/*)
|
|
291
|
+
loki_route_resolve bare "$@"; verb=bare ;;
|
|
292
|
+
esac
|
|
293
|
+
fi
|
|
294
|
+
;;
|
|
295
|
+
esac
|
|
296
|
+
if [ "$R_DECIDED" != "1" ]; then return 0; fi
|
|
297
|
+
if [ -n "$R_ERR" ]; then echo "$R_ERR" >&2; return 8; fi
|
|
298
|
+
if [ "$R_EXPLAIN" = "1" ]; then loki_route_print; return 7; fi
|
|
299
|
+
if [ -n "$R_NOTICE" ]; then echo "$R_NOTICE" >&2; fi
|
|
300
|
+
R_VERB="$verb"
|
|
301
|
+
return 0
|
|
302
|
+
}
|
package/autonomy/run.sh
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
# LOKI_RESOURCE_MEM_THRESHOLD - Memory % threshold to warn (default: 80)
|
|
32
32
|
#
|
|
33
33
|
# Budget / Cost Limits (opt-in):
|
|
34
|
-
# LOKI_BUDGET_LIMIT - Max USD spend before auto-pause (default: empty = unlimited)
|
|
34
|
+
# LOKI_BUDGET_LIMIT - Max USD spend before auto-pause (default: loki-ts/data/model-pricing.json default_max_cost_usd, 100 USD; empty = unlimited)
|
|
35
35
|
# Example: "50.00" pauses session when estimated cost >= $50
|
|
36
36
|
#
|
|
37
37
|
# Security & Autonomy Controls (Enterprise):
|
|
@@ -532,7 +532,7 @@ for key, env_var in mapping.items():
|
|
|
532
532
|
val = get_nested(data, key) or data.get(key) or data.get(key.replace('.', '_'))
|
|
533
533
|
if val and isinstance(val, str):
|
|
534
534
|
safe_val = shlex.quote(val)
|
|
535
|
-
print(f'[ -z \"\${{{env_var}:-}}\" ] && export {env_var}={safe_val}')
|
|
535
|
+
print(f'[ -z \"\${{{env_var}:-}}\" ] && export {env_var}={safe_val}' + (\" && export LOKI_BUDGET_SOURCE=config\" if env_var == 'LOKI_BUDGET_LIMIT' else ''))
|
|
536
536
|
" 2>/dev/null)" 2>/dev/null || true
|
|
537
537
|
}
|
|
538
538
|
_LOKI_SETTINGS_FILE="${TARGET_DIR:-.}/.loki/config/settings.json" _load_json_settings
|
|
@@ -565,7 +565,23 @@ RESOURCE_MEM_THRESHOLD=${LOKI_RESOURCE_MEM_THRESHOLD:-80} # Memory % thresho
|
|
|
565
565
|
# it never kills work or discards a deliverable. The user removes .loki/PAUSE
|
|
566
566
|
# to resume, or raises LOKI_BUDGET_LIMIT. Set LOKI_BUDGET_LIMIT="" to restore
|
|
567
567
|
# the old unlimited behavior explicitly.
|
|
568
|
-
|
|
568
|
+
# LM-002: the default value is read from default_max_cost_usd in loki-ts/data/model-pricing.json, the same
|
|
569
|
+
# file the Loki 10 engine reads (docs/cost-controls.md). "100.00" is only the
|
|
570
|
+
# fallback when that file is absent, and budget_cap.test.ts pins it to the file.
|
|
571
|
+
_loki_budget_default="$(sed -n 's/.*"default_max_cost_usd"[[:space:]]*:[[:space:]]*\([0-9][0-9.]*\).*/\1/p' \
|
|
572
|
+
"${SCRIPT_DIR}/../loki-ts/data/model-pricing.json" 2>/dev/null | head -n 1)"
|
|
573
|
+
case "$_loki_budget_default" in
|
|
574
|
+
'' | *[!0-9.]*) _loki_budget_default="100.00" ;;
|
|
575
|
+
*) _loki_budget_default="$(awk -v v="$_loki_budget_default" 'BEGIN{printf "%.2f", v}')" ;;
|
|
576
|
+
esac
|
|
577
|
+
# Where the active cap came from: flag (loki start --budget), config (settings.json),
|
|
578
|
+
# env (LOKI_BUDGET_LIMIT set by the caller), or default. Printed once at run start.
|
|
579
|
+
if [ "${LOKI_BUDGET_LIMIT+set}" = "set" ]; then
|
|
580
|
+
BUDGET_SOURCE="${LOKI_BUDGET_SOURCE:-env}"
|
|
581
|
+
else
|
|
582
|
+
BUDGET_SOURCE="default"
|
|
583
|
+
fi
|
|
584
|
+
BUDGET_LIMIT=${LOKI_BUDGET_LIMIT-"$_loki_budget_default"} # USD. Note `-` not `:-`: an
|
|
569
585
|
# explicit empty value means the
|
|
570
586
|
# operator chose unlimited, and is
|
|
571
587
|
# honored.
|
|
@@ -7536,7 +7552,7 @@ EOF
|
|
|
7536
7552
|
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
7537
7553
|
}
|
|
7538
7554
|
BUDGET_EOF
|
|
7539
|
-
log_info "Budget limit set: \$$BUDGET_LIMIT"
|
|
7555
|
+
log_info "Budget limit set: \$$BUDGET_LIMIT (source: ${BUDGET_SOURCE:-default})"
|
|
7540
7556
|
fi
|
|
7541
7557
|
|
|
7542
7558
|
# v7.0.0 Phase 2: remote->local hydrate. Runs ONCE at session boot (not
|
|
@@ -9041,21 +9057,25 @@ _read_iteration_cost() {
|
|
|
9041
9057
|
if [ -f "$result_cost_file" ]; then
|
|
9042
9058
|
python3 -c "
|
|
9043
9059
|
import json
|
|
9060
|
+
def _c(v):
|
|
9061
|
+
return v if isinstance(v, (int, float)) and not isinstance(v, bool) else 'unmeasured'
|
|
9044
9062
|
try:
|
|
9045
9063
|
d = json.load(open('$result_cost_file'))
|
|
9046
9064
|
print(
|
|
9047
9065
|
d.get('input_tokens', 0) or 0,
|
|
9048
9066
|
d.get('output_tokens', 0) or 0,
|
|
9049
|
-
d.get('total_cost_usd'
|
|
9067
|
+
_c(d.get('total_cost_usd')),
|
|
9050
9068
|
d.get('cache_read_tokens', 0) or 0,
|
|
9051
9069
|
d.get('cache_creation_tokens', 0) or 0,
|
|
9052
9070
|
)
|
|
9053
9071
|
except Exception:
|
|
9054
|
-
print(0, 0,
|
|
9055
|
-
" 2>/dev/null || echo "0 0
|
|
9072
|
+
print(0, 0, 'unmeasured', 0, 0)
|
|
9073
|
+
" 2>/dev/null || echo "0 0 unmeasured 0 0"
|
|
9056
9074
|
elif [ -f ".loki/context/tracking.json" ]; then
|
|
9057
9075
|
python3 -c "
|
|
9058
9076
|
import json
|
|
9077
|
+
def _c(v):
|
|
9078
|
+
return v if isinstance(v, (int, float)) and not isinstance(v, bool) else 'unmeasured'
|
|
9059
9079
|
try:
|
|
9060
9080
|
t = json.load(open('.loki/context/tracking.json'))
|
|
9061
9081
|
iters = t.get('per_iteration', [])
|
|
@@ -9065,17 +9085,17 @@ try:
|
|
|
9065
9085
|
print(
|
|
9066
9086
|
m.get('input_tokens', 0),
|
|
9067
9087
|
m.get('output_tokens', 0),
|
|
9068
|
-
m.get('cost_usd'
|
|
9088
|
+
_c(m.get('cost_usd')),
|
|
9069
9089
|
m.get('cache_read_tokens', 0),
|
|
9070
9090
|
m.get('cache_creation_tokens', 0),
|
|
9071
9091
|
)
|
|
9072
9092
|
else:
|
|
9073
|
-
print(0, 0,
|
|
9093
|
+
print(0, 0, 'unmeasured', 0, 0)
|
|
9074
9094
|
except Exception:
|
|
9075
|
-
print(0, 0,
|
|
9076
|
-
" 2>/dev/null || echo "0 0
|
|
9095
|
+
print(0, 0, 'unmeasured', 0, 0)
|
|
9096
|
+
" 2>/dev/null || echo "0 0 unmeasured 0 0"
|
|
9077
9097
|
else
|
|
9078
|
-
echo "0 0
|
|
9098
|
+
echo "0 0 unmeasured 0 0"
|
|
9079
9099
|
fi
|
|
9080
9100
|
}
|
|
9081
9101
|
|
|
@@ -9192,6 +9212,15 @@ track_iteration_complete() {
|
|
|
9192
9212
|
$iter_cost_line
|
|
9193
9213
|
ITER_COST_EOF
|
|
9194
9214
|
|
|
9215
|
+
# Unknown cost is omitted with a reason, never written as 0 (readers treat absence as unmeasured).
|
|
9216
|
+
local iter_cost_json
|
|
9217
|
+
case "${iter_cost:-unmeasured}" in
|
|
9218
|
+
unmeasured) iter_cost_json=' "cost_unmeasured_reason": "no dollar figure recorded for this iteration",
|
|
9219
|
+
' ;;
|
|
9220
|
+
*) iter_cost_json=" \"cost_usd\": ${iter_cost},
|
|
9221
|
+
" ;;
|
|
9222
|
+
esac
|
|
9223
|
+
|
|
9195
9224
|
cat > ".loki/metrics/efficiency/iteration-${iteration}.json" << EFF_EOF
|
|
9196
9225
|
{
|
|
9197
9226
|
"iteration": $iteration,
|
|
@@ -9204,8 +9233,7 @@ ITER_COST_EOF
|
|
|
9204
9233
|
"output_tokens": ${iter_output:-0},
|
|
9205
9234
|
"cache_read_tokens": ${iter_cache_read:-0},
|
|
9206
9235
|
"cache_creation_tokens": ${iter_cache_creation:-0},
|
|
9207
|
-
"
|
|
9208
|
-
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
9236
|
+
${iter_cost_json} "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
9209
9237
|
}
|
|
9210
9238
|
EFF_EOF
|
|
9211
9239
|
|
|
@@ -29819,6 +29847,12 @@ except (json.JSONDecodeError, OSError): pass
|
|
|
29819
29847
|
_loki_surface_why_hint || true
|
|
29820
29848
|
fi
|
|
29821
29849
|
|
|
29850
|
+
# OC-OUTPUT: unified run summary (same format as engine10); best-effort, never changes the exit code.
|
|
29851
|
+
if [ -f "$SCRIPT_DIR/lib/run-summary.sh" ]; then
|
|
29852
|
+
# shellcheck source=lib/run-summary.sh
|
|
29853
|
+
source "$SCRIPT_DIR/lib/run-summary.sh" 2>/dev/null && loki_run_summary "$result" 2>/dev/null || true
|
|
29854
|
+
fi
|
|
29855
|
+
|
|
29822
29856
|
exit $result
|
|
29823
29857
|
}
|
|
29824
29858
|
|