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 +2 -2
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +14 -2
- package/autonomy/completion-council.sh +34 -3
- package/autonomy/loki +162 -75
- package/autonomy/run.sh +39 -54
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +57 -25
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +66 -66
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +12 -0
- package/memory/consolidation.py +15 -1
- package/memory/engine.py +25 -5
- package/memory/retrieval.py +18 -1
- package/memory/storage.py +136 -0
- package/memory/token_economics.py +9 -0
- package/memory/vector_index.py +13 -0
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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.
|
|
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.
|
|
409
|
+
**v7.69.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.69.0
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -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).
|
|
2063
|
-
#
|
|
2064
|
-
#
|
|
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
|
-
|
|
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
|
|
7357
|
-
echo " spawn_retries
|
|
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
|
|
7517
|
-
|
|
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:
|
|
7625
|
-
echo " spawn_retries:
|
|
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': '
|
|
12507
|
+
'codebase': os.environ.get('LOKI_HEAL_CODEBASE', ''),
|
|
12493
12508
|
'started': datetime.now().isoformat(),
|
|
12494
|
-
'current_phase': '
|
|
12495
|
-
'strict_mode':
|
|
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(
|
|
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(
|
|
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
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
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(
|
|
13035
|
+
with open(os.environ["_LOKI_TEMPLATE_FILE"]) as f:
|
|
13010
13036
|
tpl = json.load(f)
|
|
13011
13037
|
|
|
13012
|
-
hooks = ClusterLifecycleHooks(tpl.get(
|
|
13038
|
+
hooks = ClusterLifecycleHooks(tpl.get("hooks", {}))
|
|
13013
13039
|
db = SqliteStateBackend()
|
|
13014
13040
|
|
|
13015
|
-
cluster_id =
|
|
13016
|
-
|
|
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=
|
|
13046
|
+
events = db.query_events(event_type="cluster_state", migration_id=cluster_id, limit=1)
|
|
13020
13047
|
if events:
|
|
13021
|
-
print(f
|
|
13048
|
+
print(f"Resuming cluster {cluster_id} from last checkpoint")
|
|
13022
13049
|
else:
|
|
13023
|
-
print(f
|
|
13050
|
+
print(f"No previous state found for {cluster_id}. Starting fresh.")
|
|
13024
13051
|
|
|
13025
13052
|
# Fire pre_run hooks
|
|
13026
|
-
results = hooks.fire(
|
|
13053
|
+
results = hooks.fire("pre_run", {"cluster_id": cluster_id, "template": template_name})
|
|
13027
13054
|
for r in results:
|
|
13028
|
-
if not r[
|
|
13029
|
-
print(
|
|
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(
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
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
|
-
|
|
13040
|
-
print(
|
|
13041
|
-
"
|
|
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,
|
|
22870
|
+
pkg_name=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22871
|
+
import json, os
|
|
22840
22872
|
try:
|
|
22841
|
-
d = json.load(open(
|
|
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,
|
|
22880
|
+
project_description=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22881
|
+
import json, os
|
|
22850
22882
|
try:
|
|
22851
|
-
d = json.load(open(
|
|
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,
|
|
22887
|
+
project_version=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22888
|
+
import json, os
|
|
22857
22889
|
try:
|
|
22858
|
-
d = json.load(open(
|
|
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,
|
|
22894
|
+
entry_points=$(LOKI_ONB_PKG="$target_path/package.json" python3 -c "
|
|
22895
|
+
import json, os
|
|
22864
22896
|
try:
|
|
22865
|
-
d = json.load(open(
|
|
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
|
-
|
|
23246
|
-
|
|
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(
|
|
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
|
-
|
|
23641
|
-
|
|
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(
|
|
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': '
|
|
23876
|
-
'stack': {'languages':
|
|
23877
|
-
'patterns':
|
|
23878
|
-
'files': {'total':
|
|
23879
|
-
'commands': {'build': '
|
|
23880
|
-
'entry_points':
|
|
23881
|
-
'monorepo':
|
|
23882
|
-
'has_docker':
|
|
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 (
|
|
1936
|
-
#
|
|
1937
|
-
#
|
|
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
|
-
|
|
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]:
|