loki-mode 7.67.0 → 7.69.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.67.0
6
+ # Loki Mode v7.69.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.67.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
409
+ **v7.69.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.67.0
1
+ 7.69.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")
package/autonomy/loki CHANGED
@@ -3071,7 +3071,7 @@ cmd_status_json() {
3071
3071
  local dashboard_port="${LOKI_DASHBOARD_PORT:-57374}"
3072
3072
  local env_provider="${LOKI_PROVIDER:-claude}"
3073
3073
 
3074
- python3 -c "
3074
+ if ! python3 -c "
3075
3075
  import json, os, sys, time
3076
3076
 
3077
3077
  skill_dir = sys.argv[1]
@@ -3359,9 +3359,14 @@ if os.path.isfile(gate_count_file):
3359
3359
  result['phase1'] = phase1
3360
3360
 
3361
3361
  print(json.dumps(result, indent=2))
3362
- " "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"
3363
-
3364
- if [ $? -ne 0 ]; then
3362
+ " "$skill_dir" "$loki_dir" "$dashboard_port" "$env_provider"; then
3363
+ # WAVE8 loki-F2: under `set -euo pipefail` a bare python3 call aborts
3364
+ # the whole function on non-zero exit, so the old post-call
3365
+ # `if [ $? -ne 0 ]` fallback was DEAD code -- a missing/broken python3
3366
+ # crashed `loki status --json` instead of degrading. Guarding the call
3367
+ # with `if ! ...; then` catches the non-zero exit and emits the honest
3368
+ # error object. (Most malformed state files already degrade internally
3369
+ # via per-file try/except; this covers the interpreter-failure case.)
3365
3370
  echo '{"error": "Failed to generate JSON status. Ensure python3 is available."}' >&2
3366
3371
  return 1
3367
3372
  fi
@@ -7353,8 +7358,8 @@ cmd_config() {
7353
7358
  echo " issue.provider Default issue provider: github, gitlab, jira, azure_devops"
7354
7359
  echo " blind_validation Blind validation mode: true, false (default: true)"
7355
7360
  echo " adversarial_testing Adversarial testing: true, false (default: true)"
7356
- echo " spawn_timeout Provider spawn timeout in seconds (default: 120)"
7357
- 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"
7358
7363
  echo " notify.slack Slack webhook URL"
7359
7364
  echo " notify.discord Discord webhook URL"
7360
7365
  echo " budget Cost budget limit in USD"
@@ -7432,6 +7437,7 @@ cmd_config_set() {
7432
7437
  if ! echo "$value" | grep -qE '^[0-9]+$'; then
7433
7438
  echo -e "${RED}Invalid $key: $value (expected: integer)${NC}"; return 1
7434
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
7435
7441
  ;;
7436
7442
  budget)
7437
7443
  if ! echo "$value" | grep -qE '^[0-9]+(\.[0-9]+)?$'; then
@@ -7513,8 +7519,8 @@ SET_CONFIG
7513
7519
  provider) export LOKI_PROVIDER="$value" ;;
7514
7520
  blind_validation) export LOKI_BLIND_VALIDATION="$value" ;;
7515
7521
  adversarial_testing) export LOKI_ADVERSARIAL_TESTING="$value" ;;
7516
- spawn_timeout) export LOKI_SPAWN_TIMEOUT="$value" ;;
7517
- spawn_retries) export LOKI_SPAWN_RETRIES="$value" ;;
7522
+ # spawn_timeout / spawn_retries: deprecated, no runtime consumer (WAVE9);
7523
+ # intentionally not exported.
7518
7524
  budget) export LOKI_BUDGET_LIMIT="$value" ;;
7519
7525
  esac
7520
7526
  }
@@ -7621,8 +7627,8 @@ cmd_config_show() {
7621
7627
  echo " maxTier: ${LOKI_MAX_TIER:-(unlimited)}"
7622
7628
  echo " blind_validation: ${LOKI_BLIND_VALIDATION:-true}"
7623
7629
  echo " adversarial_testing: ${LOKI_ADVERSARIAL_TESTING:-true}"
7624
- echo " spawn_timeout: ${LOKI_SPAWN_TIMEOUT:-120}s"
7625
- echo " spawn_retries: ${LOKI_SPAWN_RETRIES:-2}"
7630
+ echo " spawn_timeout: (deprecated, no effect)"
7631
+ echo " spawn_retries: (deprecated, no effect)"
7626
7632
  echo " issue_provider: ${LOKI_ISSUE_PROVIDER:-(auto-detect)}"
7627
7633
  echo " budget: ${LOKI_BUDGET_LIMIT:-(no limit)}"
7628
7634
  echo ""
@@ -12485,18 +12491,27 @@ for f in data.get('frictions', []):
12485
12491
 
12486
12492
  # Initialize or update healing progress
12487
12493
  if [[ ! -f "$heal_dir/healing-progress.json" ]] || [ "$do_resume" != "true" ]; then
12494
+ # WAVE8 loki-est: pass codebase/phase/strict/out-path via env instead of
12495
+ # interpolating raw bash into the python source. A codebase path or phase
12496
+ # containing an apostrophe made this a SyntaxError; under `|| true` the
12497
+ # progress file was then silently never written (and the later
12498
+ # prev_phase read would fail), breaking healing resume.
12499
+ LOKI_HEAL_CODEBASE="$codebase_path" \
12500
+ LOKI_HEAL_PHASE_VAL="$phase" \
12501
+ LOKI_HEAL_STRICT_VAL="$strict" \
12502
+ LOKI_HEAL_OUT="$heal_dir/healing-progress.json" \
12488
12503
  python3 -c "
12489
- import json
12504
+ import json, os
12490
12505
  from datetime import datetime
12491
12506
  progress = {
12492
- 'codebase': '$codebase_path',
12507
+ 'codebase': os.environ.get('LOKI_HEAL_CODEBASE', ''),
12493
12508
  'started': datetime.now().isoformat(),
12494
- 'current_phase': '$phase',
12495
- 'strict_mode': $( [ "$strict" = "true" ] && echo "True" || echo "False" ),
12509
+ 'current_phase': os.environ.get('LOKI_HEAL_PHASE_VAL', ''),
12510
+ 'strict_mode': os.environ.get('LOKI_HEAL_STRICT_VAL', '') == 'true',
12496
12511
  'components': [],
12497
12512
  'overall_health': 0.0
12498
12513
  }
12499
- with open('$heal_dir/healing-progress.json', 'w') as f:
12514
+ with open(os.environ['LOKI_HEAL_OUT'], 'w') as f:
12500
12515
  json.dump(progress, f, indent=2)
12501
12516
  " || true
12502
12517
  fi
@@ -12504,7 +12519,7 @@ with open('$heal_dir/healing-progress.json', 'w') as f:
12504
12519
  # BUG-HEAL-004: Validate phase gate when resuming from a previous phase
12505
12520
  if [ "$do_resume" = "true" ] && [[ -f "$heal_dir/healing-progress.json" ]] && type hook_healing_phase_gate &>/dev/null; then
12506
12521
  local prev_phase
12507
- prev_phase=$(python3 -c "import json; print(json.load(open('$heal_dir/healing-progress.json')).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
12522
+ prev_phase=$(LOKI_HEAL_PROG="$heal_dir/healing-progress.json" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_HEAL_PROG'])).get('current_phase', 'archaeology'))" 2>/dev/null || echo "archaeology")
12508
12523
  if [[ "$prev_phase" != "$phase" ]]; then
12509
12524
  local gate_result
12510
12525
  if ! gate_result=$(hook_healing_phase_gate "$prev_phase" "$phase" 2>&1); then
@@ -12998,47 +13013,60 @@ else:
12998
13013
  return 1
12999
13014
  fi
13000
13015
 
13001
- # Fire lifecycle hooks and record state
13002
- PYTHONPATH="${SKILL_DIR:-.}" python3 -c "
13003
- import json, sys
13004
- 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)
13005
13031
  from swarm.patterns import ClusterLifecycleHooks
13006
13032
  from state.sqlite_backend import SqliteStateBackend
13007
13033
 
13008
13034
  # Load template hooks config
13009
- with open('${template_file}') as f:
13035
+ with open(os.environ["_LOKI_TEMPLATE_FILE"]) as f:
13010
13036
  tpl = json.load(f)
13011
13037
 
13012
- hooks = ClusterLifecycleHooks(tpl.get('hooks', {}))
13038
+ hooks = ClusterLifecycleHooks(tpl.get("hooks", {}))
13013
13039
  db = SqliteStateBackend()
13014
13040
 
13015
- cluster_id = '${cluster_id}'
13016
- 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"
13017
13044
 
13018
13045
  if resume:
13019
- 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)
13020
13047
  if events:
13021
- print(f'Resuming cluster {cluster_id} from last checkpoint')
13048
+ print(f"Resuming cluster {cluster_id} from last checkpoint")
13022
13049
  else:
13023
- print(f'No previous state found for {cluster_id}. Starting fresh.')
13050
+ print(f"No previous state found for {cluster_id}. Starting fresh.")
13024
13051
 
13025
13052
  # Fire pre_run hooks
13026
- 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})
13027
13054
  for r in results:
13028
- if not r['success']:
13029
- print(f'Pre-run hook failed: {r[\"output\"]}')
13055
+ if not r["success"]:
13056
+ print("Pre-run hook failed: " + str(r.get("output", "")))
13030
13057
 
13031
13058
  # Record cluster start
13032
- db.record_event('cluster_start', {
13033
- 'cluster_id': cluster_id,
13034
- 'template': '${template_name}',
13035
- 'agents': len(tpl.get('agents', [])),
13036
- '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,
13037
13064
  }, migration_id=cluster_id)
13038
13065
 
13039
- print(f'Cluster {cluster_id} initialized with {len(tpl.get(\"agents\", []))} agents')
13040
- print('Template validated. Lifecycle hooks active.')
13041
- " 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
13042
13070
 
13043
13071
  echo -e "${GREEN}Cluster: $cluster_id${NC}"
13044
13072
  echo -e "Template: $template_name"
@@ -22834,35 +22862,39 @@ cmd_onboard() {
22834
22862
  fi
22835
22863
  # Extract metadata from package.json
22836
22864
  if command -v python3 &>/dev/null; then
22865
+ # WAVE8 loki-est (same class as cmd_explain): pass the repo path via
22866
+ # env (os.environ) instead of interpolating into the python source.
22867
+ # A path with an apostrophe made each heredoc a SyntaxError, silently
22868
+ # dropping the package.json name/version/description under `|| true`.
22837
22869
  local pkg_name
22838
- pkg_name=$(python3 -c "
22839
- import json, sys
22870
+ pkg_name=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22871
+ import json, os
22840
22872
  try:
22841
- d = json.load(open('$target_path/package.json'))
22873
+ d = json.load(open(os.environ['LOKI_ONB_PKG']))
22842
22874
  print(d.get('name', ''))
22843
22875
  except: pass
22844
22876
  " 2>/dev/null || true)
22845
22877
  if [ -n "$pkg_name" ]; then
22846
22878
  project_name="$pkg_name"
22847
22879
  fi
22848
- project_description=$(python3 -c "
22849
- import json, sys
22880
+ project_description=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22881
+ import json, os
22850
22882
  try:
22851
- d = json.load(open('$target_path/package.json'))
22883
+ d = json.load(open(os.environ['LOKI_ONB_PKG']))
22852
22884
  print(d.get('description', ''))
22853
22885
  except: pass
22854
22886
  " 2>/dev/null || true)
22855
- project_version=$(python3 -c "
22856
- import json, sys
22887
+ project_version=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22888
+ import json, os
22857
22889
  try:
22858
- d = json.load(open('$target_path/package.json'))
22890
+ d = json.load(open(os.environ['LOKI_ONB_PKG']))
22859
22891
  print(d.get('version', ''))
22860
22892
  except: pass
22861
22893
  " 2>/dev/null || true)
22862
- entry_points=$(python3 -c "
22863
- import json, sys
22894
+ entry_points=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
22895
+ import json, os
22864
22896
  try:
22865
- d = json.load(open('$target_path/package.json'))
22897
+ d = json.load(open(os.environ['LOKI_ONB_PKG']))
22866
22898
  main = d.get('main', '')
22867
22899
  if main: print(main)
22868
22900
  scripts = d.get('scripts', {})
@@ -23242,10 +23274,11 @@ $imports"
23242
23274
  if [ -f "$target_path/package.json" ]; then
23243
23275
  if command -v python3 &>/dev/null; then
23244
23276
  local scripts_json
23245
- scripts_json=$(python3 -c "
23246
- import json
23277
+ # WAVE8 loki-est: env-passed path (see cmd_onboard metadata block).
23278
+ scripts_json=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
23279
+ import json, os
23247
23280
  try:
23248
- d = json.load(open('$target_path/package.json'))
23281
+ d = json.load(open(os.environ['LOKI_ONB_PKG']))
23249
23282
  s = d.get('scripts', {})
23250
23283
  for k in ['build', 'dev', 'start', 'test', 'lint', 'format', 'check', 'typecheck']:
23251
23284
  if k in s:
@@ -23306,10 +23339,10 @@ except: pass
23306
23339
  output=$(cat <<ENDJSON
23307
23340
  {
23308
23341
  "project": {
23309
- "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\""),
23310
23343
  "description": $(_DESC="$project_description" python3 -c "import json, os; print(json.dumps(os.environ.get('_DESC','')))" 2>/dev/null || echo "\"$project_description\""),
23311
- "version": "$project_version",
23312
- "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\"")
23313
23346
  },
23314
23347
  "languages": "$(echo $languages | sed 's/ */ /g')",
23315
23348
  "frameworks": "$(echo $frameworks | sed 's/ */ /g')",
@@ -23323,9 +23356,9 @@ except: pass
23323
23356
  "docs": $doc_count
23324
23357
  },
23325
23358
  "commands": {
23326
- "build": "$build_cmd",
23327
- "run": "$run_cmd",
23328
- "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\"")
23329
23362
  },
23330
23363
  "depth": $depth
23331
23364
  }
@@ -23335,10 +23368,10 @@ ENDJSON
23335
23368
  # YAML output
23336
23369
  output=$(cat <<ENDYAML
23337
23370
  project:
23338
- name: $project_name
23339
- description: "$project_description"
23340
- version: "$project_version"
23341
- 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\"")
23342
23375
  languages: $languages
23343
23376
  frameworks: $frameworks
23344
23377
  build_system: $build_system
@@ -23637,10 +23670,14 @@ cmd_explain() {
23637
23670
 
23638
23671
  if command -v python3 &>/dev/null; then
23639
23672
  local pkg_meta
23640
- pkg_meta=$(python3 -c "
23641
- import json
23673
+ # WAVE8 loki-est: pass the path via env (os.environ) instead of
23674
+ # interpolating into the python source -- a repo path containing an
23675
+ # apostrophe broke the string literal and dropped all package.json
23676
+ # metadata.
23677
+ pkg_meta=$(LOKI_EXP_PKG="$target_path/package.json" python3 -c "
23678
+ import json, os
23642
23679
  try:
23643
- d = json.load(open('$target_path/package.json'))
23680
+ d = json.load(open(os.environ['LOKI_EXP_PKG']))
23644
23681
  print(d.get('name', ''))
23645
23682
  print(d.get('description', ''))
23646
23683
  print(d.get('version', ''))
@@ -23869,17 +23906,67 @@ $devdeps_list"
23869
23906
 
23870
23907
  # --- JSON output ---
23871
23908
  if [ "$output_json" = true ]; then
23909
+ # WAVE8 loki-est: pass every value via the environment and read it with
23910
+ # os.environ instead of interpolating raw bash into the python source.
23911
+ # Interpolation broke on any apostrophe/quote/newline in a project name,
23912
+ # version, description, or path (e.g. a dir named `my'app`), silently
23913
+ # degrading real analysis to `{"error": "JSON generation failed"}`.
23914
+ # env-passing is injection-proof and keeps the same output shape.
23915
+ LOKI_EXP_NAME="$project_name" \
23916
+ LOKI_EXP_DESC="$project_description" \
23917
+ LOKI_EXP_VERSION="$project_version" \
23918
+ LOKI_EXP_PATH="$target_path" \
23919
+ LOKI_EXP_LANGUAGES="$languages" \
23920
+ LOKI_EXP_FRAMEWORKS="$frameworks" \
23921
+ LOKI_EXP_BUILD="$build_system" \
23922
+ LOKI_EXP_PKGMGR="$package_manager" \
23923
+ LOKI_EXP_TESTFW="$test_framework" \
23924
+ LOKI_EXP_CI="$ci_system" \
23925
+ LOKI_EXP_PATTERNS="$detected_patterns" \
23926
+ LOKI_EXP_TOTAL="$total_files" \
23927
+ LOKI_EXP_SRC="$src_count" \
23928
+ LOKI_EXP_TEST="$test_count" \
23929
+ LOKI_EXP_DOC="$doc_count" \
23930
+ LOKI_EXP_CONFIG="$config_count" \
23931
+ LOKI_EXP_BUILDCMD="$build_cmd" \
23932
+ LOKI_EXP_RUNCMD="$run_cmd" \
23933
+ LOKI_EXP_TESTCMD="$test_cmd" \
23934
+ LOKI_EXP_LINTCMD="$lint_cmd" \
23935
+ LOKI_EXP_ENTRY="$major_files" \
23936
+ LOKI_EXP_MONOREPO="$is_monorepo" \
23937
+ LOKI_EXP_DOCKER="$has_docker" \
23872
23938
  python3 -c "
23873
- import json
23939
+ import json, os
23940
+
23941
+ def _s(name):
23942
+ return os.environ.get(name, '')
23943
+
23944
+ def _list(name):
23945
+ v = _s(name).strip()
23946
+ return v.split() if v else []
23947
+
23948
+ def _opt(name):
23949
+ v = _s(name).strip()
23950
+ return v or None
23951
+
23952
+ def _int(name):
23953
+ try:
23954
+ return int(_s(name).strip())
23955
+ except (ValueError, TypeError):
23956
+ return 0
23957
+
23958
+ def _bool(name):
23959
+ return _s(name).strip() == 'true'
23960
+
23874
23961
  data = {
23875
- 'project': {'name': '$project_name', 'description': '''$(echo "$project_description" | sed "s/'/\\\\'/g")''', 'version': '$project_version', 'path': '$target_path'},
23876
- 'stack': {'languages': '${languages}'.split() if '${languages}'.strip() else [], 'frameworks': '${frameworks}'.split() if '${frameworks}'.strip() else [], 'build_system': '$build_system' or None, 'package_manager': '$package_manager' or None, 'test_framework': '${test_framework}'.split() if '${test_framework}'.strip() else [], 'ci': '${ci_system}'.strip() or None},
23877
- 'patterns': '${detected_patterns}'.split() if '${detected_patterns}'.strip() else [],
23878
- 'files': {'total': $total_files, 'source': $src_count, 'test': $test_count, 'docs': $doc_count, 'config': $config_count},
23879
- 'commands': {'build': '${build_cmd}' or None, 'run': '${run_cmd}' or None, 'test': '${test_cmd}' or None, 'lint': '${lint_cmd}' or None},
23880
- 'entry_points': '${major_files}'.split() if '${major_files}'.strip() else [],
23881
- 'monorepo': $( [ "$is_monorepo" = true ] && echo "True" || echo "False" ),
23882
- 'has_docker': $( [ "$has_docker" = true ] && echo "True" || echo "False" )
23962
+ 'project': {'name': _s('LOKI_EXP_NAME'), 'description': _s('LOKI_EXP_DESC'), 'version': _s('LOKI_EXP_VERSION'), 'path': _s('LOKI_EXP_PATH')},
23963
+ 'stack': {'languages': _list('LOKI_EXP_LANGUAGES'), 'frameworks': _list('LOKI_EXP_FRAMEWORKS'), 'build_system': _opt('LOKI_EXP_BUILD'), 'package_manager': _opt('LOKI_EXP_PKGMGR'), 'test_framework': _list('LOKI_EXP_TESTFW'), 'ci': _opt('LOKI_EXP_CI')},
23964
+ 'patterns': _list('LOKI_EXP_PATTERNS'),
23965
+ 'files': {'total': _int('LOKI_EXP_TOTAL'), 'source': _int('LOKI_EXP_SRC'), 'test': _int('LOKI_EXP_TEST'), 'docs': _int('LOKI_EXP_DOC'), 'config': _int('LOKI_EXP_CONFIG')},
23966
+ 'commands': {'build': _opt('LOKI_EXP_BUILDCMD'), 'run': _opt('LOKI_EXP_RUNCMD'), 'test': _opt('LOKI_EXP_TESTCMD'), 'lint': _opt('LOKI_EXP_LINTCMD')},
23967
+ 'entry_points': _list('LOKI_EXP_ENTRY'),
23968
+ 'monorepo': _bool('LOKI_EXP_MONOREPO'),
23969
+ 'has_docker': _bool('LOKI_EXP_DOCKER')
23883
23970
  }
23884
23971
  print(json.dumps(data, indent=2))
23885
23972
  " 2>/dev/null || echo '{"error": "JSON generation failed"}'
package/autonomy/run.sh CHANGED
@@ -1932,61 +1932,32 @@ get_provider_tier_param() {
1932
1932
  }
1933
1933
 
1934
1934
  #===============================================================================
1935
- # Provider Spawn Timeout (v6.0.0)
1936
- # Wraps provider invocation with timeout + retries.
1937
- # Default: 120s timeout, 2 retries.
1935
+ # Provider Spawn Timeout (removed WAVE9 / provider-F2)
1936
+ #
1937
+ # A former invoke_with_timeout() helper (v6.0.0) wrapped a command in
1938
+ # `timeout <s> "$@"` with a retry loop. It was never wired to the main
1939
+ # provider invocation and is intentionally not revived, for two reasons:
1940
+ #
1941
+ # 1. No safe generous default. The main provider call is a long-running
1942
+ # autonomous coding agent. Any fixed timeout short enough to catch a
1943
+ # hang would also kill legitimate multi-minute iterations, and there is
1944
+ # no "generous enough" value that is both safe and useful by default.
1945
+ # 2. Wrong retry semantics. The helper re-ran the same command on timeout.
1946
+ # Re-running a coding agent mid-work (it may have already edited files)
1947
+ # is actively harmful, not protective.
1948
+ #
1949
+ # The main invocation is also a pipeline (`claude | tee | python3`), which a
1950
+ # positional-arg `timeout "$@"` wrapper cannot wrap at all. Interrupting a
1951
+ # hung provider is handled by the SIGINT trap (kill_provider_child) instead.
1952
+ #
1953
+ # The `loki config spawn_timeout` / `spawn_retries` knobs (autonomy/loki) and
1954
+ # the config->env mapping in this file (`'spawn_timeout':'LOKI_SPAWN_TIMEOUT'`
1955
+ # in the config loader) still export LOKI_SPAWN_TIMEOUT / LOKI_SPAWN_RETRIES,
1956
+ # but nothing consumes them now. The mapping line is intentionally left in place
1957
+ # (a config-schema test may enumerate it); the full inert-knob removal spans
1958
+ # autonomy/loki too and is a separate cross-file follow-up.
1938
1959
  #===============================================================================
1939
1960
 
1940
- PROVIDER_SPAWN_TIMEOUT=${LOKI_SPAWN_TIMEOUT:-120}
1941
- PROVIDER_SPAWN_RETRIES=${LOKI_SPAWN_RETRIES:-2}
1942
-
1943
- # Invoke a command with timeout and retry logic
1944
- # Usage: invoke_with_timeout <timeout_seconds> <retries> <command...>
1945
- invoke_with_timeout() {
1946
- local timeout="$1"
1947
- local max_retries="$2"
1948
- shift 2
1949
-
1950
- local attempt=0
1951
- while [ $attempt -le $max_retries ]; do
1952
- if [ $attempt -gt 0 ]; then
1953
- log_warn "Provider spawn retry $attempt/$max_retries..."
1954
- fi
1955
-
1956
- local exit_code=0
1957
- # Use timeout command if available (GNU coreutils or macOS)
1958
- if command -v timeout &>/dev/null; then
1959
- timeout "$timeout" "$@"
1960
- exit_code=$?
1961
- elif command -v gtimeout &>/dev/null; then
1962
- gtimeout "$timeout" "$@"
1963
- exit_code=$?
1964
- else
1965
- # Fallback: no timeout wrapper, run directly
1966
- log_warn "timeout/gtimeout not available - running without timeout enforcement"
1967
- "$@"
1968
- exit_code=$?
1969
- fi
1970
-
1971
- # Exit code 124 = timeout
1972
- if [ $exit_code -eq 124 ]; then
1973
- log_warn "Provider spawn timed out after ${timeout}s (attempt $((attempt+1))/$((max_retries+1)))"
1974
- ((attempt++))
1975
- continue
1976
- fi
1977
-
1978
- return $exit_code
1979
- done
1980
-
1981
- log_error "Provider spawn failed after $((max_retries+1)) attempts (timeout=${timeout}s)"
1982
- # Crash friction (retry_loop): provider spawn exhausted all retries -- a
1983
- # clear threshold (not a single retry). Best-effort, never blocks.
1984
- if type loki_crash_friction &>/dev/null; then
1985
- loki_crash_friction "retry_loop" "provider spawn failed after $((max_retries+1)) attempts" >/dev/null 2>&1 || true
1986
- fi
1987
- return 124
1988
- }
1989
-
1990
1961
  #===============================================================================
1991
1962
  # GitHub Integration Functions (v4.1.0)
1992
1963
  #===============================================================================
@@ -9978,6 +9949,16 @@ CPEOF
9978
9949
  find "$checkpoint_dir" -maxdepth 1 -type d -name "cp-*" 2>/dev/null \
9979
9950
  | while read -r p; do basename "$p"; done | sort -t'-' -k3 -n \
9980
9951
  | head -n "$to_remove" | while read -r old_cp; do
9952
+ # WAVE9 (checkpoint leak): also delete the anchored worktree-snapshot
9953
+ # ref so its stash commit becomes eligible for `git gc`. Without this,
9954
+ # refs/loki/cp/<id> (and its commit) leaked forever even after the
9955
+ # checkpoint directory was pruned. Targeted deletion of exactly the
9956
+ # ids being pruned ONLY -- never a blanket refs/loki/cp/* sweep, since
9957
+ # git refs are shared across worktrees of one repo while checkpoint
9958
+ # dirs are per-TARGET_DIR; a parallel worktree may still need a ref we
9959
+ # are not pruning here. `|| true` because not every checkpoint has a
9960
+ # ref (only those where `git stash create` returned a non-empty sha).
9961
+ git update-ref -d "refs/loki/cp/${old_cp}" 2>/dev/null || true
9981
9962
  old_cp="${checkpoint_dir}/${old_cp}"
9982
9963
  rm -rf "$old_cp" 2>/dev/null || true
9983
9964
  done
@@ -11492,7 +11473,11 @@ try:
11492
11473
  storage = MemoryStorage(f'{target_dir}/.loki/memory')
11493
11474
  retriever = MemoryRetrieval(storage)
11494
11475
  context = {'goal': goal, 'phase': phase}
11495
- results = retriever.retrieve_task_aware(context, top_k=3)
11476
+ # The autonomous RARV loop opts into persist_boost so retrieved memories are
11477
+ # reinforced on disk ("use it or lose it"). Manual surfaces (loki memory CLI,
11478
+ # dashboard, MCP) keep the default persist_boost=False so a human browsing
11479
+ # memories does not silently inflate their importance.
11480
+ results = retriever.retrieve_task_aware(context, top_k=3, persist_boost=True)
11496
11481
  if results:
11497
11482
  print('RELEVANT MEMORIES:')
11498
11483
  for r in results[:3]: