loki-mode 7.52.0 → 7.53.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 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.52.0
6
+ # Loki Mode v7.53.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -407,4 +407,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
407
407
 
408
408
  ---
409
409
 
410
- **v7.52.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
410
+ **v7.53.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.52.0
1
+ 7.53.0
package/autonomy/loki CHANGED
@@ -743,7 +743,7 @@ show_help() {
743
743
  echo " --complex Force complex complexity tier (8 phases)"
744
744
  echo " --github Enable GitHub issue import"
745
745
  echo " --no-dashboard Disable web dashboard"
746
- echo " --sandbox Run in Docker sandbox for isolation"
746
+ echo " --sandbox Run in Docker sandbox for isolation (default: off; requires Docker)"
747
747
  echo " --skip-memory Skip loading memory context at startup"
748
748
  echo " --fresh-prd Regenerate the PRD from the codebase (no-PRD runs; ignores the reusable generated PRD)"
749
749
  echo " --compliance PRESET Enable compliance mode (default|healthcare|fintech|government)"
@@ -1063,7 +1063,7 @@ cmd_start() {
1063
1063
  echo " --github Enable GitHub issue import"
1064
1064
  echo " --no-dashboard Disable web dashboard"
1065
1065
  echo " --api Start dashboard API server alongside the build"
1066
- echo " --sandbox Run in Docker sandbox"
1066
+ echo " --sandbox Run in Docker sandbox (default: off; requires Docker)"
1067
1067
  echo " --skip-memory Skip loading memory context at startup"
1068
1068
  echo " --fresh-prd Regenerate the PRD from the codebase on a no-PRD run"
1069
1069
  echo " (ignores the reusable generated PRD; aliases: --regen-prd,"
@@ -5680,7 +5680,7 @@ cmd_run() {
5680
5680
  echo " --simple Force simple complexity tier"
5681
5681
  echo " --complex Force complex complexity tier"
5682
5682
  echo " --no-dashboard Disable web dashboard"
5683
- echo " --sandbox Run in Docker sandbox"
5683
+ echo " --sandbox Run in Docker sandbox (default: off; requires Docker)"
5684
5684
  echo " --no-plan Skip auto-shown PRD analysis at startup"
5685
5685
  echo " --budget USD Set cost budget limit"
5686
5686
  echo ""
@@ -422,7 +422,11 @@ checklist_should_verify() {
422
422
  # non-cooperative agent with filesystem tools can read the reservation directly.
423
423
  #
424
424
  # Selection is idempotent and reproducible: count = clamp(round(0.25*N), 1, 5)
425
- # for N>=4 items; ordering by sha256 of each item's "id" (stable, not random).
425
+ # for N>=2 items; ordering by sha256 of each item's "id" (stable, not random).
426
+ # Small checklists (2 <= N < 4) reserve exactly 1 held-out item via the same
427
+ # sha256-rank selection (the clamp floor of 1 guarantees coverage), so a small
428
+ # spec's checklist is never fully gameable. N<2 is a no-op: holding out the only
429
+ # item of a 1-item checklist would leave nothing to verify against in the loop.
426
430
  # Written once to .loki/checklist/held-out.json; never overwritten if present.
427
431
  checklist_select_heldout() {
428
432
  local heldout_file="${CHECKLIST_DIR:-".loki/checklist"}/held-out.json"
@@ -442,7 +446,7 @@ checklist_select_heldout() {
442
446
  # PARTIAL kept=k dropped=d - some prior ids survived; we keep only survivors
443
447
  # DUP_SKIP - current checklist ids are not unique; the id-based
444
448
  # mechanism is unsound, so we reserve nothing (MEDIUM-2)
445
- # NOOP - n<4 with no prior file, or other no-write outcome
449
+ # NOOP - n<2 with no prior file, or other no-write outcome
446
450
  # Honest caveat: re-selection or partial-survival after a regen can reserve
447
451
  # items the build loop already saw in earlier prompts (the hidden-from-loop
448
452
  # guarantee is best-effort once the checklist ids change mid-run).
@@ -512,7 +516,7 @@ if os.path.exists(out_path):
512
516
 
513
517
  if prior is not None:
514
518
  prior_ids = [i for i in prior.get('held_out', []) if i]
515
- # A prior reservation of [] (e.g. an earlier n<4 run) is a valid no-op state;
519
+ # A prior reservation of [] (e.g. an earlier n<2 run) is a valid no-op state;
516
520
  # keep it idempotent rather than re-selecting now that n may have grown.
517
521
  if not prior_ids:
518
522
  print('IDEMPOTENT')
@@ -525,9 +529,11 @@ if prior is not None:
525
529
  if not survivors:
526
530
  # Fully stale: the checklist regenerated and orphaned the reservation.
527
531
  # Deterministically re-select from the CURRENT checklist.
528
- if n < 4:
532
+ if n < 2:
533
+ # N<2: cannot hold out from a 1-item checklist (reserving the only
534
+ # item leaves nothing to verify against). No-op write of an empty set.
529
535
  atomic_write({'held_out': [], 'total_items': n,
530
- 'note': 'n<4: no held-out reserved (re-selected after stale reservation)'})
536
+ 'note': 'n<2: no held-out reserved (re-selected after stale reservation)'})
531
537
  print('RESELECTED 0')
532
538
  sys.exit(0)
533
539
  held = fresh_selection()
@@ -542,11 +548,15 @@ if prior is not None:
542
548
  sys.exit(0)
543
549
 
544
550
  # No prior reservation: first selection.
545
- if n < 4:
546
- # N>=4 gate: smaller checklists get no held-out (nothing to hide reliably).
547
- atomic_write({'held_out': [], 'total_items': n, 'note': 'n<4: no held-out reserved'})
551
+ if n < 2:
552
+ # N<2 gate: a 1-item (or empty) checklist cannot meaningfully hold out an
553
+ # item -- reserving the only item would leave nothing to verify against in
554
+ # the build loop. Write an empty set so downstream reads stay well-formed.
555
+ atomic_write({'held_out': [], 'total_items': n, 'note': 'n<2: no held-out reserved'})
548
556
  print('NOOP')
549
557
  sys.exit(0)
558
+ # For 2 <= N < 4, fresh_selection() reserves exactly 1 item (select_count clamps
559
+ # round(0.25*N) up to a floor of 1), so small specs are never fully gameable.
550
560
 
551
561
  held = fresh_selection()
552
562
  atomic_write({'held_out': held, 'total_items': n})
package/autonomy/run.sh CHANGED
@@ -585,7 +585,7 @@ BACKGROUND_MODE=${LOKI_BACKGROUND:-false} # Run in background
585
585
  STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
586
586
  AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default)
587
587
  MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
588
- SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode
588
+ SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode (informational; the real dispatch reads LOKI_SANDBOX_MODE at autonomy/loki:1965 and execs sandbox.sh -- this var is not consumed in run.sh)
589
589
  ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # Empty = all paths allowed
590
590
  BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
591
591
 
@@ -8313,6 +8313,89 @@ enforce_mutation_integrity() {
8313
8313
  return 0
8314
8314
  }
8315
8315
 
8316
+ # ============================================================================
8317
+ # Semantic Test-Authenticity Gate (P1-3): wire tests/detect-semantic-test-problems.sh
8318
+ # as an OPT-IN completion gate. The detector catches the harder class of fake
8319
+ # tests that the regex detectors (gates 5+6) miss: assertions that look real but
8320
+ # verify nothing because the asserted value never flows through code under test
8321
+ # (literal-via-variable echo HIGH, mock-return echo MED, deleted assertions MED).
8322
+ #
8323
+ # ADVISORY-FIRST POSTURE (no-deadlock contract): this helper is invoked ONLY when
8324
+ # LOKI_GATE_SEMANTIC_TESTS=true (the elif guard at the completion-promise arm
8325
+ # short-circuits when off, so there is zero runtime cost on the default path).
8326
+ # When on, it runs the detector with --block-high (clean exit-code contract:
8327
+ # rc 2 iff a CRITICAL/HIGH finding exists). We surface ALL severities to a
8328
+ # findings file (advisory) and return nonzero ONLY on rc 2. Every other exit --
8329
+ # rc 0 (clean), rc 124 (timeout), detector absent, no test files, malformed
8330
+ # output -- returns 0 (pass/fall-through), so the autonomous loop can NEVER
8331
+ # deadlock on a clean run. Mirrors enforce_mock_integrity's invocation
8332
+ # (cd TARGET_DIR + LOKI_SCAN_DIR=TARGET_DIR + timeout), swapping --strict for
8333
+ # --block-high and deciding on the rc-2 contract instead of grepping stdout.
8334
+ # ============================================================================
8335
+ enforce_semantic_integrity() {
8336
+ local loki_dir="${TARGET_DIR:-.}/.loki"
8337
+ local quality_dir="$loki_dir/quality"
8338
+ mkdir -p "$quality_dir"
8339
+ local findings_file="$quality_dir/semantic-findings.txt"
8340
+ local detector="$SCRIPT_DIR/../tests/detect-semantic-test-problems.sh"
8341
+ local gate_timeout="${LOKI_GATE_TIMEOUT:-300}"
8342
+
8343
+ if [ ! -f "$detector" ]; then
8344
+ log_info "Semantic test gate: detector not found, skipping (inconclusive)"
8345
+ rm -f "$findings_file" 2>/dev/null || true
8346
+ return 0
8347
+ fi
8348
+
8349
+ local output rc
8350
+ # --block-high exits 2 iff CRITICAL/HIGH present; 0 otherwise (clean wrapper).
8351
+ output=$(cd "${TARGET_DIR:-.}" && LOKI_SCAN_DIR="${TARGET_DIR:-.}" \
8352
+ timeout "$gate_timeout" bash "$detector" --block-high 2>&1)
8353
+ rc=$?
8354
+
8355
+ # timeout exit 124 -- inconclusive, never block on a hang (deny-filter)
8356
+ if [ "$rc" -eq 124 ]; then
8357
+ log_warn "Semantic test gate: detector timed out after ${gate_timeout}s -- inconclusive"
8358
+ rm -f "$findings_file" 2>/dev/null || true
8359
+ return 0
8360
+ fi
8361
+
8362
+ if [ "$rc" -eq 2 ]; then
8363
+ # rc 2 == one or more CRITICAL/HIGH findings. Persist per-finding text.
8364
+ {
8365
+ echo "# Semantic test-authenticity findings (CRITICAL/HIGH block this completion)"
8366
+ echo "$output" | grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' || true
8367
+ } > "$findings_file"
8368
+ log_warn "Semantic test gate: CRITICAL/HIGH fake-test problems detected -- BLOCK"
8369
+ return 1
8370
+ fi
8371
+
8372
+ # rc 0 (and any other non-2, non-124 code, e.g. a malformed run) -> PASS.
8373
+ # Route any MED/LOW advisory findings to the injection file, else clear it.
8374
+ local med_low
8375
+ med_low=$(echo "$output" | grep -E '\[(MEDIUM|LOW)\]' || true)
8376
+ if [ -n "$med_low" ]; then
8377
+ {
8378
+ echo "# Semantic test advisory findings (MED/LOW, non-blocking)"
8379
+ echo "$med_low"
8380
+ } > "$findings_file"
8381
+ else
8382
+ rm -f "$findings_file" 2>/dev/null || true
8383
+ fi
8384
+ log_info "Semantic test gate: PASS"
8385
+ return 0
8386
+ }
8387
+
8388
+ # P1-3 wrapper that runs the semantic gate and returns its exact rc, mirroring
8389
+ # _evidence_gate_and_surface so the completion-promise elif arm reads cleanly
8390
+ # (`! _semantic_gate_and_surface`). Returns nonzero ONLY when enforce_semantic_integrity
8391
+ # saw an rc-2 (CRITICAL/HIGH) result; all deny-filter cases already collapse to 0
8392
+ # inside enforce_semantic_integrity, so this never blocks a clean run.
8393
+ _semantic_gate_and_surface() {
8394
+ local _rc=0
8395
+ enforce_semantic_integrity || _rc=$?
8396
+ return "$_rc"
8397
+ }
8398
+
8316
8399
  # ============================================================================
8317
8400
  # 3-Reviewer Parallel Code Review (v5.35.0)
8318
8401
  # Specialist pool from skills/quality-gates.md with blind review
@@ -12248,6 +12331,23 @@ if d.get('blocked'):
12248
12331
  gate_failure_context="${gate_failure_context}FIX THESE ISSUES BEFORE PROCEEDING WITH NEW WORK."
12249
12332
  fi
12250
12333
 
12334
+ # P1-3: surface specific semantic test-authenticity findings (which fake test,
12335
+ # which line) when the opt-in gate (LOKI_GATE_SEMANTIC_TESTS) wrote them, so a
12336
+ # block converges: the agent gets the exact files/lines to fix rather than a
12337
+ # bare gate name. The file exists only when the gate ran AND found something
12338
+ # (cleared on clean), so this is zero-cost on the default path and when off.
12339
+ # Mirrors the static-analysis/test-results detail-surfacing above. Surfaced
12340
+ # whether the run blocked (CRIT/HIGH) or only advised (MED/LOW): both inform
12341
+ # the next iteration. Independent of gate-failures.txt presence (the
12342
+ # completion-promise arm does not append a gate token).
12343
+ if [ -f "${TARGET_DIR:-.}/.loki/quality/semantic-findings.txt" ]; then
12344
+ local sem_findings
12345
+ sem_findings=$(grep -E '\[(CRITICAL|HIGH|MEDIUM|LOW)\]' "${TARGET_DIR:-.}/.loki/quality/semantic-findings.txt" 2>/dev/null | head -20 || true)
12346
+ if [ -n "$sem_findings" ]; then
12347
+ gate_failure_context="${gate_failure_context} SEMANTIC TEST-AUTHENTICITY FINDINGS (fix the fake tests; an assertion must verify a value that flows through the code under test, not echo a literal back): ${sem_findings}"
12348
+ fi
12349
+ fi
12350
+
12251
12351
  # P2-2: high-severity spec-assumption context. When DISCOVERY recorded any
12252
12352
  # high-severity assumption (the spec was ambiguous in a high-impact place),
12253
12353
  # surface it to the build agent so it implements with the gap in view (or
@@ -15347,6 +15447,20 @@ else:
15347
15447
  log_warn "Completion claim rejected: assumption ledger gate found unresolved high-severity spec assumption(s)."
15348
15448
  log_warn " Details under .loki/council/assumption-block.json ; opt out with LOKI_ASSUMPTION_GATE=0"
15349
15449
  # Fall through; keep iterating until high-sev assumptions resolve.
15450
+ # P1-3: semantic test-authenticity gate (OPT-IN, default OFF). Catches
15451
+ # fake tests that look real but verify nothing (literal-via-variable
15452
+ # echo etc.) that the regex gates 5+6 miss. ADVISORY-FIRST: the arm is
15453
+ # guarded by LOKI_GATE_SEMANTIC_TESTS=true, so by default it never runs
15454
+ # (zero runtime cost, never blocks). When enabled it runs the detector
15455
+ # with --block-high and rejects the completion ONLY on a CRITICAL/HIGH
15456
+ # finding; clean / no-test-files / detector-absent / timeout / malformed
15457
+ # all collapse to a pass inside _semantic_gate_and_surface, so the
15458
+ # autonomous loop can never deadlock on a clean run. Mirrors the
15459
+ # evidence / held-out / assumption arms above.
15460
+ elif [ "$_completion_claimed" = 1 ] && [ "${LOKI_GATE_SEMANTIC_TESTS:-false}" = "true" ] && type _semantic_gate_and_surface &>/dev/null && ! _semantic_gate_and_surface; then
15461
+ log_warn "Completion claim rejected: semantic test-authenticity gate found CRITICAL/HIGH fake-test problem(s)."
15462
+ log_warn " Details under .loki/quality/semantic-findings.txt ; opt-in gate -- disable with LOKI_GATE_SEMANTIC_TESTS=false"
15463
+ # Fall through; keep iterating until the fake tests are fixed.
15350
15464
  elif [ "$_completion_claimed" = 1 ]; then
15351
15465
  echo ""
15352
15466
  if [ -n "$COMPLETION_PROMISE" ]; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.52.0"
10
+ __version__ = "7.53.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -3248,6 +3248,121 @@ async def get_audit_summary(days: int = 7):
3248
3248
  return audit.get_audit_summary(days=days)
3249
3249
 
3250
3250
 
3251
+ # Continuous compliance surface (P3-11).
3252
+ #
3253
+ # Exposes the agent audit chain's compliance posture as an always-available
3254
+ # live endpoint. There is NO background scheduler in this surface (that is
3255
+ # infra, out of scope): the report is regenerated from the CURRENT audit
3256
+ # state on every request, so the endpoint is "continuous" in the sense that
3257
+ # it always reflects live state -- never a stale cached snapshot.
3258
+ #
3259
+ # The report is produced by the authoritative Node compliance engine
3260
+ # (src/audit/index.js, the single source of truth for SOC2/ISO/GDPR control
3261
+ # mappings) via its `report` CLI shim, so the Python surface never
3262
+ # reimplements (and never drifts from) the mapping logic. The chain it reads
3263
+ # is the JS AGENT chain at <project>/.loki/audit/audit.jsonl -- a different
3264
+ # chain from the Python dashboard chain that /api/enterprise/audit serves
3265
+ # (the two are reconciled by the cross-link verifier, not merged), so this
3266
+ # endpoint deliberately does NOT gate on audit.is_audit_enabled() (that flag
3267
+ # governs the Python chain). When the agent chain has no entries the report
3268
+ # is returned honestly with totalAuditEntries == 0; no fabricated pass.
3269
+ _COMPLIANCE_TYPES = ("soc2", "iso27001", "gdpr")
3270
+
3271
+
3272
+ @app.get("/api/compliance", dependencies=[Depends(auth.require_scope("audit"))])
3273
+ def get_compliance_status(report_type: str = Query("soc2", alias="type")):
3274
+ """Live compliance status for the active project's agent audit chain.
3275
+
3276
+ Auth/tenant scoping: requires the `audit` scope (same gate as the
3277
+ /api/enterprise/audit family). The data is filesystem state scoped to
3278
+ the active project via _get_loki_dir(), exactly like the other
3279
+ .loki-backed read endpoints; there is no DB tenant_id on a JSONL file
3280
+ to enforce against.
3281
+
3282
+ Query: ?type=soc2|iso27001|gdpr (default soc2).
3283
+
3284
+ Returns the compliance report JSON regenerated from CURRENT audit
3285
+ state on every call. If no audit data has been recorded the report is
3286
+ honestly empty (totalAuditEntries == 0), not a fabricated compliant
3287
+ verdict. If the Node engine is unavailable, returns an honest
3288
+ available:false payload (HTTP 200) rather than masquerading as "no
3289
+ compliance".
3290
+ """
3291
+ if not _read_limiter.check("compliance"):
3292
+ raise HTTPException(status_code=429, detail="Rate limit exceeded")
3293
+ if report_type not in _COMPLIANCE_TYPES:
3294
+ raise HTTPException(
3295
+ status_code=400,
3296
+ detail=f"Invalid type: {report_type}. Must be one of {list(_COMPLIANCE_TYPES)}",
3297
+ )
3298
+
3299
+ import shutil
3300
+
3301
+ # The agent audit chain lives under <project>/.loki/audit; _get_loki_dir()
3302
+ # returns the .loki dir, so the project root is its parent.
3303
+ project_dir = str(_get_loki_dir().parent.resolve())
3304
+ repo_root = _Path(__file__).resolve().parent.parent
3305
+ index_js = repo_root / "src" / "audit" / "index.js"
3306
+
3307
+ node_bin = shutil.which("node")
3308
+ if node_bin is None or not index_js.exists():
3309
+ return {
3310
+ "available": False,
3311
+ "reason": (
3312
+ "Node runtime not found"
3313
+ if node_bin is None
3314
+ else f"compliance engine not found at {index_js}"
3315
+ ),
3316
+ "reportType": report_type,
3317
+ "projectDir": project_dir,
3318
+ "report": None,
3319
+ }
3320
+
3321
+ try:
3322
+ proc = subprocess.run(
3323
+ [node_bin, str(index_js), "report", report_type, project_dir],
3324
+ capture_output=True,
3325
+ text=True,
3326
+ timeout=30,
3327
+ check=False,
3328
+ )
3329
+ except (OSError, subprocess.SubprocessError) as exc:
3330
+ return {
3331
+ "available": False,
3332
+ "reason": f"compliance engine invocation failed: {exc}",
3333
+ "reportType": report_type,
3334
+ "projectDir": project_dir,
3335
+ "report": None,
3336
+ }
3337
+
3338
+ if proc.returncode != 0:
3339
+ return {
3340
+ "available": False,
3341
+ "reason": (proc.stderr or "compliance engine returned non-zero").strip()[:500],
3342
+ "reportType": report_type,
3343
+ "projectDir": project_dir,
3344
+ "report": None,
3345
+ }
3346
+
3347
+ try:
3348
+ report = json.loads(proc.stdout.strip())
3349
+ except json.JSONDecodeError:
3350
+ return {
3351
+ "available": False,
3352
+ "reason": "compliance engine produced non-JSON output",
3353
+ "reportType": report_type,
3354
+ "projectDir": project_dir,
3355
+ "report": None,
3356
+ }
3357
+
3358
+ return {
3359
+ "available": True,
3360
+ "reportType": report_type,
3361
+ "projectDir": project_dir,
3362
+ "report": report,
3363
+ }
3364
+
3365
+
3251
3366
  # =============================================================================
3252
3367
  # File-based Session Endpoints (reads from .loki/ flat files)
3253
3368
  # =============================================================================
@@ -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.52.0
5
+ **Version:** v7.53.0
6
6
 
7
7
  ---
8
8
 
@@ -396,7 +396,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
396
396
  # Run Loki Mode in Docker (Claude provider, API-key auth)
397
397
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
398
398
  -v $(pwd):/workspace -w /workspace \
399
- asklokesh/loki-mode:7.52.0 start ./my-spec.md
399
+ asklokesh/loki-mode:7.53.0 start ./my-spec.md
400
400
  ```
401
401
 
402
402
  ##### docker compose + .env (no host install)
@@ -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.52.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}
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.53.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=D3609A2FE6BB9BAE64756E2164756E21
793
+ //# debugId=3BF6CF9B99A2BD7E64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.52.0'
60
+ __version__ = '7.53.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.52.0",
4
+ "version": "7.53.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.52.0",
5
+ "version": "7.53.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",
@@ -2,12 +2,14 @@
2
2
 
3
3
  **Never ship code without passing all quality gates.**
4
4
 
5
- ## The 8 Quality Gates
5
+ ## The Quality Gates (8 default-on + 1 opt-in)
6
6
 
7
- Every gate below is wired into the orchestration loop (`autonomy/run.sh`) and
8
- blocks completion when it fails. The table lists exactly what each gate detects,
9
- what it does NOT detect (so you never over-trust a green gate), its opt-out flag,
10
- and its blocking behavior. Transcribe this list verbatim; do not recompute it.
7
+ Every gate below is wired into the orchestration loop (`autonomy/run.sh`). The 8
8
+ numbered gates are default-on and block completion when they fail; the opt-in
9
+ gate (marked below) is default-OFF and runs only when its flag is set. The table
10
+ lists exactly what each gate detects, what it does NOT detect (so you never
11
+ over-trust a green gate), its opt-out flag, and its blocking behavior. Transcribe
12
+ this list verbatim; do not recompute it.
11
13
 
12
14
  | # | Gate | Detects | Does NOT detect | Blocking | Opt-out flag |
13
15
  |---|------|---------|-----------------|----------|--------------|
@@ -19,6 +21,7 @@ and its blocking behavior. Transcribe this list verbatim; do not recompute it.
19
21
  | 6 | Test Mutation Detector | Assertion-value churn alongside implementation changes (test-fitting), low assertion density (`tests/detect-test-mutations.sh`); HIGH blocks | Logically-correct-but-weak assertions | Yes (HIGH blocks) | `LOKI_GATE_MUTATION=false` |
20
22
  | 7 | Documentation Coverage | README presence, docs freshness within 10 commits, API docs for exported symbols in packages | Whether the docs are accurate or useful | Yes | `LOKI_GATE_DOC_COVERAGE=false` |
21
23
  | 8 | Magic Modules Debate | Spec-vs-implementation debate findings on generated Magic Modules; BLOCK-severity findings block | Issues outside the Magic Modules debate scope | Yes (BLOCK severity) | `LOKI_GATE_MAGIC_DEBATE=false` |
24
+ | 9 (opt-in, default OFF) | Semantic Test-Authenticity | Fake tests that look real but verify nothing (literal-via-variable echo, mock-return echo, deleted assertions) that gates 5+6 miss (`tests/detect-semantic-test-problems.sh --block-high`); CRITICAL/HIGH block | Deep dataflow, legitimate computed-literal assertions, Python/shell tests (JS/TS only); MED/LOW are advisory | Only when enabled, and only on CRITICAL/HIGH; runs solely on a completion claim | Opt-IN: `LOKI_GATE_SEMANTIC_TESTS=true` to enable (default off = not invoked, never blocks) |
22
25
 
23
26
  **Severity-based blocking** ties the review gates together: any Critical or High
24
27
  finding blocks completion. Medium, Low, and cosmetic findings are advisory and
@@ -83,6 +83,84 @@ function exportReport(type, opts) {
83
83
  return compliance.exportReportJson(report);
84
84
  }
85
85
 
86
+ /**
87
+ * Generate a compliance report as a plain object, with the agent-chain
88
+ * tamper-evidence verdict folded in.
89
+ *
90
+ * This is the object form intended for surfaces (e.g. the dashboard
91
+ * /api/compliance endpoint) that need the report as data rather than a
92
+ * pre-serialized string. It always reflects the REAL audit chain:
93
+ *
94
+ * - The report body is generated from the live audit entries
95
+ * (`_log.readEntries()`), never fabricated.
96
+ * - `chainIntegrity` is populated from `verifyChain()` so the report
97
+ * carries the true tamper-evidence state of the underlying chain.
98
+ * For the SOC2 report this fills the `chainIntegrity: null` slot the
99
+ * generator leaves for the caller; for the other report types it is
100
+ * attached under the same key for a uniform surface contract.
101
+ *
102
+ * When the chain has no entries the report is still returned honestly
103
+ * with `totalAuditEntries: 0` (an empty-but-valid report), never a
104
+ * fabricated "compliant" verdict.
105
+ *
106
+ * @param {string} type - 'soc2', 'iso27001', or 'gdpr'
107
+ * @param {object} [opts] - Report options (projectName, period, etc.)
108
+ * @returns {object} The compliance report object with chainIntegrity set.
109
+ */
110
+ function getReport(type, opts) {
111
+ if (!_initialized) init();
112
+ var report = generateReport(type, opts);
113
+ // Fold the real tamper-evidence verdict into the report. Do not let a
114
+ // verification error fabricate a pass: capture it honestly instead.
115
+ try {
116
+ report.chainIntegrity = _log.verifyChain();
117
+ } catch (e) {
118
+ report.chainIntegrity = {
119
+ valid: false,
120
+ entries: report.totalAuditEntries || 0,
121
+ brokenAt: null,
122
+ error: 'chain verification failed: ' + String((e && e.message) || e),
123
+ };
124
+ }
125
+ return report;
126
+ }
127
+
128
+ /**
129
+ * CLI shim so a non-Node surface (e.g. the Python dashboard) can fetch a
130
+ * compliance report for a given project directory as JSON on stdout.
131
+ *
132
+ * This mirrors the inverse of dashboard/audit.py's `_unified_cli()`
133
+ * (which lets the Node-side unified verifier read the Python chain).
134
+ *
135
+ * Invoked as:
136
+ * node src/audit/index.js report <type> <projectDir>
137
+ *
138
+ * <type> is one of soc2 | iso27001 | gdpr. <projectDir> is the project
139
+ * root whose .loki/audit/audit.jsonl chain is read. Prints a single JSON
140
+ * object to stdout. Returns exit 0 on success, 2 on usage error.
141
+ *
142
+ * The report is generated from the REAL chain; an absent/empty chain
143
+ * yields an honest empty report (totalAuditEntries: 0), not a fake pass.
144
+ */
145
+ function _cli(argv) {
146
+ var args = argv || [];
147
+ var VALID_TYPES = { soc2: true, iso27001: true, gdpr: true };
148
+ if (args.length < 2 || args[0] !== 'report' || !VALID_TYPES[args[1]]) {
149
+ process.stdout.write(JSON.stringify({
150
+ error: 'usage: index.js report {soc2|iso27001|gdpr} <projectDir>',
151
+ }) + '\n');
152
+ return 2;
153
+ }
154
+ var type = args[1];
155
+ var projectDir = args[2] || process.cwd();
156
+ destroy();
157
+ init(projectDir);
158
+ var report = getReport(type);
159
+ destroy();
160
+ process.stdout.write(JSON.stringify(report) + '\n');
161
+ return 0;
162
+ }
163
+
86
164
  /**
87
165
  * Check if a provider is allowed by data residency policy.
88
166
  */
@@ -167,6 +245,7 @@ module.exports = {
167
245
  verifyChain: verifyChain,
168
246
  generateReport: generateReport,
169
247
  exportReport: exportReport,
248
+ getReport: getReport,
170
249
  checkProvider: checkProvider,
171
250
  isAirGapped: isAirGapped,
172
251
  readEntries: readEntries,
@@ -177,3 +256,8 @@ module.exports = {
177
256
  verifyUnified: verifyUnified,
178
257
  writeWitness: writeWitness,
179
258
  };
259
+
260
+ // CLI entry point: `node src/audit/index.js report <type> <projectDir>`.
261
+ if (require.main === module) {
262
+ process.exit(_cli(process.argv.slice(2)));
263
+ }