loki-mode 7.129.5 → 8.0.0

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 (74) hide show
  1. package/README.md +81 -5
  2. package/SKILL.md +66 -5
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +37 -0
  5. package/autonomy/completion-council.sh +862 -28
  6. package/autonomy/council-v2.sh +39 -0
  7. package/autonomy/crash.sh +3 -2
  8. package/autonomy/grill.sh +42 -0
  9. package/autonomy/hooks/validate-bash.sh +301 -4
  10. package/autonomy/lib/cockpit-render.sh +8 -2
  11. package/autonomy/lib/cr-rematerialize.py +101 -4
  12. package/autonomy/lib/deadline.py +957 -0
  13. package/autonomy/lib/dependency-setup.sh +205 -0
  14. package/autonomy/lib/done-recognition.sh +45 -0
  15. package/autonomy/lib/efficiency_cost.py +13 -6
  16. package/autonomy/lib/no_mock_scan.py +793 -0
  17. package/autonomy/lib/prd-enrich.sh +42 -0
  18. package/autonomy/lib/proof-analytics-props.py +69 -0
  19. package/autonomy/lib/proof-generator.py +282 -95
  20. package/autonomy/lib/proof-template.html +15 -12
  21. package/autonomy/lib/proof-verify.py +151 -102
  22. package/autonomy/lib/requirements_contract.py +848 -0
  23. package/autonomy/lib/sdk-mode.sh +103 -0
  24. package/autonomy/lib/secret-scan.sh +139 -0
  25. package/autonomy/lib/spec-expand.sh +208 -0
  26. package/autonomy/lib/tree_digest.py +266 -0
  27. package/autonomy/lib/voter-agents.sh +58 -8
  28. package/autonomy/lib/workspace_diff.py +124 -0
  29. package/autonomy/loki +189 -17
  30. package/autonomy/playwright-verify.sh +501 -0
  31. package/autonomy/prd-checklist.sh +17 -8
  32. package/autonomy/provider-offer.sh +111 -0
  33. package/autonomy/run.sh +4417 -676
  34. package/autonomy/sandbox.sh +42 -0
  35. package/autonomy/spec-interrogation.sh +148 -5
  36. package/autonomy/spec.sh +118 -1
  37. package/autonomy/telemetry.sh +133 -7
  38. package/autonomy/verify.sh +107 -0
  39. package/bin/loki +110 -3
  40. package/completions/_loki +10 -0
  41. package/completions/loki.bash +1 -1
  42. package/dashboard/__init__.py +1 -1
  43. package/dashboard/audit.py +96 -7
  44. package/dashboard/build_supervisor.py +1619 -0
  45. package/dashboard/control.py +8 -0
  46. package/dashboard/prompt_optimizer.py +7 -0
  47. package/dashboard/server.py +577 -22
  48. package/dashboard/static/trust.html +1 -1
  49. package/docs/ARCHITECTURE-OVERVIEW.md +207 -0
  50. package/docs/AUTONOMI-ECOSYSTEM.md +107 -0
  51. package/docs/INSTALLATION.md +19 -2
  52. package/docs/MODEL-EQUIVALENCE-HARNESS-PLAN.md +70 -0
  53. package/docs/PLANS-INDEX.md +33 -0
  54. package/docs/PRIVACY.md +29 -1
  55. package/docs/SIGNED-RECEIPTS.md +102 -0
  56. package/docs/SONNET5-DEFAULT-PLAN.md +1 -1
  57. package/docs/V8-ACCEPTANCE-TRIAGE-2026-07-24.md +114 -0
  58. package/docs/V8-AGENT-SDK-PLAN.md +692 -0
  59. package/docs/V8-COMPLEXITY-AUDIT-2026-07-24.md +45 -0
  60. package/docs/V8-MAJOR-RELEASE-PLAN.md +137 -0
  61. package/docs/V8-OVERNIGHT-PLAN-2026-07-25.md +82 -0
  62. package/docs/V8-RUNTIME-TRUTH-2026-07-25.md +232 -0
  63. package/docs/V8-SDK-RESEARCH-RAW.md +129 -0
  64. package/docs/alternative-installations.md +4 -4
  65. package/loki-ts/dist/loki.js +674 -285
  66. package/loki-ts/package.json +2 -0
  67. package/mcp/__init__.py +1 -1
  68. package/package.json +7 -6
  69. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  70. package/providers/claude.sh +75 -0
  71. package/providers/codex.sh +85 -13
  72. package/references/sdk-mode.md +106 -0
  73. package/skills/model-selection.md +1 -1
  74. package/skills/quality-gates.md +22 -0
@@ -273,6 +273,45 @@ Respond ONLY with a valid JSON object. No markdown fencing."
273
273
  local result
274
274
  case "${PROVIDER_NAME:-claude}" in
275
275
  claude)
276
+ # v8 RAW-SDK JUDGE PATH (opt-in LOKI_SDK_COUNCIL_V2=1). Run this
277
+ # reviewer verdict via the pure-HTTPS @anthropic-ai/sdk (no claude
278
+ # binary) through the loki-ts `internal sdk-judge` bridge, same schema
279
+ # (council-v2-schema.json), same JSON output the extractor below
280
+ # consumes -> parity by construction. Fail-closed: any miss (flag off,
281
+ # no ANTHROPIC_API_KEY, non-zero, empty) falls through to the claude
282
+ # path. Runs BEFORE the claude-binary guard so the SDK path is
283
+ # genuinely binary-free. Default OFF = zero behavior change.
284
+ if [ "${LOKI_SDK_COUNCIL_V2:-0}" = "1" ]; then
285
+ local _c2_sdk_root _c2_sdk_loki _c2_sdk_schema _c2_sdk_pf _c2_sdk_out _c2_sdk_rc
286
+ _c2_sdk_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
287
+ _c2_sdk_loki="${_c2_sdk_root}/bin/loki"
288
+ _c2_sdk_schema="${_c2_sdk_root}/loki-ts/data/council-v2-schema.json"
289
+ if [ -x "$_c2_sdk_loki" ] && [ -f "$_c2_sdk_schema" ] && command -v bun >/dev/null 2>&1; then
290
+ _c2_sdk_pf="$(mktemp 2>/dev/null)" || _c2_sdk_pf=""
291
+ if [ -n "$_c2_sdk_pf" ]; then
292
+ printf '%s' "$full_prompt" > "$_c2_sdk_pf"
293
+ _c2_sdk_rc=0
294
+ # Explicit in-process timeout + OS-level ceiling on the bun
295
+ # subprocess (council review: default 120s + maxRetries could
296
+ # stack, and a cold-start could hang with no cap). Bound both.
297
+ local _c2_to_s="${LOKI_SDK_REVIEW_TIMEOUT:-180}"
298
+ local _c2_wrap
299
+ if command -v timeout >/dev/null 2>&1; then _c2_wrap="timeout $(( _c2_to_s + 15 ))"
300
+ elif command -v gtimeout >/dev/null 2>&1; then _c2_wrap="gtimeout $(( _c2_to_s + 15 ))"
301
+ else _c2_wrap=""; fi
302
+ _c2_sdk_out="$($_c2_wrap "$_c2_sdk_loki" internal sdk-judge \
303
+ --prompt-file "$_c2_sdk_pf" --schema-file "$_c2_sdk_schema" \
304
+ --model "${LOKI_SDK_JUDGE_MODEL:-claude-haiku-4-5}" --effort low \
305
+ --timeout-ms "$(( _c2_to_s * 1000 ))" 2>/dev/null)" || _c2_sdk_rc=$?
306
+ rm -f "$_c2_sdk_pf" 2>/dev/null || true
307
+ if [ "$_c2_sdk_rc" -eq 0 ] && [ -n "$_c2_sdk_out" ]; then
308
+ echo "$_c2_sdk_out" > "$output_file"
309
+ return 0
310
+ fi
311
+ fi
312
+ fi
313
+ # fall through to the claude path (fail-closed)
314
+ fi
276
315
  if command -v claude &>/dev/null; then
277
316
  # EMBED 2 + 3 (v7.33.0). Council-v2 reviewer verdict. $full_prompt
278
317
  # is self-contained (evidence + PRD + strict JSON output, via
package/autonomy/crash.sh CHANGED
@@ -207,11 +207,12 @@ loki_crash_friction() {
207
207
  # Kept as a callable no-op so existing callers (run.sh) do not break, and still
208
208
  # records the DISCLOSURE_SHOWN sentinel for back-compat with any reader of it.
209
209
  loki_show_disclosure_once() {
210
- local config="${HOME}/.loki/config"
210
+ local config_root="${LOKI_DIR:-${HOME}/.loki}"
211
+ local config="${config_root}/config"
211
212
  if [ -f "$config" ] && grep -q "^DISCLOSURE_SHOWN=true" "$config" 2>/dev/null; then
212
213
  return 0
213
214
  fi
214
- mkdir -p "${HOME}/.loki" 2>/dev/null || return 0
215
+ mkdir -p "$config_root" 2>/dev/null || return 0
215
216
  echo "DISCLOSURE_SHOWN=true" >> "$config" 2>/dev/null || true
216
217
  return 0
217
218
  }
package/autonomy/grill.sh CHANGED
@@ -138,6 +138,14 @@ grill_check_provider() {
138
138
  local provider="${LOKI_PROVIDER:-claude}"
139
139
  case "$provider" in
140
140
  claude)
141
+ # v8: the raw-SDK grill path (LOKI_SDK_GRILL=1) needs no claude binary,
142
+ # so the precondition passes when that path is viable (bridge + bun).
143
+ # grill_invoke_provider still fails closed to claude on an SDK miss.
144
+ if [ "${LOKI_SDK_GRILL:-0}" = "1" ] \
145
+ && [ -x "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/loki" ] \
146
+ && command -v bun >/dev/null 2>&1; then
147
+ return 0
148
+ fi
141
149
  if ! command -v claude >/dev/null 2>&1; then
142
150
  _grill_err "Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
143
151
  return $GRILL_EXIT_ERROR
@@ -181,6 +189,40 @@ grill_invoke_provider() {
181
189
 
182
190
  case "$provider" in
183
191
  claude)
192
+ # v8 RAW-SDK TEXT PATH (opt-in LOKI_SDK_GRILL=1). Free-form grill via
193
+ # the pure-HTTPS @anthropic-ai/sdk (no claude binary) through the
194
+ # loki-ts `internal sdk-text` bridge. Runs BEFORE the claude-binary
195
+ # check so the no-binary deploy win holds. Fail-closed: on ANY miss
196
+ # (flag off, no key, bun/entrypoint absent, non-zero, empty) fall
197
+ # through to the claude path below. Output is the free-form question
198
+ # text loki-grill parses, same as the claude path.
199
+ if [ "${LOKI_SDK_GRILL:-0}" = "1" ]; then
200
+ local _gs_root _gs_loki _gs_pf _gs_out _gs_rc
201
+ _gs_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
202
+ _gs_loki="${_gs_root}/bin/loki"
203
+ if [ -x "$_gs_loki" ] && command -v bun >/dev/null 2>&1; then
204
+ _gs_pf="$(mktemp 2>/dev/null)" || _gs_pf=""
205
+ if [ -n "$_gs_pf" ]; then
206
+ printf '%s' "$prompt" > "$_gs_pf"
207
+ _gs_rc=0
208
+ local _gs_to_s="${LOKI_GRILL_TIMEOUT:-180}"
209
+ local _gs_wrap
210
+ if command -v timeout >/dev/null 2>&1; then _gs_wrap="timeout $(( _gs_to_s + 15 ))"
211
+ elif command -v gtimeout >/dev/null 2>&1; then _gs_wrap="gtimeout $(( _gs_to_s + 15 ))"
212
+ else _gs_wrap=""; fi
213
+ _gs_out="$($_gs_wrap "$_gs_loki" internal sdk-text \
214
+ --prompt-file "$_gs_pf" \
215
+ --model "${LOKI_SDK_GRILL_MODEL:-claude-sonnet-5}" --effort high \
216
+ --timeout-ms "$(( _gs_to_s * 1000 ))" 2>/dev/null)" || _gs_rc=$?
217
+ rm -f "$_gs_pf" 2>/dev/null || true
218
+ if [ "$_gs_rc" -eq 0 ] && [ -n "$_gs_out" ]; then
219
+ printf '%s\n' "$_gs_out"
220
+ return 0
221
+ fi
222
+ fi
223
+ fi
224
+ # fall through to the claude path (fail-closed)
225
+ fi
184
226
  if ! command -v claude >/dev/null 2>&1; then
185
227
  _grill_err "Claude Code CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
186
228
  return $GRILL_EXIT_ERROR
@@ -5,8 +5,306 @@
5
5
  set -uo pipefail
6
6
 
7
7
  INPUT=$(cat)
8
- COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))")
9
- CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('cwd',''))")
8
+
9
+ deny() {
10
+ local reason="${1:-Blocked by Loki command policy}"
11
+ if command -v python3 >/dev/null 2>&1; then
12
+ printf '%s' "$reason" | python3 -c '
13
+ import json, sys
14
+ print(json.dumps({"hookSpecificOutput": {
15
+ "hookEventName": "PreToolUse",
16
+ "permissionDecision": "deny",
17
+ "permissionDecisionReason": sys.stdin.read(),
18
+ }}))
19
+ '
20
+ else
21
+ printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked by Loki command policy"}}'
22
+ fi
23
+ exit 2
24
+ }
25
+
26
+ HOST_GUARD=false
27
+ case "${LOKI_HOST_GUARD:-0}" in
28
+ 1|true|yes|on) HOST_GUARD=true ;;
29
+ esac
30
+
31
+ if ! command -v python3 >/dev/null 2>&1; then
32
+ "$HOST_GUARD" && deny "LOKI_HOST_GUARD: python3 is unavailable, so the Bash command cannot be inspected safely."
33
+ printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
34
+ exit 0
35
+ fi
36
+
37
+ if ! COMMAND=$(printf '%s' "$INPUT" | python3 -c '
38
+ import json, sys
39
+ try:
40
+ data = json.load(sys.stdin)
41
+ command = (data.get("tool_input") or {}).get("command")
42
+ except Exception:
43
+ raise SystemExit(2)
44
+ if not isinstance(command, str) or not command.strip():
45
+ raise SystemExit(2)
46
+ sys.stdout.write(command)
47
+ '); then
48
+ "$HOST_GUARD" && deny "LOKI_HOST_GUARD: malformed or missing Bash command input was denied."
49
+ printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
50
+ exit 0
51
+ fi
52
+
53
+ CWD=$(printf '%s' "$INPUT" | python3 -c '
54
+ import json, sys
55
+ try:
56
+ cwd = json.load(sys.stdin).get("cwd", "")
57
+ except Exception:
58
+ cwd = ""
59
+ sys.stdout.write(cwd if isinstance(cwd, str) else "")
60
+ ' 2>/dev/null || true)
61
+
62
+ if "$HOST_GUARD"; then
63
+ GUARD_ROOT="${LOKI_TARGET_DIR:-$PWD}"
64
+ if [ -z "$CWD" ] || ! _LOKI_GUARD_CWD="$CWD" _LOKI_GUARD_ROOT="$GUARD_ROOT" python3 -c '
65
+ import os
66
+ cwd = os.path.realpath(os.environ["_LOKI_GUARD_CWD"])
67
+ root = os.path.realpath(os.environ["_LOKI_GUARD_ROOT"])
68
+ raise SystemExit(0 if cwd == root or cwd.startswith(root + os.sep) else 1)
69
+ '; then
70
+ deny "LOKI_HOST_GUARD: Bash command cwd is outside the assigned workspace."
71
+ fi
72
+ fi
73
+ CWD="${CWD:-${LOKI_TARGET_DIR:-$PWD}}"
74
+
75
+ # Hosted builds run on a shared host. This guard prevents recognizable direct
76
+ # command mistakes involving host containers, processes, and listening ports.
77
+ # It is not process isolation: an allowed package script or interpreter can
78
+ # perform the same operations internally. Production builds still require a
79
+ # separate container or microVM with no host Docker socket, restricted
80
+ # credentials, and network policy. This parser only inspects text.
81
+ if "$HOST_GUARD"; then
82
+ HOST_GUARD_REASON=$(_LOKI_GUARD_COMMAND="$COMMAND" python3 - <<'PY'
83
+ import os
84
+ import re
85
+ import shlex
86
+
87
+ command = os.environ.get("_LOKI_GUARD_COMMAND", "")
88
+ blocked = {
89
+ "docker": "host Docker access",
90
+ "docker-compose": "host Docker access",
91
+ "kill": "host process control",
92
+ "killall": "host process control",
93
+ "pkill": "host process control",
94
+ "sudo": "host privilege escalation",
95
+ "doas": "host privilege escalation",
96
+ "launchctl": "host service control",
97
+ "service": "host service control",
98
+ "systemctl": "host service control",
99
+ }
100
+ wrappers = {"command", "env", "exec", "nohup", "time"}
101
+ wrapper_value_options = {
102
+ "env": {"-u", "--unset", "-C", "--chdir"},
103
+ "exec": {"-a"},
104
+ "time": {"-f", "--format", "-o", "--output"},
105
+ }
106
+ shells = {"bash", "dash", "ksh", "sh", "zsh"}
107
+ separators = {";", "&", "&&", "|", "||", "(", ")"}
108
+ assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$", re.S)
109
+
110
+
111
+ def basename(value):
112
+ return value.rstrip("/").rsplit("/", 1)[-1]
113
+
114
+
115
+ def words(value):
116
+ lexer = shlex.shlex(value.replace("\n", " ; "), posix=True,
117
+ punctuation_chars=";&|()")
118
+ lexer.whitespace_split = True
119
+ lexer.commenters = ""
120
+ return list(lexer)
121
+
122
+
123
+ def first_command(segment):
124
+ index = 0
125
+ while index < len(segment) and assignment.match(segment[index]):
126
+ index += 1
127
+ while index < len(segment):
128
+ name = basename(segment[index])
129
+ if name not in wrappers:
130
+ return name, index
131
+ index += 1
132
+ while index < len(segment):
133
+ token = segment[index]
134
+ if assignment.match(token):
135
+ index += 1
136
+ continue
137
+ if token == "--":
138
+ index += 1
139
+ break
140
+ if name == "env" and token in {"-S", "--split-string"}:
141
+ return "__uninspectable_wrapper__", index
142
+ if token in wrapper_value_options.get(name, set()):
143
+ index += 2
144
+ continue
145
+ if token.startswith("-"):
146
+ index += 1
147
+ continue
148
+ break
149
+ return "", index
150
+
151
+
152
+ def classify(value, depth=0):
153
+ if depth > 2:
154
+ return "command nesting exceeds the safe inspection limit"
155
+ try:
156
+ tokens = words(value)
157
+ except ValueError:
158
+ return "command text could not be parsed safely"
159
+
160
+ segment = []
161
+ segments = []
162
+ for token in tokens:
163
+ if token in separators:
164
+ if segment:
165
+ segments.append(segment)
166
+ segment = []
167
+ else:
168
+ segment.append(token)
169
+ if segment:
170
+ segments.append(segment)
171
+
172
+ for part in segments:
173
+ name, index = first_command(part)
174
+ if name == "__uninspectable_wrapper__":
175
+ return "command wrapper cannot be inspected safely"
176
+ if name in blocked:
177
+ return blocked[name]
178
+
179
+ args = part[index + 1:]
180
+ if name in shells:
181
+ for pos, arg in enumerate(args[:-1]):
182
+ if arg.startswith("-") and "c" in arg:
183
+ nested = classify(args[pos + 1], depth + 1)
184
+ if nested:
185
+ return nested
186
+
187
+ if name == "xargs":
188
+ for arg in args:
189
+ if arg.startswith("-"):
190
+ continue
191
+ target = basename(arg)
192
+ if target in blocked:
193
+ return blocked[target]
194
+ break
195
+
196
+ if name == "find":
197
+ for pos, arg in enumerate(args[:-1]):
198
+ if arg in {"-exec", "-execdir", "-ok", "-okdir"}:
199
+ target = basename(args[pos + 1])
200
+ if target in blocked:
201
+ return blocked[target]
202
+
203
+ if name in {"bunx", "npx"} and args:
204
+ target = basename(next((arg for arg in args if not arg.startswith("-")), ""))
205
+ if target in blocked:
206
+ return blocked[target]
207
+ if name in {"npm", "pnpm", "yarn"} and args and args[0] in {"exec", "x"}:
208
+ target = basename(next((arg for arg in args[1:] if not arg.startswith("-")), ""))
209
+ if target in blocked:
210
+ return blocked[target]
211
+
212
+ if name == "brew" and args and args[0] == "services":
213
+ return "host service control"
214
+ if name in {"nc", "ncat", "netcat"} and any(arg == "-l" or arg.startswith("-l") for arg in args):
215
+ return "host port listener"
216
+ if name == "socat" and any("LISTEN:" in arg.upper() for arg in args):
217
+ return "host port listener"
218
+
219
+ port_assignment = re.search(
220
+ r"(?<![A-Za-z0-9_])(?:PORT|HOST_PORT|LISTEN_PORT)\s*=\s*['\"]?([0-9]+)",
221
+ command,
222
+ )
223
+ if port_assignment and int(port_assignment.group(1)) != 0:
224
+ return "explicit host port claim"
225
+
226
+ port_flag = re.search(
227
+ r"(?:^|\s)(?:--port|--publish)(?:=|\s+)([0-9]+)",
228
+ command,
229
+ )
230
+ if port_flag and int(port_flag.group(1)) != 0:
231
+ return "explicit host port claim"
232
+
233
+ http_server = re.search(r"\bhttp\.server\s+([0-9]+)", command)
234
+ if http_server and int(http_server.group(1)) != 0:
235
+ return "host port listener"
236
+ return ""
237
+
238
+
239
+ reason = classify(command)
240
+ if reason:
241
+ print("LOKI_HOST_GUARD: refusing " + reason + ". Use the platform sandbox and lifecycle APIs instead.")
242
+ PY
243
+ )
244
+ if [ -n "$HOST_GUARD_REASON" ]; then
245
+ deny "$HOST_GUARD_REASON"
246
+ fi
247
+ fi
248
+
249
+ # The supervised simple-web worker prepares dependencies and owns preview,
250
+ # browser, proof, and process lifecycle. Model-launched installs, watchers, dev
251
+ # servers, and Git loops only add latency or escape the bounded execution plan.
252
+ # Enforce that contract at the command boundary so it applies to every model,
253
+ # not only to models that follow the prompt perfectly.
254
+ if [ "${LOKI_SUPERVISED_BUILD:-0}" = "1" ] \
255
+ && [ "${LOKI_BUILD_PROFILE:-}" = "simple-web" ]; then
256
+ SIMPLE_WEB_GUARD_REASON=$(_LOKI_SIMPLE_COMMAND="$COMMAND" \
257
+ _LOKI_SIMPLE_CWD="$CWD" python3 - <<'PY'
258
+ import json
259
+ import os
260
+ import re
261
+
262
+ command = os.environ.get("_LOKI_SIMPLE_COMMAND", "")
263
+ cwd = os.environ.get("_LOKI_SIMPLE_CWD", "")
264
+
265
+ rules = (
266
+ (
267
+ re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*(?:env\s+[^;&|()]*\s+)?(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|update|upgrade)\b", re.I),
268
+ "dependency changes are owned by the prepared scaffold",
269
+ ),
270
+ (
271
+ re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*(?:npm\s+run\s+dev|pnpm\s+(?:run\s+)?dev|yarn\s+dev|bun\s+run\s+dev|next\s+dev|vite(?:\s|$))", re.I),
272
+ "development servers are owned by the preview harness",
273
+ ),
274
+ (
275
+ re.compile(r"(?:^|[;&|()]|&&|\|\|)\s*git(?:\s|$)", re.I),
276
+ "Git operations are outside the hosted simple-web implementation step",
277
+ ),
278
+ (
279
+ re.compile(r"(?:^|\s)--watch(?:All)?(?:=\S+)?(?:\s|$)", re.I),
280
+ "watch-mode tests do not terminate",
281
+ ),
282
+ )
283
+
284
+ for pattern, reason in rules:
285
+ if pattern.search(command):
286
+ print(reason)
287
+ raise SystemExit(0)
288
+
289
+ # `npm test` can hide a watcher in package.json. Inspect only the assigned
290
+ # workspace manifest and direct the model to a one-shot script when available.
291
+ if re.search(r"(?:^|[;&|()]|&&|\|\|)\s*npm\s+(?:run\s+)?test(?:\s|$)", command, re.I):
292
+ try:
293
+ with open(os.path.join(cwd, "package.json"), encoding="utf-8") as handle:
294
+ script = str((json.load(handle).get("scripts") or {}).get("test") or "")
295
+ except (OSError, ValueError, TypeError):
296
+ script = ""
297
+ if re.search(r"(?:^|\s)--watch(?:All)?(?:=\S+)?(?:\s|$)", script, re.I):
298
+ print("the package test script starts a watcher; use test:ci or a one-shot runner")
299
+ raise SystemExit(0)
300
+
301
+ print("")
302
+ PY
303
+ )
304
+ if [ -n "$SIMPLE_WEB_GUARD_REASON" ]; then
305
+ deny "LOKI_SIMPLE_WEB_GUARD: ${SIMPLE_WEB_GUARD_REASON}."
306
+ fi
307
+ fi
10
308
 
11
309
  # Dangerous command patterns (matched anywhere in the command string)
12
310
  # Safe paths like /tmp/ and relative paths (./) are excluded below
@@ -62,8 +360,7 @@ for pattern in "${BLOCKED_PATTERNS[@]}"; do
62
360
  fi
63
361
  done
64
362
  "$is_safe" && continue
65
- printf '%s' '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Blocked: potentially dangerous command pattern detected"}}'
66
- exit 2
363
+ deny "Blocked: potentially dangerous command pattern detected"
67
364
  fi
68
365
  done
69
366
 
@@ -373,12 +373,18 @@ PYEV
373
373
 
374
374
  # Print a steering hint for the focused run. Steering is opt-in prompt injection;
375
375
  # this only tells the operator how, it does not inject anything.
376
+ # RUN-25 iter 22 (Wave D #3): the hint used to name .loki/steering.md, which
377
+ # NOTHING reads -- a control the UI advertised silently no-op'd (a quiet lie to
378
+ # the operator). The file the loop actually reads is .loki/HUMAN_INPUT.md, gated
379
+ # behind LOKI_PROMPT_INJECTION=1. Name the REAL file + flag (or the `loki steer`
380
+ # helper) so the advertised control genuinely steers the run.
376
381
  cockpit_steer_hint() {
377
382
  local focus_repo="$1"
378
383
  local repo="${focus_repo:-$(pwd)}"
379
384
  printf '\n'
380
385
  echo "Steer this run"
381
- echo " Drop a note the next iteration will read:"
382
- echo " echo 'your guidance' > $repo/.loki/steering.md"
386
+ echo " Drop a note the next iteration will read (needs LOKI_PROMPT_INJECTION=1):"
387
+ echo " loki steer 'your guidance' # writes it for you"
388
+ echo " echo 'your guidance' > $repo/.loki/HUMAN_INPUT.md"
383
389
  echo " Or open the dashboard chat: loki dashboard"
384
390
  }
@@ -32,8 +32,13 @@ import sys
32
32
  _BLOCKING = {"CRITICAL", "HIGH"}
33
33
 
34
34
 
35
- def rematerialize(raw):
36
- """Return legacy VERDICT/FINDINGS text, or raise ValueError(exit_code)."""
35
+ def _payload(raw):
36
+ """Return the structured payload from a CLI envelope or bare SDK result."""
37
+ raw = raw.strip()
38
+ if raw.startswith("```") and raw.endswith("```"):
39
+ first_newline = raw.find("\n")
40
+ if first_newline != -1:
41
+ raw = raw[first_newline + 1 : -3].strip()
37
42
  try:
38
43
  env = json.loads(raw)
39
44
  except Exception:
@@ -54,14 +59,92 @@ def rematerialize(raw):
54
59
  payload = json.loads(res)
55
60
  except Exception:
56
61
  payload = None
62
+ # v8 raw-SDK path: `loki internal sdk-judge` emits the BARE payload object
63
+ # (no CLI envelope), so there is no 'structured_output'/'result' wrapper. If
64
+ # the top-level dict itself carries the payload's 'verdict' key, it IS the
65
+ # payload. The stop_reason guard above already passes for a bare object
66
+ # (no 'stop_reason' key -> None -> allowed).
67
+ if not isinstance(payload, dict) and "verdict" in env:
68
+ payload = env
57
69
  if not isinstance(payload, dict):
58
70
  raise ValueError(5)
59
71
 
72
+ return payload
73
+
74
+
75
+ def _requirement_contract(payload, manifest_source):
76
+ if isinstance(manifest_source, dict):
77
+ manifest = manifest_source
78
+ else:
79
+ try:
80
+ with open(manifest_source, encoding="utf-8") as handle:
81
+ manifest = json.load(handle)
82
+ except (OSError, json.JSONDecodeError):
83
+ raise ValueError(8)
84
+ if not isinstance(manifest, dict):
85
+ raise ValueError(8)
86
+ expected = manifest.get("requirements")
87
+ actual = payload.get("requirements")
88
+ findings = payload.get("findings")
89
+ if (
90
+ set(payload)
91
+ != {"schema", "spec_sha256", "verdict", "requirements", "findings"}
92
+ or manifest.get("schema") != "loki-requirements-manifest/v1"
93
+ or payload.get("schema") != "loki-requirements-verdict/v1"
94
+ or payload.get("spec_sha256") != manifest.get("spec_sha256")
95
+ or not isinstance(expected, list)
96
+ or not expected
97
+ or not isinstance(actual, list)
98
+ or len(actual) != len(expected)
99
+ or not isinstance(findings, list)
100
+ ):
101
+ raise ValueError(8)
102
+
103
+ expected_ids = [item.get("id") for item in expected if isinstance(item, dict)]
104
+ actual_ids = [item.get("id") for item in actual if isinstance(item, dict)]
105
+ if len(expected_ids) != len(expected) or actual_ids != expected_ids:
106
+ raise ValueError(8)
107
+ for item in actual:
108
+ if (
109
+ set(item) != {"id", "status", "evidence"}
110
+ or item.get("status") not in ("PASS", "FAIL")
111
+ or not isinstance(item.get("evidence"), str)
112
+ or not item["evidence"].strip()
113
+ or len(item["evidence"]) > 2000
114
+ ):
115
+ raise ValueError(8)
116
+
117
+ for finding in findings:
118
+ if (
119
+ not isinstance(finding, dict)
120
+ or set(finding) != {"severity", "description"}
121
+ or finding.get("severity")
122
+ not in ("Critical", "High", "Medium", "Low")
123
+ or not isinstance(finding.get("description"), str)
124
+ or not finding["description"].strip()
125
+ or len(finding["description"]) > 2000
126
+ ):
127
+ raise ValueError(8)
128
+
60
129
  verdict = str(payload.get("verdict", "")).strip().upper()
61
130
  if verdict not in ("PASS", "FAIL"):
62
131
  raise ValueError(6)
132
+ if findings or any(item["status"] != "PASS" for item in actual):
133
+ verdict = "FAIL"
134
+ return verdict, findings
135
+
136
+
137
+ def rematerialize(raw, manifest_source=""):
138
+ """Return legacy VERDICT/FINDINGS text, or raise ValueError(exit_code)."""
139
+ payload = _payload(raw)
140
+ if manifest_source:
141
+ verdict, findings = _requirement_contract(payload, manifest_source)
142
+ else:
143
+ verdict = str(payload.get("verdict", "")).strip().upper()
144
+ findings = payload.get("findings")
145
+ if verdict not in ("PASS", "FAIL"):
146
+ raise ValueError(6)
63
147
 
64
- findings = payload.get("findings")
65
148
  if not isinstance(findings, list):
66
149
  findings = []
67
150
 
@@ -101,7 +184,21 @@ def main():
101
184
  return 2
102
185
 
103
186
  try:
104
- text = rematerialize(raw)
187
+ manifest_source = os.environ.get(
188
+ "_LOKI_CR_REQUIREMENTS_MANIFEST_JSON", ""
189
+ )
190
+ if manifest_source:
191
+ try:
192
+ manifest_source = json.loads(manifest_source)
193
+ except json.JSONDecodeError:
194
+ return 8
195
+ else:
196
+ manifest_source = os.environ.get(
197
+ "_LOKI_CR_REQUIREMENTS_MANIFEST", ""
198
+ )
199
+ text = rematerialize(
200
+ raw, manifest_source
201
+ )
105
202
  except ValueError as e:
106
203
  return int(e.args[0])
107
204