loki-mode 7.41.2 → 7.41.4
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 +44 -5
- package/autonomy/completion-council.sh +60 -18
- package/autonomy/council-v2.sh +13 -2
- package/autonomy/hooks/migration-hooks.sh +51 -1
- package/autonomy/loki +33 -18
- package/autonomy/run.sh +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +49 -17
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +8 -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, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.41.
|
|
6
|
+
# Loki Mode v7.41.4
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
398
398
|
|
|
399
399
|
---
|
|
400
400
|
|
|
401
|
-
**v7.41.
|
|
401
|
+
**v7.41.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.41.
|
|
1
|
+
7.41.4
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -200,6 +200,24 @@ _app_runner_reconcile_port() {
|
|
|
200
200
|
|
|
201
201
|
[ -n "$real_port" ] || return 0
|
|
202
202
|
if [ "$real_port" != "$_APP_RUNNER_PORT" ]; then
|
|
203
|
+
# Liveness guard: only overwrite the recorded port when the reconciled
|
|
204
|
+
# port ACTUALLY serves HTTP. A log line can name a non-serving port (a
|
|
205
|
+
# metrics endpoint like ":9464" or a DB connection like ":5432") emitted
|
|
206
|
+
# after the real serving URL; committing that would clobber a correct
|
|
207
|
+
# recorded port and point the preview at a dead port. We deliberately do
|
|
208
|
+
# NOT use curl -f: any HTTP response (including 404/401/500) proves a
|
|
209
|
+
# server is bound and serving on that port (Spring Boot whitelabel 404,
|
|
210
|
+
# REST-only roots, and auth-gated "/" all return non-2xx but ARE live).
|
|
211
|
+
# A dead/unbound port produces a connection error, which curl reports as
|
|
212
|
+
# a non-zero exit even without -f. If curl is unavailable we cannot
|
|
213
|
+
# verify, so fall back to the prior behavior and commit the parsed port
|
|
214
|
+
# (no regression on curl-less hosts).
|
|
215
|
+
if command -v curl >/dev/null 2>&1; then
|
|
216
|
+
if ! curl -s -o /dev/null -m 2 "http://localhost:${real_port}/" 2>/dev/null; then
|
|
217
|
+
log_info "App Runner: skipped reconcile to port $real_port (no HTTP response); keeping recorded port $_APP_RUNNER_PORT"
|
|
218
|
+
return 0
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
203
221
|
log_info "App Runner: reconciled port $_APP_RUNNER_PORT -> $real_port (from app.log listen line)"
|
|
204
222
|
_APP_RUNNER_PORT="$real_port"
|
|
205
223
|
_APP_RUNNER_URL="http://localhost:${real_port}"
|
|
@@ -212,6 +230,16 @@ _app_runner_reconcile_port() {
|
|
|
212
230
|
# shapes in priority order and returns the LAST (most recent) plausible port,
|
|
213
231
|
# tolerating ANSI color codes that dev servers emit. Validates 1-65535. Echoes
|
|
214
232
|
# the port or nothing.
|
|
233
|
+
#
|
|
234
|
+
# Tiers 1 and 2 are restricted to lines that ALSO carry a serving keyword
|
|
235
|
+
# (listen|running|ready|started|serving|server|local) so that non-serving noise
|
|
236
|
+
# such as a DB connection string ("Connecting to database on port 5432") or an
|
|
237
|
+
# outbound URL does not win. Note: a metrics endpoint line like
|
|
238
|
+
# "Prometheus metrics server listening on http://0.0.0.0:9464" DOES carry
|
|
239
|
+
# serving keywords ("server"/"listening") and so can still be returned here; the
|
|
240
|
+
# reconcile caller liveness-verifies the parsed port before committing it, which
|
|
241
|
+
# is the layer that rejects a non-serving metrics/DB port.
|
|
242
|
+
_SERVING_KEYWORDS='listen|running|ready|started|serving|server|local'
|
|
215
243
|
_parse_listen_port() {
|
|
216
244
|
local file="$1"
|
|
217
245
|
[ -f "$file" ] || return 0
|
|
@@ -220,16 +248,28 @@ _parse_listen_port() {
|
|
|
220
248
|
clean=$(sed -E $'s/\x1b\\[[0-9;]*m//g' "$file" 2>/dev/null) || clean=$(cat "$file" 2>/dev/null)
|
|
221
249
|
[ -n "$clean" ] || return 0
|
|
222
250
|
|
|
251
|
+
# Restrict candidate lines to those carrying a serving keyword. This drops
|
|
252
|
+
# DB-connection and outbound-URL noise before any port extraction.
|
|
253
|
+
local serving
|
|
254
|
+
serving=$(printf '%s\n' "$clean" | grep -iE "$_SERVING_KEYWORDS")
|
|
255
|
+
|
|
223
256
|
local candidate=""
|
|
224
257
|
# 1) Explicit URL with a port: http://host:PORT (most reliable).
|
|
225
|
-
candidate=$(printf '%s\n' "$
|
|
258
|
+
candidate=$(printf '%s\n' "$serving" \
|
|
226
259
|
| grep -oiE 'https?://[a-z0-9.\-]+:[0-9]{1,5}' \
|
|
227
260
|
| grep -oE ':[0-9]{1,5}' | tr -d ':' | tail -1)
|
|
228
|
-
#
|
|
261
|
+
# 2a) Spring Boot form: "Tomcat started on port(s): 8081". The literal
|
|
262
|
+
# "(s):" breaks the generic port[ =:]+ scan below, so match it first.
|
|
263
|
+
if [ -z "$candidate" ]; then
|
|
264
|
+
candidate=$(printf '%s\n' "$serving" \
|
|
265
|
+
| grep -ioE 'port\(s\):[ ]*[0-9]{1,5}' \
|
|
266
|
+
| grep -oE '[0-9]{1,5}' | tail -1)
|
|
267
|
+
fi
|
|
268
|
+
# 2b) A number anchored to the literal word "port": "port 8080", "port=3000",
|
|
229
269
|
# "port: 5000". This runs BEFORE the bare host:port scan so a clock-style
|
|
230
270
|
# timestamp on the same line (e.g. "12:30:45 ... port 8080") cannot win.
|
|
231
271
|
if [ -z "$candidate" ]; then
|
|
232
|
-
candidate=$(printf '%s\n' "$
|
|
272
|
+
candidate=$(printf '%s\n' "$serving" \
|
|
233
273
|
| grep -ioE 'port[ =:]+[0-9]{1,5}' \
|
|
234
274
|
| grep -oE '[0-9]{1,5}' | tail -1)
|
|
235
275
|
fi
|
|
@@ -238,8 +278,7 @@ _parse_listen_port() {
|
|
|
238
278
|
# or a dot immediately left of the colon excludes "HH:MM" timestamps,
|
|
239
279
|
# which have a digit there.
|
|
240
280
|
if [ -z "$candidate" ]; then
|
|
241
|
-
candidate=$(printf '%s\n' "$
|
|
242
|
-
| grep -iE 'listen|running on|ready|started|serving|server' \
|
|
281
|
+
candidate=$(printf '%s\n' "$serving" \
|
|
243
282
|
| grep -oiE '[a-z.][a-z0-9.\-]*:[0-9]{1,5}' \
|
|
244
283
|
| grep -oE ':[0-9]{1,5}' | tr -d ':' | tail -1)
|
|
245
284
|
fi
|
|
@@ -549,6 +549,31 @@ print(str(rc) + ' ' + json.dumps(new_state))
|
|
|
549
549
|
# Council Voting - 3 independent reviewers check completion
|
|
550
550
|
#===============================================================================
|
|
551
551
|
|
|
552
|
+
# _council_parse_vote: extract a canonical council verdict from a reviewer's
|
|
553
|
+
# raw output. Returns exactly one of APPROVE | REJECT | CANNOT_VALIDATE | ""
|
|
554
|
+
# (empty when no canonical VOTE line is present).
|
|
555
|
+
#
|
|
556
|
+
# Hardening (v7.41.3):
|
|
557
|
+
# - Word-bounded verdict: "VOTE: APPROVED" / "VOTE: APPROVE_WITH_CONCERNS"
|
|
558
|
+
# do NOT match APPROVE (non-canonical tokens are unparseable -> empty ->
|
|
559
|
+
# caller treats as REJECT). A trailing class [^A-Za-z0-9_] (or end of line)
|
|
560
|
+
# enforces the boundary; "\b" is a GNU-grep extension that BSD grep on this
|
|
561
|
+
# machine ignores, so it is deliberately avoided for dual-route parity.
|
|
562
|
+
# - Markdown / quote tolerance both BEFORE the keyword and AFTER the colon, so
|
|
563
|
+
# "**VOTE:** APPROVE", "> VOTE: APPROVE", and "VOTE:APPROVE" all match.
|
|
564
|
+
# - Conservative tie-break: only a clean canonical APPROVE yields APPROVE;
|
|
565
|
+
# anything ambiguous yields the empty string, which every caller maps to the
|
|
566
|
+
# conservative outcome (REJECT / re-iterate).
|
|
567
|
+
_council_parse_vote() {
|
|
568
|
+
local raw="$1"
|
|
569
|
+
# markdown/whitespace/quote class allowed around the keyword and colon
|
|
570
|
+
local _pat='[*_> [:space:]]*VOTE[*_ [:space:]]*:[*_> [:space:]]*(APPROVE|REJECT|CANNOT_VALIDATE)([^A-Za-z0-9_]|$)'
|
|
571
|
+
printf '%s' "$raw" \
|
|
572
|
+
| grep -oE "$_pat" \
|
|
573
|
+
| grep -oE "APPROVE|REJECT|CANNOT_VALIDATE" \
|
|
574
|
+
| head -1
|
|
575
|
+
}
|
|
576
|
+
|
|
552
577
|
council_vote() {
|
|
553
578
|
local prd_path="${COUNCIL_PRD_PATH:-}"
|
|
554
579
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
@@ -596,7 +621,7 @@ council_vote() {
|
|
|
596
621
|
verdict=$(council_member_review "$member" "$role" "$evidence_file" "$vote_dir")
|
|
597
622
|
|
|
598
623
|
local vote_result
|
|
599
|
-
vote_result=$(
|
|
624
|
+
vote_result=$(_council_parse_vote "$verdict")
|
|
600
625
|
|
|
601
626
|
# v6.0.0: Handle CANNOT_VALIDATE - validator lacks enough context to decide
|
|
602
627
|
if [ "$vote_result" = "CANNOT_VALIDATE" ]; then
|
|
@@ -691,15 +716,21 @@ print('true' if ratio > budget else 'false')
|
|
|
691
716
|
local contrarian_verdict
|
|
692
717
|
contrarian_verdict=$(council_devils_advocate "$evidence_file" "$vote_dir")
|
|
693
718
|
local contrarian_vote
|
|
694
|
-
contrarian_vote=$(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
719
|
+
contrarian_vote=$(_council_parse_vote "$contrarian_verdict")
|
|
720
|
+
|
|
721
|
+
# Conservative tie-break (v7.41.3): ONLY a clean canonical APPROVE
|
|
722
|
+
# confirms the unanimous approval. Any other outcome -- REJECT,
|
|
723
|
+
# CANNOT_VALIDATE, or an unparseable/hedged verdict (empty) -- overrides
|
|
724
|
+
# to one more verification iteration. Previously the else-branch treated
|
|
725
|
+
# an empty/hedged contrarian verdict as "confirmed approval", letting a
|
|
726
|
+
# hedged "VOTE: APPROVED" ship; this flip closes that on the veto path.
|
|
727
|
+
if [ "$contrarian_vote" = "APPROVE" ]; then
|
|
728
|
+
log_info "Anti-sycophancy: Devil's advocate confirmed approval"
|
|
729
|
+
else
|
|
730
|
+
log_warn "Anti-sycophancy: Devil's advocate did not confirm unanimous approval (verdict: ${contrarian_vote:-unparseable})"
|
|
698
731
|
log_warn "Overriding to require one more iteration for verification"
|
|
699
732
|
approve_count=$((approve_count - 1))
|
|
700
733
|
reject_count=$((reject_count + 1))
|
|
701
|
-
else
|
|
702
|
-
log_info "Anti-sycophancy: Devil's advocate confirmed approval"
|
|
703
734
|
fi
|
|
704
735
|
fi
|
|
705
736
|
|
|
@@ -860,7 +891,10 @@ if not voters:
|
|
|
860
891
|
if mdir.exists():
|
|
861
892
|
for mf in sorted(mdir.glob('member-*.txt')):
|
|
862
893
|
content = mf.read_text(errors='replace').strip()
|
|
863
|
-
|
|
894
|
+
# v7.41.3: word-bounded + markdown-tolerant. VOTE:APPROVED and
|
|
895
|
+
# VOTE:APPROVE_WITH_CONCERNS must NOT match APPROVE; bold/quoted
|
|
896
|
+
# VOTE: APPROVE must match. Unmatched -> default REJECT (conservative).
|
|
897
|
+
vote_match = re.search(r'[*_> ]*VOTE[*_ ]*:[*_> ]*(APPROVE|REJECT|CANNOT_VALIDATE)(?![A-Za-z0-9_])', content)
|
|
864
898
|
reason_match = re.search(r'REASON\s*:\s*(.+?)(?:\n|\$)', content)
|
|
865
899
|
issues = []
|
|
866
900
|
for im in re.finditer(r'ISSUES\s*:\s*(CRITICAL|HIGH|MEDIUM|LOW)\s*:\s*(.+?)(?:\n|\$)', content):
|
|
@@ -1783,27 +1817,33 @@ ISSUES: CRITICAL:description (optional, one per line per issue)"
|
|
|
1783
1817
|
# Set inline (not via a helper) so the carve-out holds even when
|
|
1784
1818
|
# this file is sourced standalone and the helpers are out of scope.
|
|
1785
1819
|
# Inlined on `claude` only (does not cross the pipe). No-op absent.
|
|
1786
|
-
|
|
1820
|
+
# v7.41.3 BUG A: do NOT tail-truncate before parsing. A thorough
|
|
1821
|
+
# reviewer that lists >~18 ISSUES lines after VOTE would push its
|
|
1822
|
+
# own VOTE: line out of a tail-20 window, making the parser find
|
|
1823
|
+
# no VOTE and default a real APPROVE to REJECT. Capture the full
|
|
1824
|
+
# output; the downstream parse already greps VOTE/REASON/ISSUES.
|
|
1825
|
+
# CAVEMAN_DEFAULT_MODE=off suppression is preserved (see above).
|
|
1826
|
+
verdict=$(echo "$prompt" | env CAVEMAN_DEFAULT_MODE=off claude "${_cm_argv[@]}" -p 2>/dev/null)
|
|
1787
1827
|
fi
|
|
1788
1828
|
;;
|
|
1789
1829
|
codex)
|
|
1790
1830
|
if command -v codex &>/dev/null; then
|
|
1791
|
-
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null
|
|
1831
|
+
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null)
|
|
1792
1832
|
fi
|
|
1793
1833
|
;;
|
|
1794
1834
|
gemini)
|
|
1795
1835
|
if command -v gemini &>/dev/null; then
|
|
1796
|
-
verdict=$(echo "$prompt" | gemini 2>/dev/null
|
|
1836
|
+
verdict=$(echo "$prompt" | gemini 2>/dev/null)
|
|
1797
1837
|
fi
|
|
1798
1838
|
;;
|
|
1799
1839
|
cline)
|
|
1800
1840
|
if command -v cline &>/dev/null; then
|
|
1801
|
-
verdict=$(cline -y "$prompt" 2>/dev/null
|
|
1841
|
+
verdict=$(cline -y "$prompt" 2>/dev/null)
|
|
1802
1842
|
fi
|
|
1803
1843
|
;;
|
|
1804
1844
|
aider)
|
|
1805
1845
|
if command -v aider &>/dev/null; then
|
|
1806
|
-
verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null
|
|
1846
|
+
verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null)
|
|
1807
1847
|
fi
|
|
1808
1848
|
;;
|
|
1809
1849
|
esac
|
|
@@ -1882,27 +1922,29 @@ REASON: your reasoning"
|
|
|
1882
1922
|
# (contrarian) vote is parsed for "VOTE:". Disable caveman
|
|
1883
1923
|
# unconditionally so compression cannot flip the contrarian vote.
|
|
1884
1924
|
# Inlined on `claude` only (does not cross the pipe). No-op absent.
|
|
1885
|
-
|
|
1925
|
+
# v7.41.3 BUG A: full capture, no tail-truncation (see member
|
|
1926
|
+
# subcall note). CAVEMAN_DEFAULT_MODE=off suppression preserved.
|
|
1927
|
+
verdict=$(echo "$prompt" | env CAVEMAN_DEFAULT_MODE=off claude "${_co_argv[@]}" -p 2>/dev/null)
|
|
1886
1928
|
fi
|
|
1887
1929
|
;;
|
|
1888
1930
|
codex)
|
|
1889
1931
|
if command -v codex &>/dev/null; then
|
|
1890
|
-
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null
|
|
1932
|
+
verdict=$(codex exec --full-auto "$prompt" 2>/dev/null)
|
|
1891
1933
|
fi
|
|
1892
1934
|
;;
|
|
1893
1935
|
gemini)
|
|
1894
1936
|
if command -v gemini &>/dev/null; then
|
|
1895
|
-
verdict=$(echo "$prompt" | gemini 2>/dev/null
|
|
1937
|
+
verdict=$(echo "$prompt" | gemini 2>/dev/null)
|
|
1896
1938
|
fi
|
|
1897
1939
|
;;
|
|
1898
1940
|
cline)
|
|
1899
1941
|
if command -v cline &>/dev/null; then
|
|
1900
|
-
verdict=$(cline -y "$prompt" 2>/dev/null
|
|
1942
|
+
verdict=$(cline -y "$prompt" 2>/dev/null)
|
|
1901
1943
|
fi
|
|
1902
1944
|
;;
|
|
1903
1945
|
aider)
|
|
1904
1946
|
if command -v aider &>/dev/null; then
|
|
1905
|
-
verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null
|
|
1947
|
+
verdict=$(aider --message "$prompt" --yes-always --no-auto-commits --no-git 2>/dev/null)
|
|
1906
1948
|
fi
|
|
1907
1949
|
;;
|
|
1908
1950
|
esac
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -151,8 +151,19 @@ print('{:.3f}'.format(detect_sycophancy(votes)))
|
|
|
151
151
|
fi
|
|
152
152
|
|
|
153
153
|
# Step 6: Calibration tracking
|
|
154
|
+
# Compute threshold using ceiling(2/3) formula, consistent with
|
|
155
|
+
# completion-council.sh (council_should_stop, council_aggregate_votes,
|
|
156
|
+
# council_evaluate). An explicit operator override via LOKI_COUNCIL_THRESHOLD
|
|
157
|
+
# is honored; otherwise scale with council_size instead of a flat default of 2.
|
|
158
|
+
local effective_threshold
|
|
159
|
+
if [ -n "${LOKI_COUNCIL_THRESHOLD:-}" ]; then
|
|
160
|
+
effective_threshold="$LOKI_COUNCIL_THRESHOLD"
|
|
161
|
+
else
|
|
162
|
+
effective_threshold=$(( (council_size * 2 + 2) / 3 ))
|
|
163
|
+
fi
|
|
164
|
+
|
|
154
165
|
local final_decision
|
|
155
|
-
if [ "$approve_count" -ge "$
|
|
166
|
+
if [ "$approve_count" -ge "$effective_threshold" ]; then
|
|
156
167
|
final_decision="approve"
|
|
157
168
|
else
|
|
158
169
|
final_decision="reject"
|
|
@@ -194,7 +205,7 @@ SUMMARY_EOF
|
|
|
194
205
|
"iteration=$iteration" \
|
|
195
206
|
"approve=$approve_count" \
|
|
196
207
|
"reject=$reject_count" \
|
|
197
|
-
"threshold=$
|
|
208
|
+
"threshold=$effective_threshold" \
|
|
198
209
|
"sycophancy_score=$sycophancy_score" \
|
|
199
210
|
"result=$(echo "$final_decision" | tr '[:lower:]' '[:upper:]')" 2>/dev/null || true
|
|
200
211
|
|
|
@@ -110,10 +110,33 @@ detect_test_command() {
|
|
|
110
110
|
elif [[ -d "${codebase_path}/tests" ]]; then
|
|
111
111
|
echo "cd '${codebase_path}' && python -m pytest tests/ -q"
|
|
112
112
|
else
|
|
113
|
-
|
|
113
|
+
# No framework detected and LOKI_TEST_COMMAND unset.
|
|
114
|
+
# In healing mode the gates MUST fail closed: "no tests" can never be
|
|
115
|
+
# treated as "tests passed", or the behavioral-preservation guarantee
|
|
116
|
+
# is silently defeated. Emit a sentinel the healing consumers detect and
|
|
117
|
+
# turn into a hard BLOCK (see is_no_test_cmd). The bare token also fails
|
|
118
|
+
# if eval'd directly (command-not-found, exit 127) so the default is
|
|
119
|
+
# fail-closed even if a string check is ever missed.
|
|
120
|
+
# Outside healing mode, preserve the prior fail-open behavior: the
|
|
121
|
+
# non-healing consumers (post_file_edit, post_step, pre_phase_gate) run
|
|
122
|
+
# this via `eval` and a bare token there would exit 127 -> taken block
|
|
123
|
+
# -> destructive (e.g. reverting a user edit in a repo with no tests).
|
|
124
|
+
if [[ "${LOKI_HEAL_MODE:-false}" == "true" ]]; then
|
|
125
|
+
echo "__LOKI_NO_TEST_CMD__"
|
|
126
|
+
else
|
|
127
|
+
echo "echo 'No test command detected. Set LOKI_TEST_COMMAND.'"
|
|
128
|
+
fi
|
|
114
129
|
fi
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
# Returns 0 (true) when detect_test_command yielded the no-test-command
|
|
133
|
+
# sentinel, i.e. no framework was detected and LOKI_TEST_COMMAND is unset.
|
|
134
|
+
# Used by the healing gates to distinguish "no tests available" (block) from
|
|
135
|
+
# "tests ran and passed" (allow) and "tests ran and failed" (block).
|
|
136
|
+
is_no_test_cmd() {
|
|
137
|
+
[[ "${1:-}" == "__LOKI_NO_TEST_CMD__" ]]
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
# Hook: post_file_edit - runs after ANY agent modifies a source file
|
|
118
141
|
hook_post_file_edit() {
|
|
119
142
|
local file_path="${1:-}"
|
|
@@ -342,6 +365,17 @@ hook_post_healing_modify() {
|
|
|
342
365
|
# Run characterization tests
|
|
343
366
|
local test_cmd
|
|
344
367
|
test_cmd=$(detect_test_command "$codebase_path")
|
|
368
|
+
|
|
369
|
+
# No test command available in healing mode -> fail closed. "No tests" can
|
|
370
|
+
# never count as "characterization tests passed". Do not git-revert here:
|
|
371
|
+
# there is no test-driven baseline to restore against, and the actionable
|
|
372
|
+
# fix is to provide a test command.
|
|
373
|
+
if is_no_test_cmd "$test_cmd"; then
|
|
374
|
+
echo "HOOK_BLOCKED: no test command available; set LOKI_TEST_COMMAND"
|
|
375
|
+
echo "Characterization tests cannot run for healing modification to ${file_path}; refusing to treat absence of tests as success."
|
|
376
|
+
return 1
|
|
377
|
+
fi
|
|
378
|
+
|
|
345
379
|
local test_result_file
|
|
346
380
|
test_result_file=$(mktemp)
|
|
347
381
|
|
|
@@ -437,6 +471,10 @@ except: print(0)
|
|
|
437
471
|
|
|
438
472
|
local test_cmd
|
|
439
473
|
test_cmd=$(detect_test_command "$codebase_path")
|
|
474
|
+
if is_no_test_cmd "$test_cmd"; then
|
|
475
|
+
echo "GATE_BLOCKED: no test command available; set LOKI_TEST_COMMAND"
|
|
476
|
+
return 1
|
|
477
|
+
fi
|
|
440
478
|
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
441
479
|
echo "GATE_BLOCKED: Characterization tests do not pass"
|
|
442
480
|
return 1
|
|
@@ -445,6 +483,10 @@ except: print(0)
|
|
|
445
483
|
stabilize:isolate)
|
|
446
484
|
local test_cmd
|
|
447
485
|
test_cmd=$(detect_test_command "$codebase_path")
|
|
486
|
+
if is_no_test_cmd "$test_cmd"; then
|
|
487
|
+
echo "GATE_BLOCKED: no test command available; set LOKI_TEST_COMMAND"
|
|
488
|
+
return 1
|
|
489
|
+
fi
|
|
448
490
|
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
449
491
|
echo "GATE_BLOCKED: Tests do not pass after stabilization"
|
|
450
492
|
return 1
|
|
@@ -453,6 +495,10 @@ except: print(0)
|
|
|
453
495
|
isolate:modernize)
|
|
454
496
|
local test_cmd
|
|
455
497
|
test_cmd=$(detect_test_command "$codebase_path")
|
|
498
|
+
if is_no_test_cmd "$test_cmd"; then
|
|
499
|
+
echo "GATE_BLOCKED: no test command available; set LOKI_TEST_COMMAND"
|
|
500
|
+
return 1
|
|
501
|
+
fi
|
|
456
502
|
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
457
503
|
echo "GATE_BLOCKED: Tests do not pass after isolation"
|
|
458
504
|
return 1
|
|
@@ -461,6 +507,10 @@ except: print(0)
|
|
|
461
507
|
modernize:validate)
|
|
462
508
|
local test_cmd
|
|
463
509
|
test_cmd=$(detect_test_command "$codebase_path")
|
|
510
|
+
if is_no_test_cmd "$test_cmd"; then
|
|
511
|
+
echo "GATE_BLOCKED: no test command available; set LOKI_TEST_COMMAND"
|
|
512
|
+
return 1
|
|
513
|
+
fi
|
|
464
514
|
if ! eval "$test_cmd" >/dev/null 2>&1; then
|
|
465
515
|
echo "GATE_BLOCKED: Tests do not pass after modernization"
|
|
466
516
|
return 1
|
package/autonomy/loki
CHANGED
|
@@ -6708,14 +6708,23 @@ cmd_assets() {
|
|
|
6708
6708
|
shift 2>/dev/null || true
|
|
6709
6709
|
|
|
6710
6710
|
local helper="$_LOKI_SCRIPT_DIR/lib/assets_bundle.py"
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6711
|
+
# The help path needs neither the python3 helper nor python3 itself, so
|
|
6712
|
+
# guard these preconditions to skip help/empty subcommands. Otherwise
|
|
6713
|
+
# `loki assets --help` would error on a missing helper/python3 instead of
|
|
6714
|
+
# printing help (same class as #574 / the cmd_monitor fix).
|
|
6715
|
+
case "$subcommand" in
|
|
6716
|
+
--help|-h|help|"") ;;
|
|
6717
|
+
*)
|
|
6718
|
+
if [ ! -f "$helper" ]; then
|
|
6719
|
+
echo -e "${RED}Error: assets helper not found at $helper${NC}" >&2
|
|
6720
|
+
return 1
|
|
6721
|
+
fi
|
|
6722
|
+
if ! command -v python3 &>/dev/null; then
|
|
6723
|
+
echo -e "${RED}Error: python3 is required for 'loki assets'${NC}" >&2
|
|
6724
|
+
return 1
|
|
6725
|
+
fi
|
|
6726
|
+
;;
|
|
6727
|
+
esac
|
|
6719
6728
|
|
|
6720
6729
|
# Project dir holds .loki/ (memory, council, wiki). Default to cwd.
|
|
6721
6730
|
local project_dir
|
|
@@ -9878,16 +9887,11 @@ QPRDEOF
|
|
|
9878
9887
|
|
|
9879
9888
|
# Docker Compose monitoring with auto-fix (v6.67.0)
|
|
9880
9889
|
cmd_monitor() {
|
|
9881
|
-
#
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
if ! docker info &>/dev/null 2>&1; then
|
|
9887
|
-
echo -e "${RED}Docker daemon is not running. Start Docker Desktop or the Docker service.${NC}"
|
|
9888
|
-
return 1
|
|
9889
|
-
fi
|
|
9890
|
-
|
|
9890
|
+
# A --help/-h in any position (e.g. `loki monitor --help`) short-circuits
|
|
9891
|
+
# to the help text BEFORE the Docker preconditions, so `monitor --help`
|
|
9892
|
+
# works even when Docker is down (same class as #574). The arg parser's
|
|
9893
|
+
# existing --help arm below prints the help; we just defer the Docker
|
|
9894
|
+
# checks until after parsing so help is reachable.
|
|
9891
9895
|
local project_dir="."
|
|
9892
9896
|
local watch_only=false
|
|
9893
9897
|
local poll_interval="${LOKI_MONITOR_INTERVAL:-10}"
|
|
@@ -9945,6 +9949,17 @@ cmd_monitor() {
|
|
|
9945
9949
|
esac
|
|
9946
9950
|
done
|
|
9947
9951
|
|
|
9952
|
+
# Verify Docker is available (deferred past arg parsing so --help works
|
|
9953
|
+
# without a running daemon).
|
|
9954
|
+
if ! command -v docker &>/dev/null; then
|
|
9955
|
+
echo -e "${RED}Docker is not installed. Install from https://docker.com${NC}"
|
|
9956
|
+
return 1
|
|
9957
|
+
fi
|
|
9958
|
+
if ! docker info &>/dev/null 2>&1; then
|
|
9959
|
+
echo -e "${RED}Docker daemon is not running. Start Docker Desktop or the Docker service.${NC}"
|
|
9960
|
+
return 1
|
|
9961
|
+
fi
|
|
9962
|
+
|
|
9948
9963
|
# Resolve to absolute path
|
|
9949
9964
|
if [[ ! "$project_dir" = /* ]]; then
|
|
9950
9965
|
project_dir="$(cd "$project_dir" 2>/dev/null && pwd)" || {
|
package/autonomy/run.sh
CHANGED
|
@@ -14116,7 +14116,7 @@ if __name__ == "__main__":
|
|
|
14116
14116
|
# refuse completion until the review passes.
|
|
14117
14117
|
local _gate_block_for_completion=""
|
|
14118
14118
|
case "${gate_failures:-}" in
|
|
14119
|
-
*code_review,*|*code_review_ESCALATED*) _gate_block_for_completion="code_review" ;;
|
|
14119
|
+
*code_review,*|*code_review_ESCALATED*|*code_review_PAUSED*) _gate_block_for_completion="code_review" ;;
|
|
14120
14120
|
esac
|
|
14121
14121
|
# DROP-FIX (v7.28): check_completion_promise -> check_task_completion_signal
|
|
14122
14122
|
# CONSUMES the completion signal (rm -f) on the FIRST successful call.
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -5787,12 +5787,20 @@ async def create_checkpoint(body: CheckpointCreate = None):
|
|
|
5787
5787
|
checkpoint_dir = checkpoints_dir / checkpoint_id
|
|
5788
5788
|
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
5789
5789
|
|
|
5790
|
-
# Capture git SHA
|
|
5790
|
+
# Capture git SHA. Pass cwd= the active project root so the recorded
|
|
5791
|
+
# git_sha belongs to the project being checkpointed, not whatever directory
|
|
5792
|
+
# the dashboard process happens to run from (correctness bug for
|
|
5793
|
+
# multi-project dashboards). Offload to a thread so the blocking git call
|
|
5794
|
+
# does not stall the single-worker uvicorn event loop.
|
|
5791
5795
|
git_sha = ""
|
|
5796
|
+
git_cwd = str(loki_dir.parent) if loki_dir.name == ".loki" else None
|
|
5792
5797
|
try:
|
|
5793
|
-
result =
|
|
5794
|
-
|
|
5795
|
-
|
|
5798
|
+
result = await asyncio.to_thread(
|
|
5799
|
+
lambda: subprocess.run(
|
|
5800
|
+
["git", "rev-parse", "HEAD"],
|
|
5801
|
+
capture_output=True, text=True, timeout=5,
|
|
5802
|
+
cwd=git_cwd,
|
|
5803
|
+
)
|
|
5796
5804
|
)
|
|
5797
5805
|
if result.returncode == 0:
|
|
5798
5806
|
git_sha = result.stdout.strip()
|
|
@@ -5812,11 +5820,15 @@ async def create_checkpoint(body: CheckpointCreate = None):
|
|
|
5812
5820
|
except Exception:
|
|
5813
5821
|
pass
|
|
5814
5822
|
|
|
5815
|
-
# Copy queue directory if present
|
|
5823
|
+
# Copy queue directory if present. Offload to a thread: a large queue tree
|
|
5824
|
+
# would otherwise block the single-worker uvicorn event loop.
|
|
5816
5825
|
queue_src = loki_dir / "queue"
|
|
5817
5826
|
if queue_src.exists():
|
|
5818
5827
|
try:
|
|
5819
|
-
|
|
5828
|
+
await asyncio.to_thread(
|
|
5829
|
+
shutil.copytree,
|
|
5830
|
+
str(queue_src), str(checkpoint_dir / "queue"), dirs_exist_ok=True,
|
|
5831
|
+
)
|
|
5820
5832
|
except Exception:
|
|
5821
5833
|
pass
|
|
5822
5834
|
|
|
@@ -5898,7 +5910,11 @@ async def rollback_checkpoint(checkpoint_id: str):
|
|
|
5898
5910
|
src = loki_dir / dname
|
|
5899
5911
|
if src.exists() and src.is_dir():
|
|
5900
5912
|
try:
|
|
5901
|
-
|
|
5913
|
+
# Offload the (potentially large) directory copy off the event loop.
|
|
5914
|
+
await asyncio.to_thread(
|
|
5915
|
+
shutil.copytree,
|
|
5916
|
+
str(src), str(pre_dir / dname), dirs_exist_ok=True,
|
|
5917
|
+
)
|
|
5902
5918
|
except Exception:
|
|
5903
5919
|
pass
|
|
5904
5920
|
pre_meta = {
|
|
@@ -5928,7 +5944,11 @@ async def rollback_checkpoint(checkpoint_id: str):
|
|
|
5928
5944
|
dest = loki_dir / item.name
|
|
5929
5945
|
try:
|
|
5930
5946
|
if item.is_dir():
|
|
5931
|
-
|
|
5947
|
+
# Offload the (potentially large) directory copy off the event loop.
|
|
5948
|
+
await asyncio.to_thread(
|
|
5949
|
+
shutil.copytree,
|
|
5950
|
+
str(item), str(dest), dirs_exist_ok=True,
|
|
5951
|
+
)
|
|
5932
5952
|
else:
|
|
5933
5953
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
5934
5954
|
shutil.copy2(str(item), str(dest))
|
|
@@ -6246,10 +6266,14 @@ async def get_github_status(token: Optional[dict] = Depends(auth.get_current_tok
|
|
|
6246
6266
|
# Detect repo from git
|
|
6247
6267
|
try:
|
|
6248
6268
|
import subprocess
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6269
|
+
# Offload the blocking git call so it does not stall the single-worker
|
|
6270
|
+
# uvicorn event loop.
|
|
6271
|
+
url = await asyncio.to_thread(
|
|
6272
|
+
lambda: subprocess.run(
|
|
6273
|
+
["git", "remote", "get-url", "origin"],
|
|
6274
|
+
capture_output=True, text=True, timeout=5,
|
|
6275
|
+
cwd=str(loki_dir.parent) if loki_dir.name == ".loki" else None,
|
|
6276
|
+
)
|
|
6253
6277
|
)
|
|
6254
6278
|
if url.returncode == 0:
|
|
6255
6279
|
repo = url.stdout.strip()
|
|
@@ -8365,11 +8389,19 @@ async def post_wiki_ask(req: WikiAskRequest):
|
|
|
8365
8389
|
if not ask_script.is_file():
|
|
8366
8390
|
raise HTTPException(status_code=503, detail="wiki-ask backend missing")
|
|
8367
8391
|
try:
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8392
|
+
# Offload the blocking subprocess to a thread so the single-worker
|
|
8393
|
+
# uvicorn event loop stays responsive (liveness, status, WS heartbeat)
|
|
8394
|
+
# while wiki-ask runs (up to 180s). A direct subprocess.run here would
|
|
8395
|
+
# freeze the whole server; this read-scoped endpoint is reachable by any
|
|
8396
|
+
# reader. Mirrors the await asyncio.to_thread(...) pattern used by the
|
|
8397
|
+
# stop endpoints.
|
|
8398
|
+
proc = await asyncio.to_thread(
|
|
8399
|
+
lambda: subprocess.run(
|
|
8400
|
+
["python3", str(ask_script), "--root", str(project_root),
|
|
8401
|
+
"--question", req.question, "--k", str(req.k), "--json"],
|
|
8402
|
+
capture_output=True, text=True, timeout=180,
|
|
8403
|
+
cwd=str(project_root),
|
|
8404
|
+
)
|
|
8373
8405
|
)
|
|
8374
8406
|
except (OSError, subprocess.SubprocessError) as e:
|
|
8375
8407
|
raise HTTPException(status_code=503, detail=f"wiki ask failed: {e}")
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -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.41.
|
|
5
|
+
**Version:** v7.41.4
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.41.
|
|
2
|
+
var n6=Object.defineProperty;var a6=($)=>$;function s6($,Q){this[$]=a6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)n6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:s6.bind(Q,Z)})};var L=($,Q)=>()=>($&&(Q=$($=0)),Q);var K$=import.meta.require;var S1={};h(S1,{lokiDir:()=>P,homeLokiDir:()=>o$,findRepoRootForVersion:()=>d$,REPO_ROOT:()=>m});import{resolve as n,dirname as l$}from"path";import{fileURLToPath as t6}from"url";import{existsSync as P$}from"fs";import{homedir as r6}from"os";function i6(){let $=N1;for(let Q=0;Q<6;Q++){if(P$(n($,"VERSION"))&&P$(n($,"autonomy/run.sh")))return $;let Z=l$($);if(Z===$)break;$=Z}return n(N1,"..","..","..")}function d$($){let Q=$;for(let Z=0;Z<6;Z++){if(P$(n(Q,"VERSION"))&&P$(n(Q,"autonomy/run.sh")))return Q;let z=l$(Q);if(z===Q)break;Q=z}return n($,"..","..","..")}function P(){return process.env.LOKI_DIR??n(process.cwd(),".loki")}function o$(){return n(r6(),".loki")}var N1,m;var C=L(()=>{N1=l$(t6(import.meta.url));m=i6()});import{readFileSync as e6}from"fs";import{resolve as $Q,dirname as QQ}from"path";import{fileURLToPath as ZQ}from"url";function F$(){if($$!==null)return $$;let $="7.41.4";if(typeof $==="string"&&$.length>0)return $$=$,$$;try{let Q=QQ(ZQ(import.meta.url)),Z=d$(Q);$$=e6($Q(Z,"VERSION"),"utf-8").trim()}catch{$$="unknown"}return $$}var $$=null;var n$=L(()=>{C()});var C1={};h(C1,{runOrThrow:()=>zQ,run:()=>j,commandVersion:()=>KQ,commandExists:()=>f,ShellError:()=>a$});async function j($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[W,K,U]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:W,stderr:K,exitCode:U}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function zQ($,Q={}){let Z=await j($,Q);if(Z.exitCode!==0)throw new a$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=XQ($),Z=await j(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function XQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function KQ($,Q="--version"){if(!await f($))return null;let z=await j([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var a$;var d=L(()=>{a$=class a$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function a($){return WQ?"":$}var WQ,T,S,I,TZ,w,R,y,q;var c=L(()=>{WQ=(process.env.NO_COLOR??"").length>0;T=a("\x1B[0;31m"),S=a("\x1B[0;32m"),I=a("\x1B[1;33m"),TZ=a("\x1B[0;34m"),w=a("\x1B[0;36m"),R=a("\x1B[1m"),y=a("\x1B[2m"),q=a("\x1B[0m")});import{existsSync as TQ}from"fs";async function Q$(){if(B$!==void 0)return B$;let $="/opt/homebrew/bin/python3.12";if(TQ($))return B$=$,$;let Q=await f("python3.12");if(Q)return B$=Q,Q;let Z=await f("python3");return B$=Z,Z}async function Z$($,Q={}){let Z=await Q$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return j([Z,"-c",$],Q)}var B$;var W$=L(()=>{d()});var t1={};h(t1,{runStatus:()=>gQ});import{existsSync as v,readFileSync as U$,readdirSync as l1,statSync as d1}from"fs";import{resolve as D,basename as xQ}from"path";import{homedir as NQ}from"os";async function DQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${q}
|
|
3
3
|
`),process.stdout.write(`Install with:
|
|
4
4
|
`),process.stdout.write(` brew install jq (macOS)
|
|
5
5
|
`),process.stdout.write(` apt install jq (Debian/Ubuntu)
|
|
@@ -789,4 +789,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
789
789
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
790
790
|
`),process.stderr.write(o6),2}}p1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var ZZ=await QZ(Bun.argv.slice(2));process.exit(ZZ);
|
|
791
791
|
|
|
792
|
-
//# debugId=
|
|
792
|
+
//# debugId=203776A170E08A4F64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -766,6 +766,14 @@ class ConsolidationPipeline:
|
|
|
766
766
|
usage_count=best_match.usage_count,
|
|
767
767
|
last_used=best_match.last_used,
|
|
768
768
|
links=best_match.links.copy(),
|
|
769
|
+
# Preserve retrieval/decay-relevant fields. The constructor previously
|
|
770
|
+
# omitted these, so the merged pattern fell back to schema defaults
|
|
771
|
+
# (importance=0.5, access_count=0, last_accessed=None), resetting a hot,
|
|
772
|
+
# high-importance pattern to the floor on every merge and corrupting
|
|
773
|
+
# apply_decay() + importance-weighted ranking in retrieval.
|
|
774
|
+
importance=max(best_match.importance, new_pattern.importance),
|
|
775
|
+
access_count=best_match.access_count,
|
|
776
|
+
last_accessed=best_match.last_accessed,
|
|
769
777
|
)
|
|
770
778
|
|
|
771
779
|
return merged
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.41.
|
|
4
|
+
"version": "7.41.4",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 11 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.41.
|
|
5
|
+
"version": "7.41.4",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 11 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|