loki-mode 7.59.2 → 7.61.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/lib/proof_redact.py +41 -0
- package/autonomy/loki +28 -8
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_v2.py +26 -5
- package/dashboard/server.py +1 -1
- package/docs/INSTALLATION.md +2 -2
- package/events/emit.sh +18 -0
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +9 -5
- package/memory/token_economics.py +10 -2
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/src/policies/check.js +32 -3
- package/src/policies/engine.js +20 -0
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.61.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
406
406
|
|
|
407
407
|
---
|
|
408
408
|
|
|
409
|
-
**v7.
|
|
409
|
+
**v7.61.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.61.0
|
|
@@ -79,6 +79,33 @@ _PATTERNS = [
|
|
|
79
79
|
# Bearer tokens: keep the scheme, redact the credential.
|
|
80
80
|
_BEARER = re.compile(r"(Bearer\s+)[A-Za-z0-9._~+/=-]{20,}")
|
|
81
81
|
|
|
82
|
+
# HTTP auth/cookie HEADER lines: redact the credential VALUE for the whole line.
|
|
83
|
+
# This owns the header-line form of Authorization (any scheme: Basic, Bearer,
|
|
84
|
+
# Digest, raw token) plus Cookie / Set-Cookie, which the keyword-based
|
|
85
|
+
# _ENV_ASSIGN rule does not cover (Authorization is excluded there via the
|
|
86
|
+
# AUTH(?!ORIZATION) lookahead, and Cookie/session are not secret keywords).
|
|
87
|
+
#
|
|
88
|
+
# Anchored with (?im):
|
|
89
|
+
# - re.IGNORECASE so "authorization"/"Authorization"/"COOKIE" all match.
|
|
90
|
+
# - re.MULTILINE so an interior header line inside a multi-line blob (crash
|
|
91
|
+
# reports, stack traces, env dumps) matches, not just offset 0. "." does
|
|
92
|
+
# not cross newlines, so the value run stops at end-of-line and adjacent
|
|
93
|
+
# normal lines are left untouched.
|
|
94
|
+
#
|
|
95
|
+
# Group layout:
|
|
96
|
+
# 1 -> line prefix up through the "header:" separator (leading whitespace +
|
|
97
|
+
# header name + colon + following whitespace), preserved verbatim.
|
|
98
|
+
# 2 -> optional auth scheme word (Basic/Bearer/Digest/Negotiate/NTLM) plus
|
|
99
|
+
# its trailing space, preserved so "Authorization: Basic [REDACTED]"
|
|
100
|
+
# keeps the scheme. Empty for Cookie / raw-token forms.
|
|
101
|
+
# Everything after that (the credential) is replaced with [REDACTED].
|
|
102
|
+
_AUTH_HEADER = re.compile(
|
|
103
|
+
r"(?im)"
|
|
104
|
+
r"^([ \t]*(?:authorization|cookie|set-cookie)[ \t]*:[ \t]*)" # 1: header prefix
|
|
105
|
+
r"((?:Basic|Bearer|Digest|Negotiate|NTLM)[ \t]+)?" # 2: optional scheme
|
|
106
|
+
r"\S.*$" # the credential value
|
|
107
|
+
)
|
|
108
|
+
|
|
82
109
|
# PEM PRIVATE KEY blocks (any -----BEGIN ... PRIVATE KEY----- ... END block).
|
|
83
110
|
# DOTALL so the body spanning newlines is matched and dropped whole.
|
|
84
111
|
_PEM = re.compile(
|
|
@@ -131,6 +158,14 @@ _ENV_ASSIGN = re.compile(
|
|
|
131
158
|
)
|
|
132
159
|
|
|
133
160
|
|
|
161
|
+
def _auth_header_sub(m):
|
|
162
|
+
"""Redact an HTTP auth/cookie header VALUE, keeping the header name and
|
|
163
|
+
(when present) the auth scheme word. Group 1 is the "header:" prefix, group
|
|
164
|
+
2 is the optional scheme (with trailing space) or None."""
|
|
165
|
+
prefix, scheme = m.group(1), m.group(2)
|
|
166
|
+
return prefix + (scheme or "") + "[REDACTED]"
|
|
167
|
+
|
|
168
|
+
|
|
134
169
|
def _env_assign_sub(m):
|
|
135
170
|
"""Redact a secret assignment value, preserving key, separator and quotes.
|
|
136
171
|
|
|
@@ -231,6 +266,12 @@ def _redact_value(s):
|
|
|
231
266
|
)
|
|
232
267
|
total += n
|
|
233
268
|
|
|
269
|
+
# HTTP auth/cookie header lines: redact the whole credential value before
|
|
270
|
+
# token-level rules run, so a token inside the header value is not matched
|
|
271
|
+
# (and double-counted) separately. Keeps the header name + scheme word.
|
|
272
|
+
s, n = _AUTH_HEADER.subn(_auth_header_sub, s)
|
|
273
|
+
total += n
|
|
274
|
+
|
|
234
275
|
# Token patterns (ordered most-specific-first).
|
|
235
276
|
for pat, repl in _PATTERNS:
|
|
236
277
|
s, n = pat.subn(repl, s)
|
package/autonomy/loki
CHANGED
|
@@ -16876,7 +16876,11 @@ PYEOF
|
|
|
16876
16876
|
export)
|
|
16877
16877
|
local output="${2:-learnings-export.json}"
|
|
16878
16878
|
|
|
16879
|
-
|
|
16879
|
+
# BUG-PU-010: pass the output filename via an environment variable
|
|
16880
|
+
# rather than interpolating $output into the python -c source, so a
|
|
16881
|
+
# filename containing quotes/backslashes/newlines cannot break out of
|
|
16882
|
+
# the string literal or inject python.
|
|
16883
|
+
LOKI_LEARNINGS_DIR="$learnings_dir" LOKI_LEARNINGS_OUT="$output" python3 -c "
|
|
16880
16884
|
import json
|
|
16881
16885
|
import os
|
|
16882
16886
|
|
|
@@ -16894,9 +16898,10 @@ for category in ['patterns', 'mistakes', 'successes']:
|
|
|
16894
16898
|
result[category].append(e)
|
|
16895
16899
|
except: pass
|
|
16896
16900
|
|
|
16897
|
-
|
|
16901
|
+
_out = os.environ['LOKI_LEARNINGS_OUT']
|
|
16902
|
+
with open(_out, 'w') as f:
|
|
16898
16903
|
json.dump(result, f, indent=2)
|
|
16899
|
-
print(f'Exported to
|
|
16904
|
+
print(f'Exported to {_out}')
|
|
16900
16905
|
" 2>/dev/null
|
|
16901
16906
|
;;
|
|
16902
16907
|
|
|
@@ -25244,8 +25249,14 @@ generate_component(
|
|
|
25244
25249
|
|
|
25245
25250
|
# 4. Register in registry
|
|
25246
25251
|
log_info "Registering component"
|
|
25252
|
+
# BUG-PU-010: pass free-text description/tags via environment variables and
|
|
25253
|
+
# read them with os.environ in the python body, instead of interpolating raw
|
|
25254
|
+
# user input into the python -c source (a value containing triple-quotes,
|
|
25255
|
+
# backslashes, newlines, or $(...) would crash the script or inject code).
|
|
25256
|
+
# Mirrors the LOKI_MEM_QUERY pattern in the memory-search path.
|
|
25257
|
+
LOKI_MAGIC_DESC="$description" LOKI_MAGIC_TAGS="$tags" \
|
|
25247
25258
|
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
25248
|
-
import sys
|
|
25259
|
+
import os, sys
|
|
25249
25260
|
try:
|
|
25250
25261
|
from magic.core.registry import register_component
|
|
25251
25262
|
except Exception as exc:
|
|
@@ -25259,8 +25270,8 @@ register_component(
|
|
|
25259
25270
|
react_path='.loki/magic/generated/react/${name}.tsx' if '$target' in ('react','both') else '',
|
|
25260
25271
|
webcomponent_path='.loki/magic/generated/webcomponent/${name}.js' if '$target' in ('webcomponent','both') else '',
|
|
25261
25272
|
test_path='.loki/magic/generated/tests/${name}.test.tsx',
|
|
25262
|
-
description='''
|
|
25263
|
-
tags=[t.strip() for t in '''
|
|
25273
|
+
description=os.environ.get('LOKI_MAGIC_DESC', '').strip(),
|
|
25274
|
+
tags=[t.strip() for t in os.environ.get('LOKI_MAGIC_TAGS', '').split(',') if t.strip()],
|
|
25264
25275
|
placement='${placement}' or None,
|
|
25265
25276
|
)
|
|
25266
25277
|
" || {
|
|
@@ -25305,13 +25316,22 @@ _magic_update() {
|
|
|
25305
25316
|
esac
|
|
25306
25317
|
done
|
|
25307
25318
|
|
|
25319
|
+
# BUG-PU-010: validate --name (defense in depth, mirroring _magic_generate)
|
|
25320
|
+
# so a malicious value cannot reach the python body, AND pass it via an
|
|
25321
|
+
# environment variable instead of interpolating it into the python -c source.
|
|
25322
|
+
if [ -n "$name" ] && ! _magic_valid_name "$name"; then
|
|
25323
|
+
log_error "Invalid component name: '$name' (must start with a letter, contain only letters, digits, _ or -)"
|
|
25324
|
+
return 1
|
|
25325
|
+
fi
|
|
25326
|
+
|
|
25308
25327
|
_magic_ensure_dirs
|
|
25309
25328
|
local py
|
|
25310
25329
|
py=$(_magic_python)
|
|
25311
25330
|
|
|
25312
25331
|
log_info "Updating components from specs (name=${name:-<all>}, force=$force)"
|
|
25332
|
+
LOKI_MAGIC_NAME="$name" \
|
|
25313
25333
|
PYTHONPATH="$(_magic_pypath)" "$py" -c "
|
|
25314
|
-
import sys
|
|
25334
|
+
import os, sys
|
|
25315
25335
|
try:
|
|
25316
25336
|
from magic.core.generator import update_components
|
|
25317
25337
|
except Exception as exc:
|
|
@@ -25319,7 +25339,7 @@ except Exception as exc:
|
|
|
25319
25339
|
sys.exit(2)
|
|
25320
25340
|
update_components(
|
|
25321
25341
|
registry_path='.loki/magic/registry.json',
|
|
25322
|
-
name='
|
|
25342
|
+
name=os.environ.get('LOKI_MAGIC_NAME', '') or None,
|
|
25323
25343
|
force=$([ "$force" = "true" ] && echo True || echo False),
|
|
25324
25344
|
)
|
|
25325
25345
|
" || {
|
package/dashboard/__init__.py
CHANGED
package/dashboard/api_v2.py
CHANGED
|
@@ -251,6 +251,25 @@ def resolve_tenant_context(
|
|
|
251
251
|
)
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
def _require_global_admin(tenant_ctx: TenantContext) -> None:
|
|
255
|
+
"""Gate a tenant-lifecycle operation behind global-admin authority.
|
|
256
|
+
|
|
257
|
+
Creating, updating, or deleting a tenant is a global-admin-only operation:
|
|
258
|
+
it manages the isolation boundaries themselves, so a tenant-scoped caller
|
|
259
|
+
(even one holding the `control` scope, which does NOT imply `admin`) must
|
|
260
|
+
not perform it. A global admin is allowed. When auth is disabled there is
|
|
261
|
+
no caller identity to isolate -- single-user local mode -- so the operation
|
|
262
|
+
is permitted, mirroring TenantContext.enforce so legitimate single-tenant
|
|
263
|
+
and local flows are not broken.
|
|
264
|
+
"""
|
|
265
|
+
if tenant_ctx.is_global_admin or not tenant_ctx.auth_enabled:
|
|
266
|
+
return
|
|
267
|
+
raise HTTPException(
|
|
268
|
+
status_code=403,
|
|
269
|
+
detail="Tenant lifecycle operations require global admin",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
254
273
|
async def _enforce_project_tenant(
|
|
255
274
|
db: AsyncSession, tenant_ctx: TenantContext, project_id: int
|
|
256
275
|
) -> None:
|
|
@@ -306,8 +325,10 @@ async def create_tenant(
|
|
|
306
325
|
db: AsyncSession = Depends(get_db),
|
|
307
326
|
_auth: None = Depends(auth.require_scope("control")),
|
|
308
327
|
token_info: Optional[dict] = Depends(auth.get_current_token),
|
|
328
|
+
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
309
329
|
):
|
|
310
|
-
"""Create a new tenant."""
|
|
330
|
+
"""Create a new tenant (global-admin only)."""
|
|
331
|
+
_require_global_admin(tenant_ctx)
|
|
311
332
|
tenant = await tenants_mod.create_tenant(
|
|
312
333
|
db, name=body.name, description=body.description, settings=body.settings,
|
|
313
334
|
)
|
|
@@ -366,8 +387,8 @@ async def update_tenant(
|
|
|
366
387
|
token_info: Optional[dict] = Depends(auth.get_current_token),
|
|
367
388
|
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
368
389
|
):
|
|
369
|
-
"""Update an existing tenant."""
|
|
370
|
-
tenant_ctx
|
|
390
|
+
"""Update an existing tenant (global-admin only)."""
|
|
391
|
+
_require_global_admin(tenant_ctx)
|
|
371
392
|
tenant = await tenants_mod.update_tenant(
|
|
372
393
|
db, tenant_id,
|
|
373
394
|
name=body.name, description=body.description, settings=body.settings,
|
|
@@ -392,8 +413,8 @@ async def delete_tenant(
|
|
|
392
413
|
token_info: Optional[dict] = Depends(auth.get_current_token),
|
|
393
414
|
tenant_ctx: TenantContext = Depends(resolve_tenant_context),
|
|
394
415
|
):
|
|
395
|
-
"""Delete a tenant."""
|
|
396
|
-
tenant_ctx
|
|
416
|
+
"""Delete a tenant (global-admin only)."""
|
|
417
|
+
_require_global_admin(tenant_ctx)
|
|
397
418
|
deleted = await tenants_mod.delete_tenant(db, tenant_id)
|
|
398
419
|
if not deleted:
|
|
399
420
|
raise HTTPException(status_code=404, detail="Tenant not found")
|
package/dashboard/server.py
CHANGED
|
@@ -6589,7 +6589,7 @@ async def resume_agent(agent_id: str):
|
|
|
6589
6589
|
|
|
6590
6590
|
|
|
6591
6591
|
@app.get("/api/logs")
|
|
6592
|
-
async def get_logs(lines: int = 100, token: Optional[dict] = Depends(auth.get_current_token)):
|
|
6592
|
+
async def get_logs(lines: int = Query(default=100, ge=1, le=10000), token: Optional[dict] = Depends(auth.get_current_token)):
|
|
6593
6593
|
"""Get recent log entries from session log files."""
|
|
6594
6594
|
log_dir = _get_loki_dir() / "logs"
|
|
6595
6595
|
entries = []
|
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.61.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.61.0 start ./my-spec.md
|
|
399
399
|
```
|
|
400
400
|
|
|
401
401
|
##### docker compose + .env (no host install)
|
package/events/emit.sh
CHANGED
|
@@ -164,5 +164,23 @@ if [ -f "$EVENTS_LOG" ]; then
|
|
|
164
164
|
fi
|
|
165
165
|
fi
|
|
166
166
|
|
|
167
|
+
# Append a flat-schema record to .loki/events.jsonl for dashboard consumption.
|
|
168
|
+
#
|
|
169
|
+
# The dashboard reads .loki/events.jsonl directly (dashboard/server.py
|
|
170
|
+
# _read_events) and run.sh's emit_event/emit_event_json write the FLAT schema
|
|
171
|
+
# {"timestamp","type","data"} -- NOT the nested pending schema written to the
|
|
172
|
+
# per-event file above. Without this append, events emitted via emit.sh land
|
|
173
|
+
# only in the pending dir and stay INVISIBLE to the dashboard.
|
|
174
|
+
#
|
|
175
|
+
# Mapping: data = the existing PAYLOAD object (mirrors emit_event_json, where
|
|
176
|
+
# `data` is a JSON object). `source` is intentionally dropped from the flat
|
|
177
|
+
# record (not part of the dashboard schema); the pending file above preserves
|
|
178
|
+
# it for other consumers. PAYLOAD is already newline-free (built on lines
|
|
179
|
+
# 127-135), so the record is a single compact line. The helper appends its own
|
|
180
|
+
# trailing newline. `|| true` keeps observability from ever aborting the emit
|
|
181
|
+
# under `set -e` (matches autonomy/run.sh:9896).
|
|
182
|
+
FLAT_EVENT="{\"timestamp\":\"$TIMESTAMP\",\"type\":\"$TYPE_ESC\",\"data\":$PAYLOAD}"
|
|
183
|
+
safe_append_event_jsonl "$EVENTS_LOG" "$FLAT_EVENT" 2>/dev/null || true
|
|
184
|
+
|
|
167
185
|
# Output event ID
|
|
168
186
|
echo "$EVENT_ID"
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.
|
|
2
|
+
var r6=Object.defineProperty;var t6=($)=>$;function i6($,Q){this[$]=t6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)r6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:i6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var D1={};h(D1,{lokiDir:()=>P,homeLokiDir:()=>n$,findRepoRootForVersion:()=>o$,REPO_ROOT:()=>g});import{resolve as n,dirname as d$}from"path";import{fileURLToPath as e6}from"url";import{existsSync as P$}from"fs";import{homedir as $Q}from"os";function QQ(){let $=S1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=d$($);if(Z===$)break;$=Z}return n(S1,"..","..","..")}function o$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=d$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function n$(){return n($Q(),".loki")}var S1,g;var b=L(()=>{S1=d$(e6(import.meta.url));g=QQ()});import{readFileSync as ZQ}from"fs";import{resolve as zQ,dirname as XQ}from"path";import{fileURLToPath as KQ}from"url";function j$(){if($$!==null)return $$;let $="7.61.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=XQ(KQ(import.meta.url)),Z=o$(Q);$$=ZQ(zQ(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var a$=L(()=>{b()});var b1={};h(b1,{runOrThrow:()=>qQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>s$});async function k($,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([new Response(Z.stdout).text(),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 qQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new s$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=VQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function VQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var s$;var d=L(()=>{s$=class s$ 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 JQ?"":$}var JQ,T,S,_,wZ,I,R,y,V;var c=L(()=>{JQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),_=a("\x1B[1;33m"),wZ=a("\x1B[0;34m"),I=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),V=a("\x1B[0m")});import{existsSync as wQ}from"fs";async function Q$(){if(G$!==void 0)return G$;let $="/opt/homebrew/bin/python3.12";if(wQ($))return G$=$,$;let Q=await f("python3.12");if(Q)return G$=Q,Q;let Z=await f("python3");return G$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var G$;var q$=L(()=>{d()});var e1={};h(e1,{runStatus:()=>uQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as C,basename as DQ}from"path";import{homedir as CQ}from"os";function n1($){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 a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*k$/Q);if(X>k$)X=k$;let q=k$-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)),J=n1($),U=n1(Q);return` ${R}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function hQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
|
|
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)
|
|
@@ -790,4 +790,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
790
790
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
791
791
|
`),process.stderr.write(s6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var KZ=await XZ(Bun.argv.slice(2));process.exit(KZ);
|
|
792
792
|
|
|
793
|
-
//# debugId=
|
|
793
|
+
//# debugId=81578235FF4BA7E564756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/retrieval.py
CHANGED
|
@@ -372,9 +372,9 @@ class MemoryRetrieval:
|
|
|
372
372
|
Returns:
|
|
373
373
|
One of: exploration, implementation, debugging, review, refactoring
|
|
374
374
|
"""
|
|
375
|
-
goal = context.get("goal"
|
|
376
|
-
action = context.get("action_type"
|
|
377
|
-
phase = context.get("phase"
|
|
375
|
+
goal = (context.get("goal") or "").lower()
|
|
376
|
+
action = (context.get("action_type") or "").lower()
|
|
377
|
+
phase = (context.get("phase") or "").lower()
|
|
378
378
|
|
|
379
379
|
scores: Dict[str, int] = {}
|
|
380
380
|
|
|
@@ -1523,7 +1523,9 @@ class MemoryRetrieval:
|
|
|
1523
1523
|
|
|
1524
1524
|
name = data.get("name", "").lower()
|
|
1525
1525
|
description = data.get("description", "").lower()
|
|
1526
|
-
steps_text = " ".join(
|
|
1526
|
+
steps_text = " ".join(
|
|
1527
|
+
s for s in (data.get("steps") or []) if isinstance(s, str)
|
|
1528
|
+
).lower()
|
|
1527
1529
|
|
|
1528
1530
|
score = sum(2 for kw in keywords if kw in name)
|
|
1529
1531
|
score += sum(1 for kw in keywords if kw in description)
|
|
@@ -1664,7 +1666,9 @@ class MemoryRetrieval:
|
|
|
1664
1666
|
continue
|
|
1665
1667
|
|
|
1666
1668
|
# Create text for embedding
|
|
1667
|
-
steps = " ".join(
|
|
1669
|
+
steps = " ".join(
|
|
1670
|
+
s for s in (data.get("steps") or []) if isinstance(s, str)
|
|
1671
|
+
)
|
|
1668
1672
|
text = f"{data.get('name', '')} {data.get('description', '')} {steps}"
|
|
1669
1673
|
|
|
1670
1674
|
# Generate embedding
|
|
@@ -223,8 +223,16 @@ def optimize_context(
|
|
|
223
223
|
except (ValueError, TypeError):
|
|
224
224
|
pass
|
|
225
225
|
|
|
226
|
-
# Get relevance score (already computed by retrieval)
|
|
227
|
-
|
|
226
|
+
# Get relevance score (already computed by retrieval).
|
|
227
|
+
# Prefer the task-aware _weighted_score (task-strategy weight x
|
|
228
|
+
# importance x confidence x recency) when retrieval has computed it,
|
|
229
|
+
# so that token-budget trimming preserves the task-aware ranking
|
|
230
|
+
# instead of re-ranking from scratch on the raw _score. Fall back to
|
|
231
|
+
# _score only when _weighted_score is absent.
|
|
232
|
+
if "_weighted_score" in memory:
|
|
233
|
+
relevance = memory.get("_weighted_score", 0.5)
|
|
234
|
+
else:
|
|
235
|
+
relevance = memory.get("_score", 0.5)
|
|
228
236
|
if relevance > 1.0:
|
|
229
237
|
# Normalize high scores
|
|
230
238
|
relevance = min(1.0, relevance / 10.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.
|
|
4
|
+
"version": "7.61.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.61.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",
|
package/src/policies/check.js
CHANGED
|
@@ -24,9 +24,38 @@ var engine;
|
|
|
24
24
|
try {
|
|
25
25
|
engine = new PolicyEngine(projectDir);
|
|
26
26
|
} catch (e) {
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// A security control that cannot instantiate must FAIL CLOSED (deny),
|
|
28
|
+
// never allow by default. An unexpected error here means we cannot make
|
|
29
|
+
// a sound policy decision, so we deny rather than silently disable
|
|
30
|
+
// enforcement.
|
|
31
|
+
process.stdout.write(JSON.stringify({
|
|
32
|
+
allowed: false,
|
|
33
|
+
decision: 'DENY',
|
|
34
|
+
reason: 'Policy engine failed to initialize: ' + e.message,
|
|
35
|
+
requiresApproval: false,
|
|
36
|
+
violations: [],
|
|
37
|
+
}));
|
|
38
|
+
process.stderr.write('Policy engine failed to initialize: ' + e.message + '\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fail-closed on a present-but-unparseable policy file. If a policy file
|
|
43
|
+
// exists on disk but could not be loaded (corrupt JSON / bad YAML), the engine
|
|
44
|
+
// records the error and leaves _policies null. Falling through to evaluate()
|
|
45
|
+
// would return the misleading "No policies configured" ALLOW, silently
|
|
46
|
+
// disabling all policy enforcement. A security control that disables itself on
|
|
47
|
+
// malformed config is fail-open; deny instead.
|
|
48
|
+
if (engine.hasLoadErrors()) {
|
|
49
|
+
var loadErrors = engine.getValidationErrors();
|
|
50
|
+
process.stdout.write(JSON.stringify({
|
|
51
|
+
allowed: false,
|
|
52
|
+
decision: 'DENY',
|
|
53
|
+
reason: 'Policy file present but could not be loaded (fail-closed): ' + loadErrors.join('; '),
|
|
54
|
+
requiresApproval: false,
|
|
55
|
+
violations: [],
|
|
56
|
+
}));
|
|
57
|
+
process.stderr.write('Policy file present but could not be loaded; denying (fail-closed): ' + loadErrors.join('; ') + '\n');
|
|
58
|
+
process.exit(1);
|
|
30
59
|
}
|
|
31
60
|
|
|
32
61
|
var result = engine.evaluate(enforcementPoint, context);
|
package/src/policies/engine.js
CHANGED
|
@@ -590,6 +590,26 @@ class PolicyEngine {
|
|
|
590
590
|
return this._validationErrors;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Whether a policy file is present on disk but could not be parsed/loaded
|
|
595
|
+
* into a usable policy object.
|
|
596
|
+
*
|
|
597
|
+
* This is the fail-closed discriminator: it is true only when a policy file
|
|
598
|
+
* exists (_policyPath set) yet parsing failed (_policies === null). It is
|
|
599
|
+
* deliberately NOT keyed off getValidationErrors() length, because a valid
|
|
600
|
+
* policy file can still carry soft warnings (e.g. an unrecognized rule
|
|
601
|
+
* string) while parsing cleanly into a non-null _policies object. Those
|
|
602
|
+
* warnings must not disable enforcement.
|
|
603
|
+
*
|
|
604
|
+
* When no policy file exists at all, _policyPath is null and this returns
|
|
605
|
+
* false, preserving the legitimate "no policies -> allow" behavior.
|
|
606
|
+
*
|
|
607
|
+
* @returns {boolean}
|
|
608
|
+
*/
|
|
609
|
+
hasLoadErrors() {
|
|
610
|
+
return this._policyPath !== null && this._policies === null;
|
|
611
|
+
}
|
|
612
|
+
|
|
593
613
|
/**
|
|
594
614
|
* Stop watching the policy file.
|
|
595
615
|
*/
|