loki-mode 7.37.1 → 7.39.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.
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-marketplace.json",
3
+ "name": "loki-mode",
4
+ "owner": {
5
+ "name": "Autonomi"
6
+ },
7
+ "description": "Autonomi marketplace: Loki Mode, the autonomous spec-to-product build system with a built-in trust layer.",
8
+ "plugins": [
9
+ {
10
+ "name": "loki-mode",
11
+ "source": "./plugins/loki-mode",
12
+ "description": "Autonomous spec-to-product build system with a built-in trust layer. Ships Loki's spec-hardening (grill), living-spec drift detection, and deterministic PR verification commands, plus the Loki MCP server (memory, task queue, code search, build management). Requires the loki-mode CLI on PATH (npm install -g loki-mode or brew install).",
13
+ "homepage": "https://github.com/asklokesh/loki-mode",
14
+ "repository": "https://github.com/asklokesh/loki-mode",
15
+ "license": "SEE LICENSE IN LICENSE",
16
+ "category": "automation",
17
+ "keywords": [
18
+ "autonomous",
19
+ "agent",
20
+ "spec-driven",
21
+ "verification",
22
+ "code-review",
23
+ "mcp",
24
+ "loki"
25
+ ]
26
+ }
27
+ ]
28
+ }
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.37.1
6
+ # Loki Mode v7.39.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
398
398
 
399
399
  ---
400
400
 
401
- **v7.37.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
401
+ **v7.39.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.37.1
1
+ 7.39.0
@@ -261,14 +261,18 @@ loki_review_guard_denylist() {
261
261
  # the dangerous forms. echo>/sed -i/python -c style writes are not enumerable and
262
262
  # remain possible; the real net is commit-before-agent-wave (see CLAUDE.md).
263
263
  #
264
- # DEFAULT OFF (opt-in LOKI_REVIEW_ALLOWLIST=1) so the default argv on BOTH routes
265
- # stays byte-identical to v7.34. Gated on CLI support so an older claude degrades
266
- # gracefully (emits nothing). Predicate + token so call sites append uniformly:
264
+ # DEFAULT ON (safety-additive least-privilege; opt OUT with LOKI_REVIEW_ALLOWLIST=0).
265
+ # Flipped default-on because deny precedence (verified live) means the denylist
266
+ # still hard-blocks every mutation form while this allowlist only narrows the
267
+ # reviewer surface to read/inspect tools -- pure safety win, no surprise spend, no
268
+ # egress. The escape hatch (LOKI_REVIEW_ALLOWLIST=0) restores the prior off state.
269
+ # Gated on CLI support so an older claude degrades gracefully (emits nothing).
270
+ # Predicate + token so call sites append uniformly:
267
271
  # if loki_review_allowlist_enabled; then
268
272
  # argv+=("--allowedTools" "$(loki_review_allowlist)")
269
273
  # fi
270
274
  loki_review_allowlist_enabled() {
271
- [ "${LOKI_REVIEW_ALLOWLIST:-0}" = "1" ] || return 1
275
+ [ "${LOKI_REVIEW_ALLOWLIST:-1}" = "0" ] && return 1
272
276
  loki_claude_flag_supported "--allowedTools"
273
277
  }
274
278
  loki_review_allowlist() {
@@ -379,6 +383,105 @@ loki_ultrareview_enabled() {
379
383
  [ "${LOKI_ULTRAREVIEW:-0}" = "1" ]
380
384
  }
381
385
 
386
+ # ---------- v7.38.0 Dynamic Workflows (ultracode) gates ----------
387
+ # Claude Code "Dynamic Workflows" are JS orchestration scripts Claude writes that
388
+ # fan out into many background subagents. They are triggered by the `ultracode`
389
+ # keyword in a prompt and fire under `claude -p` (verified empirically: a headless
390
+ # workflow returned "The workflow finished"). They are:
391
+ # - Claude-PROVIDER-ONLY (no Codex/Cline/Aider equivalent),
392
+ # - require claude CLI >= 2.1.154,
393
+ # - cost meaningfully MORE than a normal run (a trivial workflow observed at
394
+ # ~$0.71 vs ~$0.01 for a plain read; there is NO price API so we never quote
395
+ # a dollar figure -- we disclose the cost CLASS only),
396
+ # - disablable via CLAUDE_CODE_DISABLE_WORKFLOWS=1 or the `disableWorkflows`
397
+ # setting in any Claude settings.json.
398
+ #
399
+ # These predicates mirror loki_ultrareview_supported/enabled exactly: read-only,
400
+ # side-effect free, the ONLY policy surface for the optional `loki ultracode`
401
+ # passthrough and the Phase 2 opt-in analysis dispatch. The user typing
402
+ # `loki ultracode` (or setting LOKI_USE_CLAUDE_WORKFLOWS=1) IS the consent signal.
403
+
404
+ # Minimum claude CLI version that ships Dynamic Workflows.
405
+ LOKI_WORKFLOWS_MIN_VERSION="${LOKI_WORKFLOWS_MIN_VERSION:-2.1.154}"
406
+
407
+ # Parse the installed claude CLI semantic version (e.g. "2.1.177") to stdout, or
408
+ # empty on any failure. Mirrors the `claude --version | sed` pattern already used
409
+ # at autonomy/loki:7888. Cached per-process so we do not shell out repeatedly.
410
+ _loki_claude_version() {
411
+ if [ -z "${__LOKI_CLAUDE_VERSION_CACHE:-}" ]; then
412
+ if command -v claude >/dev/null 2>&1; then
413
+ __LOKI_CLAUDE_VERSION_CACHE="$(claude --version 2>/dev/null | head -1 | sed 's/[^0-9.]//g' | head -1)"
414
+ fi
415
+ # Sentinel so an empty real result does not re-shell every call.
416
+ __LOKI_CLAUDE_VERSION_CACHE="${__LOKI_CLAUDE_VERSION_CACHE:-__none__}"
417
+ export __LOKI_CLAUDE_VERSION_CACHE
418
+ fi
419
+ [ "$__LOKI_CLAUDE_VERSION_CACHE" = "__none__" ] && return 0
420
+ printf '%s' "$__LOKI_CLAUDE_VERSION_CACHE"
421
+ }
422
+
423
+ # Dotted-version >= compare: returns 0 when $1 >= $2, 1 otherwise. Pure, no
424
+ # external tools beyond awk (always present). Pads missing components with 0.
425
+ _loki_version_ge() {
426
+ local have="${1:-}" want="${2:-}"
427
+ [ -z "$have" ] && return 1
428
+ [ -z "$want" ] && return 0
429
+ awk -v a="$have" -v b="$want" '
430
+ function cmp(x, y, na, nb, i, n, xi, yi) {
431
+ na = split(x, A, ".")
432
+ nb = split(y, B, ".")
433
+ n = (na > nb) ? na : nb
434
+ for (i = 1; i <= n; i++) {
435
+ xi = (i <= na) ? A[i] + 0 : 0
436
+ yi = (i <= nb) ? B[i] + 0 : 0
437
+ if (xi > yi) return 1
438
+ if (xi < yi) return -1
439
+ }
440
+ return 0
441
+ }
442
+ BEGIN { exit (cmp(a, b) >= 0) ? 0 : 1 }
443
+ '
444
+ }
445
+
446
+ # True when CLAUDE_CODE_DISABLE_WORKFLOWS=1 OR a `disableWorkflows` setting is
447
+ # active in any Claude settings source. Best-effort, read-only (not a full JSON
448
+ # parse, but rejects the obvious cases). Returns 0 (disabled) / 1 (not disabled).
449
+ _loki_workflows_disabled() {
450
+ [ "${CLAUDE_CODE_DISABLE_WORKFLOWS:-0}" = "1" ] && return 0
451
+ local f
452
+ for f in "$HOME/.claude/settings.json" "$HOME/.config/claude/settings.json" \
453
+ "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" \
454
+ "$PWD/.claude/settings.json" "$PWD/.claude/settings.local.json"; do
455
+ [ -f "$f" ] || continue
456
+ # Match "disableWorkflows": true (tolerating whitespace around the colon).
457
+ grep -Eq '"disableWorkflows"[[:space:]]*:[[:space:]]*true' "$f" 2>/dev/null && return 0
458
+ done
459
+ return 1
460
+ }
461
+
462
+ # Capability gate: is the active provider Claude AND the claude CLI present AND
463
+ # version >= the workflows minimum AND workflows not disabled? Returns 0 when
464
+ # workflows can run, 1 otherwise (so callers emit an honest message + clean exit).
465
+ loki_workflows_supported() {
466
+ # Provider must be Claude (Tier 1). Workflows are Claude-only.
467
+ [ "${LOKI_PROVIDER:-claude}" = "claude" ] || return 1
468
+ command -v claude >/dev/null 2>&1 || return 1
469
+ _loki_workflows_disabled && return 1
470
+ local ver
471
+ ver="$(_loki_claude_version)"
472
+ _loki_version_ge "$ver" "$LOKI_WORKFLOWS_MIN_VERSION" || return 1
473
+ return 0
474
+ }
475
+
476
+ # Non-interactive opt-in: is LOKI_USE_CLAUDE_WORKFLOWS=1 set? This is the env knob
477
+ # that turns ON the Phase 2 read-only-analysis workflow dispatch. Default OFF. Any
478
+ # value other than the exact "1" returns 1 so only an explicit opt-in counts. This
479
+ # is independent of the `loki ultracode` passthrough (which is itself an explicit
480
+ # user invocation and does not require this flag).
481
+ loki_workflows_enabled() {
482
+ [ "${LOKI_USE_CLAUDE_WORKFLOWS:-0}" = "1" ]
483
+ }
484
+
382
485
  # ---------------------------------------------------------------------------
383
486
  # Session-continuity Phase 2 (GitHub #165) -- LOKI_RESUME_SESSION recovery resume
384
487
  #
package/autonomy/loki CHANGED
@@ -649,13 +649,18 @@ show_help() {
649
649
  # do NOT appear here; they live in the collapsed footer below and in
650
650
  # `loki help aliases`. The full long-tail surface is still dispatchable and
651
651
  # documented per-command via `loki <command> --help`.
652
+ #
653
+ # v7.39.0: trimmed three lower-traffic canonical entries off the front page
654
+ # to hold the lean target after `ultracode` (v7.38.0) joined Verify/trust:
655
+ # grill (pre-build spec interrogation, advanced), spec (living-spec drift,
656
+ # advanced), and cleanup (orphaned-process recovery, rare). They remain
657
+ # fully canonical (NOT aliases), dispatchable, and listed in the "More
658
+ # commands" footer below + documented via `loki <command> --help`.
652
659
  echo "Commands:"
653
660
  echo ""
654
661
  echo "Build:"
655
662
  echo " start [SPEC] Start a build (PRD file, GitHub issue, or no arg)"
656
663
  echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
657
- echo " grill [SPEC] Devil's-advocate spec interrogation before you build"
658
- echo " spec [cmd] Living spec: lock|status|sync (drift detection)"
659
664
  echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
660
665
  echo ""
661
666
  echo "Session:"
@@ -663,11 +668,11 @@ show_help() {
663
668
  echo " stop Stop execution immediately"
664
669
  echo " pause Pause after current session"
665
670
  echo " resume Resume paused execution"
666
- echo " cleanup Kill orphaned processes from crashed sessions"
667
671
  echo ""
668
672
  echo "Verify / trust:"
669
673
  echo " verify [base] Deterministic PR verification (CI-gate exit codes)"
670
674
  echo " review [opts] Standalone code review with quality gates"
675
+ echo " ultracode \"task\" Run a task as a native Claude Code Dynamic Workflow (opt-in, Claude-only)"
671
676
  echo " trust [cmd] Visible trust trajectory; 'trust detail' for metrics"
672
677
  echo ""
673
678
  echo "Observe:"
@@ -691,10 +696,10 @@ show_help() {
691
696
  echo " version Show version"
692
697
  echo " help Show this help ('loki help aliases' for old names)"
693
698
  echo ""
694
- echo "More commands (init, watch, demo, web, api, logs, github,"
695
- echo "import, council, proof, audit, agent, template, magic, docs, wiki, ci,"
696
- echo "test, bench, secrets, telemetry, crash, worktree, failover, monitor,"
697
- echo "remote, ...) are dispatchable and documented via"
699
+ echo "More commands (grill, spec, cleanup, init, watch, demo, web, api,"
700
+ echo "logs, github, import, council, proof, audit, agent, template, magic,"
701
+ echo "docs, wiki, ci, test, bench, secrets, telemetry, crash, worktree,"
702
+ echo "failover, monitor, remote, ...) are dispatchable and documented via"
698
703
  echo "'loki <command> --help'."
699
704
  echo ""
700
705
  echo "Aliases (deprecated): older command names still work; they print a"
@@ -14354,6 +14359,9 @@ main() {
14354
14359
  review)
14355
14360
  cmd_review "$@"
14356
14361
  ;;
14362
+ ultracode)
14363
+ cmd_ultracode "$@"
14364
+ ;;
14357
14365
  optimize)
14358
14366
  cmd_optimize "$@"
14359
14367
  ;;
@@ -14638,6 +14646,182 @@ _review_ultra() {
14638
14646
  return $?
14639
14647
  }
14640
14648
 
14649
+ # v7.38.0: optional native Claude Code Dynamic Workflow passthrough
14650
+ # (`loki ultracode "<task>"`). Structurally identical to _review_ultra (issue
14651
+ # #168): opt-in, capability-gated, cost-class disclosed, Claude-provider-only,
14652
+ # clean exit on any unsupported path. PURE passthrough -- Loki adds NO
14653
+ # orchestration of its own; it prepends the `ultracode` keyword to the prompt and
14654
+ # routes through `claude -p`, which fires the workflow runtime.
14655
+ #
14656
+ # Consent model (mirrors _review_ultra):
14657
+ # - The cost-CLASS disclosure ALWAYS prints. There is NO price API, so we never
14658
+ # quote a dollar figure (that would be a lie).
14659
+ # - Interactive TTY without --yes: prompt, default NO.
14660
+ # - Non-TTY/CI without --yes (and no LOKI_USE_CLAUDE_WORKFLOWS=1): REFUSE with
14661
+ # exit 2 and ZERO workflow invocation (the no-silent-bill guard).
14662
+ # - --yes or LOKI_USE_CLAUDE_WORKFLOWS=1 proceeds without prompting.
14663
+ # Args: <assume_yes> <task...>
14664
+ _run_ultracode() {
14665
+ local assume_yes="${1:-false}"
14666
+ shift || true
14667
+ local task="$*"
14668
+
14669
+ if [ -z "$task" ]; then
14670
+ echo -e "${RED}Error: loki ultracode requires a task description.${NC}" >&2
14671
+ echo "Usage: loki ultracode \"<task>\" [--yes]" >&2
14672
+ return 1
14673
+ fi
14674
+
14675
+ # Make the workflow gate predicates available even though autonomy/loki does
14676
+ # not source claude-flags.sh globally (same on-demand pattern as
14677
+ # _review_ultra). Degrade honestly if the lib is missing.
14678
+ if ! declare -F loki_workflows_supported >/dev/null 2>&1; then
14679
+ local _wf_lib=""
14680
+ if [ -f "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" ]; then
14681
+ _wf_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh"
14682
+ elif [ -f "$(dirname "$0")/lib/claude-flags.sh" ]; then
14683
+ _wf_lib="$(dirname "$0")/lib/claude-flags.sh"
14684
+ fi
14685
+ if [ -n "$_wf_lib" ]; then
14686
+ # shellcheck source=autonomy/lib/claude-flags.sh
14687
+ . "$_wf_lib" 2>/dev/null || true
14688
+ fi
14689
+ fi
14690
+
14691
+ # 1. Provider gate: workflows are Claude-only. On Codex/Cline/Aider, print the
14692
+ # honest message and exit CLEANLY (0) with ZERO invocation. Never break the
14693
+ # other providers.
14694
+ local _provider="${LOKI_PROVIDER:-claude}"
14695
+ if [ "$_provider" != "claude" ]; then
14696
+ echo -e "${YELLOW}loki ultracode: Claude provider only.${NC}" >&2
14697
+ echo "Workflows need the Claude provider and claude CLI >= 2.1.154." >&2
14698
+ echo "Active provider is '${_provider}'; nothing was run." >&2
14699
+ return 0
14700
+ fi
14701
+
14702
+ # 2. claude CLI present at all?
14703
+ if ! command -v claude >/dev/null 2>&1; then
14704
+ echo -e "${RED}Error: 'claude' CLI not found on PATH.${NC}" >&2
14705
+ echo "Workflows need the Claude provider and claude CLI >= 2.1.154." >&2
14706
+ return 1
14707
+ fi
14708
+
14709
+ # 3. Capability gate: provider==claude AND CLI present AND version >= 2.1.154
14710
+ # AND workflows not disabled. Honest message + clean exit if unsupported.
14711
+ if ! declare -F loki_workflows_supported >/dev/null 2>&1 \
14712
+ || ! loki_workflows_supported; then
14713
+ echo -e "${YELLOW}loki ultracode: workflows unavailable on this setup.${NC}" >&2
14714
+ echo "Workflows need the Claude provider and claude CLI >= 2.1.154" >&2
14715
+ echo "(and must not be disabled via CLAUDE_CODE_DISABLE_WORKFLOWS or the" >&2
14716
+ echo "disableWorkflows setting). Nothing was run." >&2
14717
+ return 0
14718
+ fi
14719
+
14720
+ # 4. Cost-CLASS disclosure -- ALWAYS printed, even with --yes, so spend is
14721
+ # never hidden. NO dollar figure (there is no price API); cost-CLASS only.
14722
+ echo -e "${BOLD}loki ultracode${NC} - native Claude Code Dynamic Workflow (optional)" >&2
14723
+ echo "" >&2
14724
+ echo -e "${YELLOW}Workflows spawn many agents and cost meaningfully more than a normal${NC}" >&2
14725
+ echo -e "${YELLOW}run; this is a Claude Code feature billed as normal usage.${NC}" >&2
14726
+ echo "" >&2
14727
+
14728
+ # 5. Consent. LOKI_USE_CLAUDE_WORKFLOWS=1 is the non-interactive opt-in
14729
+ # equivalent of --yes for THIS command.
14730
+ local proceed=false
14731
+ if [ "$assume_yes" = true ]; then
14732
+ proceed=true
14733
+ elif declare -F loki_workflows_enabled >/dev/null 2>&1 && loki_workflows_enabled; then
14734
+ proceed=true
14735
+ else
14736
+ local uc_interactive=true
14737
+ if [ ! -t 0 ] || [ -n "${CI:-}" ]; then
14738
+ uc_interactive=false
14739
+ fi
14740
+ if [ "$uc_interactive" = true ]; then
14741
+ local ans=""
14742
+ echo -n "Run the workflow now? [y/N] " >&2
14743
+ read -r ans </dev/tty 2>/dev/null || ans=""
14744
+ if [[ "$ans" =~ ^[Yy] ]]; then
14745
+ proceed=true
14746
+ else
14747
+ echo "Cancelled. Nothing was spent." >&2
14748
+ return 0
14749
+ fi
14750
+ else
14751
+ # Non-TTY/CI without --yes: never hang; refuse with exit 2 and make
14752
+ # ZERO workflow calls (the no-silent-bill guard).
14753
+ echo "workflows need confirmation; re-run with --yes (or set LOKI_USE_CLAUDE_WORKFLOWS=1) to proceed non-interactively" >&2
14754
+ return 2
14755
+ fi
14756
+ fi
14757
+
14758
+ [ "$proceed" = true ] || { echo "Cancelled. Nothing was spent." >&2; return 0; }
14759
+
14760
+ # 6. Invoke the Claude provider once with the prompt PREFIXED by "ultracode: "
14761
+ # so the workflow runtime fires. ALWAYS opus per project policy (planning-
14762
+ # grade fan-out). The keyword prefix is the only supported, stable trigger;
14763
+ # Loki never authors a workflow script itself.
14764
+ local uc_prompt="ultracode: ${task}"
14765
+ local uc_argv=("-p" "$uc_prompt" "--model" "opus")
14766
+
14767
+ echo -e "${DIM}Running: claude -p \"ultracode: ...\" --model opus${NC}" >&2
14768
+ echo "" >&2
14769
+ claude "${uc_argv[@]}"
14770
+ return $?
14771
+ }
14772
+
14773
+ # v7.38.0: `loki ultracode "<task>"` command. Thin opt-in surface that routes a
14774
+ # task through a native Claude Code Dynamic Workflow. See _run_ultracode.
14775
+ cmd_ultracode() {
14776
+ local uc_assume_yes=false
14777
+ local uc_args=()
14778
+ while [[ $# -gt 0 ]]; do
14779
+ case "$1" in
14780
+ --help|-h)
14781
+ echo -e "${BOLD}loki ultracode${NC} - run a task as a native Claude Code Dynamic Workflow"
14782
+ echo ""
14783
+ echo "Usage: loki ultracode \"<task>\" [--yes]"
14784
+ echo ""
14785
+ echo "Prepends the 'ultracode' keyword to your task and routes it through"
14786
+ echo "the Claude provider, firing Claude Code's Dynamic Workflow runtime"
14787
+ echo "(many background subagents fanning out on a read/research/audit task)."
14788
+ echo ""
14789
+ echo "This is a PASSTHROUGH: Loki adds no orchestration of its own. It is"
14790
+ echo "OPT-IN, default OFF, and Claude-provider-only. The council, the 11"
14791
+ echo "quality gates, the evidence gate, and the RARV loop are untouched."
14792
+ echo ""
14793
+ echo "Requirements:"
14794
+ echo " - Active provider is Claude (Tier 1)"
14795
+ echo " - claude CLI >= 2.1.154"
14796
+ echo " - Workflows not disabled (CLAUDE_CODE_DISABLE_WORKFLOWS / disableWorkflows)"
14797
+ echo "On any unmet requirement: an honest message prints and nothing runs."
14798
+ echo ""
14799
+ echo "Cost: workflows spawn many agents and cost meaningfully more than a"
14800
+ echo "normal run; this is a Claude Code feature billed as normal usage."
14801
+ echo "There is no price API, so no dollar figure is shown."
14802
+ echo ""
14803
+ echo "Options:"
14804
+ echo " --yes, -y Skip the confirmation prompt (consent to the cost)"
14805
+ echo " --help, -h Show this help"
14806
+ echo ""
14807
+ echo "Environment:"
14808
+ echo " LOKI_USE_CLAUDE_WORKFLOWS=1 Non-interactive opt-in (like --yes)"
14809
+ echo " CLAUDE_CODE_DISABLE_WORKFLOWS=1 Disable workflows entirely"
14810
+ echo ""
14811
+ echo "Examples:"
14812
+ echo " loki ultracode \"audit this repo for dead code\""
14813
+ echo " loki ultracode \"research the top 5 auth libraries for Node\" --yes"
14814
+ return 0
14815
+ ;;
14816
+ --yes|-y) uc_assume_yes=true; shift ;;
14817
+ --) shift; while [[ $# -gt 0 ]]; do uc_args+=("$1"); shift; done ;;
14818
+ *) uc_args+=("$1"); shift ;;
14819
+ esac
14820
+ done
14821
+ _run_ultracode "$uc_assume_yes" "${uc_args[@]}"
14822
+ return $?
14823
+ }
14824
+
14641
14825
  # Standalone code review - diff-based quality gates (v6.20.0)
14642
14826
  cmd_review() {
14643
14827
  local review_staged=false
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.37.1"
10
+ __version__ = "7.39.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.37.1
5
+ **Version:** v7.39.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.37.1";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
2
+ var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.39.0";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
789
789
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
790
790
  `),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
791
791
 
792
- //# debugId=4D62C09E1CA3E64A64756E2164756E21
792
+ //# debugId=0053955F19A92CF864756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.37.1'
60
+ __version__ = '7.39.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.37.1",
4
+ "version": "7.39.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -67,6 +67,8 @@
67
67
  "VERSION",
68
68
  "assets/",
69
69
  "tools/",
70
+ "plugins/",
71
+ ".claude-plugin/marketplace.json",
70
72
  "autonomy/",
71
73
  "providers/",
72
74
  "agents/",
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
+ "name": "loki-mode",
4
+ "displayName": "Loki Mode",
5
+ "version": "7.39.0",
6
+ "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
+ "author": {
8
+ "name": "Autonomi",
9
+ "url": "https://www.autonomi.dev/"
10
+ },
11
+ "homepage": "https://github.com/asklokesh/loki-mode",
12
+ "repository": "https://github.com/asklokesh/loki-mode",
13
+ "license": "SEE LICENSE IN LICENSE",
14
+ "keywords": [
15
+ "autonomous",
16
+ "agent",
17
+ "spec-driven",
18
+ "verification",
19
+ "code-review",
20
+ "mcp",
21
+ "loki"
22
+ ],
23
+ "commands": [
24
+ "./commands/"
25
+ ],
26
+ "mcpServers": "./.mcp.json",
27
+ "hooks": "./hooks/hooks.json"
28
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "loki-mode": {
4
+ "command": "loki",
5
+ "args": ["mcp", "--transport", "stdio"],
6
+ "env": {
7
+ "LOKI_MCP_AUTO_BOOTSTRAP": "1"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,88 @@
1
+ # Loki Mode plugin for Claude Code
2
+
3
+ Loki Mode is the autonomous spec-to-product build system with a built-in trust
4
+ layer (RARV-C closure loop, 11 quality gates, completion council). This plugin
5
+ brings Loki's spec-hardening, drift-detection, and deterministic PR verification
6
+ into Claude Code as slash commands, and wires up the Loki MCP server.
7
+
8
+ Homepage: https://github.com/asklokesh/loki-mode
9
+
10
+ ## Install
11
+
12
+ The plugin is published through the Autonomi marketplace, which lives in this
13
+ same repository.
14
+
15
+ 1. Add the marketplace (one time):
16
+
17
+ ```
18
+ /plugin marketplace add asklokesh/loki-mode
19
+ ```
20
+
21
+ 2. Install the plugin:
22
+
23
+ ```
24
+ /plugin install loki-mode@loki-mode
25
+ ```
26
+
27
+ That is it. The commands below become available immediately.
28
+
29
+ ## What you get
30
+
31
+ Slash commands (namespaced as `loki-mode:<command>`):
32
+
33
+ - `loki-mode:loki-grill` - interrogate a spec with Loki's Devil's-Advocate
34
+ grill before building, and summarize the hardest questions it surfaces.
35
+ - `loki-mode:loki-spec-status` - check whether the spec has drifted from its
36
+ lock using deterministic living-spec drift detection.
37
+ - `loki-mode:loki-verify` - run Loki's deterministic PR verification on the
38
+ current change and summarize the evidence verdict.
39
+
40
+ MCP server (`loki-mode`): exposes Loki's tools (memory, task queue, code
41
+ search, build management) to Claude Code.
42
+
43
+ ## Requirement: the loki-mode CLI must be installed
44
+
45
+ The slash commands run `loki ...` subcommands, and the MCP server is launched
46
+ via `loki mcp`. Both require the `loki` binary to be on your PATH. Install it
47
+ once:
48
+
49
+ ```
50
+ npm install -g loki-mode
51
+ ```
52
+
53
+ or with Homebrew:
54
+
55
+ ```
56
+ brew install asklokesh/tap/loki-mode
57
+ ```
58
+
59
+ The plugin ships the commands, MCP wiring, and an optional guard hook. It does
60
+ not bundle the Loki runtime itself, because a marketplace plugin is copied into
61
+ an isolated cache and cannot reach the rest of the repository. The CLI on PATH
62
+ is what provides the runtime.
63
+
64
+ On the MCP server's first launch, if the Python MCP SDK is not present, the
65
+ bundled config sets `LOKI_MCP_AUTO_BOOTSTRAP=1` as written, in-advance consent
66
+ so the server can create a project-local virtualenv at `.loki/mcp-venv` and
67
+ install its dependencies non-interactively. Remove that env var from
68
+ `.mcp.json` if you prefer to bootstrap manually (run `loki mcp` once in a
69
+ terminal and follow the printed instructions).
70
+
71
+ ## Optional Bash guard hook (off by default)
72
+
73
+ The plugin ships a `PreToolUse` hook for the Bash tool that is a no-op unless
74
+ you opt in. Set `LOKI_GUARD=1` in your environment to enable it. When enabled it
75
+ blocks a small set of clearly destructive Bash commands that have bitten Loki
76
+ runs before:
77
+
78
+ - `rm -rf` on `/tmp/loki-*` while a live Loki run may be staging files there
79
+ - `rm -rf` of a filesystem or `$HOME` root
80
+ - `git add -A` / `git add .` (Loki convention: stage files individually)
81
+
82
+ With `LOKI_GUARD` unset the hook always allows the command through, so it never
83
+ interferes unless you ask it to.
84
+
85
+ ## License
86
+
87
+ SEE LICENSE IN LICENSE (BUSL-1.1, source-available). See the LICENSE file at the
88
+ repository root.
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: Interrogate a spec with Loki's Devil's-Advocate grill before building, and summarize the hardest questions it surfaces.
3
+ argument-hint: "[spec-path] (default: prd.md / .loki/generated-prd.md)"
4
+ allowed-tools: Bash(loki grill:*), Read
5
+ ---
6
+
7
+ Harden a spec before any code is written. Loki's grill invokes the provider
8
+ once with a Devil's-Advocate prompt to surface the 10-15 hardest questions that
9
+ expose ambiguities, missing acceptance criteria, unstated assumptions, and
10
+ security/scale blind spots. A grilled spec is a better Reason input to the
11
+ RARV-C loop.
12
+
13
+ Steps:
14
+
15
+ 1. Run the interrogation on the spec ($ARGUMENTS, or the default resolution
16
+ prd.md / .loki/generated-prd.md if empty):
17
+
18
+ ```
19
+ loki grill $ARGUMENTS
20
+ ```
21
+
22
+ It writes `.loki/grill/report.md`. It requires a provider CLI and fails
23
+ cleanly (exit 3) when none is available: it never fabricates questions.
24
+
25
+ 2. Read `.loki/grill/report.md` and present the findings to the user grouped by
26
+ category:
27
+ - Ambiguities and missing acceptance criteria
28
+ - Unstated assumptions
29
+ - Security blind spots
30
+ - Scale and reliability blind spots
31
+
32
+ 3. For each hard question, suggest a concrete way to resolve it in the spec
33
+ (a precise acceptance criterion, an explicit assumption made explicit, a
34
+ security control, a stated limit). Do not silently answer them yourself;
35
+ the point is to harden the human's intent.
36
+
37
+ 4. If the user wants the questions embedded in the spec for the record, offer:
38
+
39
+ ```
40
+ loki grill --apply $ARGUMENTS
41
+ ```
42
+
43
+ which appends a "Grill findings" section to the spec file. Ask before
44
+ modifying the spec.
45
+
46
+ Report only what the grill produced. If the provider was unavailable, say so
47
+ plainly and do not invent questions.
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: Check whether the spec has drifted from its lock using Loki's living-spec drift detection, and summarize the report.
3
+ argument-hint: "[spec-path] (default: prd.md / .loki/generated-prd.md)"
4
+ allowed-tools: Bash(loki spec:*), Read
5
+ ---
6
+
7
+ Check whether the spec is still true: the spec is the contract; Loki keeps it
8
+ true. This runs deterministic drift detection (no LLM cost) comparing the
9
+ current spec against its lock.
10
+
11
+ Steps:
12
+
13
+ 1. If there is no lock yet, the status command will say so. In that case, offer
14
+ to create one:
15
+
16
+ ```
17
+ loki spec lock $ARGUMENTS
18
+ ```
19
+
20
+ The lock (`.loki/spec/spec.lock`) is a deterministic map of spec
21
+ requirements (checklist items and headings) to content hashes, plus repo
22
+ HEAD at lock time.
23
+
24
+ 2. Run the drift check:
25
+
26
+ ```
27
+ loki spec status $ARGUMENTS
28
+ ```
29
+
30
+ Exit 0 means in sync (SPEC-TRUE); exit 1 means drift detected
31
+ (SPEC-DRIFTED). It writes `.loki/spec/drift-report.json`.
32
+
33
+ 3. Read `.loki/spec/drift-report.json` and summarize for the user:
34
+ - The verdict: SPEC-TRUE or SPEC-DRIFTED.
35
+ - Counts of ADDED, REMOVED, and CHANGED requirements, then list each one.
36
+ - Whether code changed since the locked HEAD (files, insertions, deletions).
37
+
38
+ 4. If drifted, explain the choice clearly:
39
+ - If the code is the source of truth and the spec should follow, the human
40
+ updates the spec, then runs `loki spec sync $ARGUMENTS` to re-lock.
41
+ - If the spec is correct and the code lags, the change set is incomplete.
42
+
43
+ This MVP never auto-rewrites the spec. Re-locking via `loki spec sync` is an
44
+ explicit human action after review. Do not run `sync` automatically; ask
45
+ first.
46
+
47
+ Report only what the drift report shows. Do not infer requirements that are not
48
+ in the spec.
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Run Loki's deterministic PR verification on the current change and summarize the evidence verdict.
3
+ argument-hint: "[base-ref] (default: main)"
4
+ allowed-tools: Bash(loki verify:*), Read
5
+ ---
6
+
7
+ Run Loki's Autonomi Verify on the current working tree and report the verdict
8
+ with its evidence, not just an opinion in chat. The differentiator is the
9
+ auditable artifact: a verdict that refuses to silently pass on inconclusive
10
+ evidence.
11
+
12
+ Steps:
13
+
14
+ 1. Run the verifier against the base ref ($ARGUMENTS, or `main` if empty):
15
+
16
+ ```
17
+ loki verify $ARGUMENTS
18
+ ```
19
+
20
+ It computes the PR-style delta merge-base(base, HEAD)..HEAD and runs
21
+ deterministic gates (build, tests, static analysis, secret scan, dependency
22
+ audit, and spec drift when a spec lock exists). Exit codes:
23
+ 0 VERIFIED, 1 CONCERNS, 2 BLOCKED, 3 verifier error.
24
+
25
+ 2. Read the evidence artifacts it wrote:
26
+ - `.loki/verify/evidence.json` (machine-readable: schema, gates, findings)
27
+ - `.loki/verify/report.md` (human verdict + findings table)
28
+
29
+ 3. Summarize for the user:
30
+ - The verdict (VERIFIED / CONCERNS / BLOCKED) and exit code.
31
+ - Each gate and its status (pass / fail / inconclusive / skipped).
32
+ - Every finding: severity, category, file:line, and whether it is blocking.
33
+ - If the verdict is CONCERNS or BLOCKED, list exactly what to fix.
34
+
35
+ Be honest about inconclusive evidence: an inconclusive gate (for example a test
36
+ runner that could not run) is never upgraded to VERIFIED. If the diff is empty,
37
+ the verdict is CONCERNS (nothing to verify), not VERIFIED. Do not claim the
38
+ change is verified unless the evidence says VERIFIED.
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/loki-guard.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # Loki Mode opt-in Bash guard (PreToolUse hook).
3
+ #
4
+ # DEFAULT: OFF. This hook is a no-op pass-through unless the user explicitly
5
+ # opts in by setting LOKI_GUARD=1 (or true/yes/on, case-insensitive) in their
6
+ # environment. With the guard disabled the hook always exits 0 immediately, so
7
+ # it never interferes with a user's Bash tool calls.
8
+ #
9
+ # When enabled, it inspects the proposed Bash command (read from the PreToolUse
10
+ # event JSON on stdin) and blocks a small set of clearly destructive patterns
11
+ # that have bitten Loki runs in the past:
12
+ # - rm -rf on /tmp/loki-* globs while a live run may be staging there
13
+ # - rm -rf of / or $HOME roots
14
+ # - git add -A / git add . (Loki convention: stage files individually)
15
+ #
16
+ # Blocking contract (Claude Code hooks): to deny a tool call, emit a JSON object
17
+ # on stdout with permissionDecision "deny" and a reason, and exit 0. Anything
18
+ # else (empty object, exit 0) allows the call. We never hard-fail the hook so a
19
+ # parsing hiccup can never wedge the session.
20
+ #
21
+ # This script depends only on bash builtins plus python3 (already required by
22
+ # Loki) for robust JSON parsing. If python3 is missing it degrades to allow.
23
+
24
+ set -u
25
+
26
+ # 1. Opt-in gate. Unset / empty / anything-not-truthy => pass through.
27
+ guard_on=0
28
+ case "$(printf '%s' "${LOKI_GUARD:-}" | tr '[:upper:]' '[:lower:]')" in
29
+ 1 | true | yes | on | y) guard_on=1 ;;
30
+ *) guard_on=0 ;;
31
+ esac
32
+
33
+ if [ "$guard_on" -ne 1 ]; then
34
+ # Disabled: allow without comment.
35
+ printf '{}'
36
+ exit 0
37
+ fi
38
+
39
+ # 2. Read the event JSON from stdin (PreToolUse provides tool_input.command).
40
+ event="$(cat 2>/dev/null || true)"
41
+
42
+ # 3. Extract the proposed command. Prefer python3; degrade to allow on any error.
43
+ cmd=""
44
+ if command -v python3 >/dev/null 2>&1; then
45
+ cmd="$(printf '%s' "$event" | python3 -c '
46
+ import sys, json
47
+ try:
48
+ e = json.load(sys.stdin)
49
+ except Exception:
50
+ sys.exit(0)
51
+ ti = e.get("tool_input") or {}
52
+ c = ti.get("command")
53
+ if isinstance(c, str):
54
+ sys.stdout.write(c)
55
+ ' 2>/dev/null || true)"
56
+ fi
57
+
58
+ # No command parsed => allow.
59
+ if [ -z "$cmd" ]; then
60
+ printf '{}'
61
+ exit 0
62
+ fi
63
+
64
+ deny() {
65
+ # $1 = reason. Emit a deny decision and exit 0 (the hook itself succeeded).
66
+ reason="$1"
67
+ printf '%s' "$reason" | python3 -c '
68
+ import sys, json
69
+ reason = sys.stdin.read()
70
+ print(json.dumps({
71
+ "hookSpecificOutput": {
72
+ "hookEventName": "PreToolUse",
73
+ "permissionDecision": "deny",
74
+ "permissionDecisionReason": reason
75
+ }
76
+ }))
77
+ '
78
+ exit 0
79
+ }
80
+
81
+ # 4. Destructive-pattern checks (only reached when guard is ON).
82
+
83
+ # 4a. rm -rf targeting /tmp/loki-* while a live run may be staging there.
84
+ if printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+(-[A-Za-z]*r[A-Za-z]*f|-[A-Za-z]*f[A-Za-z]*r|-rf|-fr)[[:space:]].*/tmp/loki-'; then
85
+ if command -v pgrep >/dev/null 2>&1 && pgrep -f 'loki-run-' >/dev/null 2>&1; then
86
+ deny "LOKI_GUARD: refusing rm -rf on /tmp/loki-* while a live loki run is staging there (pgrep -f loki-run- matched). Scope cleanup to known-dead paths, or stop the run first."
87
+ fi
88
+ fi
89
+
90
+ # 4b. rm -rf of filesystem root or HOME root.
91
+ # shellcheck disable=SC2016 # the regex matches the literal text $HOME in the command, no expansion intended
92
+ if printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+-[A-Za-z]*[rf][A-Za-z]*[[:space:]]+(-[A-Za-z]+[[:space:]]+)*(/|/\*|"\$HOME"|\$HOME|~)([[:space:]]|$)'; then
93
+ deny "LOKI_GUARD: refusing rm -rf of a filesystem or HOME root. This is almost certainly a mistake."
94
+ fi
95
+
96
+ # 4c. git add -A / git add . (Loki convention: stage files individually).
97
+ if printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+add[[:space:]]+(-A|--all|\.)([[:space:]]|$)'; then
98
+ deny "LOKI_GUARD: 'git add -A' / 'git add .' is blocked by Loki convention. Stage files individually by name."
99
+ fi
100
+
101
+ # 5. Nothing matched => allow.
102
+ printf '{}'
103
+ exit 0