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.
Files changed (119) 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 +315 -0
  8. package/autonomy/lib/engine10-push.sh +19 -1
  9. package/autonomy/lib/help-core.sh +164 -0
  10. package/autonomy/lib/json-output.sh +38 -0
  11. package/autonomy/lib/proof-generator.py +23 -5
  12. package/autonomy/lib/proof-verify.py +607 -5
  13. package/autonomy/lib/run-summary.sh +112 -0
  14. package/autonomy/lib/setup-requirements.json +35 -0
  15. package/autonomy/loki +337 -46
  16. package/autonomy/provider-offer.sh +95 -47
  17. package/autonomy/route.sh +302 -0
  18. package/autonomy/run.sh +48 -14
  19. package/autonomy/setup-wizard.sh +170 -0
  20. package/autonomy/verify.sh +62 -3
  21. package/bin/loki +73 -18
  22. package/completions/_loki +58 -1
  23. package/completions/loki.bash +57 -5
  24. package/dashboard/__init__.py +1 -1
  25. package/docs/CLI-CORE-VERBS.md +112 -0
  26. package/docs/CLI-REFERENCE.md +9 -1
  27. package/docs/COMPARE-OPENCODE.md +94 -0
  28. package/docs/COMPONENTS.md +1 -0
  29. package/docs/EVALUATING.md +27 -0
  30. package/docs/HARNESS-INTELLIGENCE-PLAN.md +20 -0
  31. package/docs/INSTALLATION.md +56 -12
  32. package/docs/KNOWN-ISSUES.md +8 -9
  33. package/docs/PROOF-PASSPORT.md +114 -0
  34. package/docs/QUICKSTART.md +108 -0
  35. package/docs/alternative-installations.md +103 -0
  36. package/docs/cost-controls.md +139 -0
  37. package/docs/environment-variables.md +45 -2
  38. package/docs/exit-codes.md +129 -12
  39. package/docs/spec-first-intent.md +29 -0
  40. package/docs/v10/CLI-MODERN.md +11 -5
  41. package/docs/v10/ENGINE.md +32 -0
  42. package/docs/v10/GUIDE.md +270 -44
  43. package/docs/v10/OPENCODE-PARITY-PLAN.md +84 -0
  44. package/loki-ts/data/model-pricing.json +1 -0
  45. package/loki-ts/dist/loki.js +1065 -978
  46. package/mcp/__init__.py +1 -1
  47. package/package.json +2 -2
  48. package/packages/control-plane/dist/server.js +1829 -715
  49. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  50. package/plugins/loki-mode/README.md +41 -1
  51. package/plugins/loki-mode/commands/handoff.md +12 -0
  52. package/plugins/loki-mode/commands/loki-verify.md +9 -2
  53. package/plugins/loki-mode/commands/start.md +20 -0
  54. package/plugins/loki-mode/hooks/hooks.json +11 -0
  55. package/plugins/loki-mode/scripts/loki-stop-verify.sh +84 -0
  56. package/providers/claude.sh +16 -2
  57. package/providers/model_route_exec.sh +22 -0
  58. package/providers/model_routes.sh +188 -0
  59. package/skills/providers.md +13 -0
  60. package/templates/agents/planner.md +10 -0
  61. package/templates/agents/reviewer.md +10 -0
  62. package/templates/agents/tester.md +10 -0
  63. package/web-app/dist/assets/{AdminPage-QK-i_x-O.js → AdminPage-Cbu3pXL8.js} +1 -1
  64. package/web-app/dist/assets/{Avatar-DblkLNjB.js → Avatar-DGqGGcGx.js} +1 -1
  65. package/web-app/dist/assets/{Badge-DIV2ny5H.js → Badge-Bhlipt6v.js} +1 -1
  66. package/web-app/dist/assets/{Button--rePJOlQ.js → Button-CpN2sO61.js} +1 -1
  67. package/web-app/dist/assets/{CockpitPage-BZlKxpGy.js → CockpitPage-BqeDJwdL.js} +1 -1
  68. package/web-app/dist/assets/{ComparePage-3_INSVux.js → ComparePage-DUieA2ba.js} +1 -1
  69. package/web-app/dist/assets/{ErrorBoundary-DHG63CNr.js → ErrorBoundary-DTWsYrOm.js} +1 -1
  70. package/web-app/dist/assets/{EvidenceReceiptPanel-DFsBnLlO.js → EvidenceReceiptPanel-BU_B9X19.js} +1 -1
  71. package/web-app/dist/assets/{GitHubIssuesPanel-phPGah98.js → GitHubIssuesPanel-DEZoLOsP.js} +1 -1
  72. package/web-app/dist/assets/{GitHubPRsPanel-T7KQqJmx.js → GitHubPRsPanel-BEsnrq7Y.js} +1 -1
  73. package/web-app/dist/assets/{HomePage-DFjGUF5I.js → HomePage-BoUVuq7h.js} +2 -2
  74. package/web-app/dist/assets/{LoginPage-CorUid_5.js → LoginPage-CmOED7vb.js} +1 -1
  75. package/web-app/dist/assets/{MagicPage-DofbSxKD.js → MagicPage-DATZitrZ.js} +1 -1
  76. package/web-app/dist/assets/{MetricsPage-qzrhhfLC.js → MetricsPage-DGL1v0I-.js} +1 -1
  77. package/web-app/dist/assets/{NotFoundPage-BAZR0mMb.js → NotFoundPage-C2WgOjZd.js} +1 -1
  78. package/web-app/dist/assets/{ProjectPage-BOSUzG9J.js → ProjectPage-BMi1tepj.js} +3 -3
  79. package/web-app/dist/assets/{ProjectsPage-Ba0YLtzD.js → ProjectsPage-vc3y5YPh.js} +1 -1
  80. package/web-app/dist/assets/{SettingsPage-Cr10Ue2K.js → SettingsPage-kbq0Ftvi.js} +1 -1
  81. package/web-app/dist/assets/{ShowcasePage-Czn655fe.js → ShowcasePage-Bn6Q8Kbg.js} +1 -1
  82. package/web-app/dist/assets/{SystemSettingsPage-Dwjnw6Ob.js → SystemSettingsPage-Basmhs_b.js} +1 -1
  83. package/web-app/dist/assets/{TeamsPage-D8nc69Be.js → TeamsPage-BTqOc4TR.js} +1 -1
  84. package/web-app/dist/assets/{TemplatesPage-DHsaCDGv.js → TemplatesPage-2P9002VL.js} +1 -1
  85. package/web-app/dist/assets/{TerminalOutput-DBejni2a.js → TerminalOutput-CAoSPBrg.js} +1 -1
  86. package/web-app/dist/assets/{activity-B2VsBl1C.js → activity-BcWVWCqU.js} +1 -1
  87. package/web-app/dist/assets/{bell-CT77Fq2H.js → bell-CYRqqgZ6.js} +1 -1
  88. package/web-app/dist/assets/{bot-DfaWgF4H.js → bot-CU7gdn3g.js} +1 -1
  89. package/web-app/dist/assets/{check-D1yp7Ndv.js → check-DLlCpz9V.js} +1 -1
  90. package/web-app/dist/assets/{chevron-left-B6Pa8IOX.js → chevron-left-CE9Cb30C.js} +1 -1
  91. package/web-app/dist/assets/{circle-alert-D5jrL_Y7.js → circle-alert-uou4bjua.js} +1 -1
  92. package/web-app/dist/assets/{clock-DIsY_fa_.js → clock-C_dSJcDX.js} +1 -1
  93. package/web-app/dist/assets/{cloud-eGwLh1As.js → cloud-DZ22ITF6.js} +1 -1
  94. package/web-app/dist/assets/{code-xml-CGDIcZ3m.js → code-xml-BrJS0hhS.js} +1 -1
  95. package/web-app/dist/assets/{database-CWpdaykI.js → database-D2HOmxtl.js} +1 -1
  96. package/web-app/dist/assets/{dollar-sign-DZEuTCs6.js → dollar-sign-CQ1vK_6q.js} +1 -1
  97. package/web-app/dist/assets/{file-code-corner-BHBW6m9v.js → file-code-corner-w2vpGm5d.js} +1 -1
  98. package/web-app/dist/assets/{file-plus-D4h1jZSR.js → file-plus-DlF6wjkD.js} +1 -1
  99. package/web-app/dist/assets/{globe-CXdqKgwd.js → globe-Dy3lBJg1.js} +1 -1
  100. package/web-app/dist/assets/{hammer-B3YuuYI_.js → hammer-C3xIiRXN.js} +1 -1
  101. package/web-app/dist/assets/{index-BLEnboSv.js → index-4t-_IW2y.js} +3 -3
  102. package/web-app/dist/assets/{layers-Dvp5nFtw.js → layers-Y8Fs8qXI.js} +1 -1
  103. package/web-app/dist/assets/{loader-circle-DodDmIZ3.js → loader-circle-CxgN1v4m.js} +1 -1
  104. package/web-app/dist/assets/{lock-EiuaH1Ep.js → lock-DFFNbXDs.js} +1 -1
  105. package/web-app/dist/assets/{package-Cx4m2hFg.js → package-5e6qCTUg.js} +1 -1
  106. package/web-app/dist/assets/{plus-D-Sd5rpH.js → plus-C0Go_ytj.js} +1 -1
  107. package/web-app/dist/assets/{refresh-cw-C0w1cdmF.js → refresh-cw-D4i-NW3o.js} +1 -1
  108. package/web-app/dist/assets/{rotate-ccw-C_qk6MTT.js → rotate-ccw-YlKkiUW6.js} +1 -1
  109. package/web-app/dist/assets/{scroll-text-DSxiFtkc.js → scroll-text-4qXOHTap.js} +1 -1
  110. package/web-app/dist/assets/{server-NTJt7I8M.js → server-BB6zXNp9.js} +1 -1
  111. package/web-app/dist/assets/{shield-alert-HBxMQOvH.js → shield-alert-D4tzjB1H.js} +1 -1
  112. package/web-app/dist/assets/{thumbs-up-DEgsHzSD.js → thumbs-up-DZiRwdFj.js} +1 -1
  113. package/web-app/dist/assets/{trash-2-px46FnTD.js → trash-2-Dv_LkRIK.js} +1 -1
  114. package/web-app/dist/assets/{trending-down-CBosVGKa.js → trending-down-BJYp9PQe.js} +1 -1
  115. package/web-app/dist/assets/{trending-up-Dbd7zrQj.js → trending-up-BTcXLklK.js} +1 -1
  116. package/web-app/dist/assets/{upload-nhdFKAs1.js → upload-fcHqwOxr.js} +1 -1
  117. package/web-app/dist/assets/{usePolling-BgaGdinh.js → usePolling-BeJYw_4L.js} +1 -1
  118. package/web-app/dist/assets/{user-CzIihUy9.js → user-BcDwUCsO.js} +1 -1
  119. 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
- # Search the node_modules trees Loki is actually installed into. The root is
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
- BUDGET_LIMIT=${LOKI_BUDGET_LIMIT-"100.00"} # USD. Note `-` not `:-`: an
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', 0) or 0,
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, 0, 0, 0)
9055
- " 2>/dev/null || echo "0 0 0 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', 0),
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, 0, 0, 0)
9093
+ print(0, 0, 'unmeasured', 0, 0)
9074
9094
  except Exception:
9075
- print(0, 0, 0, 0, 0)
9076
- " 2>/dev/null || echo "0 0 0 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 0 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
- "cost_usd": ${iter_cost:-0},
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