loki-mode 7.118.0 → 7.120.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.118.0
6
+ # Loki Mode v7.120.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.118.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.120.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.118.0
1
+ 7.120.0
@@ -341,7 +341,20 @@ def _collect_build(loki_dir):
341
341
  out["exit_code"] = _to_int(ec, None) if ec is not None else None
342
342
  dur = raw.get("duration_sec")
343
343
  out["duration_sec"] = _to_float(dur, None) if dur is not None else None
344
- if not ran:
344
+ # HONEST APPLICABILITY (build has no writer historically -> facts.build was
345
+ # permanently "not_run" for every project, counting a CLI with no build step
346
+ # as a gap and dragging the headline to WITH GAPS). The writer now records a
347
+ # POSITIVE applicability signal: applicable:false means "this stack genuinely
348
+ # has no build phase" (a CLI, a plain script). That is N/A, not a skipped
349
+ # gap, so it must NOT be a degraded item. CRITICAL anti-fake-green rule:
350
+ # not_applicable ONLY from an EXPLICIT applicable:false in the file. An absent
351
+ # file or a missing/true applicable flag stays not_run (an honest gap) -- an
352
+ # un-built project must never flip green just because nobody wrote the file.
353
+ applicable = raw.get("applicable", True)
354
+ out["applicable"] = bool(applicable)
355
+ if applicable is False:
356
+ out["status"] = "not_applicable"
357
+ elif not ran:
345
358
  out["status"] = "not_run"
346
359
  elif out["exit_code"] == 0:
347
360
  out["status"] = "verified"
package/autonomy/run.sh CHANGED
@@ -8354,6 +8354,163 @@ COMPOUND_SCRIPT
8354
8354
  # Results stored in .loki/quality/static-analysis.json
8355
8355
  # ============================================================================
8356
8356
 
8357
+ # ---------------------------------------------------------------------------
8358
+ # enforce_build_check -- run the stack's build (if it has one) and record an
8359
+ # HONEST build fact for the Evidence Receipt (#47). Historically nothing wrote
8360
+ # .loki/quality/build-results.json, so facts.build was PERMANENTLY "not_run" for
8361
+ # every project -- a CLI with no build step read identically to a skipped real
8362
+ # build, and both dragged the receipt to "WITH GAPS". This is the missing writer.
8363
+ #
8364
+ # The distinction the receipt needs (and the founder demanded, no fabrication) is
8365
+ # THREE-WAY, not two (a council review caught the two-way version laundering
8366
+ # "unrecognized build system" into a fake N/A):
8367
+ # - a build command WE RUN exists (npm "build" / go / cargo) -> RUN it, record
8368
+ # ran/exit_code (verified on 0, failed otherwise -- a real gap kept honest).
8369
+ # - a build system EXISTS but we do not auto-run it (Make/Maven/Gradle/.NET, a
8370
+ # non-"build" npm script, a pyproject build-system) -> not_run (an HONEST GAP,
8371
+ # matching verify.sh:291's "skipped"). NEVER N/A -- a real, un-run build must
8372
+ # never read VERIFIED.
8373
+ # - genuinely NO build phase at all (a plain CLI/script) -> applicable:false
8374
+ # (status not_applicable). A POSITIVE "no build phase" determination, NOT an
8375
+ # absent file -- proof-generator maps applicable:false -> N/A (not a gap) but a
8376
+ # MISSING file or an unrecognized-real build stays not_run. One vocabulary, no
8377
+ # fake-green. Pinned by tests/test-build-check-applicability.sh.
8378
+ # ---------------------------------------------------------------------------
8379
+ enforce_build_check() {
8380
+ local tree="${TARGET_DIR:-.}"
8381
+ local loki_dir="$tree/.loki"
8382
+ local quality_dir="$loki_dir/quality"
8383
+ mkdir -p "$quality_dir"
8384
+ local out_file="$quality_dir/build-results.json"
8385
+ local timeout_s="${LOKI_GATE_TIMEOUT:-300}"
8386
+ local ts; ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
8387
+
8388
+ # THREE-WAY classification (detection is always cheap). The anti-fake-green line
8389
+ # (council-caught): N/A is ONLY for a POSITIVE "this stack has no build phase"
8390
+ # determination. An unrecognized-but-real build system (Make, Maven, Gradle,
8391
+ # .NET, a non-"build" npm build script) must stay an HONEST GAP (not_run), NOT
8392
+ # be laundered into N/A -- otherwise a real, un-run build reads VERIFIED. This
8393
+ # mirrors verify.sh:291, which records the same fall-through as a "skipped" gap.
8394
+ # runnable -> a build command WE run (npm build / go / cargo) -> verified/failed
8395
+ # present -> a build system EXISTS but we don't auto-run it -> not_run (GAP)
8396
+ # none -> positively no build phase at all -> not_applicable (N/A)
8397
+ local cmd="" ran="false" applicable="false" exit_code="null" status="not_run"
8398
+ local build_class="present"
8399
+
8400
+ # INVERTED classification (three council rounds each found a fake-green hole in
8401
+ # an allowlist of build systems -> the allowlist is structurally leaky). The
8402
+ # two error directions are NOT equal: a REAL build read as N/A -> VERIFIED is
8403
+ # DISHONEST over-claim (the cardinal fake-green sin); a no-build CLI read as a
8404
+ # not_run gap is an HONEST under-claim (merely the founder's original UX nit).
8405
+ # So not_run is the SAFE CATCH-ALL and N/A requires a POSITIVE proof-of-no-build.
8406
+ # A forgotten stack (Bazel, meson, autotools, Zig, Nix...) now falls to not_run
8407
+ # (honest), never to fake N/A -- fixing the whole CLASS, not each instance.
8408
+ # 1. runnable: a build command WE run -> verified/failed
8409
+ # 2. none: POSITIVE proof of no build phase -> not_applicable (N/A)
8410
+ # 3. else: not_run (honest gap) -- the default, covers every unknown stack
8411
+
8412
+ if [ -f "$tree/package.json" ] && grep -q '"build"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8413
+ cmd="npm run build"; applicable="true"; build_class="runnable"
8414
+ elif [ -f "$tree/go.mod" ] && command -v go >/dev/null 2>&1; then
8415
+ cmd="go build ./..."; applicable="true"; build_class="runnable"
8416
+ elif [ -f "$tree/Cargo.toml" ] && command -v cargo >/dev/null 2>&1; then
8417
+ cmd="cargo build"; applicable="true"; build_class="runnable"
8418
+ else
8419
+ # POSITIVE proof-of-no-build (the ONLY path to N/A). Two narrow cases, and
8420
+ # anything short of a clear determination stays not_run:
8421
+ # (a) a package.json whose scripts exist but include NO build-ish script
8422
+ # AND there is no foreign build manifest -> a JS CLI/lib with no build.
8423
+ # (b) NO build manifest of ANY known kind at all -> a plain script dir.
8424
+ # The build-ish regex is used as an EXCLUSION so a build hidden under a
8425
+ # non-"build" script name (compile/dist/...) does NOT misfire N/A.
8426
+ local has_foreign_manifest="false"
8427
+ if [ -f "$tree/go.mod" ] || [ -f "$tree/Cargo.toml" ] \
8428
+ || [ -f "$tree/Makefile" ] || [ -f "$tree/makefile" ] || [ -f "$tree/GNUmakefile" ] \
8429
+ || [ -f "$tree/Makefile.in" ] || [ -f "$tree/configure" ] || [ -f "$tree/configure.ac" ] || [ -f "$tree/configure.in" ] \
8430
+ || [ -f "$tree/CMakeLists.txt" ] || [ -f "$tree/meson.build" ] \
8431
+ || [ -f "$tree/pom.xml" ] || [ -f "$tree/build.xml" ] \
8432
+ || [ -f "$tree/build.gradle" ] || [ -f "$tree/build.gradle.kts" ] \
8433
+ || [ -f "$tree/settings.gradle" ] || [ -f "$tree/settings.gradle.kts" ] || [ -f "$tree/gradlew" ] \
8434
+ || [ -f "$tree/build.sbt" ] \
8435
+ || [ -f "$tree/WORKSPACE" ] || [ -f "$tree/WORKSPACE.bazel" ] || [ -f "$tree/MODULE.bazel" ] \
8436
+ || [ -f "$tree/setup.py" ] || [ -f "$tree/pyproject.toml" ] \
8437
+ || compgen -G "$tree/*.csproj" >/dev/null 2>&1 \
8438
+ || compgen -G "$tree/*.sln" >/dev/null 2>&1 \
8439
+ || compgen -G "$tree/*.fsproj" >/dev/null 2>&1; then
8440
+ has_foreign_manifest="true"
8441
+ fi
8442
+ # N/A fires on POSITIVE proof-of-no-build, which we can only assert for a
8443
+ # package.json project: it EXISTS and its scripts are build-ish-free and no
8444
+ # foreign manifest / build-tool devDep is present -> a JS CLI/lib that
8445
+ # genuinely has no build step (the founder's invoice case). CRITICAL (the
8446
+ # council's convergence line): a project with NO package.json is "I don't
8447
+ # recognize a build here" = IGNORANCE, not proof-of-no-build. Ignorance must
8448
+ # map to not_run (honest gap), NEVER N/A -- else Zig/autotools/bare-C/meson/
8449
+ # bazel/anything-unborn silently reads VERIFIED. This makes the ENTIRE
8450
+ # no-package.json universe honest by CONSTRUCTION (list-free), which is why
8451
+ # this converges where four rounds of adding-to-a-list could not.
8452
+ if [ "$has_foreign_manifest" = "false" ] && [ -f "$tree/package.json" ]; then
8453
+ local has_build_devdep="false"
8454
+ # A build TOOL as a (dev)dependency means a build exists even if it runs
8455
+ # under a non-"build" script name -> stay not_run, do not claim N/A.
8456
+ if grep -qE '"(vite|webpack|rollup|esbuild|parcel|typescript|tsc|@swc/core|turbopack|rspack|tsup|microbundle)"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8457
+ has_build_devdep="true"
8458
+ fi
8459
+ if [ "$has_build_devdep" = "false" ] \
8460
+ && ! grep -qE '"(build|compile|dist|bundle|webpack|vite|tsc|rollup|esbuild|prepare|prepack)"[[:space:]]*:' "$tree/package.json" 2>/dev/null; then
8461
+ applicable="false"; build_class="none"
8462
+ fi
8463
+ # else: a build-ish script or a build-tool devDep we don't auto-run ->
8464
+ # stays not_run (an honest gap). Known bounded edge: a polyglot repo with
8465
+ # a package.json (no build) co-present with an UNLISTED foreign build
8466
+ # manifest could read N/A; not common and not list-free-fixable, so it is
8467
+ # a documented CONCERN, not a blocker.
8468
+ fi
8469
+ # No package.json (foreign or not) -> build_class stays "present" -> not_run.
8470
+ # has_foreign_manifest=true -> also not_run. Only the narrow package.json-
8471
+ # no-build case above reaches N/A.
8472
+ fi
8473
+
8474
+ if [ "$build_class" = "present" ]; then
8475
+ # A build phase exists but we did not run it -> honest not_run gap. applicable
8476
+ # stays true (a build IS applicable), status not_run (we did not verify it).
8477
+ applicable="true"; ran="false"; status="not_run"
8478
+ log_info "Build check: a build system is present but not auto-run -> honest gap (not verified)"
8479
+ elif [ "$applicable" = "true" ]; then
8480
+ # EXECUTION is expensive (a real vite/webpack/go/cargo build, seconds to
8481
+ # minutes) so we run it AT MOST ONCE per build run, not every iteration --
8482
+ # honoring the "fastest / lowest cost" goal. A freshness marker (mirrors the
8483
+ # test-results.iter pattern) records that the build already ran this run; a
8484
+ # later iteration reuses the recorded verified/failed status instead of
8485
+ # re-building. LOKI_BUILD_CHECK=0 opts out entirely (records N/A-style skip
8486
+ # without running). If a prior result exists and is still valid, keep it.
8487
+ local marker="$quality_dir/.build-check.done"
8488
+ if [ "${LOKI_BUILD_CHECK:-1}" = "0" ]; then
8489
+ log_info "Build check: execution disabled (LOKI_BUILD_CHECK=0); recording applicability only"
8490
+ ran="false"; exit_code="null"; status="not_run"
8491
+ elif [ -f "$marker" ] && [ -f "$out_file" ]; then
8492
+ log_info "Build check: already ran this build; reusing recorded result"
8493
+ return 0
8494
+ else
8495
+ local rc=0
8496
+ ( cd "$tree" && timeout "$timeout_s" sh -c "$cmd" >/dev/null 2>&1 ) || rc=$?
8497
+ ran="true"; exit_code="$rc"
8498
+ if [ "$rc" -eq 0 ]; then status="verified"; else status="failed"; fi
8499
+ : > "$marker" 2>/dev/null || true
8500
+ log_info "Build check: $cmd -> exit $rc"
8501
+ fi
8502
+ else
8503
+ # build_class="none": a POSITIVE proof-of-no-build determination -> honest
8504
+ # N/A (the founder's CLI). applicable already false; status not_applicable.
8505
+ applicable="false"; ran="false"; status="not_applicable"
8506
+ log_info "Build check: no build step for this stack (N/A)"
8507
+ fi
8508
+
8509
+ printf '{"timestamp":"%s","command":"%s","ran":%s,"applicable":%s,"exit_code":%s,"duration_sec":null,"status":"%s"}\n' \
8510
+ "$ts" "$cmd" "$ran" "$applicable" "$exit_code" "$status" > "$out_file"
8511
+ return 0
8512
+ }
8513
+
8357
8514
  enforce_static_analysis() {
8358
8515
  local loki_dir="${TARGET_DIR:-.}/.loki"
8359
8516
  local quality_dir="$loki_dir/quality"
@@ -9160,6 +9317,21 @@ _loki_zero_tests_executed() {
9160
9317
  fi
9161
9318
  return 1
9162
9319
  ;;
9320
+ unittest)
9321
+ # #139: `python3 -m unittest discover` on ZERO discovered tests prints
9322
+ # "Ran 0 tests" + "NO TESTS RAN". Depending on the Python version it
9323
+ # exits 5 (newer, already not-pass) OR 0 (older = a silent fake-green,
9324
+ # the #82/#89 trap). POSITIVE detection so it is inconclusive on BOTH:
9325
+ # a "Ran 0 tests" line (authoritative), or the "NO TESTS RAN" banner,
9326
+ # AND no ran-count >= 1. A real run prints "Ran N test(s)" with N>=1
9327
+ # and "OK"/"FAILED", so it is never downgraded. Verified live: zero ->
9328
+ # "Ran 0 tests"/"NO TESTS RAN"; one -> "Ran 1 test"/"OK".
9329
+ if printf '%s' "$_zt_out" | grep -qE '^Ran 0 tests' 2>/dev/null \
9330
+ || printf '%s' "$_zt_out" | grep -qE '^NO TESTS RAN$' 2>/dev/null; then
9331
+ return 0
9332
+ fi
9333
+ return 1
9334
+ ;;
9163
9335
  *)
9164
9336
  # Unknown/unparseable runner -> never claim zero-tests (safe default).
9165
9337
  return 1
@@ -9326,6 +9498,17 @@ sys.stdout.write(t.strip())
9326
9498
  -print -quit 2>/dev/null | grep -q .; then
9327
9499
  has_python_project=true
9328
9500
  fi
9501
+ elif find "${TARGET_DIR:-.}" -maxdepth 2 -type f \
9502
+ \( -name 'test_*.py' -o -name '*_test.py' \) \
9503
+ -not -path '*/.loki/*' -not -path '*/.git/*' -not -path '*/node_modules/*' \
9504
+ -not -path '*/.venv/*' -not -path '*/venv/*' \
9505
+ -print -quit 2>/dev/null | grep -q .; then
9506
+ # #139: a ROOT-LEVEL (or shallow) test_*.py / *_test.py with NO tests/
9507
+ # dir and NO config file. The founder's invoice CLI had exactly this
9508
+ # shape (test_invoice_cli.py at the root) -> was missed -> tests read
9509
+ # "not run" on a genuinely-passing 16-test unittest suite. This is the
9510
+ # same false-negative class as #79 (validated work reads as unvalidated).
9511
+ has_python_project=true
9329
9512
  fi
9330
9513
  if [ "$has_python_project" = "true" ] && command -v pytest &>/dev/null; then
9331
9514
  test_runner="pytest"
@@ -9342,6 +9525,29 @@ sys.stdout.write(t.strip())
9342
9525
  [ "$pytest_exit" -ne 0 ] && test_passed=false
9343
9526
  details="pytest: $(echo "$output" | tail -5 | tr '\n' ' ')"
9344
9527
  fi
9528
+ elif [ "$has_python_project" = "true" ] && command -v python3 &>/dev/null; then
9529
+ # #139: pytest ABSENT but a Python test suite exists. stdlib unittest
9530
+ # discovery runs test_*.py with ZERO third-party deps -- so a legit
9531
+ # unittest suite (the invoice CLI's 16 tests) is verified instead of
9532
+ # silently read as "not run". The zero-test guard (unittest case) makes
9533
+ # a zero-discovery run inconclusive, NOT a fake-green pass (unittest
9534
+ # prints "NO TESTS RAN" and exits 0 -- the #82/#89 trap, guarded).
9535
+ test_runner="unittest"
9536
+ local output unittest_exit _ut_to
9537
+ _ut_to="${LOKI_PYTEST_TIMEOUT:-${LOKI_GATE_TIMEOUT:-300}}"
9538
+ local _ut_cmd=(timeout "${_ut_to}s")
9539
+ command -v gtimeout &>/dev/null && _ut_cmd=(gtimeout "${_ut_to}s")
9540
+ command -v timeout &>/dev/null || command -v gtimeout &>/dev/null || _ut_cmd=()
9541
+ output=$(cd "${TARGET_DIR:-.}" && "${_ut_cmd[@]}" python3 -m unittest discover -p 'test_*.py' 2>&1)
9542
+ unittest_exit=$?
9543
+ if [ "$unittest_exit" -eq 124 ]; then
9544
+ test_passed=false
9545
+ log_warn "unittest gate timed out after ${_ut_to}s (exit 124)"
9546
+ details="unittest: TIMED OUT after ${_ut_to}s -- $(echo "$output" | tail -3 | tr '\n' ' ')"
9547
+ else
9548
+ [ "$unittest_exit" -ne 0 ] && test_passed=false
9549
+ details="unittest: $(echo "$output" | tail -5 | tr '\n' ' ')"
9550
+ fi
9345
9551
  fi
9346
9552
  fi
9347
9553
 
@@ -17574,6 +17780,14 @@ if __name__ == "__main__":
17574
17780
  fi
17575
17781
  emit_stage_complete "static_analysis" "$_stg_ok" "$_stg_t0"
17576
17782
  fi
17783
+ # Build check (#47). Records .loki/quality/build-results.json each
17784
+ # iteration so the Evidence Receipt tells the truth about the build:
17785
+ # a real build that ran (verified/failed) or an HONEST N/A when the
17786
+ # stack has no build step. Advisory (best-effort): a build failure
17787
+ # surfaces as a receipt gap via facts.build, never a hard loop block,
17788
+ # mirroring the security scan. Absent writer historically = facts.build
17789
+ # permanently not_run for every project (the founder-reported defect).
17790
+ enforce_build_check || true
17577
17791
  # Secure-by-default scan (v7.87.0). Advisory by default (never
17578
17792
  # blocks); records .loki/quality/security-findings.json each
17579
17793
  # iteration. Blocks only on un-waived HIGH when LOKI_SECURE_GATE=block.
@@ -350,6 +350,18 @@ _verify_zero_tests_executed() {
350
350
  fi
351
351
  return 1
352
352
  ;;
353
+ unittest)
354
+ # #139: `python3 -m unittest discover` on ZERO discovered tests prints
355
+ # "Ran 0 tests" + "NO TESTS RAN". On older Python it EXITS 0 (a silent
356
+ # fake-green), on newer it exits 5 (already not-pass). POSITIVE detection
357
+ # so it is inconclusive on BOTH. A real run prints "Ran N test(s)" (N>=1)
358
+ # + "OK"/"FAILED" -> never downgraded. Mirrors run.sh's guard exactly.
359
+ if printf '%s' "$_zt_out" | grep -qE '^Ran 0 tests' 2>/dev/null \
360
+ || printf '%s' "$_zt_out" | grep -qE '^NO TESTS RAN$' 2>/dev/null; then
361
+ return 0
362
+ fi
363
+ return 1
364
+ ;;
353
365
  *)
354
366
  return 1
355
367
  ;;
@@ -421,9 +433,19 @@ verify_gate_tests() {
421
433
  if command -v pytest >/dev/null 2>&1; then
422
434
  runner="pytest"
423
435
  out="$(cd "$tree" && timeout "$timeout_s" pytest --tb=short 2>&1)" || rc=$?
436
+ elif command -v python3 >/dev/null 2>&1; then
437
+ # #139 parity: pytest ABSENT but a Python suite exists -> run it with
438
+ # stdlib unittest (zero third-party deps) instead of going blindly
439
+ # inconclusive. A real passing test_*.py suite is now VERIFIED here
440
+ # too, matching run.sh's enforce_test_coverage. Zero-discovery is
441
+ # handled below (unittest exits 5 on newer Python = fail-not-pass, or
442
+ # a "Ran 0 tests" line detected by the zero-test guard on the run.sh
443
+ # mirror) so this never green-washes an empty suite.
444
+ runner="unittest"
445
+ out="$(cd "$tree" && timeout "$timeout_s" python3 -m unittest discover -p 'test_*.py' 2>&1)" || rc=$?
424
446
  else
425
447
  # Applicable but cannot run -> inconclusive (Entanglement 2).
426
- _verify_add_gate "tests" "inconclusive" "pytest" "python project detected but pytest not on PATH" "true"
448
+ _verify_add_gate "tests" "inconclusive" "pytest" "python project detected but no pytest/python3 on PATH" "true"
427
449
  return 0
428
450
  fi
429
451
  fi
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.118.0"
10
+ __version__ = "7.120.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -0,0 +1,79 @@
1
+ # Evidence Receipt: honest build applicability (done once, for all customers)
2
+
3
+ ## The defect (founder-reported, 2026-07-03)
4
+
5
+ A sophisticated user built a Node CLI (invoice tool) and saw the Evidence Receipt say
6
+ "Build: not run - build not run", "Live health check: not captured", "No screenshot",
7
+ headline "Working, with gaps". He concluded *nothing was validated* -- and that read is the
8
+ bug: it destroys trust in genuinely-validated work.
9
+
10
+ ## Ground truth (from the persisted proof of run-20260703034719)
11
+
12
+ - `facts.tests` = `verified`, exit 0 -- tests GENUINELY ran and passed.
13
+ - `facts.security` = `ran:true, clean` -- security GENUINELY ran, real clean scan.
14
+ - `facts.build` = `ran:false, status:not_run, command:""` -- and this is counted as a GAP
15
+ by `_compute_degraded` (proof-generator.py:903-908), which forces "WITH GAPS".
16
+ - `facts.quality_gates` = `[]` (EMPTY) -- verify.sh's deterministic build gate does NOT
17
+ reach the SaaS proof.
18
+
19
+ ## Root cause (systemic, every build, every customer)
20
+
21
+ `build-results.json` has **NO writer**. `_collect_build` (proof-generator.py:319) reads
22
+ `.loki/quality/build-results.json`, which nothing ever writes, so `facts.build` defaults to
23
+ `not_run` (line 333) for EVERY build. The sibling collectors DO have writers in run.sh
24
+ (`enforce_static_analysis` 8357, `run_secure_scan` 8720, `enforce_test_coverage` 9170 ->
25
+ test-results.json / security-findings.json / static-analysis.json). Build was never wired.
26
+
27
+ So "Build: not run" is not a per-project decision -- it is a permanent, structural
28
+ mislabel. A build that has no build step (a CLI) and a build that genuinely skipped a real
29
+ build both read identically as "not run" and both count as a gap.
30
+
31
+ ## The fix (honest, no fabrication, no mocking -- the founder's hard constraint)
32
+
33
+ Add the missing writer as a sibling collector, with a POSITIVE applicability determination:
34
+
35
+ 1. **run.sh `enforce_build_check()`** (new, sibling to enforce_static_analysis): detect the
36
+ stack's build command (npm "build" script / go build / cargo build -- same detectors as
37
+ verify.sh:250-291). Then:
38
+ - build command exists -> RUN it -> record `{ran:true, command, exit_code, status:
39
+ verified|failed}`.
40
+ - genuinely NO build step for this stack -> record `{ran:false, applicable:false,
41
+ status:not_applicable, command:""}`. This is a POSITIVE "no build phase" signal, not an
42
+ absent file.
43
+ Write `.loki/quality/build-results.json`. Call it where the sibling collectors are called.
44
+
45
+ 2. **proof-generator.py `_collect_build`**: pass through `applicable:false` -> status
46
+ `not_applicable`. CRITICAL: `not_applicable` ONLY from an explicit `applicable:false` in
47
+ the file. File ABSENT or unknown status -> `not_run` (honest gap), NEVER N/A. Absent must
48
+ never mean N/A (that would flip un-built projects green -- fake-green).
49
+
50
+ 3. **proof-generator.py `_compute_degraded`**: `not_applicable` is NOT a gap (excluded from
51
+ the weak set for build). A failed/skipped/not_run build stays a gap. Mirror-image guard:
52
+ a real build script that FAILS still emits `failed` -> gap -> never green.
53
+
54
+ 4. **proof-generator.py status normalizer (line 215)**: `not_applicable` passes through.
55
+
56
+ 5. **SPA adapter.ts**: `not_applicable` renders "N/A - no build step for this stack" (a
57
+ neutral/pass tier, NOT a warn/gap), distinct from "not run".
58
+
59
+ ## Anti-fake-green invariants (the trust moat)
60
+
61
+ - N/A ONLY on positive `applicable:false`. Absent file / unknown status -> honest gap.
62
+ - A build script that got skipped or failed STILL shows a gap and CANNOT go green.
63
+ - The directional regression test (write FIRST): Node project WITH a build script whose
64
+ build FAILS -> `facts.build=failed` -> degraded -> headline NOT green. And: build gate
65
+ MISSING -> still a gap, not N/A.
66
+ - N/A does not gate the seal any more than a gap does; it is honest applicability, not a pass.
67
+
68
+ ## Measurable before/after
69
+
70
+ Before: invoice CLI -> "Build: not run", headline "Working, with gaps".
71
+ After: fresh invoice CLI build -> "Build: N/A (no build step for this stack)", tests passed,
72
+ security scanned-clean, headline VERIFIED (no un-closed applicable gaps).
73
+
74
+ ## Constraints
75
+
76
+ Verdict-core: council 3/3 + local-ci + 14 version locations. Verify via
77
+ function-extraction/golden harness -- NEVER run the engine build from the worktree. Run the
78
+ EXISTING tests/test_proof_generator.py green before AND after (it has build-results.json
79
+ fixtures that this change touches). Deploy to live SaaS is `bun install -g loki-mode@<ver>`.
@@ -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.118.0
5
+ **Version:** v7.120.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.118.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){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 R($,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([c1(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 S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 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 b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){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 z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
2
+ var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.120.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){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 R($,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([c1(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 S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 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 b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){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 z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}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)
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
814
814
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
815
815
  `),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
816
816
 
817
- //# debugId=01D367D3C33A5E7664756E2164756E21
817
+ //# debugId=4C6F828B3BD8743B64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.118.0'
60
+ __version__ = '7.120.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.118.0",
4
+ "version": "7.120.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.118.0",
5
+ "version": "7.120.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",