loki-mode 7.40.0 → 7.41.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +138 -3
- package/autonomy/completion-council.sh +14 -2
- package/autonomy/council-v2.sh +10 -1
- package/autonomy/grill.sh +9 -1
- package/autonomy/lib/claude-flags.sh +321 -0
- package/autonomy/lib/voter-agents.sh +7 -1
- package/autonomy/loki +70 -6
- package/autonomy/run.sh +418 -16
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +95 -2
- package/dashboard/static/index.html +58 -32
- package/docs/INSTALLATION.md +15 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/skills/quality-gates.md +70 -0
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.41.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
398
398
|
|
|
399
399
|
---
|
|
400
400
|
|
|
401
|
-
**v7.
|
|
401
|
+
**v7.41.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.41.1
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -136,6 +136,121 @@ HEALTH_EOF
|
|
|
136
136
|
mv "$tmp_file" "$_APP_RUNNER_DIR/health.json"
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
# Re-derive a detection.json field (type/command) so we can rewrite it after a
|
|
140
|
+
# port reconcile without threading those values through globals. Echoes the raw
|
|
141
|
+
# string value (empty on miss). Mirrors the grep-based read style used by
|
|
142
|
+
# app_runner_status.
|
|
143
|
+
_read_detection_field() {
|
|
144
|
+
local field="$1"
|
|
145
|
+
[ -f "$_APP_RUNNER_DIR/detection.json" ] || return 0
|
|
146
|
+
grep -o "\"${field}\": *\"[^\"]*\"" "$_APP_RUNNER_DIR/detection.json" 2>/dev/null \
|
|
147
|
+
| head -1 | sed 's/.*"\([^"]*\)"$/\1/'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Rewrite detection.json with the reconciled port, preserving type/command.
|
|
151
|
+
_rewrite_detection_port() {
|
|
152
|
+
local d_type d_command
|
|
153
|
+
d_type=$(_read_detection_field "type")
|
|
154
|
+
d_command=$(_read_detection_field "command")
|
|
155
|
+
[ -n "$d_type" ] || return 0
|
|
156
|
+
_write_detection "$d_type" "$d_command"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Fix #2 (finding #597): reconcile the recorded port with the port the app
|
|
160
|
+
# ACTUALLY bound, using the listen line in app.log as the source of truth. This
|
|
161
|
+
# corrects the dashboard Live Preview even when the app ignores PORT and picks
|
|
162
|
+
# its own port. Bounded poll: returns as soon as a listen line is found, and
|
|
163
|
+
# never runs for docker (compose URLs come from published-port mapping) or when
|
|
164
|
+
# no port was recorded. Default window LOKI_APP_PORT_RECONCILE_SECS (default 12)
|
|
165
|
+
# at 0.5s intervals. On no match within the window the recorded port is kept (no
|
|
166
|
+
# regression). Stdout: nothing; mutates _APP_RUNNER_PORT / _APP_RUNNER_URL and
|
|
167
|
+
# rewrites state.json + detection.json only when the real port differs.
|
|
168
|
+
_app_runner_reconcile_port() {
|
|
169
|
+
[ "$_APP_RUNNER_IS_DOCKER" != true ] || return 0
|
|
170
|
+
[ -n "$_APP_RUNNER_PORT" ] && [ "$_APP_RUNNER_PORT" -gt 0 ] 2>/dev/null || return 0
|
|
171
|
+
local log_file="$_APP_RUNNER_DIR/app.log"
|
|
172
|
+
|
|
173
|
+
# Fast path: if the recorded port already serves HTTP, the app honored our
|
|
174
|
+
# chosen port (fix #1 worked) or otherwise bound it -- nothing to reconcile,
|
|
175
|
+
# and we avoid the poll latency entirely. Covers quiet-but-serving apps that
|
|
176
|
+
# never log a recognizable listen line.
|
|
177
|
+
if command -v curl >/dev/null 2>&1 && \
|
|
178
|
+
curl -sf -o /dev/null -m 2 "http://localhost:${_APP_RUNNER_PORT}/" 2>/dev/null; then
|
|
179
|
+
return 0
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
local max_secs="${LOKI_APP_PORT_RECONCILE_SECS:-12}"
|
|
183
|
+
[[ "$max_secs" =~ ^[0-9]+$ ]] || max_secs=12
|
|
184
|
+
local max_iter=$(( max_secs * 2 ))
|
|
185
|
+
[ "$max_iter" -gt 0 ] || max_iter=1
|
|
186
|
+
|
|
187
|
+
local real_port="" iter=0
|
|
188
|
+
while [ "$iter" -lt "$max_iter" ]; do
|
|
189
|
+
if [ -f "$log_file" ]; then
|
|
190
|
+
real_port=$(_parse_listen_port "$log_file")
|
|
191
|
+
[ -n "$real_port" ] && break
|
|
192
|
+
fi
|
|
193
|
+
# Stop early if the process already died (failed start): nothing to wait for.
|
|
194
|
+
if [ -n "$_APP_RUNNER_PID" ] && ! kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
195
|
+
break
|
|
196
|
+
fi
|
|
197
|
+
sleep 0.5
|
|
198
|
+
iter=$(( iter + 1 ))
|
|
199
|
+
done
|
|
200
|
+
|
|
201
|
+
[ -n "$real_port" ] || return 0
|
|
202
|
+
if [ "$real_port" != "$_APP_RUNNER_PORT" ]; then
|
|
203
|
+
log_info "App Runner: reconciled port $_APP_RUNNER_PORT -> $real_port (from app.log listen line)"
|
|
204
|
+
_APP_RUNNER_PORT="$real_port"
|
|
205
|
+
_APP_RUNNER_URL="http://localhost:${real_port}"
|
|
206
|
+
_rewrite_detection_port
|
|
207
|
+
fi
|
|
208
|
+
return 0
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Parse the actual bound port from an app log file. Scans known listen-line
|
|
212
|
+
# shapes in priority order and returns the LAST (most recent) plausible port,
|
|
213
|
+
# tolerating ANSI color codes that dev servers emit. Validates 1-65535. Echoes
|
|
214
|
+
# the port or nothing.
|
|
215
|
+
_parse_listen_port() {
|
|
216
|
+
local file="$1"
|
|
217
|
+
[ -f "$file" ] || return 0
|
|
218
|
+
# Strip ANSI SGR sequences (\e[...m) so color-wrapped URLs still match.
|
|
219
|
+
local clean
|
|
220
|
+
clean=$(sed -E $'s/\x1b\\[[0-9;]*m//g' "$file" 2>/dev/null) || clean=$(cat "$file" 2>/dev/null)
|
|
221
|
+
[ -n "$clean" ] || return 0
|
|
222
|
+
|
|
223
|
+
local candidate=""
|
|
224
|
+
# 1) Explicit URL with a port: http://host:PORT (most reliable).
|
|
225
|
+
candidate=$(printf '%s\n' "$clean" \
|
|
226
|
+
| grep -oiE 'https?://[a-z0-9.\-]+:[0-9]{1,5}' \
|
|
227
|
+
| grep -oE ':[0-9]{1,5}' | tr -d ':' | tail -1)
|
|
228
|
+
# 2) A number anchored to the literal word "port": "port 8080", "port=3000",
|
|
229
|
+
# "port: 5000". This runs BEFORE the bare host:port scan so a clock-style
|
|
230
|
+
# timestamp on the same line (e.g. "12:30:45 ... port 8080") cannot win.
|
|
231
|
+
if [ -z "$candidate" ]; then
|
|
232
|
+
candidate=$(printf '%s\n' "$clean" \
|
|
233
|
+
| grep -ioE 'port[ =:]+[0-9]{1,5}' \
|
|
234
|
+
| grep -oE '[0-9]{1,5}' | tail -1)
|
|
235
|
+
fi
|
|
236
|
+
# 3) Keyword listen lines with a real host token before the colon:
|
|
237
|
+
# "localhost:5173", "0.0.0.0:8080", "127.0.0.1:3000". Requiring a letter
|
|
238
|
+
# or a dot immediately left of the colon excludes "HH:MM" timestamps,
|
|
239
|
+
# which have a digit there.
|
|
240
|
+
if [ -z "$candidate" ]; then
|
|
241
|
+
candidate=$(printf '%s\n' "$clean" \
|
|
242
|
+
| grep -iE 'listen|running on|ready|started|serving|server' \
|
|
243
|
+
| grep -oiE '[a-z.][a-z0-9.\-]*:[0-9]{1,5}' \
|
|
244
|
+
| grep -oE ':[0-9]{1,5}' | tr -d ':' | tail -1)
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
[ -n "$candidate" ] || return 0
|
|
248
|
+
# Validate range 1-65535.
|
|
249
|
+
if [ "$candidate" -ge 1 ] 2>/dev/null && [ "$candidate" -le 65535 ] 2>/dev/null; then
|
|
250
|
+
printf '%s\n' "$candidate"
|
|
251
|
+
fi
|
|
252
|
+
}
|
|
253
|
+
|
|
139
254
|
# Rotate app.log if it exceeds max lines
|
|
140
255
|
_rotate_app_log() {
|
|
141
256
|
local log_file="$_APP_RUNNER_DIR/app.log"
|
|
@@ -606,6 +721,21 @@ app_runner_start() {
|
|
|
606
721
|
log_step "App Runner: starting application ($_APP_RUNNER_METHOD on port $_APP_RUNNER_PORT)..."
|
|
607
722
|
_rotate_app_log
|
|
608
723
|
|
|
724
|
+
# Fix #1 (finding #597): pass Loki's chosen port to the app via the env so the
|
|
725
|
+
# app honors it instead of binding its own default (e.g. a Node app reading
|
|
726
|
+
# `process.env.PORT || 4000` would otherwise bind 4000 while Loki recorded the
|
|
727
|
+
# guessed 3000, leaving the dashboard Live Preview pointed at a dead port).
|
|
728
|
+
# We export PORT plus the common ecosystem aliases. An app that ignores these
|
|
729
|
+
# vars is unaffected; an ignored env var is harmless by definition. We do NOT
|
|
730
|
+
# set HOST/BIND -- changing the bind address can break apps. For docker (which
|
|
731
|
+
# gets its port via published-port mapping, not the child env) this is a no-op
|
|
732
|
+
# at the binary boundary, so we only export for the direct-exec path.
|
|
733
|
+
local _port_env_prefix=""
|
|
734
|
+
if [ "$_APP_RUNNER_IS_DOCKER" != true ] && \
|
|
735
|
+
[ -n "$_APP_RUNNER_PORT" ] && [ "$_APP_RUNNER_PORT" -gt 0 ] 2>/dev/null; then
|
|
736
|
+
_port_env_prefix="export PORT=$_APP_RUNNER_PORT HTTP_PORT=$_APP_RUNNER_PORT SERVER_PORT=$_APP_RUNNER_PORT APP_PORT=$_APP_RUNNER_PORT; "
|
|
737
|
+
fi
|
|
738
|
+
|
|
609
739
|
# Start the process in a new process group
|
|
610
740
|
if command -v setsid >/dev/null 2>&1; then
|
|
611
741
|
_APP_RUNNER_HAS_SETSID=true
|
|
@@ -615,7 +745,7 @@ app_runner_start() {
|
|
|
615
745
|
# Note: $_APP_RUNNER_METHOD has passed _validate_app_command (whitelist).
|
|
616
746
|
# The `--` after `bash -lc` prevents flag injection if the assembled
|
|
617
747
|
# script string ever begins with a `-`.
|
|
618
|
-
(cd "$dir" && setsid bash -lc -- 'echo $$ > "'"$_pgid_file"'"; exec '"$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
748
|
+
(cd "$dir" && setsid bash -lc -- "$_port_env_prefix"'echo $$ > "'"$_pgid_file"'"; exec '"$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
619
749
|
local _subshell_pid=$!
|
|
620
750
|
# Wait briefly for the pgid file to appear, then read the real PGID
|
|
621
751
|
local _pgid_wait=0
|
|
@@ -633,7 +763,7 @@ app_runner_start() {
|
|
|
633
763
|
_APP_RUNNER_HAS_SETSID=false
|
|
634
764
|
# Note: $_APP_RUNNER_METHOD has passed _validate_app_command (whitelist).
|
|
635
765
|
# The `--` after `bash -lc` prevents flag injection.
|
|
636
|
-
(cd "$dir" && bash -lc -- "$_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
766
|
+
(cd "$dir" && bash -lc -- "${_port_env_prefix}exec $_APP_RUNNER_METHOD" >> "$_APP_RUNNER_DIR/app.log" 2>&1) &
|
|
637
767
|
_APP_RUNNER_PID=$!
|
|
638
768
|
fi
|
|
639
769
|
# Register with central PID registry if available
|
|
@@ -675,8 +805,13 @@ app_runner_start() {
|
|
|
675
805
|
return 1
|
|
676
806
|
fi
|
|
677
807
|
elif kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
|
|
808
|
+
# Reconcile recorded port with the port the app actually bound (finding
|
|
809
|
+
# #597), so state.json / detection.json / the preview URL point at the
|
|
810
|
+
# live port even when the app ignored PORT. Mutates globals before the
|
|
811
|
+
# state write below. Bounded; no-op when the app honored the chosen port.
|
|
812
|
+
_app_runner_reconcile_port
|
|
678
813
|
_write_app_state "running"
|
|
679
|
-
log_info "App Runner: application started (PID: $_APP_RUNNER_PID)"
|
|
814
|
+
log_info "App Runner: application started (PID: $_APP_RUNNER_PID) on port $_APP_RUNNER_PORT"
|
|
680
815
|
return 0
|
|
681
816
|
else
|
|
682
817
|
log_error "App Runner: application failed to start"
|
|
@@ -1775,7 +1775,15 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
1775
1775
|
if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
|
|
1776
1776
|
_cm_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
|
|
1777
1777
|
fi
|
|
1778
|
-
|
|
1778
|
+
# caveman HARD-SUPPRESS (parsed output): this council vote is
|
|
1779
|
+
# parsed for "VOTE: APPROVE|REJECT|CANNOT_VALIDATE". A globally-
|
|
1780
|
+
# active caveman would compress/reword that line and silently flip
|
|
1781
|
+
# the vote to the default REJECT, corrupting completion detection.
|
|
1782
|
+
# Disable caveman UNCONDITIONALLY with CAVEMAN_DEFAULT_MODE=off.
|
|
1783
|
+
# Set inline (not via a helper) so the carve-out holds even when
|
|
1784
|
+
# this file is sourced standalone and the helpers are out of scope.
|
|
1785
|
+
# Inlined on `claude` only (does not cross the pipe). No-op absent.
|
|
1786
|
+
verdict=$(echo "$prompt" | env CAVEMAN_DEFAULT_MODE=off claude "${_cm_argv[@]}" -p 2>/dev/null | tail -20)
|
|
1779
1787
|
fi
|
|
1780
1788
|
;;
|
|
1781
1789
|
codex)
|
|
@@ -1870,7 +1878,11 @@ REASON: your reasoning"
|
|
|
1870
1878
|
if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
|
|
1871
1879
|
_co_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
|
|
1872
1880
|
fi
|
|
1873
|
-
|
|
1881
|
+
# caveman HARD-SUPPRESS (parsed output): the devil's-advocate
|
|
1882
|
+
# (contrarian) vote is parsed for "VOTE:". Disable caveman
|
|
1883
|
+
# unconditionally so compression cannot flip the contrarian vote.
|
|
1884
|
+
# Inlined on `claude` only (does not cross the pipe). No-op absent.
|
|
1885
|
+
verdict=$(echo "$prompt" | env CAVEMAN_DEFAULT_MODE=off claude "${_co_argv[@]}" -p 2>/dev/null | tail -20)
|
|
1874
1886
|
fi
|
|
1875
1887
|
;;
|
|
1876
1888
|
codex)
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -276,7 +276,16 @@ Respond ONLY with a valid JSON object. No markdown fencing."
|
|
|
276
276
|
if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
|
|
277
277
|
_c2_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
|
|
278
278
|
fi
|
|
279
|
-
|
|
279
|
+
# caveman HARD-SUPPRESS (parsed output, v7.41.0): this reviewer
|
|
280
|
+
# verdict is captured and parsed for the JSON "verdict" field. A
|
|
281
|
+
# globally-active caveman would compress/reword that JSON and
|
|
282
|
+
# silently flip the verdict to the REJECT fallback. The tree-wide
|
|
283
|
+
# default-off export in claude-flags.sh already covers this (the
|
|
284
|
+
# whole subprocess tree inherits CAVEMAN_DEFAULT_MODE=off); the
|
|
285
|
+
# inline prefix here is belt-and-suspenders so the carve-out is
|
|
286
|
+
# self-documenting and robust to sourcing order. No-op when caveman
|
|
287
|
+
# is absent.
|
|
288
|
+
result=$(echo "$full_prompt" | CAVEMAN_DEFAULT_MODE=off claude "${_c2_argv[@]}" -p 2>/dev/null || echo '{"verdict":"REJECT","reasoning":"review execution failed","issues":[]}')
|
|
280
289
|
else
|
|
281
290
|
result='{"verdict":"REJECT","reasoning":"reviewer CLI unavailable","issues":[]}'
|
|
282
291
|
fi
|
package/autonomy/grill.sh
CHANGED
|
@@ -204,8 +204,16 @@ grill_invoke_provider() {
|
|
|
204
204
|
if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
|
|
205
205
|
_gr_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
|
|
206
206
|
fi
|
|
207
|
+
# caveman HARD-SUPPRESS (parsed output, v7.41.0): the grill output is
|
|
208
|
+
# parsed downstream by loki-grill (and written to report.md as the
|
|
209
|
+
# hardest spec questions). Treat it as parsed: a globally-active
|
|
210
|
+
# caveman would compress/reword the questions. The tree-wide default-off
|
|
211
|
+
# export in claude-flags.sh (sourced at grill.sh:45) already covers
|
|
212
|
+
# this; the inline `env` prefix is belt-and-suspenders. `env` is used
|
|
213
|
+
# (not a bare VAR=val prefix) because the call goes through
|
|
214
|
+
# _grill_with_timeout, where the first token is exec'd as the command.
|
|
207
215
|
out="$(printf '%s' "$prompt" \
|
|
208
|
-
| _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" claude "${_gr_argv[@]}" -p - 2>/dev/null)"
|
|
216
|
+
| _grill_with_timeout "${LOKI_GRILL_TIMEOUT:-180}" env CAVEMAN_DEFAULT_MODE=off claude "${_gr_argv[@]}" -p - 2>/dev/null)"
|
|
209
217
|
if [ -z "$out" ]; then
|
|
210
218
|
_grill_err "provider returned no output (timeout or invocation error)"
|
|
211
219
|
return $GRILL_EXIT_ERROR
|
|
@@ -482,6 +482,327 @@ loki_workflows_enabled() {
|
|
|
482
482
|
[ "${LOKI_USE_CLAUDE_WORKFLOWS:-0}" = "1" ]
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
+
# ---------- v7.x caveman output-token compressor gates ----------
|
|
486
|
+
# caveman (https://github.com/JuliusBrussee/caveman, MIT, vendor-less pin) is a
|
|
487
|
+
# Claude Code SKILL + SessionStart hook that instructs the model to compress its
|
|
488
|
+
# OUTPUT TOKENS only (prose style: lite / full / ultra / wenyan), keeping all
|
|
489
|
+
# technical substance. It wraps NO API, needs NO key, has NO network of its own.
|
|
490
|
+
# Once installed it activates GLOBALLY in Claude Code via a SessionStart hook
|
|
491
|
+
# that reads getDefaultMode() (env CAVEMAN_DEFAULT_MODE > repo .caveman config >
|
|
492
|
+
# user config > "full") and, unless the mode is "off", injects its ruleset.
|
|
493
|
+
#
|
|
494
|
+
# THE MOAT RISK (central, why this is wired the way it is): Loki's trust gates
|
|
495
|
+
# parse EXACT model prose -- council "VOTE: APPROVE|REJECT|CANNOT_VALIDATE", code
|
|
496
|
+
# review "^VERDICT:", the legacy completion-promise grep, the evidence-gate
|
|
497
|
+
# sentinels. A globally-active caveman would compress those subcall outputs and
|
|
498
|
+
# silently flip a verdict to the default (REJECT / not-complete), corrupting the
|
|
499
|
+
# loop. This is the same failure class as the --bare OAuth footgun documented at
|
|
500
|
+
# claude-flags.sh:152-161.
|
|
501
|
+
#
|
|
502
|
+
# THE DESIGN (off by construction, not by convention):
|
|
503
|
+
# - ACTIVATE compression only on FREE-FORM generation (main RARV dev loop +
|
|
504
|
+
# read-only codebase-analysis): inline `CAVEMAN_DEFAULT_MODE=<level> claude`.
|
|
505
|
+
# - HARD-SUPPRESS on EVERY parsed-output subcall (council vote, ^VERDICT:
|
|
506
|
+
# review, adversarial probe, conflict-merge, USAGE regen): inline
|
|
507
|
+
# `CAVEMAN_DEFAULT_MODE=off claude`. The activate hook then deletes its flag
|
|
508
|
+
# and emits nothing (verified: `CAVEMAN_DEFAULT_MODE=off node
|
|
509
|
+
# caveman-activate.js` prints "OK" with no ruleset).
|
|
510
|
+
#
|
|
511
|
+
# Suppression is UNCONDITIONAL and UNGATED (see loki_caveman_suppress_env): it is
|
|
512
|
+
# a harmless no-op env value when caveman is absent and the essential carve-out
|
|
513
|
+
# when caveman is globally present (surface b, or a user's own install) even with
|
|
514
|
+
# LOKI_CAVEMAN=0. NEVER gate suppression on supported/enabled -- that would leave
|
|
515
|
+
# the trust gates unprotected exactly when a user has caveman on but Loki off.
|
|
516
|
+
#
|
|
517
|
+
# Disclosure (honest, no fabricated figures): caveman compresses OUTPUT tokens
|
|
518
|
+
# only, not input/thinking; savings are real but bounded. There is no price API,
|
|
519
|
+
# so we disclose the savings CLASS, never a dollar amount (same posture as the
|
|
520
|
+
# ultrareview/workflows gates).
|
|
521
|
+
|
|
522
|
+
# Version pin (vendor-less). Upgrade by bumping this. The upstream installer pins
|
|
523
|
+
# its hook downloads to PINNED_REF = CAVEMAN_REF || 'v1.9.0' (a git tag), and the
|
|
524
|
+
# curl|bash path delegates to `npx -y github:JuliusBrussee/caveman#<ref>`. We
|
|
525
|
+
# default to 1.9.0 and derive the `v`-prefixed tag in the bootstrap helper.
|
|
526
|
+
LOKI_CAVEMAN_VERSION="${LOKI_CAVEMAN_VERSION:-1.9.0}"
|
|
527
|
+
|
|
528
|
+
# The compression level for free-form activation. Maps directly to caveman's
|
|
529
|
+
# CAVEMAN_DEFAULT_MODE values: lite | full | ultra | wenyan | wenyan-lite |
|
|
530
|
+
# wenyan-full | wenyan-ultra. Never "off" here -- "off" is the suppression value,
|
|
531
|
+
# not an activation level.
|
|
532
|
+
#
|
|
533
|
+
# v7.x #593 -- INTELLIGENT AUTO-SELECTION (no new user knob): when the user does
|
|
534
|
+
# NOT set LOKI_CAVEMAN_LEVEL explicitly, the level is INFERRED per-invocation from
|
|
535
|
+
# the run's existing RARV-tier signal (see _loki_caveman_infer_level). When the
|
|
536
|
+
# user DOES set it, that value overrides the inference entirely (opt-out escape
|
|
537
|
+
# hatch). Capture set-ness BEFORE the ":-full" default clobbers it -- once the
|
|
538
|
+
# default fills the var, "user set full" and "defaulted to full" are
|
|
539
|
+
# indistinguishable, so the inference would silently never fire. ${var+set} is
|
|
540
|
+
# non-empty only when the var was genuinely set (even to empty). The "full"
|
|
541
|
+
# default is kept so the public var still reads "full" for external readers and
|
|
542
|
+
# so a child re-source re-derives USERSET correctly (unexported default).
|
|
543
|
+
if [ -z "${LOKI_CAVEMAN_LEVEL_USERSET+x}" ]; then
|
|
544
|
+
LOKI_CAVEMAN_LEVEL_USERSET="${LOKI_CAVEMAN_LEVEL+set}"
|
|
545
|
+
fi
|
|
546
|
+
LOKI_CAVEMAN_LEVEL="${LOKI_CAVEMAN_LEVEL:-full}"
|
|
547
|
+
|
|
548
|
+
# ---------- DEFAULT-SUPPRESS: off by construction, tree-wide ----------
|
|
549
|
+
# THE MOAT GUARANTEE (v7.41.0 council fix): instead of hand-enumerating every
|
|
550
|
+
# parsed-output trust-gate subcall and remembering to prefix each with
|
|
551
|
+
# CAVEMAN_DEFAULT_MODE=off (a missed site silently corrupts a verdict -- caveman
|
|
552
|
+
# exits 0 with mangled prose and the `|| REJECT` fallback never fires), we flip
|
|
553
|
+
# the ENTIRE process tree to suppressed at the one module every tree sources.
|
|
554
|
+
#
|
|
555
|
+
# claude-flags.sh is sourced by EVERY tree that can spawn a parsed claude
|
|
556
|
+
# subcall: the run.sh orchestrator (via providers/claude.sh), grill.sh (standalone
|
|
557
|
+
# `loki grill`), lib/voter-agents.sh (Phase C agent-dispatch voters), and the
|
|
558
|
+
# loki standalone review/workflows commands (on-demand). council-v2.sh carries no
|
|
559
|
+
# source of its own but only ever runs inside completion-council.sh, which is in
|
|
560
|
+
# the run.sh tree, so it inherits this export too. Exporting off HERE makes the
|
|
561
|
+
# whole spawned subprocess tree inherit suppression -- caveman's SessionStart
|
|
562
|
+
# hook reads process.env CAVEMAN_DEFAULT_MODE -- closing council-v2.sh,
|
|
563
|
+
# voter-agents.sh, grill.sh, every existing parsed subcall, and any FUTURE one by
|
|
564
|
+
# construction. ACTIVATION on the handful of free-form generation sites overrides
|
|
565
|
+
# this per-invocation (CAVEMAN_DEFAULT_MODE=<level> claude ...).
|
|
566
|
+
#
|
|
567
|
+
# Capture the user's pre-existing global CAVEMAN_DEFAULT_MODE BEFORE we clobber
|
|
568
|
+
# it, so the activation path can respect (never RAISE) a user's lower level (see
|
|
569
|
+
# loki_caveman_activate_env). Guarded on UNSET (not empty): a child process that
|
|
570
|
+
# inherits our exported LOKI_CAVEMAN_USER_MODE="" (user had no global mode) and
|
|
571
|
+
# re-sources this file must NOT recapture the now-exported CAVEMAN_DEFAULT_MODE=off
|
|
572
|
+
# as the user mode. ${var+x} is empty only when var is genuinely unset, so the
|
|
573
|
+
# capture runs exactly once across the whole process tree, never recapturing "off".
|
|
574
|
+
if [ -z "${LOKI_CAVEMAN_USER_MODE+x}" ]; then
|
|
575
|
+
LOKI_CAVEMAN_USER_MODE="${CAVEMAN_DEFAULT_MODE:-}"
|
|
576
|
+
fi
|
|
577
|
+
export LOKI_CAVEMAN_USER_MODE
|
|
578
|
+
export CAVEMAN_DEFAULT_MODE=off
|
|
579
|
+
|
|
580
|
+
# ---------- v7.x #593 intelligent compression-level inference ----------
|
|
581
|
+
# Infer the caveman compression level from the run's existing RARV-tier signal,
|
|
582
|
+
# so the level is DECIDED by inspecting the work rather than asked of the user.
|
|
583
|
+
# No new user input is introduced: the tier already drives effort/model selection
|
|
584
|
+
# (loki_effort_for_tier, get_rarv_tier). On the bash route the tier is read from
|
|
585
|
+
# LOKI_CURRENT_TIER (exported by run_autonomous each iteration); the TS mirror
|
|
586
|
+
# (cavemanActivateEnv) receives the same tier vocabulary (planning|development|
|
|
587
|
+
# fast) via call.tier, so both routes infer identically from the same signal.
|
|
588
|
+
#
|
|
589
|
+
# INFERENCE RULE (deterministic, conservative-for-accuracy):
|
|
590
|
+
# planning (Reason phase -- architecture / design / nuanced reasoning) -> lite
|
|
591
|
+
# development (Act / Reflect -- implementation, the prior default) -> full
|
|
592
|
+
# fast (Verify phase -- testing / validation, more routine) -> full
|
|
593
|
+
# unknown / empty tier -> full
|
|
594
|
+
# The auto ceiling is "full": inference NEVER selects ultra. ultra is reachable
|
|
595
|
+
# only via an explicit LOKI_CAVEMAN_LEVEL override (the opt-out escape hatch), so
|
|
596
|
+
# the autonomous path can never compress hard enough to lose technical nuance.
|
|
597
|
+
# "lite" on planning protects the highest-nuance output (architecture/design);
|
|
598
|
+
# everything else stays at the established "full" default. When the tier is
|
|
599
|
+
# unknown we pick the SAFER (established) "full", never something more aggressive.
|
|
600
|
+
_loki_caveman_infer_level() {
|
|
601
|
+
local tier="${1:-${LOKI_CURRENT_TIER:-}}"
|
|
602
|
+
case "$tier" in
|
|
603
|
+
planning) printf '%s' "lite" ;;
|
|
604
|
+
*) printf '%s' "full" ;;
|
|
605
|
+
esac
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# Rank a caveman mode by compression aggressiveness for the no-raise comparison.
|
|
609
|
+
# Higher number = more aggressive (drops more nuance). "off" is the floor; unknown
|
|
610
|
+
# or empty modes rank as -1 (treated as "no opinion", so they never win a min()).
|
|
611
|
+
# The wenyan-* variants mirror their plain counterparts' aggressiveness.
|
|
612
|
+
_loki_caveman_level_rank() {
|
|
613
|
+
case "${1:-}" in
|
|
614
|
+
off) printf '0' ;;
|
|
615
|
+
lite|wenyan-lite) printf '1' ;;
|
|
616
|
+
full|wenyan|wenyan-full) printf '2' ;;
|
|
617
|
+
ultra|wenyan-ultra) printf '3' ;;
|
|
618
|
+
*) printf '%s' '-1' ;;
|
|
619
|
+
esac
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
# Caveman config dir resolution mirrors caveman-config.js getConfigDir(): honors
|
|
623
|
+
# CLAUDE_CONFIG_DIR for the flag file location used to detect an existing install.
|
|
624
|
+
_loki_caveman_claude_dir() {
|
|
625
|
+
printf '%s' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
# True (0) when caveman appears installed: its SessionStart hook file exists in
|
|
629
|
+
# the resolved Claude config dir. Best-effort, read-only. We check the hook the
|
|
630
|
+
# upstream installer writes to ~/.claude/hooks/caveman-activate.js (standalone)
|
|
631
|
+
# OR a plugin install marker. Either presence means activation will fire.
|
|
632
|
+
_loki_caveman_installed() {
|
|
633
|
+
local dir
|
|
634
|
+
dir="$(_loki_caveman_claude_dir)"
|
|
635
|
+
[ -f "$dir/hooks/caveman-activate.js" ] && return 0
|
|
636
|
+
# Plugin install: the activate hook lives under a plugin root; the flag file
|
|
637
|
+
# path is stable. Treat a prior-session flag file as a weaker install signal.
|
|
638
|
+
[ -f "$dir/.caveman-active" ] && return 0
|
|
639
|
+
return 1
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# Capability gate: can caveman compression be USED on this run? Provider is
|
|
643
|
+
# Claude AND the claude CLI is present AND caveman is installed-or-bootstrappable
|
|
644
|
+
# AND it is not disabled by the LOKI_CAVEMAN knob. Returns 0 when usable, 1
|
|
645
|
+
# otherwise (callers emit an honest message + degrade to an uncompressed run).
|
|
646
|
+
# Mirrors loki_workflows_supported's shape (provider + CLI + not-disabled).
|
|
647
|
+
loki_caveman_supported() {
|
|
648
|
+
# Provider must be Claude (Tier 1). caveman is Claude-Code-only.
|
|
649
|
+
[ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
|
|
650
|
+
command -v claude >/dev/null 2>&1 || return 1
|
|
651
|
+
# Opt-out knob also suppresses the capability (no activation when off).
|
|
652
|
+
[ "${LOKI_CAVEMAN:-1}" = "0" ] && return 1
|
|
653
|
+
# Installed now, OR bootstrappable (node + npx present so the pin can install
|
|
654
|
+
# on demand). Either way activation can take effect this run or the next.
|
|
655
|
+
if _loki_caveman_installed; then
|
|
656
|
+
return 0
|
|
657
|
+
fi
|
|
658
|
+
command -v node >/dev/null 2>&1 && command -v npx >/dev/null 2>&1 && return 0
|
|
659
|
+
return 1
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# Activation knob: is caveman compression ENABLED for free-form subcalls?
|
|
663
|
+
# DEFAULT ON (LOKI_CAVEMAN unset or 1). Opt out with LOKI_CAVEMAN=0.
|
|
664
|
+
#
|
|
665
|
+
# CROSS-COUPLING GUARD (moat safety): when LOKI_LEGACY_COMPLETION_MATCH=true the
|
|
666
|
+
# runner detects completion by grepping the MAIN-loop prose for the completion
|
|
667
|
+
# promise (run.sh:9641). Compressing the main loop would mangle that prose and
|
|
668
|
+
# break the legacy detector, so caveman activation is DISABLED whenever the
|
|
669
|
+
# legacy prose-match path is in use. The default completion path (the
|
|
670
|
+
# loki_complete_task MCP tool / COMPLETION_REQUESTED signal file) is immune to
|
|
671
|
+
# compression, so the default config keeps caveman on.
|
|
672
|
+
loki_caveman_enabled() {
|
|
673
|
+
[ "${LOKI_CAVEMAN:-1}" = "0" ] && return 1
|
|
674
|
+
[ "${LOKI_LEGACY_COMPLETION_MATCH:-false}" = "true" ] && return 1
|
|
675
|
+
return 0
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
# The activation env VALUE for a free-form subcall: the configured level, or
|
|
679
|
+
# empty when activation is not warranted (caveman unsupported or disabled). The
|
|
680
|
+
# caller inlines it as a per-invocation env prefix (NEVER `export` -- a persisted
|
|
681
|
+
# export would bleed into later parsed subcalls and defeat the carve-out):
|
|
682
|
+
# _cm_lvl="$(loki_caveman_activate_env)"
|
|
683
|
+
# if [ -n "$_cm_lvl" ]; then
|
|
684
|
+
# CAVEMAN_DEFAULT_MODE="$_cm_lvl" claude ... # free-form only
|
|
685
|
+
# else
|
|
686
|
+
# claude ...
|
|
687
|
+
# fi
|
|
688
|
+
#
|
|
689
|
+
# NO-RAISE (v7.41.0 R2 finding 4): the level returned is the configured Loki
|
|
690
|
+
# level, EXCEPT we never silently RAISE a user who set a LOWER global caveman
|
|
691
|
+
# level. If the user globally chose "lite" (less aggressive, preserves more
|
|
692
|
+
# nuance) we honor "lite" rather than forcing "full" on their free-form output.
|
|
693
|
+
# We only ever lower toward the user's preference, never above it; the activation
|
|
694
|
+
# level itself is the conservative-for-accuracy ceiling. The user's global mode is
|
|
695
|
+
# captured at source time into LOKI_CAVEMAN_USER_MODE before the default-off
|
|
696
|
+
# export clobbers CAVEMAN_DEFAULT_MODE. Unknown / empty user modes (rank -1) are
|
|
697
|
+
# ignored so a malformed value can never accidentally suppress activation.
|
|
698
|
+
loki_caveman_activate_env() {
|
|
699
|
+
loki_caveman_supported || return 0
|
|
700
|
+
loki_caveman_enabled || return 0
|
|
701
|
+
# #593: the level is the EXPLICIT user value when LOKI_CAVEMAN_LEVEL was set
|
|
702
|
+
# (override / opt-out escape hatch), else the INFERRED level from the RARV
|
|
703
|
+
# tier. The no-raise guard below then runs unchanged on this base, so an
|
|
704
|
+
# explicit level is still lowered toward a user's lower global mode exactly
|
|
705
|
+
# as before -- "override" means override the inference, not the no-raise.
|
|
706
|
+
local level
|
|
707
|
+
if [ -n "${LOKI_CAVEMAN_LEVEL_USERSET:-}" ]; then
|
|
708
|
+
level="${LOKI_CAVEMAN_LEVEL:-full}"
|
|
709
|
+
else
|
|
710
|
+
level="$(_loki_caveman_infer_level)"
|
|
711
|
+
fi
|
|
712
|
+
# Respect (never exceed) a user's explicitly-lower global level. A user who
|
|
713
|
+
# globally set CAVEMAN_DEFAULT_MODE=off opted OUT of compression entirely;
|
|
714
|
+
# honor that by activating nothing (empty -> bare claude invocation).
|
|
715
|
+
local user_mode="${LOKI_CAVEMAN_USER_MODE:-}"
|
|
716
|
+
if [ "$user_mode" = "off" ]; then
|
|
717
|
+
return 0
|
|
718
|
+
fi
|
|
719
|
+
if [ -n "$user_mode" ]; then
|
|
720
|
+
local user_rank level_rank
|
|
721
|
+
user_rank="$(_loki_caveman_level_rank "$user_mode")"
|
|
722
|
+
level_rank="$(_loki_caveman_level_rank "$level")"
|
|
723
|
+
# Only defer to the user when their mode is a recognized, lower level.
|
|
724
|
+
if [ "$user_rank" -ge 0 ] && [ "$level_rank" -ge 0 ] && [ "$user_rank" -lt "$level_rank" ]; then
|
|
725
|
+
level="$user_mode"
|
|
726
|
+
fi
|
|
727
|
+
fi
|
|
728
|
+
printf '%s' "$level"
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
# The suppression env VALUE for a parsed-output subcall: ALWAYS "off",
|
|
732
|
+
# UNCONDITIONALLY. Not gated on supported/enabled (see the design note above):
|
|
733
|
+
# it must hard-disable caveman on a trust-gate subcall even when a user has
|
|
734
|
+
# caveman globally on but LOKI_CAVEMAN=0, and it is a harmless no-op env value
|
|
735
|
+
# when caveman is absent. Every parsed-output call site uses this ONE helper so
|
|
736
|
+
# the carve-out is uniform:
|
|
737
|
+
# CAVEMAN_DEFAULT_MODE="$(loki_caveman_suppress_env)" claude ...
|
|
738
|
+
loki_caveman_suppress_env() {
|
|
739
|
+
printf '%s' "off"
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
# Idempotent on-demand bootstrap of caveman at the pinned version. Best-effort:
|
|
743
|
+
# installs once per machine, caches a marker under .loki/ so repeat runs are a
|
|
744
|
+
# no-op, degrades cleanly (run proceeds UNCOMPRESSED) with an honest stderr line
|
|
745
|
+
# if anything is missing or the upstream installer is unreachable. NEVER blocks
|
|
746
|
+
# or fails the run. Returns 0 if caveman is (now) installed, 1 on clean degrade.
|
|
747
|
+
#
|
|
748
|
+
# Opt out with LOKI_CAVEMAN_AUTO_BOOTSTRAP=0. Only attempts when provider==claude
|
|
749
|
+
# and the LOKI_CAVEMAN knob is on.
|
|
750
|
+
#
|
|
751
|
+
# GLOBAL SIDE EFFECT (disclosed): caveman installs GLOBALLY -- the upstream
|
|
752
|
+
# installer adds a SessionStart hook to ~/.claude/settings.json (or
|
|
753
|
+
# $CLAUDE_CONFIG_DIR) that affects EVERY Claude Code session on this machine, not
|
|
754
|
+
# only Loki runs. This is caveman's only install mode. The bootstrap therefore
|
|
755
|
+
# runs the upstream installer exactly as a user's own `curl|bash` would; we do
|
|
756
|
+
# not author or vendor any caveman file. The one-time stderr line below names
|
|
757
|
+
# this so the operator is never surprised.
|
|
758
|
+
#
|
|
759
|
+
# HARDENING: the npx call is forced non-interactive (--non-interactive, plus
|
|
760
|
+
# </dev/null so no stdin read can ever block) and time-bounded (timeout when
|
|
761
|
+
# available) so a stalled network or an unexpected prompt can never hang a user's
|
|
762
|
+
# first `loki start`. caveman's installer is already auto-non-interactive without
|
|
763
|
+
# a TTY, but we belt-and-suspenders it.
|
|
764
|
+
loki_caveman_bootstrap() {
|
|
765
|
+
[ "${LOKI_CAVEMAN:-1}" = "0" ] && return 1
|
|
766
|
+
[ "${LOKI_CAVEMAN_AUTO_BOOTSTRAP:-1}" = "0" ] && return 1
|
|
767
|
+
[ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
|
|
768
|
+
# Already installed -> nothing to do.
|
|
769
|
+
if _loki_caveman_installed; then
|
|
770
|
+
return 0
|
|
771
|
+
fi
|
|
772
|
+
local ver="${LOKI_CAVEMAN_VERSION:-1.9.0}"
|
|
773
|
+
local marker_dir=".loki/state"
|
|
774
|
+
local marker="$marker_dir/caveman-bootstrap-${ver}.done"
|
|
775
|
+
# Cached attempt marker: do not re-attempt a failed install over and over
|
|
776
|
+
# within the same project tree (idempotent one-shot per pinned version).
|
|
777
|
+
if [ -f "$marker" ]; then
|
|
778
|
+
_loki_caveman_installed && return 0 || return 1
|
|
779
|
+
fi
|
|
780
|
+
if ! command -v node >/dev/null 2>&1 || ! command -v npx >/dev/null 2>&1; then
|
|
781
|
+
printf '%s\n' "[caveman] node>=18 + npx required to bootstrap; skipping (run proceeds uncompressed). Install Node or set LOKI_CAVEMAN=0 to silence." >&2
|
|
782
|
+
mkdir -p "$marker_dir" 2>/dev/null && : > "$marker" 2>/dev/null || true
|
|
783
|
+
return 1
|
|
784
|
+
fi
|
|
785
|
+
printf '%s\n' "[caveman] bootstrapping output-token compressor v${ver} (one-time, pinned). NOTE: caveman installs GLOBALLY (a Claude Code SessionStart hook in ~/.claude affecting every Claude Code session). Loki applies it only to free-form generation, NEVER to trust-gate subcalls. Opt out: LOKI_CAVEMAN=0." >&2
|
|
786
|
+
# Pin via the git tag (v-prefixed) on the npx ref AND CAVEMAN_REF so the
|
|
787
|
+
# downloaded hooks match the pinned release. Default install (no --all) wires
|
|
788
|
+
# the Claude Code hook for the detected `claude` CLI. A timeout backstops a
|
|
789
|
+
# network stall; </dev/null guarantees no interactive stdin read blocks.
|
|
790
|
+
local _cm_runner="npx"
|
|
791
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
792
|
+
_cm_runner="timeout 120 npx"
|
|
793
|
+
fi
|
|
794
|
+
if CAVEMAN_REF="v${ver}" $_cm_runner -y "github:JuliusBrussee/caveman#v${ver}" -- --non-interactive >/dev/null 2>&1 </dev/null; then
|
|
795
|
+
mkdir -p "$marker_dir" 2>/dev/null && : > "$marker" 2>/dev/null || true
|
|
796
|
+
if _loki_caveman_installed; then
|
|
797
|
+
printf '%s\n' "[caveman] installed v${ver}." >&2
|
|
798
|
+
return 0
|
|
799
|
+
fi
|
|
800
|
+
fi
|
|
801
|
+
printf '%s\n' "[caveman] bootstrap unavailable (upstream unreachable, timed out, or install failed); run proceeds uncompressed." >&2
|
|
802
|
+
mkdir -p "$marker_dir" 2>/dev/null && : > "$marker" 2>/dev/null || true
|
|
803
|
+
return 1
|
|
804
|
+
}
|
|
805
|
+
|
|
485
806
|
# ---------------------------------------------------------------------------
|
|
486
807
|
# Session-continuity Phase 2 (GitHub #165) -- LOKI_RESUME_SESSION recovery resume
|
|
487
808
|
#
|
|
@@ -250,7 +250,13 @@ loki_council_dispatch_agents() {
|
|
|
250
250
|
local rc=0
|
|
251
251
|
local stderr_log="$COUNCIL_STATE_DIR/votes/dispatch-stderr-${iteration}.log"
|
|
252
252
|
mkdir -p "$(dirname "$stderr_log")" 2>/dev/null || true
|
|
253
|
-
|
|
253
|
+
# caveman HARD-SUPPRESS (parsed output, v7.41.0): the response is parsed for
|
|
254
|
+
# findings[].vote against the JSON Schema. A globally-active caveman would
|
|
255
|
+
# compress/reword it and break the schema match or flip a vote. The tree-wide
|
|
256
|
+
# default-off export in claude-flags.sh (sourced above) already covers this;
|
|
257
|
+
# the inline prefix is belt-and-suspenders, self-documenting, and a no-op when
|
|
258
|
+
# caveman is absent.
|
|
259
|
+
response=$(CAVEMAN_DEFAULT_MODE=off claude --dangerously-skip-permissions \
|
|
254
260
|
-p "$prompt" \
|
|
255
261
|
--agents "$agents_json" \
|
|
256
262
|
--json-schema "$schema_path" 2>"$stderr_log") || rc=$?
|