loki-mode 11.12.1 → 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/README.md +2 -2
- 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/backlog.py +18 -3
- package/autonomy/lib/chat.sh +117 -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 +56 -7
- package/autonomy/lib/prd-enrich.sh +4 -2
- package/autonomy/lib/proof-generator.py +21 -0
- package/autonomy/lib/proof-verify.py +109 -1
- package/autonomy/lib/trust_trajectory.py +139 -0
- package/autonomy/lib/voter-agents.sh +3 -1
- package/autonomy/lib/workspace.py +2 -3
- package/autonomy/loki +427 -74
- package/autonomy/quickstart.sh +21 -2
- package/autonomy/run.sh +55 -21
- package/autonomy/trigger-server.py +461 -32
- package/bin/loki +4 -1
- package/completions/_loki +1 -1
- package/completions/loki.bash +10 -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 +6 -4
- package/docs/DEPLOYMENT.md +12 -5
- package/docs/EVALUATING.md +13 -0
- package/docs/INSTALLATION.md +2 -2
- package/docs/SIGNED-RECEIPTS.md +7 -0
- package/docs/WORKSPACES.md +2 -2
- package/docs/cost-controls.md +26 -0
- package/docs/merge-queue.md +19 -0
- package/docs/sentry-intake.md +4 -0
- package/docs/slack.md +2 -1
- package/docs/trackers.md +12 -1
- package/docs/v10/BOARD.md +148 -123
- package/docs/v10/CLI-MODERN.md +5 -5
- package/docs/v10/DECISIONS.md +2 -0
- package/docs/v10/ENGINE.md +2 -0
- package/docs/v10/FAILURE-CLASSES.md +18 -0
- package/docs/v10/MODERNIZE.md +32 -1
- package/docs/v10/PROGRESS.md +7 -0
- package/loki-ts/data/finding-schema.json +2 -1
- package/loki-ts/dist/loki.js +1222 -1128
- 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 +5895 -5292
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +6 -3
- package/providers/model_catalog.json +6 -0
- package/schemas/loki-yaml.schema.json +5 -0
- package/src/policies/claude_permissions.js +114 -0
- package/web-app/dist/assets/{AdminPage-DuT74pf5.js → AdminPage-OVzN6eLm.js} +1 -1
- package/web-app/dist/assets/{Avatar-BtgidR5a.js → Avatar-hCCrxrCZ.js} +1 -1
- package/web-app/dist/assets/{Badge-D2nt8G_I.js → Badge-CDcpkhbJ.js} +1 -1
- package/web-app/dist/assets/{Button-BKl2jOyb.js → Button-8mwWlhEu.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DaWr0EUX.js → CockpitPage-D8cf3u22.js} +1 -1
- package/web-app/dist/assets/{ComparePage-aIiNhxLr.js → ComparePage-CIgkbDRY.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-C1Q7rV23.js → ErrorBoundary-99kJH9g2.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C-C0-tOx.js → EvidenceReceiptPanel-C9fj32eS.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-l3IaXDag.js → GitHubIssuesPanel-De43YQ_C.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DweLBgr5.js → GitHubPRsPanel-7g7ZTnkx.js} +1 -1
- package/web-app/dist/assets/{HomePage-B_KK7AvY.js → HomePage-CcNA24ny.js} +2 -2
- package/web-app/dist/assets/{LoginPage-CKS-KLdH.js → LoginPage-BYjlb-IK.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Cl6CEp8J.js → MagicPage-C0Dwt9Na.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-n9CPUaX0.js → MetricsPage-jDGmdcgg.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D85eWn70.js → NotFoundPage-C_ZO8tLE.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-CDN7LpSo.js → ProjectPage-B5wnurfQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-D4SDmU4-.js → ProjectsPage--EyKt5-D.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-Dby6GnKm.js → SettingsPage-cB0lnfti.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CgHxeqoU.js → ShowcasePage-Dl16FDkK.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BaewGHlc.js → SystemSettingsPage-BmAXd4V2.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-905MJyke.js → TeamsPage-BFkWVOZe.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DMrUHcvF.js → TemplatesPage-Bf2kmEUM.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BsbEbBYX.js → TerminalOutput-BItv9Wa6.js} +1 -1
- package/web-app/dist/assets/{activity-BuaXhBsC.js → activity-Df2Awg6I.js} +1 -1
- package/web-app/dist/assets/{bell-BfEQ71OG.js → bell-D-1ztTsJ.js} +1 -1
- package/web-app/dist/assets/{bot-DOGeYewg.js → bot-A4qgZsKi.js} +1 -1
- package/web-app/dist/assets/{check-zbjPpb3E.js → check-BPfFpF0c.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DWTOgfFt.js → chevron-left-CfSJvmDi.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BEPKfRzR.js → circle-alert-wx2d1dzb.js} +1 -1
- package/web-app/dist/assets/{clock-C_fX3OHo.js → clock-pQMai6l-.js} +1 -1
- package/web-app/dist/assets/{cloud-DUXHygXQ.js → cloud-DWL470r6.js} +1 -1
- package/web-app/dist/assets/{code-xml-BEVl_9Hw.js → code-xml-BaEQpQLZ.js} +1 -1
- package/web-app/dist/assets/{database-_a22uh7l.js → database-h-N7WkQN.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-l2en7xfg.js → dollar-sign-B5oYiD27.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DItKl6H4.js → file-code-corner-fbTtTy0o.js} +1 -1
- package/web-app/dist/assets/{file-plus-BSk7qFMA.js → file-plus-PaF6HMo8.js} +1 -1
- package/web-app/dist/assets/{globe-2TveFE5l.js → globe-Ct-GlWID.js} +1 -1
- package/web-app/dist/assets/{hammer-BU6OJlQj.js → hammer-C1oZFX51.js} +1 -1
- package/web-app/dist/assets/{index-DGnZXUYk.js → index-C2egrRhz.js} +3 -3
- package/web-app/dist/assets/{layers-DKw7MNuq.js → layers-Dt5ZK4v2.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CoYfykzD.js → loader-circle-DrpMziic.js} +1 -1
- package/web-app/dist/assets/{lock-D9JzrXDL.js → lock-DOez8MmK.js} +1 -1
- package/web-app/dist/assets/{package-BZtqTIqH.js → package-ClGIkerr.js} +1 -1
- package/web-app/dist/assets/{plus-BGJfo8bV.js → plus-C004i9ua.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-F6lowBgR.js → refresh-cw-Ddfk-7Sy.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DHOK8dM4.js → rotate-ccw-Bu1KgDjS.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CKcROoMV.js → scroll-text-3q1lW5pm.js} +1 -1
- package/web-app/dist/assets/{server-DbG8MDv6.js → server-pd6x4HW1.js} +1 -1
- package/web-app/dist/assets/{shield-alert-RNKNXdJT.js → shield-alert-B9jGJepT.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Yxp-UBxS.js → thumbs-up-Cdcc9nQf.js} +1 -1
- package/web-app/dist/assets/{trash-2-DY-eba1n.js → trash-2-foSojXHK.js} +1 -1
- package/web-app/dist/assets/{trending-down-CARiDB3t.js → trending-down-CTTiqlJ1.js} +1 -1
- package/web-app/dist/assets/{trending-up-brQqWbA3.js → trending-up-B_MgmRAx.js} +1 -1
- package/web-app/dist/assets/{upload-D6wRjKhz.js → upload-g6BfALzG.js} +1 -1
- package/web-app/dist/assets/{usePolling-BMrqNbAI.js → usePolling-DrHwvzuh.js} +1 -1
- package/web-app/dist/assets/{user-CK5-EXIj.js → user-COp75m7l.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/autonomy/lib/doctor-fix.py +0 -279
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ Time: 4m12s (intake 11s, plan 20s, implement 3m10s, verify 31s)
|
|
|
102
102
|
| `loki agents list\|new\|show\|export --claude` | Custom child agents in `.loki/agents/*.md` |
|
|
103
103
|
| `loki status [run-id]` | Status of the latest run, or of a given run |
|
|
104
104
|
| `loki verify [run-id]` | Check a receipt's hash, signature and event log |
|
|
105
|
-
| `loki answer [run-id]` |
|
|
105
|
+
| `loki answer [run-id]` | Start a follow-up run from a BLOCKED run with your answer (`--text "..."`) |
|
|
106
106
|
| `loki doctor [--fix] [--json]` | Check prerequisites, providers and login; `--fix` applies safe fixes |
|
|
107
107
|
| `loki control serve` | Start the Control Plane (see below) |
|
|
108
108
|
| `loki dashboard` | Open the Control Plane UI, starting it if needed |
|
|
@@ -139,7 +139,7 @@ Required:
|
|
|
139
139
|
|
|
140
140
|
- Claude Code (`claude`), installed and logged in: the default provider and the only one with every feature. The bundled Agent SDK with `ANTHROPIC_API_KEY` can stand in on the main `loki start` path, but `loki edit` and `loki chat` call the `claude` CLI.
|
|
141
141
|
- Python 3.8+ (`python3`)
|
|
142
|
-
- Node.js
|
|
142
|
+
- Node.js 20+ (`node`; 22 LTS is the CI target)
|
|
143
143
|
- `jq`
|
|
144
144
|
- Git 2.x (`git`)
|
|
145
145
|
- `curl`
|
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/backlog.py
CHANGED
|
@@ -308,9 +308,17 @@ def main(argv):
|
|
|
308
308
|
child_env["LOKI_PROVIDER"] = a.provider
|
|
309
309
|
elif cfg.get("provider"):
|
|
310
310
|
child_env["LOKI_PROVIDER"] = cfg["provider"]
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
# per-run cap forwarded to each child (budgets.per_run is the TS-side alias); an exported LOKI_BUDGET_LIMIT wins over loki.yaml
|
|
312
|
+
bud = cfg.get("budgets", {})
|
|
313
|
+
per_run = bud.get("per_run_usd") or bud.get("per_run")
|
|
314
|
+
if per_run and not env.get("LOKI_BUDGET_LIMIT"):
|
|
315
|
+
child_env["LOKI_BUDGET_LIMIT"] = str(per_run)
|
|
316
|
+
group_cap = None # a DAG group shares one total: the run cap bounds the sum of its units, not each unit
|
|
317
|
+
if dag_units:
|
|
318
|
+
try:
|
|
319
|
+
group_cap = float(env.get("LOKI_BUDGET_LIMIT") or 0) or None
|
|
320
|
+
except ValueError:
|
|
321
|
+
group_cap = None
|
|
314
322
|
day_cap = cfg.get("budgets", {}).get("per_day_usd")
|
|
315
323
|
|
|
316
324
|
# One job table for issues and units: they share one slot pool.
|
|
@@ -444,6 +452,13 @@ def main(argv):
|
|
|
444
452
|
for k in [k for k, (p, _l, _w) in running.items() if p.poll() is not None]:
|
|
445
453
|
finish(k)
|
|
446
454
|
while pending and len(running) < conc:
|
|
455
|
+
group_spent = sum(st["cost"] or 0 for st in state.values())
|
|
456
|
+
if group_cap and group_spent >= group_cap:
|
|
457
|
+
for k in pending:
|
|
458
|
+
state[k].update(status="BUDGET_STOP: group budget $%.2f reached ($%.2f spent)" % (group_cap, group_spent), ok=False)
|
|
459
|
+
say("backlog: %s %s" % (spec[k]["label"], state[k]["status"]))
|
|
460
|
+
pending = []
|
|
461
|
+
break
|
|
447
462
|
spent = spent_today() if day_cap else 0.0
|
|
448
463
|
if day_cap and (spent is None or spent >= day_cap):
|
|
449
464
|
for k in pending:
|
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
|
|
@@ -52,7 +59,9 @@ Loki chat commands:
|
|
|
52
59
|
/model <id> set model for this session
|
|
53
60
|
/provider <name> switch provider (claude, codex, cline, aider, opencode)
|
|
54
61
|
/cost loki cost
|
|
62
|
+
/merge <args> loki merge (pr <pr>, watch <pr> to repair red CI, run, list)
|
|
55
63
|
/diff git diff --stat plus diff
|
|
64
|
+
/doctor [--json] loki doctor --repo: repo readiness score and fixes (alias /ready)
|
|
56
65
|
/undo [args] loki undo
|
|
57
66
|
/help, /exit
|
|
58
67
|
Attached mode (loki chat --attach or /attach): /status /steer <msg> /diff /verify /receipt /stop
|
|
@@ -62,6 +71,98 @@ HLP
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
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
|
+
|
|
65
166
|
# Run a delegate without letting a non-zero exit end the session (autonomy/loki runs under set -e).
|
|
66
167
|
_chat_do() { local _rc=0; "$@" || _rc=$?; [ "$_rc" -eq 0 ] || echo "(exit $_rc)"; return 0; }
|
|
67
168
|
|
|
@@ -316,7 +417,7 @@ Question: $line" || true
|
|
|
316
417
|
/status) _chat_do "$loki" status; continue ;;
|
|
317
418
|
/steer)
|
|
318
419
|
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
319
|
-
|
|
420
|
+
_chat_steer "$loki" "$rest"; continue ;;
|
|
320
421
|
/stop) _chat_do "$loki" stop; continue ;;
|
|
321
422
|
/detach) attached=0; echo "Detached."; continue ;;
|
|
322
423
|
esac
|
|
@@ -326,10 +427,10 @@ Question: $line" || true
|
|
|
326
427
|
/help) _chat_help ;;
|
|
327
428
|
/plan) mode="plan"; echo "Plan mode: read-only." ;;
|
|
328
429
|
/build) mode="build"; echo "Build mode: edits allowed." ;;
|
|
329
|
-
/run)
|
|
430
|
+
/run|/start)
|
|
330
431
|
[ -n "$rest" ] || { echo "usage: /run <prd|brief>"; continue; }
|
|
331
|
-
|
|
332
|
-
_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 ;;
|
|
333
434
|
/workspace)
|
|
334
435
|
local _ws_name="${rest%% *}" _ws_brief=""
|
|
335
436
|
[ "$_ws_name" != "$rest" ] && _ws_brief="${rest#* }"
|
|
@@ -342,8 +443,13 @@ Question: $line" || true
|
|
|
342
443
|
/status) _chat_do "$loki" status ;;
|
|
343
444
|
/steer)
|
|
344
445
|
[ -n "$rest" ] || { echo "usage: /steer <message>"; continue; }
|
|
345
|
-
|
|
446
|
+
_chat_steer "$loki" "$rest" ;;
|
|
346
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" ;;
|
|
347
453
|
/detach) echo "Not attached." ;;
|
|
348
454
|
/quick)
|
|
349
455
|
[ -n "$rest" ] || { echo "usage: /quick <task>"; continue; }
|
|
@@ -355,8 +461,12 @@ Question: $line" || true
|
|
|
355
461
|
_chat_do "$loki" verify $rest ;;
|
|
356
462
|
/receipt) _chat_do "$loki" receipt ;;
|
|
357
463
|
/cost) _chat_do "$loki" cost ;;
|
|
464
|
+
/merge) # shellcheck disable=SC2086
|
|
465
|
+
_chat_do "$loki" merge $rest ;;
|
|
358
466
|
/undo) # shellcheck disable=SC2086
|
|
359
467
|
_chat_do "$loki" undo $rest ;;
|
|
468
|
+
/doctor|/ready) # shellcheck disable=SC2086
|
|
469
|
+
_chat_do "$loki" doctor --repo $rest ;;
|
|
360
470
|
/diff) _chat_do git diff --stat; _chat_do git diff ;;
|
|
361
471
|
/model)
|
|
362
472
|
if [ -n "$rest" ]; then _CHAT_MODEL="$rest"; echo "model: $rest"; else echo "model: ${_CHAT_MODEL:-default}"; fi ;;
|
|
@@ -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,9 +2,10 @@
|
|
|
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
|
-
# engine10-push.sh issue-comment <issue-ref> <body-file>
|
|
8
|
+
# engine10-push.sh issue-comment [--update] <issue-ref> <body-file>
|
|
8
9
|
# engine10-push.sh status <sha> <pending|success|failure|error> <description>
|
|
9
10
|
#
|
|
10
11
|
# Holds GitHub credentials, runs no LLM, reads no untrusted text. Inputs come
|
|
@@ -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,13 +168,30 @@ 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
|
-
|
|
162
|
-
if [ -z "$url" ]
|
|
171
|
+
url="$(_e10_find_pr "$branch")" || die "gh pr list failed"
|
|
172
|
+
_e10_pr_state=reused
|
|
173
|
+
if [ -z "$url" ]; then
|
|
174
|
+
_e10_pr_state=created
|
|
163
175
|
url="$(_e10_gh pr create --repo "$_e10_repo" --head "$branch" --title "$title" --body-file "$body" "${draft[@]}" "${base[@]}")" \
|
|
164
176
|
|| die "gh pr create failed"
|
|
165
177
|
url="$(printf '%s\n' "$url" | tail -n 1)"
|
|
166
178
|
fi
|
|
179
|
+
printf '%s\n' "$_e10_pr_state" # marker line before the URL: reused (open PR found) or created
|
|
180
|
+
printf '%s\n' "$url"
|
|
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"
|
|
167
195
|
printf '%s\n' "$url"
|
|
168
196
|
;;
|
|
169
197
|
comment)
|
|
@@ -176,11 +204,32 @@ case "$mode" in
|
|
|
176
204
|
issue-comment)
|
|
177
205
|
# E-67: a FAILED run with no diff (or no remote) has no PR to open; comment on the issue
|
|
178
206
|
# instead so the run's reason is never silently lost. <issue-ref> is owner/repo#N or #N.
|
|
179
|
-
|
|
207
|
+
# --update: the status projector. One comment carrying the marker below is edited in place (created when absent),
|
|
208
|
+
# so an issue shows live run state without a new comment per event. Only a comment written by the authenticated
|
|
209
|
+
# gh user counts, and the body file must carry the marker (it is built by the supervisor, never from task text).
|
|
210
|
+
_e10_upd=""
|
|
211
|
+
[ "${1:-}" != "--update" ] || { _e10_upd=1; shift; }
|
|
212
|
+
[ "$#" -eq 2 ] || die "usage: issue-comment [--update] <issue-ref> <body-file>"
|
|
180
213
|
num="${1##*#}"
|
|
181
214
|
case "$num" in '' | *[!0-9]*) die "cannot extract issue number from: $1" ;; esac
|
|
182
215
|
[ -f "$2" ] || die "body file not found: $2"
|
|
183
216
|
[ -z "$_e10_local" ] || { log_info "local origin: no issue to comment on (no-op)"; exit 0; }
|
|
217
|
+
if [ -n "$_e10_upd" ]; then
|
|
218
|
+
_e10_marker='<!-- loki-status -->'
|
|
219
|
+
grep -qF -- "$_e10_marker" "$2" || die "status body must carry $_e10_marker"
|
|
220
|
+
_e10_me="$(_e10_gh api user --jq .login 2>/dev/null)" || _e10_me=""
|
|
221
|
+
_e10_cid=""
|
|
222
|
+
if [ -n "$_e10_me" ]; then
|
|
223
|
+
_e10_cid="$(_e10_gh api "repos/$_e10_repo/issues/$num/comments" --paginate \
|
|
224
|
+
--jq ".[] | select(.user.login == \"$_e10_me\") | select(.body | contains(\"$_e10_marker\")) | .id" 2>/dev/null | head -n 1)" || _e10_cid=""
|
|
225
|
+
fi
|
|
226
|
+
case "$_e10_cid" in
|
|
227
|
+
'') _e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed" ;;
|
|
228
|
+
*[!0-9]*) die "unexpected comment id" ;;
|
|
229
|
+
*) _e10_gh api -X PATCH "repos/$_e10_repo/issues/comments/$_e10_cid" -F "body=@$2" >/dev/null || die "gh api comment update failed" ;;
|
|
230
|
+
esac
|
|
231
|
+
exit 0
|
|
232
|
+
fi
|
|
184
233
|
_e10_gh issue comment "$num" --repo "$_e10_repo" --body-file "$2" || die "gh issue comment failed"
|
|
185
234
|
;;
|
|
186
235
|
status)
|
|
@@ -191,5 +240,5 @@ case "$mode" in
|
|
|
191
240
|
_e10_gh api "repos/$_e10_repo/statuses/$1" -f "state=$2" -f context=loki/deep-verify \
|
|
192
241
|
-f "description=$3" >/dev/null || die "gh api status failed"
|
|
193
242
|
;;
|
|
194
|
-
*) 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 ..." ;;
|
|
195
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
|