loki-mode 7.44.0 → 7.45.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.
package/README.md CHANGED
@@ -105,7 +105,8 @@ loki quick "build a landing page with a signup form"
105
105
  |--------|---------|-------|
106
106
  | **Bun (recommended)** | `bun install -g loki-mode` | Fastest startup for CLI commands. |
107
107
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
108
- | **Docker** | `docker pull asklokesh/loki-mode:7.40.0 && docker run --rm asklokesh/loki-mode:7.40.0 start prd.md` | Bun pre-installed in image |
108
+ | **Docker (easiest)** | `loki docker start prd.md` | Host wrapper: runs loki in the published image with zero config. Bind-mounts the current folder so `.loki` state, resume, and continuity work exactly like local. Auto-detects auth (`ANTHROPIC_API_KEY`, else your host Claude Code login). Needs loki + Docker on the host. See DOCKER_README.md |
109
+ | **Docker (raw)** | `docker pull asklokesh/loki-mode:7.45.0 && docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" asklokesh/loki-mode:7.45.0 start prd.md` | Bun + Claude CLI pre-installed; needs an API key, or use docker compose with a .env file, see DOCKER_README.md |
109
110
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
110
111
 
111
112
  **Upgrading:**
@@ -165,7 +166,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
165
166
  | Method | Command |
166
167
  |--------|---------|
167
168
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
168
- | **Docker** | `docker pull asklokesh/loki-mode:7.40.0` |
169
+ | **Docker** | `docker pull asklokesh/loki-mode:7.45.0` |
169
170
  | **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
170
171
  | **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
171
172
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.44.0
6
+ # Loki Mode v7.45.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -219,6 +219,15 @@ loki start --provider aider ./prd.md # Aider (18+ providers), degraded m
219
219
  loki start ./prd.md --parallel
220
220
  loki start 123 --ship # Issue -> PR -> auto-merge
221
221
 
222
+ # Run any loki command inside the published Docker image, zero config (v7.45.0).
223
+ # Bind-mounts the current folder to /workspace so .loki state, resume, and
224
+ # continuity behave exactly like the local CLI. Auth auto-detected: ANTHROPIC_API_KEY,
225
+ # else the host Claude Code login (Max/Pro), else an honest error. Requires loki + Docker on the host.
226
+ loki docker start prd.md # full local experience in Docker
227
+ loki docker status # any loki command works
228
+ loki docker --dry-run start prd.md # print the docker command, do not run
229
+ loki docker --image IMG start prd.md # override the image
230
+
222
231
  # Legacy: `loki run <issue>` still works but prints a deprecation notice.
223
232
  # It is an alias for `loki start <issue>` and will be removed in a future major.
224
233
  ```
@@ -398,4 +407,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
398
407
 
399
408
  ---
400
409
 
401
- **v7.44.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
410
+ **v7.45.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.44.0
1
+ 7.45.0
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env bash
2
+ #===============================================================================
3
+ # Loki Mode - zero-friction Docker host wrapper
4
+ #
5
+ # Lets a user run loki inside the published Docker image with the SAME
6
+ # experience as the local CLI: run it in a project folder and it just works.
7
+ # .loki/ state (memory, session, queue, checkpoints) persists via the workspace
8
+ # bind mount, so resume and continuity behave exactly like `loki start` on the
9
+ # host.
10
+ #
11
+ # Auth precedence (zero config for Claude Code subscribers):
12
+ # 1. ANTHROPIC_API_KEY set in the environment -> passed through (explicit).
13
+ # 2. else host Claude Code login auto-detected -> credentials extracted to a
14
+ # per-run temp copy and mounted read-write so the in-container claude can
15
+ # refresh the short-lived token for the duration of the run. Each run
16
+ # re-extracts a fresh token from the host store, so a long-idle token is
17
+ # never an issue. (The temp copy is wiped on exit; refreshes are not written
18
+ # back to the host store, which is why per-run re-extraction matters.)
19
+ # 3. else -> honest error with guidance.
20
+ #
21
+ # Token handling: the extracted credentials file is written to a private temp
22
+ # path (0600) and removed on exit. It is NEVER written into the project, never
23
+ # committed, never logged.
24
+ #
25
+ # Sourced by autonomy/loki (cmd_docker). Also runnable standalone for tests:
26
+ # loki_docker_detect_auth -> prints: apikey | oauth | none
27
+ # loki_docker_extract_creds <dest> -> writes host OAuth creds to <dest>
28
+ # loki_docker_build_argv ... -> prints the docker argv it would run
29
+ #===============================================================================
30
+
31
+ # Default image. Overridable for local builds / pinning.
32
+ : "${LOKI_DOCKER_IMAGE:=asklokesh/loki-mode:latest}"
33
+ # In-container path claude reads OAuth credentials from (Linux, no keychain).
34
+ _LOKI_DOCKER_CRED_DEST="/home/loki/.claude/.credentials.json"
35
+ # Dashboard port (kept identical to the local default).
36
+ : "${LOKI_DASHBOARD_PORT:=57374}"
37
+
38
+ # Detect which auth method is available on this host.
39
+ # Echoes one of: apikey | oauth | none
40
+ loki_docker_detect_auth() {
41
+ if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
42
+ echo "apikey"
43
+ return 0
44
+ fi
45
+ # macOS: token in the login keychain under "Claude Code-credentials".
46
+ if command -v security >/dev/null 2>&1; then
47
+ if security find-generic-password -s "Claude Code-credentials" -w >/dev/null 2>&1; then
48
+ echo "oauth"
49
+ return 0
50
+ fi
51
+ fi
52
+ # Linux / non-keychain: claude stores creds as a plain file.
53
+ if [ -f "${HOME}/.claude/.credentials.json" ]; then
54
+ echo "oauth"
55
+ return 0
56
+ fi
57
+ echo "none"
58
+ return 0
59
+ }
60
+
61
+ # Extract host Claude Code OAuth credentials into $1 (0600). Returns non-zero
62
+ # if no host login is found. Writes ONLY the claudeAiOauth object claude needs;
63
+ # never the unrelated mcpOAuth blobs.
64
+ loki_docker_extract_creds() {
65
+ local dest="$1"
66
+ [ -n "$dest" ] || { echo "loki_docker_extract_creds: missing dest" >&2; return 2; }
67
+
68
+ # Create the file privately BEFORE writing the token (no race on perms).
69
+ ( umask 077; : > "$dest" ) || return 2
70
+ chmod 600 "$dest" 2>/dev/null || true
71
+
72
+ local raw=""
73
+ if command -v security >/dev/null 2>&1; then
74
+ raw="$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true)"
75
+ fi
76
+ if [ -z "$raw" ] && [ -f "${HOME}/.claude/.credentials.json" ]; then
77
+ raw="$(cat "${HOME}/.claude/.credentials.json" 2>/dev/null || true)"
78
+ fi
79
+ if [ -z "$raw" ]; then
80
+ echo "loki_docker_extract_creds: no host Claude Code login found" >&2
81
+ return 1
82
+ fi
83
+
84
+ # Normalize to {"claudeAiOauth": {...}}. Prefer jq; fall back to raw if jq
85
+ # is absent and the payload already looks like the right shape.
86
+ if command -v jq >/dev/null 2>&1; then
87
+ if ! printf '%s' "$raw" | jq -e 'has("claudeAiOauth")' >/dev/null 2>&1; then
88
+ echo "loki_docker_extract_creds: host credentials missing claudeAiOauth" >&2
89
+ return 1
90
+ fi
91
+ printf '%s' "$raw" | jq '{claudeAiOauth}' > "$dest" 2>/dev/null || return 2
92
+ else
93
+ printf '%s' "$raw" > "$dest"
94
+ fi
95
+ chmod 600 "$dest" 2>/dev/null || true
96
+ return 0
97
+ }
98
+
99
+ # Assemble the docker argv (printed one arg per line so callers can read it
100
+ # into an array safely). $1 = auth method (apikey|oauth|none), $2 = creds path
101
+ # (for oauth), remaining args = the loki command to run in the container.
102
+ # $1 = auth, $2 = creds path, $3 = with_api (1 to publish the dashboard port +
103
+ # enable the dashboard, like local `loki start --api`; 0 for a plain one-shot
104
+ # build). Remaining args = the loki command.
105
+ loki_docker_build_argv() {
106
+ local auth="$1"; shift
107
+ local creds="$1"; shift
108
+ local with_api="${1:-0}"; shift
109
+ local workspace; workspace="$(pwd)"
110
+
111
+ local -a argv=(docker run --rm)
112
+ # Allocate a TTY / keep stdin open ONLY when we actually have a terminal.
113
+ # `-it` against a non-terminal (piped, CI, headless) fails with
114
+ # "cannot attach stdin to a TTY-enabled container because stdin is not a
115
+ # terminal". Probe stdin (-t 0) and stdout (-t 1) and add the flags
116
+ # conditionally so the wrapper works both interactively and headless.
117
+ # Override with LOKI_DOCKER_TTY=1 (force) or =0 (never).
118
+ local _tty="${LOKI_DOCKER_TTY:-auto}"
119
+ if [ "$_tty" = "1" ]; then
120
+ argv+=(-it)
121
+ elif [ "$_tty" = "auto" ] && [ -t 0 ] && [ -t 1 ]; then
122
+ argv+=(-it)
123
+ elif [ "$_tty" = "auto" ] && [ -t 0 ]; then
124
+ argv+=(-i)
125
+ fi
126
+ # Workspace mount: this is what makes .loki state (and thus resume +
127
+ # continuity) persist exactly like the local CLI.
128
+ argv+=(-v "${workspace}:/workspace:rw" -w /workspace)
129
+ # Dashboard: OFF by default, exactly like local `loki start` (which only
130
+ # starts the dashboard with --api). A plain one-shot build does not need it,
131
+ # and publishing a fixed host port made two `loki docker` runs collide on
132
+ # 57374 and left a lingering container holding the port. With --api we both
133
+ # enable the dashboard inside the container AND publish the port.
134
+ if [ "$with_api" = "1" ]; then
135
+ argv+=(-p "${LOKI_DASHBOARD_PORT}:${LOKI_DASHBOARD_PORT}")
136
+ argv+=(-e "LOKI_DASHBOARD=true" -e "LOKI_DASHBOARD_PORT=${LOKI_DASHBOARD_PORT}")
137
+ else
138
+ argv+=(-e "LOKI_DASHBOARD=false")
139
+ fi
140
+ # Forward git/gh identity so commits + PRs work like local. Mounting just
141
+ # ~/.gitconfig is NOT enough: it commonly uses `includeIf` to pull the real
142
+ # identity from per-host files (e.g. a github.com identity in a separate
143
+ # included file). Those includes are absent in the container, so git would
144
+ # silently fall back to the top-level [user] -- which can be a corporate
145
+ # default -- and mis-attribute commits. A `loki docker` build is destined for
146
+ # github.com, so the container must commit with the GitHub identity. We
147
+ # resolve it by preference:
148
+ # 1. the identity from the user's github includeIf target (the file pulled
149
+ # in for github.com remotes), if we can find it -- this is the correct
150
+ # identity for the autonomous greenfield flow (build -> add github
151
+ # remote -> commit -> push), which a workspace-context lookup would miss
152
+ # because a fresh repo has no github remote yet;
153
+ # 2. else the host's globally-configured identity.
154
+ # Forwarded as authoritative GIT_AUTHOR_*/GIT_COMMITTER_* env. NOTE: this
155
+ # freezes the identity for the run (env overrides per-commit includeIf
156
+ # re-evaluation), which is the intended behavior here -- the container has
157
+ # ONE purpose (github.com builds) so one frozen GitHub identity is correct.
158
+ [ -f "${HOME}/.gitconfig" ] && argv+=(-v "${HOME}/.gitconfig:/home/loki/.gitconfig:ro")
159
+ if command -v git >/dev/null 2>&1; then
160
+ local _gname="" _gemail="" _ghinc=""
161
+ # Find the path referenced by a github includeIf, if any.
162
+ if [ -f "${HOME}/.gitconfig" ]; then
163
+ _ghinc="$(git config --file "${HOME}/.gitconfig" --get-regexp 'includeif\..*github.*\.path' 2>/dev/null | awk '{print $2; exit}')"
164
+ # Expand a leading ~ to $HOME. (The "~/" here is a literal case
165
+ # pattern we are matching against, not a path to expand.)
166
+ # shellcheck disable=SC2088
167
+ case "$_ghinc" in "~/"*) _ghinc="${HOME}/${_ghinc#\~/}";; esac
168
+ fi
169
+ if [ -n "$_ghinc" ] && [ -f "$_ghinc" ]; then
170
+ _gname="$(git config --file "$_ghinc" --get user.name 2>/dev/null || true)"
171
+ _gemail="$(git config --file "$_ghinc" --get user.email 2>/dev/null || true)"
172
+ fi
173
+ # Fallback to the globally-resolved identity if the github include did
174
+ # not yield one.
175
+ [ -z "$_gname" ] && _gname="$(git config --get user.name 2>/dev/null || true)"
176
+ [ -z "$_gemail" ] && _gemail="$(git config --get user.email 2>/dev/null || true)"
177
+ if [ -n "$_gname" ]; then
178
+ argv+=(-e "GIT_AUTHOR_NAME=${_gname}" -e "GIT_COMMITTER_NAME=${_gname}")
179
+ fi
180
+ if [ -n "$_gemail" ]; then
181
+ argv+=(-e "GIT_AUTHOR_EMAIL=${_gemail}" -e "GIT_COMMITTER_EMAIL=${_gemail}")
182
+ fi
183
+ fi
184
+ [ -d "${HOME}/.config/gh" ] && argv+=(-v "${HOME}/.config/gh:/home/loki/.config/gh:ro")
185
+ # Forward GitHub tokens if set.
186
+ [ -n "${GITHUB_TOKEN:-}" ] && argv+=(-e "GITHUB_TOKEN=${GITHUB_TOKEN}")
187
+ [ -n "${GH_TOKEN:-}" ] && argv+=(-e "GH_TOKEN=${GH_TOKEN}")
188
+ # No desktop notifications inside a container.
189
+ argv+=(-e "LOKI_NOTIFICATIONS=false")
190
+ # Multi-repo unified dashboard (Option B): the in-container run.sh must NOT
191
+ # register into the project registry. A container only sees /workspace, not
192
+ # the real host path, and registry.register_project() hard-fails on a path
193
+ # that does not exist inside the container -- so in-container registration
194
+ # is both wrong (every repo would key as /workspace) and a silent no-op.
195
+ # Instead cmd_docker registers on the HOST with the real $(pwd), no pid, so
196
+ # the existing host dashboard aggregates every `loki docker` repo exactly
197
+ # like it aggregates host `loki start` repos (state read from the
198
+ # bind-mounted .loki/session.json). See loki_register_running_project.
199
+ argv+=(-e "LOKI_SKIP_PROJECT_REGISTRY=1")
200
+ # Deterministic per-host-path container name: two repos get two distinct
201
+ # concurrent containers (multi-repo parity with the host CLI) and a stable
202
+ # handle for `loki docker stop`. Hash the workspace path; fall back to a
203
+ # sanitized basename if no sha tool is present.
204
+ local _name_hash=""
205
+ if command -v shasum >/dev/null 2>&1; then
206
+ _name_hash="$(printf '%s' "$workspace" | shasum -a 256 2>/dev/null | cut -c1-12)"
207
+ elif command -v sha256sum >/dev/null 2>&1; then
208
+ _name_hash="$(printf '%s' "$workspace" | sha256sum 2>/dev/null | cut -c1-12)"
209
+ fi
210
+ if [ -n "$_name_hash" ]; then
211
+ argv+=(--name "loki-${_name_hash}")
212
+ else
213
+ argv+=(--name "loki-$(basename "$workspace" | tr -c 'A-Za-z0-9_.-' '_')")
214
+ fi
215
+ # The container IS the session boundary, so the runner must NOT setsid into
216
+ # a new, detached session: setsid detach inside a `--rm` container makes the
217
+ # `docker run` exit code report 0 even when the runner failed (a user with
218
+ # an expired token would get RC=0 and a silently empty folder). Running the
219
+ # runner in the foreground propagates the real exit code out through
220
+ # `docker run`, so the wrapper exits non-zero on a failed build -- exactly
221
+ # like the local CLI. Stop semantics still work: `docker stop` / Ctrl+C
222
+ # signals the container's main process.
223
+ argv+=(-e "LOKI_NO_NEW_SESSION=1")
224
+
225
+ case "$auth" in
226
+ apikey)
227
+ argv+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}")
228
+ ;;
229
+ oauth)
230
+ # rw so the in-container claude can refresh the short-lived token
231
+ # for the duration of THIS run. (The mounted file is a per-run temp
232
+ # copy that is wiped on exit, so the refresh does not persist to the
233
+ # host store -- but each run re-extracts a fresh, host-refreshed
234
+ # token, so an idle token is never a problem.)
235
+ argv+=(-v "${creds}:${_LOKI_DOCKER_CRED_DEST}:rw")
236
+ ;;
237
+ esac
238
+
239
+ argv+=("$LOKI_DOCKER_IMAGE")
240
+ argv+=("$@")
241
+
242
+ printf '%s\n' "${argv[@]}"
243
+ }
package/autonomy/loki CHANGED
@@ -1931,6 +1931,21 @@ cmd_start() {
1931
1931
  if [ -f "$_start_pid_file" ]; then
1932
1932
  local _existing_pid
1933
1933
  _existing_pid=$(cat "$_start_pid_file" 2>/dev/null | tr -dc '0-9')
1934
+ # Docker resume fix: in a `--rm` container every run starts a fresh PID
1935
+ # namespace where the orchestrator is PID 1. A prior run that wrote
1936
+ # loki.pid=1 leaves that marker in the bind-mounted .loki on the host;
1937
+ # the NEXT container is ALSO PID 1, so `kill -0 1` (a self-signal) always
1938
+ # succeeds and the lock falsely reads "another instance is running",
1939
+ # making resume in the same folder impossible. Two cases are NOT a live
1940
+ # sibling and must be cleared: (a) the recorded pid equals OUR OWN pid
1941
+ # ($$) -- this is the in-container resume case, where the new run is PID 1
1942
+ # and the stale marker is also 1; (b) the recorded pid is 1 but we are
1943
+ # NOT pid 1 (a host reading a container's leftover marker). Mirrors the
1944
+ # downstream guard in run.sh. A genuine concurrent sibling can never
1945
+ # share our $$, so this never masks a real running instance.
1946
+ if [ "$_existing_pid" = "$$" ] || { [ "$_existing_pid" = "1" ] && [ "$$" != "1" ]; }; then
1947
+ _existing_pid=""
1948
+ fi
1934
1949
  if [ -n "$_existing_pid" ] && kill -0 "$_existing_pid" 2>/dev/null; then
1935
1950
  echo -e "${RED}Error: another loki instance is running (pid $_existing_pid).${NC}" >&2
1936
1951
  echo -e "${YELLOW}Run 'loki stop' first, then retry 'loki start'.${NC}" >&2
@@ -14370,6 +14385,9 @@ main() {
14370
14385
  web)
14371
14386
  cmd_web "$@"
14372
14387
  ;;
14388
+ docker)
14389
+ cmd_docker "$@"
14390
+ ;;
14373
14391
  preview)
14374
14392
  cmd_preview "$@"
14375
14393
  ;;
@@ -28360,4 +28378,178 @@ PYEOF
28360
28378
  esac
28361
28379
  }
28362
28380
 
28381
+ # ---------------------------------------------------------------------------
28382
+ # loki docker - zero-friction Docker host wrapper
28383
+ #
28384
+ # Runs loki inside the published image with the SAME experience as the local
28385
+ # CLI: invoke it in a project folder and it just works. The current directory
28386
+ # is bind-mounted to /workspace, so .loki/ state (memory, session, queue,
28387
+ # checkpoints) persists on the host and resume + continuity behave exactly like
28388
+ # `loki start` locally. Auth is auto-detected (ANTHROPIC_API_KEY if set, else
28389
+ # the host Claude Code login), so Claude Code subscribers get zero-config runs.
28390
+ #
28391
+ # Usage:
28392
+ # loki docker start prd.md # run a build in the container, .loki persists
28393
+ # loki docker <any-loki-command> # forwards any command into the container
28394
+ # loki docker --dry-run start # print the docker argv without running
28395
+ # loki docker --image IMG ... # override the image (default published tag)
28396
+ # ---------------------------------------------------------------------------
28397
+ cmd_docker() {
28398
+ local docker_mod="$_LOKI_SCRIPT_DIR/docker-run.sh"
28399
+ if [ ! -f "$docker_mod" ]; then
28400
+ echo -e "${RED}Error: docker wrapper module not found at $docker_mod${NC}" >&2
28401
+ return 3
28402
+ fi
28403
+
28404
+ local sub="${1:-}"
28405
+ case "$sub" in
28406
+ ""|--help|-h|help)
28407
+ echo -e "${BOLD}loki docker${NC} - run loki inside the Docker image, zero config"
28408
+ echo ""
28409
+ echo "Usage: loki docker [--image IMG] [--dry-run] <loki-command> [args]"
28410
+ echo ""
28411
+ echo "Runs the published Loki image with your current folder mounted at"
28412
+ echo "/workspace, so .loki state persists and resume works exactly like"
28413
+ echo "the local CLI. Auth is auto-detected:"
28414
+ echo " 1. ANTHROPIC_API_KEY (if set in your environment), else"
28415
+ echo " 2. your host Claude Code login (Max/Pro subscription), else"
28416
+ echo " 3. an error with setup guidance."
28417
+ echo ""
28418
+ echo "Examples:"
28419
+ echo " loki docker start prd.md"
28420
+ echo " loki docker status"
28421
+ echo " loki docker --dry-run start prd.md"
28422
+ echo ""
28423
+ echo "Options:"
28424
+ echo " --image IMG Override the image (default: ${LOKI_DOCKER_IMAGE:-asklokesh/loki-mode:latest})"
28425
+ echo " --dry-run Print the docker command that would run, then exit"
28426
+ [ "$sub" = "" ] && return 1
28427
+ return 0
28428
+ ;;
28429
+ esac
28430
+
28431
+ if ! command -v docker >/dev/null 2>&1; then
28432
+ echo -e "${RED}Error: docker is not installed or not on PATH.${NC}" >&2
28433
+ echo "Install Docker Desktop (https://www.docker.com/products/docker-desktop/) and retry." >&2
28434
+ return 4
28435
+ fi
28436
+
28437
+ # Parse wrapper-only flags (everything else is the loki command to forward).
28438
+ # --api is BOTH a forwarded loki flag (start --api) and a wrapper signal to
28439
+ # publish the dashboard port, so we detect it without consuming it.
28440
+ local dry_run=0
28441
+ local with_api=0
28442
+ local -a fwd=()
28443
+ while [ $# -gt 0 ]; do
28444
+ case "$1" in
28445
+ --image) shift; export LOKI_DOCKER_IMAGE="${1:-}"; shift ;;
28446
+ --dry-run) dry_run=1; shift ;;
28447
+ --api) with_api=1; fwd+=("$1"); shift ;;
28448
+ *) fwd+=("$1"); shift ;;
28449
+ esac
28450
+ done
28451
+
28452
+ # shellcheck source=/dev/null
28453
+ source "$docker_mod"
28454
+
28455
+ local auth; auth="$(loki_docker_detect_auth)"
28456
+ local creds=""
28457
+ local cleanup_creds=0
28458
+
28459
+ case "$auth" in
28460
+ apikey)
28461
+ echo -e "${DIM:-}Auth: ANTHROPIC_API_KEY (from environment)${NC}" >&2
28462
+ ;;
28463
+ oauth)
28464
+ creds="$(mktemp "${TMPDIR:-/tmp}/loki-docker-creds.XXXXXX")" || {
28465
+ echo -e "${RED}Error: could not create a temp file for credentials.${NC}" >&2
28466
+ return 2
28467
+ }
28468
+ cleanup_creds=1
28469
+ # Wipe the token file on any exit path.
28470
+ # shellcheck disable=SC2064
28471
+ trap "rm -f '$creds' 2>/dev/null || true" EXIT INT TERM
28472
+ if ! loki_docker_extract_creds "$creds"; then
28473
+ echo -e "${RED}Error: found a host Claude Code login but could not read its token.${NC}" >&2
28474
+ return 2
28475
+ fi
28476
+ echo -e "${DIM:-}Auth: host Claude Code login (auto-detected, mounted read-write)${NC}" >&2
28477
+ ;;
28478
+ none)
28479
+ echo -e "${RED}Error: no auth available for the Docker run.${NC}" >&2
28480
+ echo "Do ONE of the following, then retry:" >&2
28481
+ echo " - export ANTHROPIC_API_KEY=sk-ant-... (get one at https://console.anthropic.com/settings/keys)" >&2
28482
+ echo " - or log in to Claude Code on this host (claude) so loki can reuse it." >&2
28483
+ return 5
28484
+ ;;
28485
+ esac
28486
+
28487
+ # Default the command to 'help' if the user passed only wrapper flags.
28488
+ [ ${#fwd[@]} -eq 0 ] && fwd=(help)
28489
+
28490
+ local -a docker_argv=()
28491
+ while IFS= read -r line; do docker_argv+=("$line"); done < <(loki_docker_build_argv "$auth" "$creds" "$with_api" "${fwd[@]}")
28492
+
28493
+ if [ "$dry_run" = "1" ]; then
28494
+ echo "Would run:"
28495
+ # Mask the API key value: dry-run output is exactly what users paste into
28496
+ # bug reports / CI logs, so never print the secret.
28497
+ local _a
28498
+ for _a in "${docker_argv[@]}"; do
28499
+ case "$_a" in
28500
+ ANTHROPIC_API_KEY=*) printf ' %q' "ANTHROPIC_API_KEY=***" ;;
28501
+ *) printf ' %q' "$_a" ;;
28502
+ esac
28503
+ done
28504
+ echo ""
28505
+ [ "$cleanup_creds" = "1" ] && rm -f "$creds" 2>/dev/null || true
28506
+ return 0
28507
+ fi
28508
+
28509
+ # Multi-repo unified dashboard (Option B): register THIS project on the host
28510
+ # with the real cwd and NO pid, so the existing host dashboard
28511
+ # (`loki dashboard`) lists it alongside host `loki start` projects and reads
28512
+ # its live status from the bind-mounted .loki/session.json. Best-effort and
28513
+ # failure-swallowed: registry bookkeeping must never block a build.
28514
+ _loki_docker_register_host running
28515
+ "${docker_argv[@]}"
28516
+ local rc=$?
28517
+ _loki_docker_register_host stopped
28518
+ [ "$cleanup_creds" = "1" ] && rm -f "$creds" 2>/dev/null || true
28519
+ return $rc
28520
+ }
28521
+
28522
+ # Host-side project registry bookkeeping for `loki docker` (Option B). Registers
28523
+ # $(pwd) in ~/.loki/dashboard/projects.json with NO pid (pid=None) so the
28524
+ # dashboard's "running" decision falls back to .loki/session.json (which the
28525
+ # container writes into the bind-mounted workspace). $1 = running|stopped.
28526
+ _loki_docker_register_host() {
28527
+ local _state="${1:-running}"
28528
+ command -v python3 >/dev/null 2>&1 || return 0
28529
+ local _skill="${SKILL_DIR:-$_LOKI_SCRIPT_DIR/..}"
28530
+ LOKI_REG_TARGET="$(pwd)" LOKI_REG_SKILL="$_skill" LOKI_REG_STATE="$_state" \
28531
+ python3 - <<'PYDOCKERREG' >/dev/null 2>&1 || true
28532
+ import os, sys
28533
+ sys.path.insert(0, os.environ.get("LOKI_REG_SKILL", "."))
28534
+ try:
28535
+ from dashboard import registry
28536
+ target = os.path.abspath(os.environ["LOKI_REG_TARGET"])
28537
+ state = os.environ.get("LOKI_REG_STATE", "running")
28538
+ if state == "stopped":
28539
+ registry.mark_project_stopped(target)
28540
+ else:
28541
+ registry.register_project(target)
28542
+ reg = registry._load_registry()
28543
+ pid_key = registry._generate_project_id(target)
28544
+ if pid_key in reg.get("projects", {}):
28545
+ # No host pid for a containerized run: the dashboard uses the
28546
+ # bind-mounted .loki/session.json to decide "running".
28547
+ reg["projects"][pid_key]["pid"] = None
28548
+ reg["projects"][pid_key]["status"] = "running"
28549
+ registry._save_registry(reg)
28550
+ except Exception:
28551
+ pass
28552
+ PYDOCKERREG
28553
+ }
28554
+
28363
28555
  main "$@"
package/autonomy/run.sh CHANGED
@@ -1550,7 +1550,24 @@ validate_api_keys() {
1550
1550
 
1551
1551
  local key_var=""
1552
1552
  case "$provider" in
1553
- claude) key_var="ANTHROPIC_API_KEY" ;;
1553
+ claude)
1554
+ # Inside Docker, the Claude Code CLI can authenticate either via
1555
+ # ANTHROPIC_API_KEY OR via a mounted OAuth credentials file (the
1556
+ # zero-friction `loki docker` wrapper mounts the host login at
1557
+ # ~/.claude/.credentials.json). If that file is present, the CLI
1558
+ # has a valid login and we must NOT block on the env var -- doing
1559
+ # so was the bug that made OAuth-based Docker runs exit at
1560
+ # pre-flight with "Required API key ... is not set". Mirror the
1561
+ # OAuth-aware doctor check (run.sh:9268).
1562
+ if [[ -z "${ANTHROPIC_API_KEY:-}" ]]; then
1563
+ local _claude_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
1564
+ if [[ -s "$_claude_creds" ]]; then
1565
+ log_info "Claude Code OAuth credentials detected ($_claude_creds); using CLI login."
1566
+ return 0
1567
+ fi
1568
+ fi
1569
+ key_var="ANTHROPIC_API_KEY"
1570
+ ;;
1554
1571
  codex) key_var="OPENAI_API_KEY" ;;
1555
1572
  cline) # Cline manages its own keys via `cline auth`
1556
1573
  if ! command -v cline &>/dev/null; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.44.0"
10
+ __version__ = "7.45.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2911,13 +2911,28 @@ async def stop_running_project(request: Request, body: RunningProjectStopRequest
2911
2911
 
2912
2912
  pid = project.get("pid")
2913
2913
  if not isinstance(pid, int) or pid <= 0:
2914
- # Already not running: nothing to signal, just reconcile the registry.
2914
+ # No host pid. Two sub-cases:
2915
+ # - A `loki docker` project registers with pid=None but may still be
2916
+ # actively building inside a container. Its runner polls .loki/STOP
2917
+ # (bind-mounted to the host path), so writing STOP here actually
2918
+ # stops the containerized build -- this is the unified-dashboard Stop
2919
+ # parity for Docker projects (the container pid is meaningless on the
2920
+ # host, so os.kill is not an option).
2921
+ # - A genuinely stopped project: the STOP write is harmless.
2922
+ # Write STOP when we resolved a real .loki dir, then reconcile.
2923
+ stop_signaled = False
2924
+ if loki_dir is not None:
2925
+ try:
2926
+ (loki_dir / "STOP").write_text(datetime.now(timezone.utc).isoformat())
2927
+ stop_signaled = True
2928
+ except OSError:
2929
+ pass
2915
2930
  registry.mark_project_stopped(project_id)
2916
2931
  return {
2917
2932
  "success": True,
2918
2933
  "project_id": project_id,
2919
- "stopped": False,
2920
- "already_stopped": True,
2934
+ "stopped": stop_signaled,
2935
+ "already_stopped": not stop_signaled,
2921
2936
  }
2922
2937
 
2923
2938
  # Write the STOP file so the runner's own cleanup STOP-branch fires for a
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.44.0
5
+ **Version:** v7.45.0
6
6
 
7
7
  ---
8
8
 
@@ -341,20 +341,86 @@ loki start ./my-spec.md --provider cline
341
341
 
342
342
  #### Docker
343
343
 
344
- Pass the provider as an environment variable:
344
+ Docker is where sandboxes and runs happen. The easiest way to run Loki in a
345
+ container is the `loki docker` host wrapper, which gives you the full local
346
+ experience (including `.loki/` state, resume, and continuity) in one command.
347
+ The raw `docker run` and `docker compose` methods below remain as alternatives.
348
+
349
+ ##### loki docker (easiest)
350
+
351
+ If you have loki installed on the host (npm/brew/bun) plus Docker, `loki docker`
352
+ runs any loki command inside the published `asklokesh/loki-mode` image with zero
353
+ config:
345
354
 
346
355
  ```bash
347
- # Use Codex with Docker
348
- docker run -e LOKI_PROVIDER=codex \
349
- -v $(pwd):/workspace -w /workspace \
350
- asklokesh/loki-mode:latest start ./my-spec.md
356
+ loki docker start prd.md # full local experience in Docker
357
+ loki docker status # any loki command works
358
+ loki docker --dry-run start prd.md # print the docker command, do not run
359
+ ```
351
360
 
352
- # Use Cline with Docker
353
- docker run -e LOKI_PROVIDER=cline \
361
+ It bind-mounts the current folder to `/workspace`, so `.loki/` state (memory,
362
+ session, queue, checkpoints) persists on the host and resume and continuity
363
+ behave exactly like the local `loki` CLI. Auth is auto-detected:
364
+ `ANTHROPIC_API_KEY` if set, else your host's existing Claude Code login
365
+ (Max/Pro subscribers need no API key), else an honest error with guidance. It
366
+ also forwards `~/.gitconfig` and `~/.config/gh` (read-only) plus
367
+ `GITHUB_TOKEN`/`GH_TOKEN` if set, so commits and PRs work like local, and
368
+ exposes the dashboard on port 57374. Use `--image IMG` to override the image.
369
+
370
+ `loki docker` is a thin host wrapper around the image; it requires loki and
371
+ Docker installed on the host.
372
+
373
+ Multi-repo + unified dashboard: you can run `loki docker start` in several
374
+ different repos at once, exactly like the host CLI. Each repo gets its own
375
+ container (deterministic name `loki-<hash-of-path>`) and its own bind-mounted
376
+ `.loki/` state. Every `loki docker` project registers with the host dashboard,
377
+ so running `loki dashboard` on the host shows ALL your projects in one unified
378
+ view, whether they run via local `loki start` or via `loki docker start`. Builds
379
+ run with the dashboard off by default (so concurrent runs do not collide on port
380
+ 57374); use the host `loki dashboard`, or `loki docker start --api` for a single
381
+ containerized run's UI.
382
+
383
+ ##### docker run
384
+
385
+ As of v7.45.0 the image ships the Claude Code CLI, so the default `claude`
386
+ provider works inside the container. Provide auth with your Anthropic API key:
387
+
388
+ ```bash
389
+ # Run Loki Mode in Docker (Claude provider, API-key auth)
390
+ docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
354
391
  -v $(pwd):/workspace -w /workspace \
355
- asklokesh/loki-mode:latest start ./my-spec.md
392
+ asklokesh/loki-mode:7.45.0 start ./my-spec.md
393
+ ```
394
+
395
+ ##### docker compose + .env (no host install)
396
+
397
+ If you prefer not to install loki on the host, the repo ships a
398
+ `docker-compose.yml` with `env_file: .env`. Copy the example,
399
+ set your key once, then run without retyping long `-e` flags:
400
+
401
+ ```bash
402
+ cp .env.example .env # then edit .env and set ANTHROPIC_API_KEY
403
+ docker compose run loki start prd.md
356
404
  ```
357
405
 
406
+ `.env` is gitignored. Edit it and re-run any time; compose reloads it
407
+ automatically.
408
+
409
+ ##### Host OAuth (Claude Code Max/Pro subscribers, no API key)
410
+
411
+ If you have a Claude Code subscription and no API key, you can reuse your host
412
+ login by mounting your credentials file at
413
+ `/home/loki/.claude/.credentials.json` instead of setting `ANTHROPIC_API_KEY`.
414
+ See [DOCKER_README.md](../DOCKER_README.md) "Option 2: host OAuth" for the
415
+ one-line export command and the compose volume to uncomment.
416
+
417
+ ##### Other providers in Docker
418
+
419
+ The image ships only the Claude Code CLI. Codex, Cline, and Aider are
420
+ bring-your-own-CLI: install the provider CLI in a derived image (or mount it),
421
+ then select it with `-e LOKI_PROVIDER=<name>`. See
422
+ [DOCKER_README.md](../DOCKER_README.md) for details.
423
+
358
424
  ### Degraded Mode
359
425
 
360
426
  When using `codex`, `cline`, or `aider` providers, Loki Mode operates in **degraded mode**:
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.44.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
2
+ var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.45.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
789
789
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
790
790
  `),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
791
791
 
792
- //# debugId=EAB4A25E0B3FC92864756E2164756E21
792
+ //# debugId=7FCBCE9F1C748AE964756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.44.0'
60
+ __version__ = '7.45.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.44.0",
4
+ "version": "7.45.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.44.0",
5
+ "version": "7.45.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",