loki-mode 7.68.0 → 7.70.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.68.0
6
+ # Loki Mode v7.70.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -406,4 +406,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
406
406
 
407
407
  ---
408
408
 
409
- **v7.68.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.70.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.68.0
1
+ 7.70.0
@@ -1799,8 +1799,14 @@ app_runner_watchdog() {
1799
1799
  log_info "App Runner: auto-restarting in ${backoff}s..."
1800
1800
  sleep "$backoff"
1801
1801
 
1802
- # Clear PID and restart
1802
+ # Clear PID and restart. Remove the identity token alongside app.pid (LOW-3):
1803
+ # the token belongs to the now-dead process, and if the upcoming start fails
1804
+ # (e.g. the old port is still held) no new token is written, so a leftover
1805
+ # token would outlive its pid and could mislead a later _app_runner_pid_is_ours
1806
+ # check. Every site that removes app.pid removes app.token (cf. stop:1443,
1807
+ # watchdog crash-limit:1789).
1803
1808
  rm -f "$_APP_RUNNER_DIR/app.pid"
1809
+ rm -f "$_APP_RUNNER_DIR/app.token"
1804
1810
  _APP_RUNNER_PID=""
1805
1811
  app_runner_start || log_warn "App Runner: auto-restart failed"
1806
1812
  }
@@ -1829,8 +1835,14 @@ app_runner_cleanup() {
1829
1835
  fi
1830
1836
  fi
1831
1837
 
1832
- # Remove PID file
1838
+ # Remove PID file and its paired identity token (LOW-3). app_runner_stop
1839
+ # above removes both when a pid is present, but it early-returns without
1840
+ # touching either when called with no pid (the post-failed-restart leftover
1841
+ # state: token present, app.pid already gone). Removing the token here too
1842
+ # guarantees no stale token survives session end regardless of how cleanup
1843
+ # was reached.
1833
1844
  rm -f "$_APP_RUNNER_DIR/app.pid"
1845
+ rm -f "$_APP_RUNNER_DIR/app.token"
1834
1846
 
1835
1847
  # Update state
1836
1848
  _write_app_state "stopped"
@@ -1971,6 +1971,9 @@ council_member_review() {
1971
1971
  fi
1972
1972
 
1973
1973
  local verdict=""
1974
+ # bash-F4: exit code of the provider subcall (0 when no subcall ran, i.e.
1975
+ # no-provider degraded mode). 124/137/143 => timeout => conservative REJECT.
1976
+ local _provider_rc=0
1974
1977
  local role_instruction=""
1975
1978
  case "$role" in
1976
1979
  requirements_verifier)
@@ -2059,34 +2062,62 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
2059
2062
  # CAVEMAN_DEFAULT_MODE=off suppression is preserved (see above).
2060
2063
  # bash-F3: timeout-guard the provider subcall so a hung CLI can
2061
2064
  # not stall the whole council. Default 600s matches the Bun route
2062
- # (council.ts LOKI_COUNCIL_TIMEOUT_MS=600000). A timeout yields
2063
- # empty output, which the [ -z "$verdict" ] fallback below turns
2064
- # into a conservative heuristic review.
2065
+ # (council.ts LOKI_COUNCIL_TIMEOUT_MS=600000).
2066
+ # bash-F4 (WAVE10 SAFE-DEFAULT): capture the subcall exit code so a
2067
+ # timeout (124) is NOT silently routed into council_heuristic_review.
2068
+ # The heuristic fallback defaults to APPROVE on benign evidence, so
2069
+ # a full provider timeout used to let a 2-of-3 heuristic APPROVE mark
2070
+ # the project COMPLETE (force-review path) -- the opposite of the
2071
+ # required safe default. pipefail (run.sh:172) makes the assignment's
2072
+ # $? equal timeout's 124 when the CLI is killed. _provider_rc is read
2073
+ # after the case to force a conservative REJECT on timeout.
2065
2074
  verdict=$(echo "$prompt" | timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" env CAVEMAN_DEFAULT_MODE=off claude "${_cm_argv[@]}" -p 2>/dev/null)
2075
+ _provider_rc=$?
2066
2076
  fi
2067
2077
  ;;
2068
2078
  codex)
2069
2079
  if command -v codex &>/dev/null; then
2070
2080
  verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" codex exec --sandbox workspace-write "$prompt" 2>/dev/null)
2081
+ _provider_rc=$?
2071
2082
  fi
2072
2083
  ;;
2073
2084
  gemini)
2074
2085
  if command -v gemini &>/dev/null; then
2075
2086
  verdict=$(echo "$prompt" | timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" gemini 2>/dev/null)
2087
+ _provider_rc=$?
2076
2088
  fi
2077
2089
  ;;
2078
2090
  cline)
2079
2091
  if command -v cline &>/dev/null; then
2080
2092
  verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" cline -y "$prompt" 2>/dev/null)
2093
+ _provider_rc=$?
2081
2094
  fi
2082
2095
  ;;
2083
2096
  aider)
2084
2097
  if command -v aider &>/dev/null; then
2085
2098
  verdict=$(timeout "${LOKI_COUNCIL_REVIEW_TIMEOUT:-600}" aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null)
2099
+ _provider_rc=$?
2086
2100
  fi
2087
2101
  ;;
2088
2102
  esac
2089
2103
 
2104
+ # bash-F4 (WAVE10 SAFE-DEFAULT): a provider timeout (124, incl. 128+SIGTERM
2105
+ # variants 137/143 if a wrapper kills it) must NEVER fall through to the
2106
+ # APPROVE-leaning heuristic review. A reviewer whose CLI hung produced NO
2107
+ # judgement, so the only safe verdict is REJECT (conservative re-iterate).
2108
+ # Note: when no provider CLI is installed, the command -v guard above means
2109
+ # the subcall never runs and _provider_rc stays 0, so legitimate no-provider
2110
+ # degraded mode still reaches the heuristic fallback unchanged.
2111
+ if [ "$_provider_rc" -eq 124 ] || [ "$_provider_rc" -eq 137 ] || [ "$_provider_rc" -eq 143 ]; then
2112
+ # run.sh's log_warn writes to STDOUT (see bash-F2 note); council_member_review's
2113
+ # stdout is captured as the verdict, so redirect to stderr to keep the
2114
+ # captured verdict clean (the log line carries no VOTE: token, so the
2115
+ # parse stays REJECT regardless, but this avoids polluting the capture).
2116
+ log_warn "Council member $member_id ($role): provider review timed out (rc=$_provider_rc); defaulting to REJECT" >&2
2117
+ verdict="VOTE:REJECT
2118
+ REASON: Provider review timed out (rc=$_provider_rc); no judgement produced, defaulting to conservative REJECT"
2119
+ fi
2120
+
2090
2121
  # Fallback: if no AI provider available, use heuristic-based review
2091
2122
  if [ -z "$verdict" ]; then
2092
2123
  verdict=$(council_heuristic_review "$role" "$evidence_file")
@@ -393,7 +393,7 @@ for friction in data.get('frictions', []):
393
393
  print(f'BLOCKED (strict): Friction {friction.get(\"id\", \"?\")} in {loc} - strict mode requires explicit approval')
394
394
  sys.exit(0)
395
395
  print('OK')
396
- " "$file_path" "$strict" "$heal_dir/friction-map.json" 2>/dev/null || echo "OK")
396
+ " "$file_path" "$strict" "$heal_dir/friction-map.json" 2>/dev/null || echo "BLOCKED: friction-map check failed (corrupt/unreadable friction-map.json or python3 unavailable) -- failing closed")
397
397
 
398
398
  if [[ "$blocked" == BLOCKED* ]]; then
399
399
  echo "HOOK_BLOCKED: $blocked"
package/autonomy/loki CHANGED
@@ -7358,8 +7358,8 @@ cmd_config() {
7358
7358
  echo " issue.provider Default issue provider: github, gitlab, jira, azure_devops"
7359
7359
  echo " blind_validation Blind validation mode: true, false (default: true)"
7360
7360
  echo " adversarial_testing Adversarial testing: true, false (default: true)"
7361
- echo " spawn_timeout Provider spawn timeout in seconds (default: 120)"
7362
- echo " spawn_retries Provider spawn retry count (default: 2)"
7361
+ echo " spawn_timeout [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
7362
+ echo " spawn_retries [DEPRECATED] No effect since WAVE9 (no consumer); accepted for back-compat"
7363
7363
  echo " notify.slack Slack webhook URL"
7364
7364
  echo " notify.discord Discord webhook URL"
7365
7365
  echo " budget Cost budget limit in USD"
@@ -7437,6 +7437,7 @@ cmd_config_set() {
7437
7437
  if ! echo "$value" | grep -qE '^[0-9]+$'; then
7438
7438
  echo -e "${RED}Invalid $key: $value (expected: integer)${NC}"; return 1
7439
7439
  fi
7440
+ echo -e "${YELLOW}Note: '$key' is deprecated and has no effect since WAVE9 (no consumer in the runtime). Accepted for back-compat only.${NC}" >&2
7440
7441
  ;;
7441
7442
  budget)
7442
7443
  if ! echo "$value" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
@@ -7518,8 +7519,8 @@ SET_CONFIG
7518
7519
  provider) export LOKI_PROVIDER="$value" ;;
7519
7520
  blind_validation) export LOKI_BLIND_VALIDATION="$value" ;;
7520
7521
  adversarial_testing) export LOKI_ADVERSARIAL_TESTING="$value" ;;
7521
- spawn_timeout) export LOKI_SPAWN_TIMEOUT="$value" ;;
7522
- spawn_retries) export LOKI_SPAWN_RETRIES="$value" ;;
7522
+ # spawn_timeout / spawn_retries: deprecated, no runtime consumer (WAVE9);
7523
+ # intentionally not exported.
7523
7524
  budget) export LOKI_BUDGET_LIMIT="$value" ;;
7524
7525
  esac
7525
7526
  }
@@ -7626,8 +7627,8 @@ cmd_config_show() {
7626
7627
  echo " maxTier: ${LOKI_MAX_TIER:-(unlimited)}"
7627
7628
  echo " blind_validation: ${LOKI_BLIND_VALIDATION:-true}"
7628
7629
  echo " adversarial_testing: ${LOKI_ADVERSARIAL_TESTING:-true}"
7629
- echo " spawn_timeout: ${LOKI_SPAWN_TIMEOUT:-120}s"
7630
- echo " spawn_retries: ${LOKI_SPAWN_RETRIES:-2}"
7630
+ echo " spawn_timeout: (deprecated, no effect)"
7631
+ echo " spawn_retries: (deprecated, no effect)"
7631
7632
  echo " issue_provider: ${LOKI_ISSUE_PROVIDER:-(auto-detect)}"
7632
7633
  echo " budget: ${LOKI_BUDGET_LIMIT:-(no limit)}"
7633
7634
  echo ""
@@ -13012,47 +13013,60 @@ else:
13012
13013
  return 1
13013
13014
  fi
13014
13015
 
13015
- # Fire lifecycle hooks and record state
13016
- PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
13017
- import json, sys
13018
- sys.path.insert(0, '${SKILL_DIR:-.}')
13016
+ # Fire lifecycle hooks and record state.
13017
+ # WAVE10: pass cluster_id / template_name / template_file / SKILL_DIR
13018
+ # via os.environ instead of interpolating into the python source.
13019
+ # A --cluster-id containing a single quote (e.g. "o'brien") made the
13020
+ # heredoc body a SyntaxError, skipping all state recording while the
13021
+ # green "Cluster: ..." line still printed (silent false success).
13022
+ _LOKI_SKILL_DIR="${SKILL_DIR:-.}" \
13023
+ _LOKI_TEMPLATE_FILE="${template_file}" \
13024
+ _LOKI_CLUSTER_ID="${cluster_id}" \
13025
+ _LOKI_TEMPLATE_NAME="${template_name}" \
13026
+ _LOKI_DO_RESUME="${do_resume}" \
13027
+ PYTHONPATH="${SKILL_DIR:-.}" python3 -c '
13028
+ import json, sys, os
13029
+ skill_dir = os.environ["_LOKI_SKILL_DIR"]
13030
+ sys.path.insert(0, skill_dir)
13019
13031
  from swarm.patterns import ClusterLifecycleHooks
13020
13032
  from state.sqlite_backend import SqliteStateBackend
13021
13033
 
13022
13034
  # Load template hooks config
13023
- with open('${template_file}') as f:
13035
+ with open(os.environ["_LOKI_TEMPLATE_FILE"]) as f:
13024
13036
  tpl = json.load(f)
13025
13037
 
13026
- hooks = ClusterLifecycleHooks(tpl.get('hooks', {}))
13038
+ hooks = ClusterLifecycleHooks(tpl.get("hooks", {}))
13027
13039
  db = SqliteStateBackend()
13028
13040
 
13029
- cluster_id = '${cluster_id}'
13030
- resume = '${do_resume}' == 'true'
13041
+ cluster_id = os.environ["_LOKI_CLUSTER_ID"]
13042
+ template_name = os.environ["_LOKI_TEMPLATE_NAME"]
13043
+ resume = os.environ["_LOKI_DO_RESUME"] == "true"
13031
13044
 
13032
13045
  if resume:
13033
- events = db.query_events(event_type='cluster_state', migration_id=cluster_id, limit=1)
13046
+ events = db.query_events(event_type="cluster_state", migration_id=cluster_id, limit=1)
13034
13047
  if events:
13035
- print(f'Resuming cluster {cluster_id} from last checkpoint')
13048
+ print(f"Resuming cluster {cluster_id} from last checkpoint")
13036
13049
  else:
13037
- print(f'No previous state found for {cluster_id}. Starting fresh.')
13050
+ print(f"No previous state found for {cluster_id}. Starting fresh.")
13038
13051
 
13039
13052
  # Fire pre_run hooks
13040
- results = hooks.fire('pre_run', {'cluster_id': cluster_id, 'template': '${template_name}'})
13053
+ results = hooks.fire("pre_run", {"cluster_id": cluster_id, "template": template_name})
13041
13054
  for r in results:
13042
- if not r['success']:
13043
- print(f'Pre-run hook failed: {r[\"output\"]}')
13055
+ if not r["success"]:
13056
+ print("Pre-run hook failed: " + str(r.get("output", "")))
13044
13057
 
13045
13058
  # Record cluster start
13046
- db.record_event('cluster_start', {
13047
- 'cluster_id': cluster_id,
13048
- 'template': '${template_name}',
13049
- 'agents': len(tpl.get('agents', [])),
13050
- 'resume': resume,
13059
+ db.record_event("cluster_start", {
13060
+ "cluster_id": cluster_id,
13061
+ "template": template_name,
13062
+ "agents": len(tpl.get("agents", [])),
13063
+ "resume": resume,
13051
13064
  }, migration_id=cluster_id)
13052
13065
 
13053
- print(f'Cluster {cluster_id} initialized with {len(tpl.get(\"agents\", []))} agents')
13054
- print('Template validated. Lifecycle hooks active.')
13055
- " 2>&1
13066
+ _n_agents = len(tpl.get("agents", []))
13067
+ print(f"Cluster {cluster_id} initialized with {_n_agents} agents")
13068
+ print("Template validated. Lifecycle hooks active.")
13069
+ ' 2>&1
13056
13070
 
13057
13071
  echo -e "${GREEN}Cluster: $cluster_id${NC}"
13058
13072
  echo -e "Template: $template_name"
@@ -23325,10 +23339,10 @@ except: pass
23325
23339
  output=$(cat <<ENDJSON
23326
23340
  {
23327
23341
  "project": {
23328
- "name": "$project_name",
23342
+ "name": $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\""),
23329
23343
  "description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
23330
- "version": "$project_version",
23331
- "path": "$target_path"
23344
+ "version": $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\""),
23345
+ "path": $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
23332
23346
  },
23333
23347
  "languages": "$(echo $languages | sed 's/ */ /g')",
23334
23348
  "frameworks": "$(echo $frameworks | sed 's/ */ /g')",
@@ -23342,9 +23356,9 @@ except: pass
23342
23356
  "docs": $doc_count
23343
23357
  },
23344
23358
  "commands": {
23345
- "build": "$build_cmd",
23346
- "run": "$run_cmd",
23347
- "test": "$test_cmd"
23359
+ "build": $(_VAL="$build_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$build_cmd\""),
23360
+ "run": $(_VAL="$run_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$run_cmd\""),
23361
+ "test": $(_VAL="$test_cmd" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$test_cmd\"")
23348
23362
  },
23349
23363
  "depth": $depth
23350
23364
  }
@@ -23354,10 +23368,10 @@ ENDJSON
23354
23368
  # YAML output
23355
23369
  output=$(cat <<ENDYAML
23356
23370
  project:
23357
- name: $project_name
23358
- description: "$project_description"
23359
- version: "$project_version"
23360
- path: $target_path
23371
+ name: $(_VAL="$project_name" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_name\"")
23372
+ description: $(_VAL="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_description\"")
23373
+ version: $(_VAL="$project_version" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$project_version\"")
23374
+ path: $(_VAL="$target_path" python3 -c "import json, os; print(json.dumps(os.environ.get('_VAL','')))" 2>/dev/null || echo "\"$target_path\"")
23361
23375
  languages: $languages
23362
23376
  frameworks: $frameworks
23363
23377
  build_system: $build_system
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.68.0"
10
+ __version__ = "7.70.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -122,32 +122,43 @@ class _RateLimiter:
122
122
  self._window = window_seconds
123
123
  self._max_keys = max_keys
124
124
  self._calls: dict[str, list[float]] = defaultdict(list)
125
+ # Sync route handlers (plain `def`) run in Starlette's threadpool, so
126
+ # check() can be entered by several threads at once against this one
127
+ # shared instance. Without a guard, one thread iterating self._calls
128
+ # (the empty-key prune or the LRU-eviction sort) while another inserts
129
+ # or deletes a key raises "dictionary changed size during iteration",
130
+ # which surfaces to the caller as a 500 on a trivial rate-limit guard.
131
+ # The lock is held only around the in-memory bookkeeping (no I/O, no
132
+ # await), so contention is negligible and it cannot deadlock async
133
+ # callers that reach this via run_in_threadpool.
134
+ self._lock = threading.Lock()
125
135
 
126
136
  def check(self, key: str) -> bool:
127
137
  now = time.time()
128
- # Prune old timestamps for this key
129
- self._calls[key] = [t for t in self._calls[key] if now - t < self._window]
130
-
131
- # Remove keys with empty timestamp lists
132
- empty_keys = [k for k, v in self._calls.items() if not v]
133
- for k in empty_keys:
134
- del self._calls[k]
135
-
136
- # Evict least-recently-accessed keys if max_keys exceeded
137
- if len(self._calls) > self._max_keys:
138
- # Sort by last-access time (most recent timestamp), evict least recent
139
- sorted_keys = sorted(
140
- self._calls.items(),
141
- key=lambda x: max(x[1]) if x[1] else 0
142
- )
143
- keys_to_remove = len(self._calls) - self._max_keys
144
- for k, _ in sorted_keys[:keys_to_remove]:
138
+ with self._lock:
139
+ # Prune old timestamps for this key
140
+ self._calls[key] = [t for t in self._calls[key] if now - t < self._window]
141
+
142
+ # Remove keys with empty timestamp lists
143
+ empty_keys = [k for k, v in self._calls.items() if not v]
144
+ for k in empty_keys:
145
145
  del self._calls[k]
146
146
 
147
- if len(self._calls[key]) >= self._max_calls:
148
- return False
149
- self._calls[key].append(now)
150
- return True
147
+ # Evict least-recently-accessed keys if max_keys exceeded
148
+ if len(self._calls) > self._max_keys:
149
+ # Sort by last-access time (most recent timestamp), evict least recent
150
+ sorted_keys = sorted(
151
+ self._calls.items(),
152
+ key=lambda x: max(x[1]) if x[1] else 0
153
+ )
154
+ keys_to_remove = len(self._calls) - self._max_keys
155
+ for k, _ in sorted_keys[:keys_to_remove]:
156
+ del self._calls[k]
157
+
158
+ if len(self._calls[key]) >= self._max_calls:
159
+ return False
160
+ self._calls[key].append(now)
161
+ return True
151
162
 
152
163
 
153
164
  _control_limiter = _RateLimiter(max_calls=10, window_seconds=60)
@@ -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.68.0
5
+ **Version:** v7.70.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.68.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.70.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)