loki-mode 11.15.0 → 11.17.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 +3 -3
- package/VERSION +1 -1
- package/autonomy/grill.sh +3 -1
- package/autonomy/hooks/validate-bash.sh +51 -13
- package/autonomy/lib/chat.sh +111 -7
- package/autonomy/lib/claude-flags.sh +20 -0
- package/autonomy/lib/claude_permissions.py +40 -0
- package/autonomy/lib/done-recognition.sh +8 -4
- package/autonomy/lib/engine10-push.sh +30 -5
- package/autonomy/lib/prd-enrich.sh +4 -2
- package/autonomy/lib/proof-generator.py +5 -0
- package/autonomy/lib/proof-verify.py +109 -1
- package/autonomy/lib/trust_trajectory.py +13 -4
- package/autonomy/lib/voter-agents.sh +3 -1
- package/autonomy/loki +175 -26
- package/autonomy/quickstart.sh +21 -2
- package/autonomy/run.sh +16 -6
- package/autonomy/trigger-server.py +11 -5
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +2 -2
- package/dashboard/build_supervisor.py +2 -0
- package/docs/CLI-REFERENCE.md +2 -1
- package/docs/EVALUATING.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BOARD.md +25 -0
- package/docs/v10/CLI-MODERN.md +3 -3
- package/docs/v10/DECISIONS.md +1 -0
- package/docs/v10/ENGINE.md +1 -1
- package/docs/v10/FAILURE-CLASSES.md +18 -0
- package/docs/v10/PROGRESS.md +4 -0
- package/loki-ts/data/finding-schema.json +2 -1
- package/loki-ts/dist/loki.js +1135 -1114
- package/magic/core/debate.py +27 -1
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +3761 -3454
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +6 -3
- package/providers/model_catalog.json +6 -0
- package/src/policies/claude_permissions.js +114 -0
- package/web-app/dist/assets/{AdminPage-bZlvH4TL.js → AdminPage-OVzN6eLm.js} +1 -1
- package/web-app/dist/assets/{Avatar-BZrBstQe.js → Avatar-hCCrxrCZ.js} +1 -1
- package/web-app/dist/assets/{Badge-Q8l8r4Vq.js → Badge-CDcpkhbJ.js} +1 -1
- package/web-app/dist/assets/{Button-B8Bt0A3S.js → Button-8mwWlhEu.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-CIYExrXa.js → CockpitPage-D8cf3u22.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CBOmHdam.js → ComparePage-CIgkbDRY.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BKwDPSnK.js → ErrorBoundary-99kJH9g2.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-KqzjphWl.js → EvidenceReceiptPanel-C9fj32eS.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-lF3GkHRv.js → GitHubIssuesPanel-De43YQ_C.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BCzipPWD.js → GitHubPRsPanel-7g7ZTnkx.js} +1 -1
- package/web-app/dist/assets/{HomePage-CIdnkzaN.js → HomePage-CcNA24ny.js} +2 -2
- package/web-app/dist/assets/{LoginPage-D-178nTT.js → LoginPage-BYjlb-IK.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BmHuzMT8.js → MagicPage-C0Dwt9Na.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-CPCiyDFn.js → MetricsPage-jDGmdcgg.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-B2T6n1gx.js → NotFoundPage-C_ZO8tLE.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DQmpFhb0.js → ProjectPage-B5wnurfQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-BmugzFEd.js → ProjectsPage--EyKt5-D.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-ZB-RrmgM.js → SettingsPage-cB0lnfti.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-BAhwHt4_.js → ShowcasePage-Dl16FDkK.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Bt7sf2Vu.js → SystemSettingsPage-BmAXd4V2.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B-v5j_8q.js → TeamsPage-BFkWVOZe.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-bdqUL-LG.js → TemplatesPage-Bf2kmEUM.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CRHEXBwS.js → TerminalOutput-BItv9Wa6.js} +1 -1
- package/web-app/dist/assets/{activity-C-ROkmXt.js → activity-Df2Awg6I.js} +1 -1
- package/web-app/dist/assets/{bell-DbiCBxQA.js → bell-D-1ztTsJ.js} +1 -1
- package/web-app/dist/assets/{bot-DsIgE_qr.js → bot-A4qgZsKi.js} +1 -1
- package/web-app/dist/assets/{check-CkAdmHWP.js → check-BPfFpF0c.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CYTLivAo.js → chevron-left-CfSJvmDi.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BVIcAsFJ.js → circle-alert-wx2d1dzb.js} +1 -1
- package/web-app/dist/assets/{clock-Dg3Rq0go.js → clock-pQMai6l-.js} +1 -1
- package/web-app/dist/assets/{cloud-CenrqHFl.js → cloud-DWL470r6.js} +1 -1
- package/web-app/dist/assets/{code-xml-C2mvfoug.js → code-xml-BaEQpQLZ.js} +1 -1
- package/web-app/dist/assets/{database-BKa0_eCb.js → database-h-N7WkQN.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DfelUK1S.js → dollar-sign-B5oYiD27.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-CZKWoHVg.js → file-code-corner-fbTtTy0o.js} +1 -1
- package/web-app/dist/assets/{file-plus-CZ-m1zMX.js → file-plus-PaF6HMo8.js} +1 -1
- package/web-app/dist/assets/{globe-S0kv_GwC.js → globe-Ct-GlWID.js} +1 -1
- package/web-app/dist/assets/{hammer-BW1MDdu1.js → hammer-C1oZFX51.js} +1 -1
- package/web-app/dist/assets/{index-ACEYsTc2.js → index-C2egrRhz.js} +3 -3
- package/web-app/dist/assets/{layers-BrXHYZ-L.js → layers-Dt5ZK4v2.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DOdhKFx0.js → loader-circle-DrpMziic.js} +1 -1
- package/web-app/dist/assets/{lock-BcFTyg8J.js → lock-DOez8MmK.js} +1 -1
- package/web-app/dist/assets/{package-D34cMVQG.js → package-ClGIkerr.js} +1 -1
- package/web-app/dist/assets/{plus-CEaMeBxL.js → plus-C004i9ua.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D2Rzxr5U.js → refresh-cw-Ddfk-7Sy.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-C_-EjRLh.js → rotate-ccw-Bu1KgDjS.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BSSCMI_c.js → scroll-text-3q1lW5pm.js} +1 -1
- package/web-app/dist/assets/{server-BKvLonWt.js → server-pd6x4HW1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-DbbQbPdW.js → shield-alert-B9jGJepT.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-NoBD1fUR.js → thumbs-up-Cdcc9nQf.js} +1 -1
- package/web-app/dist/assets/{trash-2-BXz8rAyd.js → trash-2-foSojXHK.js} +1 -1
- package/web-app/dist/assets/{trending-down-D8u535Re.js → trending-down-CTTiqlJ1.js} +1 -1
- package/web-app/dist/assets/{trending-up-q8k3kgiy.js → trending-up-B_MgmRAx.js} +1 -1
- package/web-app/dist/assets/{upload-Cp7_zeBI.js → upload-g6BfALzG.js} +1 -1
- package/web-app/dist/assets/{usePolling-CTHVdcuX.js → usePolling-DrHwvzuh.js} +1 -1
- package/web-app/dist/assets/{user-DF7bfSf_.js → user-COp75m7l.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.17.1
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -200,7 +200,7 @@ This protocol governs **skill module** loading -- task-scoped instruction files
|
|
|
200
200
|
<!-- generated:commands -->
|
|
201
201
|
Commands (generated from `loki-ts/src/cli/registry.ts`; full reference in `docs/CLI-REFERENCE.md`):
|
|
202
202
|
|
|
203
|
-
`version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `proof`, `wiki`, `control`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`
|
|
203
|
+
`version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `proof`, `wiki`, `control`, `intent`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`, `steer`
|
|
204
204
|
<!-- /generated -->
|
|
205
205
|
|
|
206
206
|
---
|
|
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
**v11.
|
|
486
|
+
**v11.17.1 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.17.1
|
package/autonomy/grill.sh
CHANGED
|
@@ -245,7 +245,9 @@ grill_invoke_provider() {
|
|
|
245
245
|
# must never mutate the tree (defense-in-depth). Deny Edit/Write/
|
|
246
246
|
# NotebookEdit + git mutations. Opt out LOKI_REVIEW_TOOL_GUARD=0.
|
|
247
247
|
# Type-guarded so an environment without the helper degrades cleanly.
|
|
248
|
-
|
|
248
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
249
|
+
! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
|
|
250
|
+
local _gr_argv=("${_LOKI_PERM_ARGV[@]}" "--model" "${LOKI_GRILL_MODEL:-sonnet}")
|
|
249
251
|
if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
|
|
250
252
|
_gr_argv+=("--bare")
|
|
251
253
|
fi
|
|
@@ -28,8 +28,16 @@ case "${LOKI_HOST_GUARD:-0}" in
|
|
|
28
28
|
1|true|yes|on) HOST_GUARD=true ;;
|
|
29
29
|
esac
|
|
30
30
|
|
|
31
|
+
# P12-26d: under LOKI_PERMISSION_POLICY=compiled the compiled allow/deny list
|
|
32
|
+
# and permission mode decide. An explicit hook "allow" skips the permission
|
|
33
|
+
# prompt for every command that passes the blocklist, which would widen the
|
|
34
|
+
# compiled policy, so the hook never emits one and fails closed on bad input.
|
|
35
|
+
COMPILED_POLICY=false
|
|
36
|
+
[ "${LOKI_PERMISSION_POLICY:-}" = "compiled" ] && COMPILED_POLICY=true
|
|
37
|
+
|
|
31
38
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
32
39
|
"$HOST_GUARD" && deny "LOKI_HOST_GUARD: python3 is unavailable, so the Bash command cannot be inspected safely."
|
|
40
|
+
"$COMPILED_POLICY" && deny "LOKI_PERMISSION_POLICY=compiled: python3 is unavailable, so the Bash command cannot be inspected safely."
|
|
33
41
|
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
34
42
|
exit 0
|
|
35
43
|
fi
|
|
@@ -46,6 +54,7 @@ if not isinstance(command, str) or not command.strip():
|
|
|
46
54
|
sys.stdout.write(command)
|
|
47
55
|
'); then
|
|
48
56
|
"$HOST_GUARD" && deny "LOKI_HOST_GUARD: malformed or missing Bash command input was denied."
|
|
57
|
+
"$COMPILED_POLICY" && deny "LOKI_PERMISSION_POLICY=compiled: malformed or missing Bash command input was denied."
|
|
49
58
|
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
|
|
50
59
|
exit 0
|
|
51
60
|
fi
|
|
@@ -342,24 +351,50 @@ BLOCKED_PATTERNS=(
|
|
|
342
351
|
":\(\) *\{.*\|.*&"
|
|
343
352
|
)
|
|
344
353
|
|
|
345
|
-
# Safe path patterns that override blocked pattern matches
|
|
354
|
+
# Safe path patterns that override blocked pattern matches. They are matched
|
|
355
|
+
# per SEGMENT (the command split on ; & | newline CR), never against the whole
|
|
356
|
+
# string: an unanchored or per-line match let `rm -rf /tmp/x; rm -rf /` and
|
|
357
|
+
# `rm -rf /tmp/x<newline>rm -rf /` ride the override. Only the segment that hit
|
|
358
|
+
# a blocked pattern must be safe; an optional trailing /dev/null redirection is
|
|
359
|
+
# allowed, and `*` and `$$` in /tmp names. A `..` anywhere in the segment is
|
|
360
|
+
# never safe.
|
|
361
|
+
_SAFE_REDIR='([[:space:]]+[0-9]*>>?[[:space:]]*/dev/null)*[[:space:]]*'
|
|
362
|
+
_SAFE_ENV='([A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9._/:-]*[[:space:]]+)*'
|
|
363
|
+
# A dot-leading path component with a glob char (`/tmp/.*` expands to `/tmp/..`).
|
|
364
|
+
_DOT_GLOB='/\.[^/[:space:]]*[*?[]'
|
|
346
365
|
SAFE_PATTERNS=(
|
|
347
|
-
"rm -rf /tmp/"
|
|
348
|
-
"rm -rf \.loki/queue/dead-letter"
|
|
366
|
+
'^[[:space:]]*'"$_SAFE_ENV"'rm -rf (/tmp/([A-Za-z0-9._/*-]|\$\$)+[[:space:]]*)+'"$_SAFE_REDIR"'$'
|
|
367
|
+
'^[[:space:]]*'"$_SAFE_ENV"'rm -rf \.loki/queue/dead-letter[A-Za-z0-9._/-]*'"$_SAFE_REDIR"'$'
|
|
349
368
|
)
|
|
350
369
|
|
|
370
|
+
_segment_is_safe() {
|
|
371
|
+
local seg="$1" safe
|
|
372
|
+
[[ "$seg" == *".."* ]] && return 1
|
|
373
|
+
[[ "$seg" =~ $_DOT_GLOB ]] && return 1
|
|
374
|
+
for safe in "${SAFE_PATTERNS[@]}"; do
|
|
375
|
+
[[ "$seg" =~ $safe ]] && return 0
|
|
376
|
+
done
|
|
377
|
+
return 1
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Succeeds when every segment hitting $1 is safe and at least one segment hits
|
|
381
|
+
# (a pattern that only matches across segments, like `curl x | sh`, is unsafe).
|
|
382
|
+
_blocked_hits_all_safe() {
|
|
383
|
+
local pat="$1" flat seg hit=false
|
|
384
|
+
# Strip fd duplication (2>&1) first: its `&` is not a command separator, and removing it hides no command.
|
|
385
|
+
flat=$(printf '%s' "$COMMAND" | sed -E 's/[0-9]*>&[0-9]+//g' | tr ';&|\r' '\n')
|
|
386
|
+
while IFS= read -r seg; do
|
|
387
|
+
printf '%s' "$seg" | grep -qE "$pat" || continue
|
|
388
|
+
hit=true
|
|
389
|
+
_segment_is_safe "$seg" || return 1
|
|
390
|
+
done <<< "$flat"
|
|
391
|
+
"$hit"
|
|
392
|
+
}
|
|
393
|
+
|
|
351
394
|
# Check for blocked patterns
|
|
352
395
|
for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
353
396
|
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
354
|
-
|
|
355
|
-
is_safe=false
|
|
356
|
-
for safe in "${SAFE_PATTERNS[@]}"; do
|
|
357
|
-
if echo "$COMMAND" | grep -qE "$safe"; then
|
|
358
|
-
is_safe=true
|
|
359
|
-
break
|
|
360
|
-
fi
|
|
361
|
-
done
|
|
362
|
-
"$is_safe" && continue
|
|
397
|
+
_blocked_hits_all_safe "$pattern" && continue
|
|
363
398
|
deny "Blocked: potentially dangerous command pattern detected"
|
|
364
399
|
fi
|
|
365
400
|
done
|
|
@@ -370,7 +405,10 @@ mkdir -p "$LOG_DIR"
|
|
|
370
405
|
printf '%s' "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"command\":$(echo "$COMMAND" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')}" >> "$LOG_DIR/bash-audit.jsonl"
|
|
371
406
|
echo >> "$LOG_DIR/bash-audit.jsonl"
|
|
372
407
|
|
|
373
|
-
# Allow command
|
|
408
|
+
# Allow command. Compiled policy: no decision, the permission flow decides.
|
|
409
|
+
if "$COMPILED_POLICY"; then
|
|
410
|
+
exit 0
|
|
411
|
+
fi
|
|
374
412
|
printf '%s' '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}}'
|
|
375
413
|
|
|
376
414
|
exit 0
|
package/autonomy/lib/chat.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Sourced by autonomy/loki; defines cmd_chat. Free text goes to the provider
|
|
4
4
|
# in the current mode; slash commands delegate to existing loki commands.
|
|
5
5
|
|
|
6
|
-
_LOKI_CHAT_SYSTEM_PROMPT="You are running inside Loki Mode chat. Be concise. Prefer small, verifiable changes. Loki commands available to the user: loki start (also --workspace and --deploy), loki quick, loki verify, loki receipt, loki undo."
|
|
6
|
+
_LOKI_CHAT_SYSTEM_PROMPT="You are running inside Loki Mode chat. Be concise. Prefer small, verifiable changes. Loki commands available to the user: loki start (also --workspace and --deploy), loki quick, loki verify, loki receipt, loki undo. During a run, /steer sends a mid-run directive via loki steer (applied at the next plan or implement stage); /pause and /resume control the run, and /answer replies to a blocked run."
|
|
7
7
|
|
|
8
8
|
_chat_loki_bin() {
|
|
9
9
|
local d="${_LOKI_SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
@@ -41,7 +41,14 @@ _chat_help() {
|
|
|
41
41
|
Loki chat commands:
|
|
42
42
|
/plan read-only mode (no edits)
|
|
43
43
|
/build edit mode
|
|
44
|
-
/run <prd|brief> loki start with this session's model and provider
|
|
44
|
+
/run <prd|brief> loki start with this session's model and provider, in the background (alias /start)
|
|
45
|
+
/start <prd|brief> alias of /run
|
|
46
|
+
/status loki status
|
|
47
|
+
/steer <msg> send a mid-run directive via loki steer (shows the "Steer rev N received" ack)
|
|
48
|
+
/stop loki stop
|
|
49
|
+
/pause loki pause (the run halts at the next iteration boundary)
|
|
50
|
+
/resume loki resume
|
|
51
|
+
/answer <text> loki answer --text: answer a BLOCKED run with a follow-up run
|
|
45
52
|
/workspace <name> <brief> loki start --workspace (multi-repo run, composite receipt)
|
|
46
53
|
/deploy [--health-url U] [--auto-rollback] loki deploy --execute after you confirm (gated on a VERIFIED receipt)
|
|
47
54
|
/attach attach to the current or most recent loki start run in this repo
|
|
@@ -64,6 +71,98 @@ HLP
|
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
|
|
74
|
+
# /steer: record source=chat and the chat user as actor (D02). Echo loki steer's own
|
|
75
|
+
# "Steer rev N received" ack; never claim receipt it did not print.
|
|
76
|
+
_chat_steer() {
|
|
77
|
+
local loki="$1" msg="$2" out rc=0 first
|
|
78
|
+
case "$msg" in
|
|
79
|
+
""|help|h) echo "usage: /steer <message> (not help)"; return 0 ;;
|
|
80
|
+
esac
|
|
81
|
+
out="$(LOKI_STEER_SOURCE=chat LOKI_STEER_ACTOR="${LOKI_STEER_ACTOR:-${USER:-chat}}" "$loki" steer -- "$msg" 2>&1)" || rc=$?
|
|
82
|
+
[ -z "$out" ] || printf '%s\n' "$out"
|
|
83
|
+
if [ "$rc" -ne 0 ]; then echo "(exit $rc)"
|
|
84
|
+
elif ! printf '%s\n' "$out" | grep -q 'Steer rev [0-9][0-9]* received'; then
|
|
85
|
+
first="$(printf '%s\n' "$out" | tr -d '\000-\010\013-\037\177' | sed -n '/[^[:space:]]/{p;q;}' | cut -c1-120)"
|
|
86
|
+
echo "Steer not confirmed: ${first:-no confirmation from loki steer}"
|
|
87
|
+
fi
|
|
88
|
+
return 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Monotonic milliseconds and a ms sleep; both are functions so tests can inject a fake clock.
|
|
92
|
+
# The clock source is chosen once per launch (_CHAT_CLOCK) so every read shares one base:
|
|
93
|
+
# perl clock_gettime, else whole seconds from $SECONDS.
|
|
94
|
+
_chat_clock_pick() {
|
|
95
|
+
if perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -e 'exit 0' >/dev/null 2>&1; then _CHAT_CLOCK=perl; else _CHAT_CLOCK=seconds; fi
|
|
96
|
+
}
|
|
97
|
+
_chat_now_ms() {
|
|
98
|
+
[ -n "${_CHAT_CLOCK:-}" ] || _chat_clock_pick
|
|
99
|
+
if [ "$_CHAT_CLOCK" = perl ]; then
|
|
100
|
+
perl -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC -e 'printf "%d\n", clock_gettime(CLOCK_MONOTONIC) * 1000'
|
|
101
|
+
else
|
|
102
|
+
echo $(( SECONDS * 1000 ))
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
_chat_sleep_ms() {
|
|
106
|
+
local ms="$1"
|
|
107
|
+
[ "$ms" -gt 0 ] 2>/dev/null || return 0
|
|
108
|
+
sleep "$(( ms / 1000 )).$(printf '%03d' $(( ms % 1000 )))"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# /run launch: background `loki start`, then print "Run started" only after the same
|
|
112
|
+
# signals `loki status` reads show a live new run (a new engine10 run dir or a live pid file
|
|
113
|
+
# that was not live before). A start that exits first reports its real error and the log path.
|
|
114
|
+
# Returns non-zero when the start is not proven.
|
|
115
|
+
_chat_run_launch() {
|
|
116
|
+
local loki="$1" input="$2" root="${LOKI_DIR:-.loki}" log pid rc=0 start_ms deadline_ms now_ms rem_ms iter=0 max_iter before_dir before_state
|
|
117
|
+
log="$root/chat-run.log"
|
|
118
|
+
mkdir -p "$root"
|
|
119
|
+
_chat_clock_pick
|
|
120
|
+
start_ms="$(_chat_now_ms)" || start_ms=""
|
|
121
|
+
case "$start_ms" in
|
|
122
|
+
''|*[!0-9]*)
|
|
123
|
+
echo "Run not started (clock read failed before launch). Retry /run." >&2
|
|
124
|
+
return 1
|
|
125
|
+
;;
|
|
126
|
+
esac
|
|
127
|
+
before_dir="$(_chat_run_dir)"; before_state="$(_chat_run_state "$before_dir")"
|
|
128
|
+
_chat_run_start "$loki" "$input" < /dev/null > "$log" 2>&1 &
|
|
129
|
+
pid=$!
|
|
130
|
+
local wait_s="${LOKI_CHAT_START_WAIT:-15}"
|
|
131
|
+
case "$wait_s" in ''|*[!0-9]*) wait_s=15 ;; esac
|
|
132
|
+
[ "$wait_s" -ge 1 ] || wait_s=1
|
|
133
|
+
deadline_ms=$(( start_ms + wait_s * 1000 ))
|
|
134
|
+
# Whole-second clock floors, so allow 1s of slack rather than give up early.
|
|
135
|
+
[ "$_CHAT_CLOCK" = perl ] || deadline_ms=$(( deadline_ms + 1000 ))
|
|
136
|
+
max_iter=$(( wait_s * 5 + 10 ))
|
|
137
|
+
while :; do
|
|
138
|
+
iter=$(( iter + 1 ))
|
|
139
|
+
local nd ns
|
|
140
|
+
nd="$(_chat_run_dir)"; ns="$(_chat_run_state "$nd")"
|
|
141
|
+
if [ "$ns" = "running" ] && { { [ "$nd" != "$before_dir" ] && _chat_pidfile_live "$nd/run.pid"; } || { [ "$nd" = "$before_dir" ] && [ "$before_state" != "running" ]; }; }; then
|
|
142
|
+
echo "Run started in the background (pid $pid). Output: $log"
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
146
|
+
wait "$pid" 2>/dev/null || true
|
|
147
|
+
rc="$(sed -n 's/.*(run finished, exit \([0-9][0-9]*\).*/\1/p' "$log" 2>/dev/null | tail -n1)"
|
|
148
|
+
case "$rc" in ''|0) rc=1 ;; esac
|
|
149
|
+
echo "Run did not start (loki start exited $rc before a live run appeared). Log: $log" >&2
|
|
150
|
+
tail -n 5 "$log" 2>/dev/null | cut -c1-200 >&2
|
|
151
|
+
return "$rc"
|
|
152
|
+
fi
|
|
153
|
+
# A failed or non-numeric clock read (perl fork EAGAIN) counts as the deadline: never mix clock bases.
|
|
154
|
+
now_ms="$(_chat_now_ms)" || break
|
|
155
|
+
case "$now_ms" in ''|*[!0-9]*) break ;; esac
|
|
156
|
+
rem_ms=$(( deadline_ms - now_ms ))
|
|
157
|
+
[ "$rem_ms" -gt 0 ] || break
|
|
158
|
+
[ "$iter" -lt "$max_iter" ] || break
|
|
159
|
+
[ "$rem_ms" -le 200 ] || rem_ms=200
|
|
160
|
+
_chat_sleep_ms "$rem_ms"
|
|
161
|
+
done
|
|
162
|
+
echo "Run start not confirmed after ${wait_s}s (no live run state; launcher pid $pid still running). Log: $log" >&2
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
|
|
67
166
|
# Run a delegate without letting a non-zero exit end the session (autonomy/loki runs under set -e).
|
|
68
167
|
_chat_do() { local _rc=0; "$@" || _rc=$?; [ "$_rc" -eq 0 ] || echo "(exit $_rc)"; return 0; }
|
|
69
168
|
|
|
@@ -318,7 +417,7 @@ Question: $line" || true
|
|
|
318
417
|
/status) _chat_do "$loki" status; continue ;;
|
|
319
418
|
/steer)
|
|
320
419
|
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
321
|
-
|
|
420
|
+
_chat_steer "$loki" "$rest"; continue ;;
|
|
322
421
|
/stop) _chat_do "$loki" stop; continue ;;
|
|
323
422
|
/detach) attached=0; echo "Detached."; continue ;;
|
|
324
423
|
esac
|
|
@@ -328,10 +427,10 @@ Question: $line" || true
|
|
|
328
427
|
/help) _chat_help ;;
|
|
329
428
|
/plan) mode="plan"; echo "Plan mode: read-only." ;;
|
|
330
429
|
/build) mode="build"; echo "Build mode: edits allowed." ;;
|
|
331
|
-
/run)
|
|
430
|
+
/run|/start)
|
|
332
431
|
[ -n "$rest" ] || { echo "usage: /run <prd|brief>"; continue; }
|
|
333
|
-
|
|
334
|
-
_chat_attach_banner || true ;;
|
|
432
|
+
local _lrc=0; _chat_run_launch "$loki" "$rest" || _lrc=$?
|
|
433
|
+
if [ "$_lrc" -eq 0 ]; then _chat_attach_banner || true; else echo "(exit $_lrc)"; fi ;;
|
|
335
434
|
/workspace)
|
|
336
435
|
local _ws_name="${rest%% *}" _ws_brief=""
|
|
337
436
|
[ "$_ws_name" != "$rest" ] && _ws_brief="${rest#* }"
|
|
@@ -344,8 +443,13 @@ Question: $line" || true
|
|
|
344
443
|
/status) _chat_do "$loki" status ;;
|
|
345
444
|
/steer)
|
|
346
445
|
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
347
|
-
|
|
446
|
+
_chat_steer "$loki" "$rest" ;;
|
|
348
447
|
/stop) _chat_do "$loki" stop ;;
|
|
448
|
+
/pause) _chat_do "$loki" pause ;;
|
|
449
|
+
/resume) _chat_do "$loki" resume ;;
|
|
450
|
+
/answer)
|
|
451
|
+
[ -n "$rest" ] || { echo "usage: /answer <text>"; continue; }
|
|
452
|
+
_chat_do "$loki" answer --text "$rest" ;;
|
|
349
453
|
/detach) echo "Not attached." ;;
|
|
350
454
|
/quick)
|
|
351
455
|
[ -n "$rest" ] || { echo "usage: /quick <task>"; continue; }
|
|
@@ -972,3 +972,23 @@ loki_export_subagent_capacity() {
|
|
|
972
972
|
# subprocess tree inherits it, so every subcall site gets the same capacity
|
|
973
973
|
# without each one having to remember to call this.
|
|
974
974
|
loki_export_subagent_capacity
|
|
975
|
+
|
|
976
|
+
# P12-26: LOKI_PERMISSION_POLICY=compiled swaps --dangerously-skip-permissions for
|
|
977
|
+
# --permission-mode / --allowedTools / --disallowedTools compiled from the task policy
|
|
978
|
+
# by src/policies/claude_permissions.js (shared with the TS permissionArgv). Unset =
|
|
979
|
+
# unchanged. Result lands in _LOKI_PERM_ARGV. A compile failure fails closed to plan.
|
|
980
|
+
_loki_claude_permission_argv() {
|
|
981
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
982
|
+
[ "${LOKI_PERMISSION_POLICY:-}" = "compiled" ] || return 0
|
|
983
|
+
local _helper _out _a
|
|
984
|
+
_helper="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/src/policies/claude_permissions.js"
|
|
985
|
+
_LOKI_PERM_ARGV=(--permission-mode plan)
|
|
986
|
+
if ! command -v node >/dev/null 2>&1 || [ ! -f "$_helper" ]; then
|
|
987
|
+
echo "[loki] WARNING: LOKI_PERMISSION_POLICY=compiled: compiler unavailable; using most restrictive mode (plan)" >&2
|
|
988
|
+
return 0
|
|
989
|
+
fi
|
|
990
|
+
_out="$(node "$_helper" "${LOKI_PROJECT_DIR:-$PWD}")" || return 0
|
|
991
|
+
[ -n "$_out" ] || return 0
|
|
992
|
+
_LOKI_PERM_ARGV=()
|
|
993
|
+
while IFS= read -r _a; do _LOKI_PERM_ARGV+=("$_a"); done <<< "$_out"
|
|
994
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""P12-26b: Python entry to the shared Claude permission policy.
|
|
2
|
+
|
|
3
|
+
New file: the python launch sites (dashboard, magic) need the same argv as the
|
|
4
|
+
bash and TS routes; this is a thin caller of src/policies/claude_permissions.js
|
|
5
|
+
(the single compiler), not a second implementation. Unset LOKI_PERMISSION_POLICY
|
|
6
|
+
returns the legacy flag byte-identically.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
_LEGACY = ["--dangerously-skip-permissions"]
|
|
17
|
+
_FAIL_CLOSED = ["--permission-mode", "plan"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def claude_permission_argv(project_dir: str | None = None, env: dict | None = None) -> list:
|
|
21
|
+
env = os.environ if env is None else env
|
|
22
|
+
if env.get("LOKI_PERMISSION_POLICY", "") != "compiled":
|
|
23
|
+
return list(_LEGACY)
|
|
24
|
+
helper = Path(__file__).resolve().parents[2] / "src" / "policies" / "claude_permissions.js"
|
|
25
|
+
node = shutil.which("node")
|
|
26
|
+
if node is None or not helper.is_file():
|
|
27
|
+
print("[loki] WARNING: LOKI_PERMISSION_POLICY=compiled: compiler unavailable; using most restrictive mode (plan)", file=sys.stderr)
|
|
28
|
+
return list(_FAIL_CLOSED)
|
|
29
|
+
target = project_dir or env.get("LOKI_PROJECT_DIR") or os.getcwd()
|
|
30
|
+
try:
|
|
31
|
+
done = subprocess.run([node, str(helper), str(target)], capture_output=True, text=True,
|
|
32
|
+
timeout=30, env=dict(env), check=False)
|
|
33
|
+
except (OSError, subprocess.SubprocessError):
|
|
34
|
+
return list(_FAIL_CLOSED)
|
|
35
|
+
if done.stderr:
|
|
36
|
+
sys.stderr.write(done.stderr)
|
|
37
|
+
args = [a for a in done.stdout.split("\n") if a]
|
|
38
|
+
if done.returncode != 0 or not args:
|
|
39
|
+
return list(_FAIL_CLOSED)
|
|
40
|
+
return args
|
|
@@ -157,13 +157,15 @@ _loki_done_recog_invoke() {
|
|
|
157
157
|
local _dr_schema_content _dr_json _dr_obj
|
|
158
158
|
_dr_schema_content="$(cat "$_dr_schema" 2>/dev/null)" || _dr_schema_content=""
|
|
159
159
|
if [ -n "$_dr_schema_content" ]; then
|
|
160
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
161
|
+
! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
|
|
160
162
|
if command -v timeout >/dev/null 2>&1; then
|
|
161
163
|
_dr_json=$(CAVEMAN_DEFAULT_MODE=off timeout "${LOKI_DONE_RECOG_TIMEOUT}" \
|
|
162
|
-
claude
|
|
164
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" \
|
|
163
165
|
--json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
|
|
164
166
|
else
|
|
165
167
|
_dr_json=$(CAVEMAN_DEFAULT_MODE=off \
|
|
166
|
-
claude
|
|
168
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" \
|
|
167
169
|
--json-schema "$_dr_schema_content" --output-format json 2>/dev/null) || _dr_json=""
|
|
168
170
|
fi
|
|
169
171
|
local _dr_py
|
|
@@ -192,12 +194,14 @@ except Exception:
|
|
|
192
194
|
|
|
193
195
|
local rc=0
|
|
194
196
|
local out=""
|
|
197
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
198
|
+
! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
|
|
195
199
|
if command -v timeout >/dev/null 2>&1; then
|
|
196
200
|
out=$(CAVEMAN_DEFAULT_MODE=off timeout "${LOKI_DONE_RECOG_TIMEOUT}" \
|
|
197
|
-
claude
|
|
201
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
|
|
198
202
|
else
|
|
199
203
|
out=$(CAVEMAN_DEFAULT_MODE=off \
|
|
200
|
-
claude
|
|
204
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
|
|
201
205
|
fi
|
|
202
206
|
[ "$rc" -ne 0 ] && return 1
|
|
203
207
|
[ -z "$out" ] && return 1
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Loki 10 engine P4 push child (ENGINE.md sections 6 and 7).
|
|
3
3
|
#
|
|
4
4
|
# engine10-push.sh push-pr <repo-dir> <branch> <title> <body-file> [--draft] [--base <branch>]
|
|
5
|
-
# engine10-push.sh
|
|
5
|
+
# engine10-push.sh pr-ready <repo-dir> <branch> <body-file> <pr-url> (reused open PR: replace its body, then mark it ready; <pr-url> must be the pinned-owner PR)
|
|
6
|
+
# engine10-push.sh check-origin (exit 0 when the pinned origin is acceptable to push-pr, else the refusal; no network)
|
|
6
7
|
# engine10-push.sh comment <pr-number> <body-file>
|
|
7
8
|
# engine10-push.sh issue-comment [--update] <issue-ref> <body-file>
|
|
8
9
|
# engine10-push.sh status <sha> <pending|success|failure|error> <description>
|
|
@@ -117,6 +118,16 @@ _e10_local_push() {
|
|
|
117
118
|
|
|
118
119
|
_e10_gh() { _loki_with_github_tokens _loki_run_neutral "$_e10_repo" command gh "$@"; }
|
|
119
120
|
|
|
121
|
+
# The one open-PR lookup for push-pr reuse and pr-ready. gh cannot filter --head by owner, so a stranger's fork PR with the
|
|
122
|
+
# same branch name would match; select only a same-repo PR from the pinned owner. Prints the URL, or nothing.
|
|
123
|
+
_e10_find_pr() {
|
|
124
|
+
local owner="${_e10_repo%%/*}"
|
|
125
|
+
case "$owner" in '' | *[!A-Za-z0-9_.-]*) return 1 ;; esac
|
|
126
|
+
_e10_gh pr list --repo "$_e10_repo" --head "$1" --state open \
|
|
127
|
+
--json url,isCrossRepository,headRepositoryOwner,headRefName,state \
|
|
128
|
+
--jq "[.[] | select(.isCrossRepository == false and .state == \"OPEN\" and (.headRepositoryOwner.login | ascii_downcase) == \"$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')\")] | .[0].url // empty"
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
mode="${1:-}"
|
|
121
132
|
shift || true
|
|
122
133
|
case "$mode" in
|
|
@@ -157,10 +168,9 @@ case "$mode" in
|
|
|
157
168
|
else
|
|
158
169
|
_loki_trusted_push _loki_with_github_tokens "$dir" "$branch" || die "push refused or failed (rc=$?)"
|
|
159
170
|
fi
|
|
160
|
-
url="$(
|
|
161
|
-
|| die "gh pr list failed"
|
|
171
|
+
url="$(_e10_find_pr "$branch")" || die "gh pr list failed"
|
|
162
172
|
_e10_pr_state=reused
|
|
163
|
-
if [ -z "$url" ]
|
|
173
|
+
if [ -z "$url" ]; then
|
|
164
174
|
_e10_pr_state=created
|
|
165
175
|
url="$(_e10_gh pr create --repo "$_e10_repo" --head "$branch" --title "$title" --body-file "$body" "${draft[@]}" "${base[@]}")" \
|
|
166
176
|
|| die "gh pr create failed"
|
|
@@ -169,6 +179,21 @@ case "$mode" in
|
|
|
169
179
|
printf '%s\n' "$_e10_pr_state" # marker line before the URL: reused (open PR found) or created
|
|
170
180
|
printf '%s\n' "$url"
|
|
171
181
|
;;
|
|
182
|
+
pr-ready)
|
|
183
|
+
# M-21 N3: a reused wave PR whose units are all PROVEN keeps its old draft state and body; fix both, same pinned origin and tokens as push-pr.
|
|
184
|
+
[ "$#" -eq 4 ] || die "usage: pr-ready <repo-dir> <branch> <body-file> <pr-url>"
|
|
185
|
+
branch="$2" body="$3" want_url="$4"
|
|
186
|
+
[ -f "$body" ] || die "body file not found: $body"
|
|
187
|
+
git check-ref-format --branch "$branch" >/dev/null 2>&1 || die "invalid branch: $branch"
|
|
188
|
+
case "$branch" in main | master | HEAD) die "refusing to edit a PR for default branch: $branch" ;; esac
|
|
189
|
+
[ -z "$_e10_local" ] || { log_info "local origin: no PR to mark ready (no-op)"; exit 0; }
|
|
190
|
+
url="$(_e10_find_pr "$branch")" || die "gh pr list failed"
|
|
191
|
+
[ -n "$url" ] || die "no open PR from $_e10_repo for branch: $branch"
|
|
192
|
+
[ "$url" = "$want_url" ] || die "refusing: PR $want_url is not the pinned-owner PR for $branch ($url)"
|
|
193
|
+
_e10_gh pr edit "$url" --repo "$_e10_repo" --body-file "$body" >/dev/null || die "gh pr edit failed"
|
|
194
|
+
_e10_gh pr ready "$url" --repo "$_e10_repo" >/dev/null || die "gh pr ready failed"
|
|
195
|
+
printf '%s\n' "$url"
|
|
196
|
+
;;
|
|
172
197
|
comment)
|
|
173
198
|
[ "$#" -eq 2 ] || die "usage: comment <pr-number> <body-file>"
|
|
174
199
|
case "$1" in '' | *[!0-9]*) die "pr number must be digits" ;; esac
|
|
@@ -215,5 +240,5 @@ case "$mode" in
|
|
|
215
240
|
_e10_gh api "repos/$_e10_repo/statuses/$1" -f "state=$2" -f context=loki/deep-verify \
|
|
216
241
|
-f "description=$3" >/dev/null || die "gh api status failed"
|
|
217
242
|
;;
|
|
218
|
-
*) die "usage: engine10-push.sh push-pr|comment|issue-comment|status ..." ;;
|
|
243
|
+
*) die "usage: engine10-push.sh push-pr|pr-ready|comment|issue-comment|status ..." ;;
|
|
219
244
|
esac
|
|
@@ -86,14 +86,16 @@ _loki_prd_enrich_invoke() {
|
|
|
86
86
|
command -v claude >/dev/null 2>&1 || return 1
|
|
87
87
|
local rc=0
|
|
88
88
|
local out=""
|
|
89
|
+
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
90
|
+
! type _loki_claude_permission_argv >/dev/null 2>&1 || _loki_claude_permission_argv
|
|
89
91
|
if command -v timeout >/dev/null 2>&1; then
|
|
90
92
|
out=$(CAVEMAN_DEFAULT_MODE=off timeout "${LOKI_PRD_ENRICH_TIMEOUT}" \
|
|
91
|
-
claude
|
|
93
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
|
|
92
94
|
else
|
|
93
95
|
# No coreutils timeout (e.g. bare macOS). Run without it; the model
|
|
94
96
|
# call is a one-shot and the caller still tolerates failure.
|
|
95
97
|
out=$(CAVEMAN_DEFAULT_MODE=off \
|
|
96
|
-
claude
|
|
98
|
+
claude "${_LOKI_PERM_ARGV[@]}" -p "$prompt" 2>/dev/null) || rc=$?
|
|
97
99
|
fi
|
|
98
100
|
[ "$rc" -ne 0 ] && return 1
|
|
99
101
|
[ -z "$out" ] && return 1
|
|
@@ -1455,6 +1455,11 @@ def _collect_journey(loki_dir):
|
|
|
1455
1455
|
},
|
|
1456
1456
|
"waited_s": ws,
|
|
1457
1457
|
}
|
|
1458
|
+
# active_s (measured active human seconds) rides along only when
|
|
1459
|
+
# recorded; absent stays absent, never a fabricated 0.
|
|
1460
|
+
act = iv.get("active_s")
|
|
1461
|
+
if isinstance(act, int) and not isinstance(act, bool) and act >= 0:
|
|
1462
|
+
out["interventions_detail"]["active_s"] = act
|
|
1458
1463
|
|
|
1459
1464
|
# PR state/URL, written by the consent-gated PR step. "prepared" means a PR
|
|
1460
1465
|
# body exists locally and GitHub was NOT mutated.
|
|
@@ -1173,10 +1173,34 @@ def git_head(repo):
|
|
|
1173
1173
|
return out.strip() if out and out.strip() else ""
|
|
1174
1174
|
|
|
1175
1175
|
|
|
1176
|
+
def _sparse_included(repo, paths):
|
|
1177
|
+
"""Set of `paths` git's own sparse matcher includes; None when git cannot answer.
|
|
1178
|
+
|
|
1179
|
+
One `git sparse-checkout check-rules` call for the whole batch: it echoes
|
|
1180
|
+
the paths the patterns include, for cone, non-cone and negation alike.
|
|
1181
|
+
Known limit: a .git directory writable by the subject can drop paths from
|
|
1182
|
+
info/sparse-checkout. That hole predates this check (core.sparseCheckout=true
|
|
1183
|
+
alone was enough).
|
|
1184
|
+
"""
|
|
1185
|
+
enc = [p.encode("utf-8", "surrogateescape") for p in paths]
|
|
1186
|
+
try:
|
|
1187
|
+
r = subprocess.run(
|
|
1188
|
+
["git", "-C", repo or ".", "sparse-checkout", "check-rules", "-z"],
|
|
1189
|
+
input=b"".join(e + b"\0" for e in enc),
|
|
1190
|
+
capture_output=True, timeout=60, env=_scoped_env())
|
|
1191
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
1192
|
+
return None
|
|
1193
|
+
if r.returncode != 0:
|
|
1194
|
+
return None
|
|
1195
|
+
out = set(r.stdout.split(b"\0"))
|
|
1196
|
+
return set(p for p, e in zip(paths, enc) if e in out)
|
|
1197
|
+
|
|
1198
|
+
|
|
1176
1199
|
def git_dirty(repo):
|
|
1177
1200
|
"""True/False, or None when git cannot answer. .loki/ is excluded: the
|
|
1178
1201
|
verifier writes its own artifacts there."""
|
|
1179
|
-
out = _vr_git_args(repo, ["status", "--porcelain"
|
|
1202
|
+
out = _vr_git_args(repo, ["status", "--porcelain", "--untracked-files=all",
|
|
1203
|
+
"--no-renames", "--ignore-submodules=none"])
|
|
1180
1204
|
if out is None:
|
|
1181
1205
|
return None
|
|
1182
1206
|
for line in out.splitlines():
|
|
@@ -1185,6 +1209,90 @@ def git_dirty(repo):
|
|
|
1185
1209
|
path = path.strip('"')
|
|
1186
1210
|
if not (path.startswith(".loki/") or path == ".loki"):
|
|
1187
1211
|
return True
|
|
1212
|
+
# A skip-worktree (S/s) or assume-unchanged (lowercase tag) flag hides edits
|
|
1213
|
+
# from status. A flagged path is dirty only when its content differs from
|
|
1214
|
+
# the index; a skip-worktree path absent on disk is sparse-excluded (clean).
|
|
1215
|
+
flagged = _vr_git_args(repo, ["ls-files", "-v", "-z"])
|
|
1216
|
+
if flagged is None:
|
|
1217
|
+
return None
|
|
1218
|
+
cand = []
|
|
1219
|
+
for ent in flagged.split("\0"):
|
|
1220
|
+
tag, path = ent[:1], ent[2:]
|
|
1221
|
+
if not path or path.startswith(".loki/"):
|
|
1222
|
+
continue
|
|
1223
|
+
if tag in ("S", "s") or tag.islower():
|
|
1224
|
+
cand.append((tag in ("S", "s"), path))
|
|
1225
|
+
if not cand:
|
|
1226
|
+
return False
|
|
1227
|
+
staged = _vr_git_args(repo, ["ls-files", "-s", "-z"])
|
|
1228
|
+
if staged is None:
|
|
1229
|
+
return None
|
|
1230
|
+
blobs = {}
|
|
1231
|
+
for ent in staged.split("\0"):
|
|
1232
|
+
meta, _, p = ent.partition("\t")
|
|
1233
|
+
f = meta.split()
|
|
1234
|
+
if len(f) == 3 and f[2] == "0":
|
|
1235
|
+
blobs[p] = (f[0], f[1])
|
|
1236
|
+
sparse = (_vr_git_args(repo, ["config", "--bool", "core.sparseCheckout"]) or "").strip() == "true"
|
|
1237
|
+
root = repo or "."
|
|
1238
|
+
bad = None
|
|
1239
|
+
todo = []
|
|
1240
|
+
sparse_missing = []
|
|
1241
|
+
for skip, path in cand:
|
|
1242
|
+
full = os.path.join(root, path)
|
|
1243
|
+
mode, want = blobs.get(path, ("", ""))
|
|
1244
|
+
if not os.path.lexists(full):
|
|
1245
|
+
if skip and sparse:
|
|
1246
|
+
# Decided after the loop in one batched git call: clean only when
|
|
1247
|
+
# git's sparse patterns exclude the path. An in-cone path flagged
|
|
1248
|
+
# by hand and deleted is a real deletion; fail closed otherwise.
|
|
1249
|
+
sparse_missing.append(path)
|
|
1250
|
+
continue
|
|
1251
|
+
bad = (skip, path)
|
|
1252
|
+
break
|
|
1253
|
+
if mode == "160000":
|
|
1254
|
+
continue
|
|
1255
|
+
if os.path.islink(full):
|
|
1256
|
+
data = os.readlink(full).encode("utf-8", "surrogateescape")
|
|
1257
|
+
if hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() != want:
|
|
1258
|
+
bad = (skip, path)
|
|
1259
|
+
break
|
|
1260
|
+
continue
|
|
1261
|
+
todo.append((skip, path))
|
|
1262
|
+
if sparse_missing and not bad:
|
|
1263
|
+
included = _sparse_included(repo, sparse_missing)
|
|
1264
|
+
for path in sparse_missing:
|
|
1265
|
+
if included is None or path in included:
|
|
1266
|
+
bad = (True, path)
|
|
1267
|
+
break
|
|
1268
|
+
for i in range(0, len(todo), 200):
|
|
1269
|
+
if bad:
|
|
1270
|
+
break
|
|
1271
|
+
chunk = todo[i:i + 200]
|
|
1272
|
+
out = _vr_git_args(repo, ["hash-object", "--"] + [p for _, p in chunk])
|
|
1273
|
+
if out is None:
|
|
1274
|
+
return None
|
|
1275
|
+
miss = [(s, p) for (s, p), h in zip(chunk, out.split())
|
|
1276
|
+
if h != blobs.get(p, ("", ""))[1]]
|
|
1277
|
+
if miss:
|
|
1278
|
+
# Identical raw bytes mean clean (CRLF in the index plus autocrlf).
|
|
1279
|
+
# The clean filter is not idempotent: the index may hold unfiltered
|
|
1280
|
+
# bytes that a filtered hash of the worktree file never reproduces,
|
|
1281
|
+
# so the filtered hash alone would call such a file dirty.
|
|
1282
|
+
raw = _vr_git_args(repo, ["hash-object", "--no-filters", "--"] + [p for _, p in miss])
|
|
1283
|
+
if raw is None:
|
|
1284
|
+
return None
|
|
1285
|
+
for (skip, path), h in zip(miss, raw.split()):
|
|
1286
|
+
if h != blobs.get(path, ("", ""))[1]:
|
|
1287
|
+
bad = (skip, path)
|
|
1288
|
+
break
|
|
1289
|
+
if bad:
|
|
1290
|
+
skip, path = bad
|
|
1291
|
+
kind, fix = (("skip-worktree", "--no-skip-worktree") if skip
|
|
1292
|
+
else ("assume-unchanged", "--no-assume-unchanged"))
|
|
1293
|
+
sys.stderr.write("dirty: %s is flagged %s and differs from the index, which hides edits from the proof. "
|
|
1294
|
+
"Run: git update-index %s -- %s\n" % (path, kind, fix, path))
|
|
1295
|
+
return True
|
|
1188
1296
|
return False
|
|
1189
1297
|
|
|
1190
1298
|
|