loki-mode 7.90.2 → 7.91.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +19 -1
- package/autonomy/run.sh +72 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +176 -4
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/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, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.91.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.91.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.91.0
|
package/autonomy/loki
CHANGED
|
@@ -9834,7 +9834,25 @@ cmd_doctor() {
|
|
|
9834
9834
|
echo -e " ${GREEN}PASS${NC} ANTHROPIC_API_KEY is set"
|
|
9835
9835
|
pass_count=$((pass_count + 1))
|
|
9836
9836
|
elif command -v claude &>/dev/null; then
|
|
9837
|
-
|
|
9837
|
+
# Zero-network check: warn if the Claude OAuth login has expired. An
|
|
9838
|
+
# expired token would otherwise pass and then 401 mid-build (the build
|
|
9839
|
+
# stalls at BOOTSTRAP). Mirror run.sh's fail-fast preflight here so a
|
|
9840
|
+
# user catches it with `loki doctor` BEFORE starting a build.
|
|
9841
|
+
_claude_creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
9842
|
+
if [ -s "$_claude_creds" ] && [ "$(python3 -c "
|
|
9843
|
+
import json, sys, time
|
|
9844
|
+
try:
|
|
9845
|
+
d = json.load(open(sys.argv[1]))
|
|
9846
|
+
exp = d.get('claudeAiOauth', {}).get('expiresAt')
|
|
9847
|
+
print('expired' if isinstance(exp,(int,float)) and exp>0 and (exp/1000.0)<=(time.time()+60) else 'ok')
|
|
9848
|
+
except Exception:
|
|
9849
|
+
print('ok')
|
|
9850
|
+
" "$_claude_creds" 2>/dev/null || echo ok)" = "expired" ]; then
|
|
9851
|
+
echo -e " ${YELLOW}WARN${NC} Claude login has EXPIRED -- run 'claude login' before a build (it would otherwise stall)"
|
|
9852
|
+
warn_count=$((warn_count + 1))
|
|
9853
|
+
else
|
|
9854
|
+
echo -e " ${DIM} -- ${NC} ANTHROPIC_API_KEY not set (Claude CLI uses its own login)"
|
|
9855
|
+
fi
|
|
9838
9856
|
fi
|
|
9839
9857
|
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
9840
9858
|
echo -e " ${GREEN}PASS${NC} OPENAI_API_KEY is set"
|
package/autonomy/run.sh
CHANGED
|
@@ -829,6 +829,12 @@ CONCURRENCY_CRITICAL_THRESHOLD=${LOKI_CONCURRENCY_CRITICAL_THRESHOLD:-95}
|
|
|
829
829
|
GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3}
|
|
830
830
|
GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5}
|
|
831
831
|
GATE_PAUSE_LIMIT=${LOKI_GATE_PAUSE_LIMIT:-10}
|
|
832
|
+
# Workspace resolution: the build runs against TARGET_DIR, defaulting to the
|
|
833
|
+
# launch cwd. A caller can pin a per-build workspace by exporting
|
|
834
|
+
# LOKI_TARGET_DIR (and the matching LOKI_DIR=<dir>/.loki); the dashboard
|
|
835
|
+
# /api/control/start `workspace` param (Track-1 S1) does exactly this so a
|
|
836
|
+
# hosted build runs in its own dir instead of the engine's repo. Every
|
|
837
|
+
# $TARGET_DIR/.loki reference below then resolves under that workspace.
|
|
832
838
|
TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
|
|
833
839
|
PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
|
|
834
840
|
AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
|
|
@@ -1428,6 +1434,30 @@ _loki_trust_run_id() {
|
|
|
1428
1434
|
printf '%s' ""
|
|
1429
1435
|
}
|
|
1430
1436
|
|
|
1437
|
+
# _advance_current_phase: atomically advance currentPhase in orchestrator.json.
|
|
1438
|
+
# This is the single source of truth for the build dashboard and the BFF
|
|
1439
|
+
# reconciliation gate (isTerminalPhase). The engine initialises it to "BOOTSTRAP"
|
|
1440
|
+
# at session start; call this to advance through the SDLC lifecycle phases.
|
|
1441
|
+
# Args: $1 = the new phase value (REASONING, BUILDING, VERIFYING, COMPLETED, etc.)
|
|
1442
|
+
_advance_current_phase() {
|
|
1443
|
+
local new_phase="${1:?phase required}"
|
|
1444
|
+
local loki_dir="${LOKI_DIR:-${TARGET_DIR:-.}}/.loki"
|
|
1445
|
+
local orch="$loki_dir/state/orchestrator.json"
|
|
1446
|
+
[ -f "$orch" ] || return 0
|
|
1447
|
+
# Values are passed via argv (not interpolated into the source) so a phase or
|
|
1448
|
+
# path containing quotes can never break or inject into the python.
|
|
1449
|
+
python3 -c "
|
|
1450
|
+
import json, sys
|
|
1451
|
+
f, phase = sys.argv[1], sys.argv[2]
|
|
1452
|
+
try:
|
|
1453
|
+
d = json.load(open(f))
|
|
1454
|
+
d['currentPhase'] = phase
|
|
1455
|
+
json.dump(d, open(f, 'w'))
|
|
1456
|
+
except (json.JSONDecodeError, OSError):
|
|
1457
|
+
pass
|
|
1458
|
+
" "$orch" "$new_phase" 2>/dev/null || true
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1431
1461
|
# Usage: record_trust_event_bash <event_type> [key=value ...]
|
|
1432
1462
|
# Pass LOKI_TRUST_RUN_ID in the environment to override the resolved id (the
|
|
1433
1463
|
# run_start site sets it to the freshly minted id so the first event matches).
|
|
@@ -1808,6 +1838,37 @@ validate_api_keys() {
|
|
|
1808
1838
|
local masked="${key_value:0:8}...${key_value: -4}"
|
|
1809
1839
|
log_info "API key $key_var: $masked (${#key_value} chars)"
|
|
1810
1840
|
|
|
1841
|
+
# Fail-fast auth preflight (v7.91): for Claude OAuth logins, a present-but-
|
|
1842
|
+
# EXPIRED token passes the presence check above but then 401s on the first
|
|
1843
|
+
# provider call, leaving the build stalled at BOOTSTRAP with no clear cause.
|
|
1844
|
+
# Catch that here with a zero-network, zero-token local expiry check so the
|
|
1845
|
+
# user gets an instant, copy-pasteable fix instead of a silent stall.
|
|
1846
|
+
# Opt out with LOKI_SKIP_AUTH_PREFLIGHT=1 (offline/odd setups).
|
|
1847
|
+
if [[ "$provider" == "claude" && "${LOKI_SKIP_AUTH_PREFLIGHT:-}" != "1" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
|
|
1848
|
+
local _creds="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.credentials.json"
|
|
1849
|
+
if [[ -s "$_creds" ]]; then
|
|
1850
|
+
local _expired
|
|
1851
|
+
_expired=$(python3 -c "
|
|
1852
|
+
import json, sys, time
|
|
1853
|
+
try:
|
|
1854
|
+
d = json.load(open(sys.argv[1]))
|
|
1855
|
+
exp = d.get('claudeAiOauth', {}).get('expiresAt')
|
|
1856
|
+
# expiresAt is epoch milliseconds; compare with a 60s safety margin.
|
|
1857
|
+
if isinstance(exp, (int, float)) and exp > 0 and (exp / 1000.0) <= (time.time() + 60):
|
|
1858
|
+
print('expired')
|
|
1859
|
+
except Exception:
|
|
1860
|
+
pass # unreadable/unknown schema -> do not block (fail open, let the call decide)
|
|
1861
|
+
" "$_creds" 2>/dev/null || true)
|
|
1862
|
+
if [[ "$_expired" == "expired" ]]; then
|
|
1863
|
+
log_error "Your Claude Code login has expired -- the build would stall instead of running."
|
|
1864
|
+
log_error "Fix it in one step, then retry:"
|
|
1865
|
+
log_error " claude login"
|
|
1866
|
+
log_error "(or set ANTHROPIC_API_KEY, or LOKI_SKIP_AUTH_PREFLIGHT=1 to bypass this check)"
|
|
1867
|
+
return 1
|
|
1868
|
+
fi
|
|
1869
|
+
fi
|
|
1870
|
+
fi
|
|
1871
|
+
|
|
1811
1872
|
return 0
|
|
1812
1873
|
}
|
|
1813
1874
|
|
|
@@ -17852,7 +17913,10 @@ main() {
|
|
|
17852
17913
|
# Cleanup parallel streams
|
|
17853
17914
|
cleanup_parallel_streams
|
|
17854
17915
|
else
|
|
17855
|
-
# Standard mode: single session
|
|
17916
|
+
# Standard mode: single session. Advance phase from BOOTSTRAP to BUILDING
|
|
17917
|
+
# before the first iteration so the dashboard shows real progress, not
|
|
17918
|
+
# a stuck "Planning" state.
|
|
17919
|
+
_advance_current_phase "BUILDING"
|
|
17856
17920
|
run_autonomous "$PRD_PATH" || result=$?
|
|
17857
17921
|
fi
|
|
17858
17922
|
|
|
@@ -17939,6 +18003,13 @@ main() {
|
|
|
17939
18003
|
generate_proof_of_run "$result" || true
|
|
17940
18004
|
fi
|
|
17941
18005
|
|
|
18006
|
+
# Advance currentPhase to COMPLETED so the BFF reconciliation gate and the
|
|
18007
|
+
# engine's own is_completed() recognise this session as terminal. Write the
|
|
18008
|
+
# file-system COMPLETED marker (checked by is_completed() in the main loop).
|
|
18009
|
+
_advance_current_phase "COMPLETED"
|
|
18010
|
+
local loki_dir="${LOKI_DIR:-${TARGET_DIR:-.}}/.loki"
|
|
18011
|
+
echo "Session completed at $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$loki_dir/COMPLETED" 2>/dev/null || true
|
|
18012
|
+
|
|
17942
18013
|
# Finish-and-own (v7.88.0): write a plain-English ownership handoff
|
|
17943
18014
|
# (HANDOFF.md) for a non-technical owner. Runs AFTER the proof so the
|
|
17944
18015
|
# "is it working?" verdict reads the receipt's honest headline. Default-on,
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -430,6 +430,15 @@ class StatusResponse(BaseModel):
|
|
|
430
430
|
# correlation-only (v7.34); "resume" = LOKI_RESUME_SESSION recovery resume.
|
|
431
431
|
# Empty when the run predates this field or no claude session was stamped.
|
|
432
432
|
claude_session_mode: str = ""
|
|
433
|
+
# Track-1 (S1): the trust run_id (the proof key, format
|
|
434
|
+
# run-<ts>-<pid>-<rand>) of the run in THIS engine's resolved .loki dir, read
|
|
435
|
+
# from .loki/state/trust-run-id. Lets a caller correlate the run with its
|
|
436
|
+
# proof/receipt without parsing the pid. Empty when no run has minted one
|
|
437
|
+
# yet, or the run predates the field. NOTE for hosted callers: this reflects
|
|
438
|
+
# the engine's GLOBAL/cwd .loki (_get_loki_dir), so for a build started with a
|
|
439
|
+
# distinct workspace param it does NOT track that workspace's run -- read
|
|
440
|
+
# <workspace>/.loki/state/trust-run-id directly for the per-workspace build.
|
|
441
|
+
current_run_id: str = ""
|
|
433
442
|
# Concurrent sessions (v6.4.0)
|
|
434
443
|
sessions: list[SessionInfo] = []
|
|
435
444
|
|
|
@@ -1158,6 +1167,18 @@ async def get_status() -> StatusResponse:
|
|
|
1158
1167
|
except (json.JSONDecodeError, OSError, KeyError, AttributeError):
|
|
1159
1168
|
pass
|
|
1160
1169
|
|
|
1170
|
+
# Track-1 (S1): the trust run_id (proof key) for the run in this resolved
|
|
1171
|
+
# .loki dir. Best-effort read; empty when no run has minted one yet. Lets a
|
|
1172
|
+
# caller correlate the run with its proof without parsing the pid.
|
|
1173
|
+
current_run_id = ""
|
|
1174
|
+
trust_run_id_file = loki_dir / "state" / "trust-run-id"
|
|
1175
|
+
if trust_run_id_file.exists():
|
|
1176
|
+
try:
|
|
1177
|
+
_rid = trust_run_id_file.read_text(encoding="utf-8").strip()
|
|
1178
|
+
current_run_id = _rid if isinstance(_rid, str) else ""
|
|
1179
|
+
except OSError:
|
|
1180
|
+
pass
|
|
1181
|
+
|
|
1161
1182
|
# Read dashboard state (with retry for concurrent writes)
|
|
1162
1183
|
_has_dashboard_state = False
|
|
1163
1184
|
if state_file.exists():
|
|
@@ -1412,6 +1433,7 @@ async def get_status() -> StatusResponse:
|
|
|
1412
1433
|
current_task=current_task,
|
|
1413
1434
|
claude_session_id=claude_session_id,
|
|
1414
1435
|
claude_session_mode=claude_session_mode,
|
|
1436
|
+
current_run_id=current_run_id,
|
|
1415
1437
|
sessions=active_session_list,
|
|
1416
1438
|
)
|
|
1417
1439
|
|
|
@@ -3018,6 +3040,14 @@ class StartBuildRequest(BaseModel):
|
|
|
3018
3040
|
prd_path: Optional[str] = None
|
|
3019
3041
|
provider: str = "claude"
|
|
3020
3042
|
parallel: bool = False
|
|
3043
|
+
# Track-1 (S1): an OPTIONAL per-build workspace directory. When present and
|
|
3044
|
+
# path-guarded, the build runs against THIS dir (its own cwd + .loki) instead
|
|
3045
|
+
# of the engine's own project dir, so a hosted caller (the SaaS BFF) can run a
|
|
3046
|
+
# build outside the engine repo without polluting it. When OMITTED, behavior
|
|
3047
|
+
# is byte-identical to before this field existed (the global project dir).
|
|
3048
|
+
# The path is path-guarded against ALLOWED_WORKSPACE_ROOTS (see
|
|
3049
|
+
# _validate_workspace) -- it is NOT a free-form filesystem write target.
|
|
3050
|
+
workspace: Optional[str] = None
|
|
3021
3051
|
|
|
3022
3052
|
def validate_provider(self) -> None:
|
|
3023
3053
|
"""Validate provider is from the supported list.
|
|
@@ -3071,6 +3101,88 @@ def _validate_prd_path(raw_path: str, project_dir: _Path) -> _Path:
|
|
|
3071
3101
|
raise ValueError(f"PRD path is outside allowed directories: {raw_path}")
|
|
3072
3102
|
|
|
3073
3103
|
|
|
3104
|
+
def _allowed_workspace_roots() -> list[_Path]:
|
|
3105
|
+
"""Resolved roots a caller-supplied workspace may live under (Track-1 S1).
|
|
3106
|
+
|
|
3107
|
+
Configured via LOKI_WORKSPACE_ROOTS (os.pathsep-separated absolute dirs).
|
|
3108
|
+
Each entry is expanded and resolved; nonexistent / non-absolute / relative
|
|
3109
|
+
entries are dropped (a misconfigured root must NOT silently widen the guard
|
|
3110
|
+
to the CWD). When the env var is unset or yields no valid root, the list is
|
|
3111
|
+
empty and _validate_workspace rejects every workspace -- the feature is
|
|
3112
|
+
opt-in by configuration, fail-closed by default.
|
|
3113
|
+
|
|
3114
|
+
This is deliberately NARROWER than the PRD-path roots ([project_dir, home]):
|
|
3115
|
+
a build cwd runs an autonomous agent, so its root must be an explicitly
|
|
3116
|
+
operator-allowed location (e.g. the BFF's ENGINE_WORKSPACE_ROOT, default
|
|
3117
|
+
/workspaces), never the engine repo or the whole home directory.
|
|
3118
|
+
"""
|
|
3119
|
+
raw = os.environ.get("LOKI_WORKSPACE_ROOTS", "").strip()
|
|
3120
|
+
if not raw:
|
|
3121
|
+
return []
|
|
3122
|
+
roots: list[_Path] = []
|
|
3123
|
+
for entry in raw.split(os.pathsep):
|
|
3124
|
+
entry = entry.strip()
|
|
3125
|
+
if not entry:
|
|
3126
|
+
continue
|
|
3127
|
+
candidate = _Path(entry).expanduser()
|
|
3128
|
+
if not candidate.is_absolute():
|
|
3129
|
+
continue
|
|
3130
|
+
try:
|
|
3131
|
+
resolved = candidate.resolve()
|
|
3132
|
+
except OSError:
|
|
3133
|
+
continue
|
|
3134
|
+
if resolved.is_dir():
|
|
3135
|
+
roots.append(resolved)
|
|
3136
|
+
return roots
|
|
3137
|
+
|
|
3138
|
+
|
|
3139
|
+
def _validate_workspace(raw_path: str) -> _Path:
|
|
3140
|
+
"""Path-guard a caller-supplied per-build workspace dir (Track-1 S1).
|
|
3141
|
+
|
|
3142
|
+
Unlike _validate_prd_path, the workspace need NOT exist yet (the BFF passes
|
|
3143
|
+
<ENGINE_WORKSPACE_ROOT>/<buildId>, created on first build); this guard
|
|
3144
|
+
normalizes a possibly-nonexistent path and verifies CONTAINMENT under one of
|
|
3145
|
+
LOKI_WORKSPACE_ROOTS, then the caller mkdirs it. Returns the resolved,
|
|
3146
|
+
verified, absolute workspace path. Raises ValueError on any unsafe path.
|
|
3147
|
+
|
|
3148
|
+
Guard order:
|
|
3149
|
+
1. Reject literal ".." traversal sequences before any resolution.
|
|
3150
|
+
2. Require an absolute input (a relative path would resolve against the
|
|
3151
|
+
dashboard CWD -- never an intended workspace).
|
|
3152
|
+
3. resolve() (follows symlinks on existing parents) then assert the real
|
|
3153
|
+
path is under an allowed root -- catches a symlinked parent that
|
|
3154
|
+
escaped the no-".." check.
|
|
3155
|
+
"""
|
|
3156
|
+
if not raw_path or not raw_path.strip():
|
|
3157
|
+
raise ValueError("workspace must be a non-empty path")
|
|
3158
|
+
raw_path = raw_path.strip()
|
|
3159
|
+
if ".." in raw_path:
|
|
3160
|
+
raise ValueError("workspace contains path traversal sequence (..)")
|
|
3161
|
+
|
|
3162
|
+
candidate = _Path(raw_path).expanduser()
|
|
3163
|
+
if not candidate.is_absolute():
|
|
3164
|
+
raise ValueError("workspace must be an absolute path")
|
|
3165
|
+
|
|
3166
|
+
try:
|
|
3167
|
+
ws = candidate.resolve()
|
|
3168
|
+
except OSError as e:
|
|
3169
|
+
raise ValueError(f"workspace path could not be resolved: {e}")
|
|
3170
|
+
|
|
3171
|
+
roots = _allowed_workspace_roots()
|
|
3172
|
+
if not roots:
|
|
3173
|
+
raise ValueError(
|
|
3174
|
+
"workspace param is not enabled: set LOKI_WORKSPACE_ROOTS to one or "
|
|
3175
|
+
"more absolute directories before passing a workspace"
|
|
3176
|
+
)
|
|
3177
|
+
for root in roots:
|
|
3178
|
+
try:
|
|
3179
|
+
ws.relative_to(root)
|
|
3180
|
+
return ws
|
|
3181
|
+
except ValueError:
|
|
3182
|
+
continue
|
|
3183
|
+
raise ValueError(f"workspace is outside allowed roots: {raw_path}")
|
|
3184
|
+
|
|
3185
|
+
|
|
3074
3186
|
def _write_spec_text(prd_text: str, project_dir: _Path) -> _Path:
|
|
3075
3187
|
"""Persist an inline spec to .loki/specs/ and return its path.
|
|
3076
3188
|
|
|
@@ -3153,10 +3265,34 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
3153
3265
|
detail="Provide exactly one of prd_text or prd_path",
|
|
3154
3266
|
)
|
|
3155
3267
|
|
|
3156
|
-
# Resolve the target project directory
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3268
|
+
# Resolve the target project directory. When the caller supplies a
|
|
3269
|
+
# path-guarded workspace (Track-1 S1), the build runs against THAT dir (its
|
|
3270
|
+
# own cwd + .loki) so a hosted caller can build outside the engine repo.
|
|
3271
|
+
# When omitted, behavior is byte-identical to before: the active dashboard
|
|
3272
|
+
# project (the engine's own _get_loki_dir).
|
|
3273
|
+
workspace_dir: Optional[_Path] = None
|
|
3274
|
+
if body.workspace and body.workspace.strip():
|
|
3275
|
+
try:
|
|
3276
|
+
workspace_dir = _validate_workspace(body.workspace)
|
|
3277
|
+
except ValueError as e:
|
|
3278
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
3279
|
+
|
|
3280
|
+
if workspace_dir is not None:
|
|
3281
|
+
# The build's project dir IS the workspace; its .loki lives inside it.
|
|
3282
|
+
# Create it now so single-flight + spec-write below operate on the real
|
|
3283
|
+
# build dir (the BFF passes <root>/<buildId>, absent on first build).
|
|
3284
|
+
try:
|
|
3285
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
3286
|
+
except OSError as e:
|
|
3287
|
+
raise HTTPException(
|
|
3288
|
+
status_code=500, detail=f"Could not create workspace: {e}"
|
|
3289
|
+
)
|
|
3290
|
+
project_dir = workspace_dir
|
|
3291
|
+
loki_dir = workspace_dir / ".loki"
|
|
3292
|
+
else:
|
|
3293
|
+
loki_dir = _get_loki_dir()
|
|
3294
|
+
project_dir = loki_dir.parent if loki_dir.name == ".loki" else _Path.cwd()
|
|
3295
|
+
project_dir = project_dir.resolve()
|
|
3160
3296
|
|
|
3161
3297
|
# Single-flight: refuse if a run is already active in this project.
|
|
3162
3298
|
active_pid = _project_run_active(loki_dir)
|
|
@@ -3191,6 +3327,19 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
3191
3327
|
args.append("--bg")
|
|
3192
3328
|
args.append(str(spec_file))
|
|
3193
3329
|
|
|
3330
|
+
# When a workspace is given, pass an explicit env that PINS run.sh to the
|
|
3331
|
+
# workspace. cwd alone is not enough: `loki` exports LOKI_DIR (default
|
|
3332
|
+
# .loki) into the dashboard process, and run.sh resolves its workspace as
|
|
3333
|
+
# ${LOKI_DIR:-${LOKI_TARGET_DIR:-$(pwd)}/.loki} -- an inherited LOKI_DIR
|
|
3334
|
+
# would override the workspace cwd and the build would write into the
|
|
3335
|
+
# engine's own .loki. Setting both LOKI_DIR and LOKI_TARGET_DIR makes the
|
|
3336
|
+
# workspace authoritative. When no workspace is given, env=None (inherit) so
|
|
3337
|
+
# behavior is byte-identical to before this feature.
|
|
3338
|
+
popen_env = None
|
|
3339
|
+
if workspace_dir is not None:
|
|
3340
|
+
popen_env = dict(os.environ)
|
|
3341
|
+
popen_env["LOKI_TARGET_DIR"] = str(workspace_dir)
|
|
3342
|
+
popen_env["LOKI_DIR"] = str(loki_dir)
|
|
3194
3343
|
try:
|
|
3195
3344
|
process = subprocess.Popen(
|
|
3196
3345
|
args,
|
|
@@ -3198,6 +3347,7 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
3198
3347
|
stderr=subprocess.DEVNULL,
|
|
3199
3348
|
start_new_session=True,
|
|
3200
3349
|
cwd=str(project_dir),
|
|
3350
|
+
env=popen_env,
|
|
3201
3351
|
)
|
|
3202
3352
|
except (OSError, subprocess.SubprocessError) as e:
|
|
3203
3353
|
raise HTTPException(status_code=500, detail=f"Failed to start build: {e}")
|
|
@@ -3242,12 +3392,34 @@ async def start_build(request: Request, body: StartBuildRequest):
|
|
|
3242
3392
|
ip_address=request.client.host if request.client else None,
|
|
3243
3393
|
)
|
|
3244
3394
|
|
|
3395
|
+
# Best-effort: surface the trust run_id (the proof key) so the caller can
|
|
3396
|
+
# correlate this build to its Evidence Receipt without parsing the pid.
|
|
3397
|
+
# run.sh mints it into <loki_dir>/state/trust-run-id at run-start; the 0.3s
|
|
3398
|
+
# liveness poll above usually does NOT outlast the mint, so this is often
|
|
3399
|
+
# null at start time. It is RELIABLY derivable later by the caller, which
|
|
3400
|
+
# owns the workspace path: read <workspace>/.loki/state/trust-run-id (or the
|
|
3401
|
+
# newest proof under <workspace>/.loki). Null here is expected, not an error.
|
|
3402
|
+
run_id = ""
|
|
3403
|
+
try:
|
|
3404
|
+
rid_file = loki_dir / "state" / "trust-run-id"
|
|
3405
|
+
if rid_file.is_file():
|
|
3406
|
+
run_id = rid_file.read_text(encoding="utf-8").strip()
|
|
3407
|
+
except OSError:
|
|
3408
|
+
pass
|
|
3409
|
+
|
|
3245
3410
|
return {
|
|
3246
3411
|
"success": True,
|
|
3247
3412
|
"message": f"Build started with provider {body.provider}",
|
|
3248
3413
|
"pid": process.pid,
|
|
3249
3414
|
"spec": str(spec_file),
|
|
3250
3415
|
"provider": body.provider,
|
|
3416
|
+
# The workspace this build runs in (echoed back; empty == engine's own
|
|
3417
|
+
# project dir, today's default). The caller derives the proof path from
|
|
3418
|
+
# this: <workspace>/.loki/state/trust-run-id.
|
|
3419
|
+
"workspace": str(workspace_dir) if workspace_dir is not None else "",
|
|
3420
|
+
# The trust run_id if already minted, else "" (see note above). When "",
|
|
3421
|
+
# derive it from the workspace's .loki/state/trust-run-id.
|
|
3422
|
+
"run_id": run_id,
|
|
3251
3423
|
}
|
|
3252
3424
|
|
|
3253
3425
|
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.
|
|
5
|
+
**Version:** v7.91.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
|
|
|
395
395
|
# Run Loki Mode in Docker (Claude provider, API-key auth)
|
|
396
396
|
docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
|
397
397
|
-v $(pwd):/workspace -w /workspace \
|
|
398
|
-
asklokesh/loki-mode:7.
|
|
398
|
+
asklokesh/loki-mode:7.91.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var z8=Object.defineProperty;var X8=($)=>$;function K8($,Q){this[$]=X8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)z8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:K8.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var y1={};b(y1,{lokiDir:()=>P,homeLokiDir:()=>T$,findRepoRootForVersion:()=>e$,REPO_ROOT:()=>h});import{resolve as t,dirname as i$}from"path";import{fileURLToPath as q8}from"url";import{existsSync as E$}from"fs";import{homedir as J8}from"os";function V8(){let $=h1;for(let Q=0;Q<6;Q++){if(E$(t($,"VERSION"))&&E$(t($,"autonomy/run.sh")))return $;let Z=i$($);if(Z===$)break;$=Z}return t(h1,"..","..","..")}function e$($){let Q=$;for(let Z=0;Z<6;Z++){if(E$(t(Q,"VERSION"))&&E$(t(Q,"autonomy/run.sh")))return Q;let z=i$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function P(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(J8(),".loki")}var h1,h;var C=L(()=>{h1=i$(q8(import.meta.url));h=V8()});import{readFileSync as W8}from"fs";import{resolve as U8,dirname as H8}from"path";import{fileURLToPath as G8}from"url";function N$(){if(Q$!==null)return Q$;let $="7.91.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=H8(G8(import.meta.url)),Z=e$(Q);Q$=W8(U8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var $1=L(()=>{C()});var u1={};b(u1,{runOrThrow:()=>x8,run:()=>F,readStreamCapped:()=>f1,commandVersion:()=>N8,commandExists:()=>f,ShellError:()=>Q1,MAX_STDOUT_BYTES:()=>m1});async function f1($,Q=m1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function F($,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[q,K,W]=await Promise.all([f1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function x8($,Q={}){let Z=await F($,Q);if(Z.exitCode!==0)throw new Q1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=E8($),Z=await F(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function E8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function N8($,Q="--version"){if(!await f($))return null;let z=await F([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var m1=16777216,Q1;var d=L(()=>{Q1=class Q1 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 r($){return S8?"":$}var S8,T,S,_,lZ,I,k,y,J;var c=L(()=>{S8=(process.env.NO_COLOR??"").length>0;T=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),lZ=r("\x1B[0;34m"),I=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as u8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(u8($))return A$=$,$;let Q=await f("python3.12");if(Q)return A$=Q,Q;let Z=await f("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return F([Z,"-c",$],Q)}var A$;var V$=L(()=>{d()});var q0={};b(q0,{runStatus:()=>W3});import{existsSync as v,readFileSync as U$,readdirSync as i1,statSync as e1}from"fs";import{resolve as D,basename as $3}from"path";import{homedir as Q3}from"os";function $0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function Q0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*D$/Q);if(X>D$)X=D$;let q=D$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=$0($),U=$0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${U})`}async function z3(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${J}
|
|
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)
|
|
@@ -802,4 +802,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
802
802
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
803
803
|
`),process.stderr.write(Z8),2}}r1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var FZ=await jZ(Bun.argv.slice(2));process.exit(FZ);
|
|
804
804
|
|
|
805
|
-
//# debugId=
|
|
805
|
+
//# debugId=2A4ACA24DD51A05A64756E2164756E21
|
package/mcp/__init__.py
CHANGED
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.
|
|
4
|
+
"version": "7.91.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 8 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.
|
|
5
|
+
"version": "7.91.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 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",
|