loki-mode 7.95.0 → 7.97.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.95.0
6
+ # Loki Mode v7.97.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.95.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.97.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.95.0
1
+ 7.97.0
@@ -162,7 +162,14 @@ hook_pre_file_edit() {
162
162
  [[ -z "$file_path" ]] && return 0
163
163
  local snap_base
164
164
  snap_base=$(_migration_snapshot_dir)
165
- _heal_snapshot_save "$snap_base" "$file_path"
165
+ # Enforce the pairing contract: if the snapshot cannot be captured, BLOCK the
166
+ # edit. Proceeding would leave post_file_edit's block_and_rollback path with
167
+ # no snapshot to restore -- a test failure would then leave the broken edit
168
+ # in place (silent revert failure). Fail loud/closed here instead.
169
+ if ! _heal_snapshot_save "$snap_base" "$file_path"; then
170
+ echo "HOOK_BLOCKED: could not capture a pre-edit snapshot for ${file_path}; refusing to edit without a revert path. Check that ${snap_base}/snapshots is writable."
171
+ return 1
172
+ fi
166
173
  return 0
167
174
  }
168
175
 
@@ -348,6 +355,19 @@ hook_pre_healing_modify() {
348
355
  [[ "${LOKI_HEAL_MODE:-false}" != "true" ]] && return 0
349
356
  [[ -z "$file_path" ]] && return 0
350
357
 
358
+ # Fail CLOSED if the friction map is absent. Without it the friction safety
359
+ # gate below cannot run, so deleting (or never producing) friction-map.json
360
+ # would otherwise BYPASS protection and let unclassified institutional
361
+ # knowledge be destroyed -- and _heal_snapshot_save would still run, masking
362
+ # the gap. The map is produced by the archaeology phase (the phase gate
363
+ # blocks archaeology -> stabilize until friction-map.json has >=1 entry), so
364
+ # any modify in stabilize/isolate/modernize legitimately has one. Block
365
+ # before any friction check or snapshot proceeds.
366
+ if [[ ! -f "$heal_dir/friction-map.json" ]]; then
367
+ echo "HOOK_BLOCKED: friction-map.json missing at ${heal_dir}/friction-map.json. Run the archaeology phase first (loki heal <path> --phase archaeology) to catalog friction before modifying files in healing mode."
368
+ return 1
369
+ fi
370
+
351
371
  # Check if file has friction points
352
372
  if [[ -f "$heal_dir/friction-map.json" ]]; then
353
373
  local blocked
@@ -405,7 +425,15 @@ print('OK')
405
425
  # Capture a pre-edit snapshot so post_healing_modify can revert ONLY the
406
426
  # healing edit on test failure (not unrelated uncommitted changes, and not
407
427
  # via git checkout which discards everything). Keyed by file path.
408
- _heal_snapshot_save "$heal_dir" "$file_path"
428
+ #
429
+ # Enforce the pairing contract: if the snapshot cannot be captured, BLOCK the
430
+ # edit. Proceeding would leave the edit with no revert path -- a later test
431
+ # failure would then leave the broken edit in place while honestly reporting
432
+ # "no snapshot" (silent revert failure). Fail loud/closed here instead.
433
+ if ! _heal_snapshot_save "$heal_dir" "$file_path"; then
434
+ echo "HOOK_BLOCKED: could not capture a pre-edit snapshot for ${file_path}; refusing to modify it without a revert path. Check that ${heal_dir}/snapshots is writable."
435
+ return 1
436
+ fi
409
437
 
410
438
  return 0
411
439
  }
@@ -425,27 +453,63 @@ _heal_snapshot_path() {
425
453
  # healing edit will CREATE it), write a sentinel marker instead so the revert
426
454
  # path knows to remove the file rather than restore content.
427
455
  #
428
- # Pairing contract: hook_pre_healing_modify (which calls this) MUST run for a
429
- # file before hook_post_healing_modify reverts it. The snapshot is refreshed on
456
+ # Pairing contract (ENFORCED, fail-closed): hook_pre_healing_modify (which calls
457
+ # this) MUST run for a file before hook_post_healing_modify reverts it, AND that
458
+ # pre call MUST leave behind a revertable snapshot. The snapshot is refreshed on
430
459
  # every pre call, so a post without a matching fresh pre could restore a stale
431
460
  # blob. On the success path the snapshot is intentionally left in place; the
432
461
  # next pre overwrites it.
462
+ #
463
+ # Returns 0 ONLY when a revertable snapshot demonstrably exists on disk after
464
+ # this call: exactly one of {content snapshot, ".absent" marker}. Returns 1 on
465
+ # ANY failure (cannot create the snapshot dir, cp/sentinel write failed, the
466
+ # wrong marker survived, or both/neither markers exist). A 1 return MUST block
467
+ # the edit at the caller -- otherwise the edit proceeds with no revert path and a
468
+ # later test failure leaves the broken edit in place (the silent-revert-failure
469
+ # this guard exists to prevent). NEVER return 0 on a failure path.
433
470
  _heal_snapshot_save() {
434
471
  local heal_dir="$1"
435
472
  local file_path="$2"
473
+ # Empty file_path: nothing to snapshot. Callers treat empty file_path as a
474
+ # no-op (return 0 before reaching here in the hooks); not a contract breach.
436
475
  [[ -z "$file_path" ]] && return 0
437
476
  local snap_dir="$heal_dir/snapshots"
438
- mkdir -p "$snap_dir" 2>/dev/null || return 0
477
+ if ! mkdir -p "$snap_dir" 2>/dev/null || [[ ! -d "$snap_dir" ]]; then
478
+ return 1
479
+ fi
439
480
  local snap
440
481
  snap=$(_heal_snapshot_path "$heal_dir" "$file_path")
441
482
  if [[ -f "$file_path" ]]; then
442
- cp "$file_path" "$snap" 2>/dev/null || return 0
443
- rm -f "$snap.absent" 2>/dev/null || true
483
+ # Capture content, then drop any stale absent-marker so EXACTLY the
484
+ # content snapshot survives. Fail closed if either step fails.
485
+ if ! cp "$file_path" "$snap" 2>/dev/null; then
486
+ return 1
487
+ fi
488
+ if ! rm -f "$snap.absent" 2>/dev/null; then
489
+ return 1
490
+ fi
491
+ # Verify exactly the content snapshot exists and the absent-marker is
492
+ # gone. A lingering marker is a contract violation (restore checks the
493
+ # content snapshot first, but the inconsistency must not be tolerated).
494
+ if [[ ! -f "$snap" || -f "$snap.absent" ]]; then
495
+ return 1
496
+ fi
444
497
  else
445
498
  # File does not exist pre-edit: record an "absent" marker, drop any
446
- # stale content snapshot.
447
- rm -f "$snap" 2>/dev/null || true
448
- : > "$snap.absent" 2>/dev/null || true
499
+ # stale content snapshot. CRITICAL: if the stale content snapshot is not
500
+ # removed, restore checks the content snapshot FIRST and would restore
501
+ # content for a file that should have been REMOVED. Fail closed if the
502
+ # stale snapshot cannot be cleared or the marker cannot be written.
503
+ if ! rm -f "$snap" 2>/dev/null; then
504
+ return 1
505
+ fi
506
+ if ! : > "$snap.absent" 2>/dev/null; then
507
+ return 1
508
+ fi
509
+ # Verify exactly the absent-marker exists and no content snapshot does.
510
+ if [[ ! -f "$snap.absent" || -f "$snap" ]]; then
511
+ return 1
512
+ fi
449
513
  fi
450
514
  return 0
451
515
  }
package/autonomy/run.sh CHANGED
@@ -3801,6 +3801,82 @@ effective_session_cap() {
3801
3801
  return 0
3802
3802
  }
3803
3803
 
3804
+ # Auto-flag parity for parallel worktree Claude sessions (wave-5 fix).
3805
+ # The main RARV loop applies adaptive cost/resilience flags to its claude
3806
+ # invocation (run.sh:16007-16030 effort/max-budget/fallback) plus an MCP
3807
+ # bundle, but the parallel worktree spawn shipped a bare invocation that missed
3808
+ # them entirely. This populates the global _LOKI_WT_AUTO_FLAGS array with ONLY
3809
+ # the flags that are safe on the plain (non stream-json) worktree invocation:
3810
+ # --effort adaptive reasoning depth (dev tier)
3811
+ # --max-budget-usd per-call hard backstop
3812
+ # --fallback-model resilience to model overload/unavailability
3813
+ # --mcp-config (+ --strict-mcp-config) the same MCP bundle the Bun
3814
+ # route emits; a SUPERSET of the bash main loop, which does
3815
+ # NOT emit --mcp-config (see ledger run.sh:15990-15996).
3816
+ # Worktree dev streams benefit from the bundled MCP servers.
3817
+ # We deliberately do NOT replicate the stream-json-coupled flags
3818
+ # (--include-hook-events, --include-partial-messages) nor --output-format /
3819
+ # --session-id: this invocation logs free-form text, not parsed stream-json, so
3820
+ # those would be invalid or unwanted here. Each flag is gated on CLI support +
3821
+ # its opt-out env var, matching the main loop. Extracted from spawn_worktree_session
3822
+ # so it is unit-testable (tests/test-worktree-auto-flags.sh).
3823
+ _loki_build_worktree_claude_flags() {
3824
+ _LOKI_WT_AUTO_FLAGS=()
3825
+ # Non-claude providers never receive these flags.
3826
+ if [ "${PROVIDER_NAME:-claude}" != "claude" ]; then
3827
+ return 0
3828
+ fi
3829
+ # Dev-tier model param drives the fallback derivation (worktree streams are
3830
+ # development work). Resolve via the provider helper when available.
3831
+ local _loki_wt_primary=""
3832
+ if type provider_get_tier_param >/dev/null 2>&1; then
3833
+ _loki_wt_primary="$(provider_get_tier_param development 2>/dev/null || true)"
3834
+ fi
3835
+ if [ "${LOKI_AUTO_EFFORT:-on}" != "off" ] \
3836
+ && type loki_effort_for_tier >/dev/null 2>&1 \
3837
+ && type loki_claude_flag_supported >/dev/null 2>&1 \
3838
+ && loki_claude_flag_supported "--effort"; then
3839
+ local _loki_wt_effort
3840
+ _loki_wt_effort="$(loki_effort_for_tier development "${DETECTED_COMPLEXITY:-${LOKI_COMPLEXITY:-standard}}")"
3841
+ [ -n "$_loki_wt_effort" ] && _LOKI_WT_AUTO_FLAGS+=("--effort" "$_loki_wt_effort")
3842
+ fi
3843
+ if [ "${LOKI_AUTO_BUDGET:-on}" != "off" ] \
3844
+ && type loki_remaining_budget >/dev/null 2>&1 \
3845
+ && type loki_claude_flag_supported >/dev/null 2>&1 \
3846
+ && loki_claude_flag_supported "--max-budget-usd"; then
3847
+ local _loki_wt_rem
3848
+ _loki_wt_rem="$(loki_remaining_budget)"
3849
+ [ -n "$_loki_wt_rem" ] && _LOKI_WT_AUTO_FLAGS+=("--max-budget-usd" "$_loki_wt_rem")
3850
+ fi
3851
+ if [ "${LOKI_AUTO_FALLBACK:-on}" != "off" ] \
3852
+ && [ -n "$_loki_wt_primary" ] \
3853
+ && type loki_fallback_for_primary >/dev/null 2>&1 \
3854
+ && type loki_claude_flag_supported >/dev/null 2>&1 \
3855
+ && loki_claude_flag_supported "--fallback-model"; then
3856
+ local _loki_wt_fb
3857
+ _loki_wt_fb="$(loki_fallback_for_primary "$_loki_wt_primary")"
3858
+ [ -n "$_loki_wt_fb" ] && _LOKI_WT_AUTO_FLAGS+=("--fallback-model" "$_loki_wt_fb")
3859
+ fi
3860
+ if type loki_mcp_config_argv >/dev/null 2>&1 \
3861
+ && type loki_claude_flag_supported >/dev/null 2>&1 \
3862
+ && loki_claude_flag_supported "--mcp-config"; then
3863
+ local _loki_wt_mcp
3864
+ if _loki_wt_mcp="$(loki_mcp_config_argv)" && [ -n "$_loki_wt_mcp" ]; then
3865
+ _LOKI_WT_AUTO_FLAGS+=("--mcp-config")
3866
+ local _loki_wt_mcp_path
3867
+ for _loki_wt_mcp_path in $_loki_wt_mcp; do
3868
+ _LOKI_WT_AUTO_FLAGS+=("$_loki_wt_mcp_path")
3869
+ done
3870
+ # --strict-mcp-config only alongside a real bundle, never bare.
3871
+ if [ "${LOKI_STRICT_MCP:-1}" != "0" ] \
3872
+ && loki_claude_flag_supported "--strict-mcp-config"; then
3873
+ _LOKI_WT_AUTO_FLAGS+=("--strict-mcp-config")
3874
+ fi
3875
+ fi
3876
+ fi
3877
+ return 0
3878
+ }
3879
+
3804
3880
  # Spawn a Claude session in a worktree
3805
3881
  spawn_worktree_session() {
3806
3882
  local stream_name="$1"
@@ -3843,6 +3919,11 @@ spawn_worktree_session() {
3843
3919
 
3844
3920
  log_step "Spawning ${PROVIDER_DISPLAY_NAME:-Claude} session: $stream_name"
3845
3921
 
3922
+ # Build the worktree auto-flag set (effort/max-budget/fallback/mcp-config)
3923
+ # BEFORE the ( subshell so it is inherited (a `bash -c` would not). Populates
3924
+ # the global _LOKI_WT_AUTO_FLAGS array. See _loki_build_worktree_claude_flags.
3925
+ _loki_build_worktree_claude_flags
3926
+
3846
3927
  (
3847
3928
  cd "$worktree_path" || exit 1
3848
3929
  _wt_exit=0
@@ -3858,12 +3939,17 @@ spawn_worktree_session() {
3858
3939
  if type loki_caveman_activate_env >/dev/null 2>&1; then
3859
3940
  _loki_wt_cm="$(loki_caveman_activate_env)"
3860
3941
  fi
3942
+ # Expand the auto-flag array with the bash-3.2 empty-array guard
3943
+ # (${arr[@]+...}) so a bare "${arr[@]}" under set -u does not
3944
+ # abort with "unbound variable" when no flags were collected.
3861
3945
  if [ -n "$_loki_wt_cm" ]; then
3862
3946
  CAVEMAN_DEFAULT_MODE="$_loki_wt_cm" claude --dangerously-skip-permissions \
3947
+ "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
3863
3948
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
3864
3949
  >> "$log_file" 2>&1 || _wt_exit=$?
3865
3950
  else
3866
3951
  claude --dangerously-skip-permissions \
3952
+ "${_LOKI_WT_AUTO_FLAGS[@]+"${_LOKI_WT_AUTO_FLAGS[@]}"}" \
3867
3953
  -p "Loki Mode: $task_prompt. Read .loki/CONTINUITY.md for context." \
3868
3954
  >> "$log_file" 2>&1 || _wt_exit=$?
3869
3955
  fi
@@ -17773,7 +17859,24 @@ check_human_intervention() {
17773
17859
  if type ensure_completion_test_evidence &>/dev/null; then
17774
17860
  ensure_completion_test_evidence || true
17775
17861
  fi
17776
- if type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
17862
+ # TRUST MOAT (fail-CLOSED, parity with the default route ~17153): each gate
17863
+ # arm below is `type fn && ! fn`, so a missing gate fn (council library
17864
+ # failed to source) silently skips that gate and the chain can reach
17865
+ # council_vote and force-approve completion UNGATED -- a fake-green. Probe
17866
+ # the core gate fns first; if any is missing, refuse the force-review
17867
+ # approval and fall through to continue iterating. No-op on healthy loads.
17868
+ _frgate_loadable=1
17869
+ for _frgate_fn in council_checklist_gate council_evidence_gate council_heldout_gate council_assumption_ledger_gate; do
17870
+ if ! type "$_frgate_fn" &>/dev/null; then
17871
+ log_error "Council force-review: gate unavailable: ${_frgate_fn} (council library incomplete)."
17872
+ _frgate_loadable=0
17873
+ fi
17874
+ done
17875
+ if [ "$_frgate_loadable" -eq 0 ]; then
17876
+ log_error "Council force-review: required gates missing; refusing force approval (fail-closed)."
17877
+ unset _frgate_loadable _frgate_fn
17878
+ elif type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
17879
+ unset _frgate_loadable _frgate_fn
17777
17880
  log_info "Council force-review: blocked by checklist hard gate"
17778
17881
  elif type council_evidence_gate &>/dev/null && ! _evidence_gate_and_surface; then
17779
17882
  log_info "Council force-review: blocked by evidence hard gate"
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.95.0"
10
+ __version__ = "7.97.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: