loki-mode 7.113.0 → 7.114.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/completion-council.sh +131 -5
- package/autonomy/prd-checklist.sh +329 -21
- package/autonomy/run.sh +106 -6
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/mcp/lsp_proxy.py +41 -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.114.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -408,4 +408,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
408
408
|
|
|
409
409
|
---
|
|
410
410
|
|
|
411
|
-
**v7.
|
|
411
|
+
**v7.114.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.114.0
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
# LOKI_COUNCIL_THRESHOLD - Votes needed for completion (default: 2)
|
|
26
26
|
# LOKI_COUNCIL_CHECK_INTERVAL - Check every N iterations (default: 5)
|
|
27
27
|
# LOKI_COUNCIL_MIN_ITERATIONS - Minimum iterations before council runs (default: 3)
|
|
28
|
+
# LOKI_COUNCIL_CONVERGENCE_EARLY - No-claim early-check bypass: when evidence is
|
|
29
|
+
# affirmatively green (real suite passed +
|
|
30
|
+
# checklist not failing) the convergence path
|
|
31
|
+
# may evaluate off the CHECK_INTERVAL boundary
|
|
32
|
+
# (>= MIN_ITERATIONS). Default 1; set 0 to
|
|
33
|
+
# restore interval-only behavior (RANK 15).
|
|
28
34
|
# LOKI_COUNCIL_CONVERGENCE_WINDOW - Iterations to track for convergence (default: 3)
|
|
29
35
|
# LOKI_COUNCIL_STAGNATION_LIMIT - Max iterations with no git changes (default: 5)
|
|
30
36
|
# LOKI_COUNCIL_DONE_SIGNAL_LIMIT - Max total done signals before force stop (default: 10)
|
|
@@ -3351,6 +3357,122 @@ PYEOF
|
|
|
3351
3357
|
# Main Entry Point - Should the loop stop?
|
|
3352
3358
|
#===============================================================================
|
|
3353
3359
|
|
|
3360
|
+
#===============================================================================
|
|
3361
|
+
# Convergence-floor early-check helpers (RANK 15)
|
|
3362
|
+
#
|
|
3363
|
+
# Lower the effective convergence floor for the NO-explicit-claim path ONLY.
|
|
3364
|
+
# By default the council convergence path only evaluates on the CHECK_INTERVAL
|
|
3365
|
+
# boundary (every 5th iteration), so a no-promise/analysis run that is verifiably
|
|
3366
|
+
# finished at iteration 2 still grinds to iteration 5 before the council is even
|
|
3367
|
+
# allowed to look. These helpers let that path evaluate as soon as there is
|
|
3368
|
+
# AFFIRMATIVE evidence of done, without weakening any gate: they only flip WHEN
|
|
3369
|
+
# the council evaluates, never WHETHER it approves (council_evaluate still runs
|
|
3370
|
+
# the full checklist / held-out / evidence / assumption gates + the vote + the
|
|
3371
|
+
# devil's advocate on every trigger).
|
|
3372
|
+
#
|
|
3373
|
+
# Two knobs preserve full opt-out to the historical behavior:
|
|
3374
|
+
# LOKI_COUNCIL_CONVERGENCE_EARLY=0 -> disable the early-check bypass entirely
|
|
3375
|
+
# (byte-identical to prior interval-only).
|
|
3376
|
+
# LOKI_COUNCIL_MIN_ITERATIONS -> existing floor (already clamps to >=1);
|
|
3377
|
+
# the early check never fires below it.
|
|
3378
|
+
#===============================================================================
|
|
3379
|
+
|
|
3380
|
+
# _council_convergence_evidence_green: read-only, side-effect-free probe that
|
|
3381
|
+
# returns 0 ONLY when there is AFFIRMATIVE positive evidence a no-promise run is
|
|
3382
|
+
# done -- i.e. a real test suite ran and passed, AND (if a checklist exists) it
|
|
3383
|
+
# has no failing items. Deliberately does NOT reuse council_evidence_gate: that
|
|
3384
|
+
# gate PASSES on inconclusive inputs (no git repo / no baseline / runner=="none"
|
|
3385
|
+
# / missing test-results.json all pass-through). Inconclusive is "not red", not
|
|
3386
|
+
# "green" -- fast-pathing on it would be an unverified early stop. Affirmative
|
|
3387
|
+
# green means tests ACTUALLY ran and passed. Does NOT call
|
|
3388
|
+
# council_reverify_checklist (that has side effects; council_evaluate runs it).
|
|
3389
|
+
_council_convergence_evidence_green() {
|
|
3390
|
+
local tr_file="${TARGET_DIR:-.}/.loki/quality/test-results.json"
|
|
3391
|
+
# Affirmative test-green is REQUIRED: a real runner that passed. A missing
|
|
3392
|
+
# file or runner=="none" (no suite) is NOT affirmative evidence -> not green.
|
|
3393
|
+
[ -f "$tr_file" ] || return 1
|
|
3394
|
+
local tr_state
|
|
3395
|
+
tr_state=$(_TR_FILE="$tr_file" python3 -c "
|
|
3396
|
+
import json, os, sys
|
|
3397
|
+
try:
|
|
3398
|
+
with open(os.environ['_TR_FILE']) as f:
|
|
3399
|
+
d = json.load(f)
|
|
3400
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
3401
|
+
print('no'); sys.exit(0)
|
|
3402
|
+
runner = d.get('runner', 'none')
|
|
3403
|
+
passed = d.get('pass', True)
|
|
3404
|
+
# Affirmative green requires a REAL suite (runner != none) that did not fail.
|
|
3405
|
+
print('yes' if (runner != 'none' and passed is not False) else 'no')
|
|
3406
|
+
" 2>/dev/null || echo "no")
|
|
3407
|
+
[ "$tr_state" = "yes" ] || return 1
|
|
3408
|
+
|
|
3409
|
+
# If a checklist exists, it must have NO failing items to be affirmatively
|
|
3410
|
+
# green. Absence of a checklist is fine (test-green alone carries the signal);
|
|
3411
|
+
# the council's own hard checklist gate still runs inside council_evaluate.
|
|
3412
|
+
local results_file="${TARGET_DIR:-.}/.loki/checklist/verification-results.json"
|
|
3413
|
+
if [ -f "$results_file" ]; then
|
|
3414
|
+
local cl_state
|
|
3415
|
+
cl_state=$(_RESULTS_FILE="$results_file" python3 -c "
|
|
3416
|
+
import json, os, sys
|
|
3417
|
+
try:
|
|
3418
|
+
with open(os.environ['_RESULTS_FILE']) as f:
|
|
3419
|
+
r = json.load(f)
|
|
3420
|
+
except (json.JSONDecodeError, IOError, KeyError, ValueError):
|
|
3421
|
+
print('no'); sys.exit(0)
|
|
3422
|
+
items = [it for cat in r.get('categories', []) for it in cat.get('items', [])]
|
|
3423
|
+
if not items:
|
|
3424
|
+
# A present-but-empty checklist is not affirmative done-evidence.
|
|
3425
|
+
print('no'); sys.exit(0)
|
|
3426
|
+
failing = any(it.get('status') == 'failing' for it in items)
|
|
3427
|
+
print('no' if failing else 'yes')
|
|
3428
|
+
" 2>/dev/null || echo "no")
|
|
3429
|
+
[ "$cl_state" = "yes" ] || return 1
|
|
3430
|
+
fi
|
|
3431
|
+
return 0
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
# _council_should_check_now: pure decision function -- given the current gate
|
|
3435
|
+
# inputs (via env/args) returns 0 if the council should evaluate this iteration,
|
|
3436
|
+
# 1 otherwise. Extracted so the convergence-floor bypass is unit-testable without
|
|
3437
|
+
# driving a real 3-member vote. Inputs (all read from the shell, mirroring
|
|
3438
|
+
# council_should_stop's locals):
|
|
3439
|
+
# $1 = circuit_triggered ("true"/other)
|
|
3440
|
+
# $2 = completion_claimed ("true"/other)
|
|
3441
|
+
# ITERATION_COUNT, COUNCIL_CHECK_INTERVAL, COUNCIL_MIN_ITERATIONS (globals)
|
|
3442
|
+
# LOKI_COUNCIL_CONVERGENCE_EARLY (knob; default on)
|
|
3443
|
+
# Precedence matches council_should_stop: circuit breaker, then explicit claim,
|
|
3444
|
+
# then interval boundary, then the NEW evidence-green early check (last, so it
|
|
3445
|
+
# never changes the explicit-claim or circuit paths).
|
|
3446
|
+
_council_should_check_now() {
|
|
3447
|
+
local circuit_triggered="${1:-false}"
|
|
3448
|
+
local completion_claimed="${2:-false}"
|
|
3449
|
+
local iter="${ITERATION_COUNT:-0}"
|
|
3450
|
+
local interval="${COUNCIL_CHECK_INTERVAL:-5}"
|
|
3451
|
+
local min_iter="${COUNCIL_MIN_ITERATIONS:-3}"
|
|
3452
|
+
|
|
3453
|
+
# Circuit breaker and explicit-claim paths are UNCHANGED (they already bypass
|
|
3454
|
+
# the interval); return check-now for them without touching the early logic.
|
|
3455
|
+
if [ "$circuit_triggered" = "true" ]; then
|
|
3456
|
+
return 0
|
|
3457
|
+
fi
|
|
3458
|
+
if [ "$completion_claimed" = "true" ]; then
|
|
3459
|
+
return 0
|
|
3460
|
+
fi
|
|
3461
|
+
# Standard interval boundary.
|
|
3462
|
+
if [ $((iter % interval)) -eq 0 ]; then
|
|
3463
|
+
return 0
|
|
3464
|
+
fi
|
|
3465
|
+
# NEW convergence-floor early check (no explicit claim): once we are at/above
|
|
3466
|
+
# the MIN_ITERATIONS floor AND there is affirmative evidence of done, evaluate
|
|
3467
|
+
# now instead of waiting for the next interval boundary. Opt-out preserved.
|
|
3468
|
+
if [ "${LOKI_COUNCIL_CONVERGENCE_EARLY:-1}" != "0" ] \
|
|
3469
|
+
&& [ "$iter" -ge "$min_iter" ] 2>/dev/null \
|
|
3470
|
+
&& _council_convergence_evidence_green; then
|
|
3471
|
+
return 0
|
|
3472
|
+
fi
|
|
3473
|
+
return 1
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3354
3476
|
council_should_stop() {
|
|
3355
3477
|
# bash-F1: reset the force-stop sentinel at entry. A return-0 from the
|
|
3356
3478
|
# genuine approval path leaves this 0; only the safety-valve paths below
|
|
@@ -3413,13 +3535,17 @@ council_should_stop() {
|
|
|
3413
3535
|
completion_claimed=true
|
|
3414
3536
|
fi
|
|
3415
3537
|
|
|
3538
|
+
# Decide WHETHER to convene the council this iteration. Precedence (unchanged
|
|
3539
|
+
# for the circuit-breaker and explicit-claim paths): circuit breaker, explicit
|
|
3540
|
+
# completion claim, interval boundary, then the RANK-15 convergence-floor early
|
|
3541
|
+
# check (no explicit claim + affirmative evidence-green -> evaluate now instead
|
|
3542
|
+
# of waiting for the next interval boundary). _council_should_check_now is a
|
|
3543
|
+
# pure decision helper (unit-tested) so this stays a single call.
|
|
3416
3544
|
local should_check=false
|
|
3417
|
-
if [ "$
|
|
3418
|
-
should_check=true
|
|
3419
|
-
elif [ "$completion_claimed" = "true" ]; then
|
|
3420
|
-
should_check=true
|
|
3545
|
+
if [ "$completion_claimed" = "true" ] && [ "$circuit_triggered" != "true" ]; then
|
|
3421
3546
|
log_info "[Council] Completion claimed this iteration -- evaluating now (not deferring to the ${COUNCIL_CHECK_INTERVAL}-iteration interval)"
|
|
3422
|
-
|
|
3547
|
+
fi
|
|
3548
|
+
if _council_should_check_now "$circuit_triggered" "$completion_claimed"; then
|
|
3423
3549
|
should_check=true
|
|
3424
3550
|
fi
|
|
3425
3551
|
|
|
@@ -120,24 +120,43 @@ checklist_init() {
|
|
|
120
120
|
# .loki/checklist/oracle-findings.json and surfaced to the completion council via
|
|
121
121
|
# checklist_as_evidence(), and a best-effort trust event is recorded.
|
|
122
122
|
#
|
|
123
|
-
# Honest scope (
|
|
124
|
-
# IMPLEMENTED
|
|
125
|
-
#
|
|
126
|
-
#
|
|
123
|
+
# Honest scope (source-grounded triangulation, rank-2 backlog):
|
|
124
|
+
# IMPLEMENTED, four legs, each an independent oracle against source reality:
|
|
125
|
+
# (1) DATASTORE conflict -- spec names DB X but the codebase wires a
|
|
126
|
+
# DIFFERENT mutually-exclusive DB Y and NOT X. Highest-signal, zero-FP:
|
|
127
|
+
# choices are mutually exclusive with unambiguous dependency markers.
|
|
128
|
+
# (2) HTTP ROUTE grounding -- every endpoint the spec NAMES must map to a
|
|
129
|
+
# real route/handler discovered in source. Fires High ONLY when source
|
|
130
|
+
# yields a recognizable route set AND a specific spec endpoint is
|
|
131
|
+
# provably absent from it. If ZERO routes parse out of source we
|
|
132
|
+
# fail-open (no finding): un-parsed framework syntax must never read as
|
|
133
|
+
# "route missing". Path params are normalized across frameworks
|
|
134
|
+
# (/orders/:id ~ /orders/{id} ~ /orders/<id>).
|
|
135
|
+
# (3) PUBLIC API SYMBOL existence -- symbols the spec names as public API are
|
|
136
|
+
# verified to EXIST via the LSP proxy (mcp.lsp_proxy --check-symbols,
|
|
137
|
+
# the same one-shot contract as the LSP-diagnostics gate), NOT via an
|
|
138
|
+
# LLM-authored grep. When no language server is reachable this leg is
|
|
139
|
+
# INCONCLUSIVE (fail-open, never a fabricated pass or false High).
|
|
140
|
+
# (4) DOMAIN INVARIANT -- >=1 high-value invariant enforced deterministically.
|
|
141
|
+
# First invariant: plaintext-password-store detection (a password column
|
|
142
|
+
# / field persisted without a hash function anywhere near it). Fires High
|
|
143
|
+
# only on positive evidence of storage-without-hashing.
|
|
127
144
|
#
|
|
128
|
-
# DEFERRED (documented follow-up, NOT faked):
|
|
129
|
-
#
|
|
130
|
-
# PII
|
|
131
|
-
#
|
|
132
|
-
# detection to avoid false positives; a regex grep would be noisy. They are
|
|
133
|
-
# intentionally left out of this slice. Backlog: P2-3 follow-up "domain-
|
|
134
|
-
# invariant oracle". See the conflict-detection design here as the template.
|
|
145
|
+
# DEFERRED (documented follow-up, NOT faked): additional stack axes (web
|
|
146
|
+
# framework, language runtime) and further domain invariants (money-not-float,
|
|
147
|
+
# PII-encryption). These need a broader catalog plus per-language AST-aware
|
|
148
|
+
# detection to stay zero-FP; left out of this slice.
|
|
135
149
|
#
|
|
136
150
|
# Non-conflict cases that must NOT fire (guarded + tested):
|
|
137
151
|
# - spec asserts DB X, codebase has nothing wired yet (greenfield): ABSENT is
|
|
138
152
|
# not a contradiction -> no finding.
|
|
139
153
|
# - spec asserts DB X, codebase wires DB X: agreement -> no finding.
|
|
140
154
|
# - spec asserts nothing about a datastore: no spec oracle -> no finding.
|
|
155
|
+
# - spec names an endpoint that DOES exist in source -> no finding.
|
|
156
|
+
# - source has NO parseable routes at all (greenfield / unknown framework) ->
|
|
157
|
+
# route leg fails open, no finding.
|
|
158
|
+
# - no language server on PATH -> symbol leg inconclusive, no finding.
|
|
159
|
+
# - passwords stored WITH a hash near them -> invariant leg does not fire.
|
|
141
160
|
#
|
|
142
161
|
# Opt-out: LOKI_CHECKLIST_ORACLE=0 (default on). When off, this is a no-op.
|
|
143
162
|
|
|
@@ -158,6 +177,15 @@ checklist_oracle_triangulate() {
|
|
|
158
177
|
local project_dir
|
|
159
178
|
project_dir="$(pwd)"
|
|
160
179
|
|
|
180
|
+
# Loki install dir (where the `mcp` package lives) so the symbol leg can shell
|
|
181
|
+
# out to `python3 -m mcp.lsp_proxy` with the correct import path. This file is
|
|
182
|
+
# autonomy/prd-checklist.sh, so the install root is its parent's parent. When
|
|
183
|
+
# sourced by run.sh, LOKI_ORIGINAL_PROJECT_DIR already points there; prefer it.
|
|
184
|
+
local oracle_install_dir="${LOKI_ORIGINAL_PROJECT_DIR:-}"
|
|
185
|
+
if [ -z "$oracle_install_dir" ]; then
|
|
186
|
+
oracle_install_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)"
|
|
187
|
+
fi
|
|
188
|
+
|
|
161
189
|
# Feed the detector to python3 via a quoted heredoc (delimiter in quotes) so
|
|
162
190
|
# bash performs NO interpolation: no dollar-digit footgun, no quote-escaping
|
|
163
191
|
# hazards. All inputs arrive through _ORACLE_* env vars.
|
|
@@ -165,6 +193,7 @@ checklist_oracle_triangulate() {
|
|
|
165
193
|
status_token=$(_ORACLE_SPEC="$CHECKLIST_PRD_PATH" \
|
|
166
194
|
_ORACLE_OUT="$findings_file" \
|
|
167
195
|
_ORACLE_PROJECT="$project_dir" \
|
|
196
|
+
_ORACLE_INSTALL_DIR="$oracle_install_dir" \
|
|
168
197
|
python3 - <<'ORACLE_PY' 2>/dev/null || echo "NOOP"
|
|
169
198
|
import json, os, re, sys, tempfile, glob
|
|
170
199
|
|
|
@@ -234,12 +263,11 @@ for name, cfg in DATASTORES.items():
|
|
|
234
263
|
# If the spec names exactly one PRIMARY datastore, we can triangulate it. If it
|
|
235
264
|
# names several primaries, the spec itself is ambiguous about the datastore;
|
|
236
265
|
# triangulation of "the" choice is unsound, so skip (spec-interrogation owns
|
|
237
|
-
# spec-internal ambiguity, not this oracle).
|
|
266
|
+
# spec-internal ambiguity, not this oracle). spec_db is None when there is no
|
|
267
|
+
# single primary datastore to triangulate -- the datastore leg then contributes
|
|
268
|
+
# nothing, but the route/symbol/invariant legs below still run.
|
|
238
269
|
spec_primary = sorted(spec_asserts - SECONDARY)
|
|
239
|
-
if len(spec_primary)
|
|
240
|
-
print("NO_SPEC_DB")
|
|
241
|
-
sys.exit(0)
|
|
242
|
-
spec_db = spec_primary[0]
|
|
270
|
+
spec_db = spec_primary[0] if len(spec_primary) == 1 else None
|
|
243
271
|
|
|
244
272
|
# --- Reality oracle: scan a bounded set of dependency/lock/source manifests for
|
|
245
273
|
# datastore markers. We scan manifests first (highest signal, lowest noise). ---
|
|
@@ -299,11 +327,13 @@ for name in DATASTORES:
|
|
|
299
327
|
|
|
300
328
|
findings = []
|
|
301
329
|
|
|
330
|
+
# --- Leg 1: DATASTORE conflict ---
|
|
302
331
|
# Conflict iff: spec asserts spec_db, AND reality shows a DIFFERENT primary db,
|
|
303
332
|
# AND reality does NOT also show the spec db. Absent reality (greenfield) and
|
|
304
|
-
# agreement both yield no finding.
|
|
305
|
-
|
|
306
|
-
if
|
|
333
|
+
# agreement both yield no finding. Skipped entirely when the spec names no single
|
|
334
|
+
# primary datastore (spec_db is None).
|
|
335
|
+
contradicting = sorted(d for d in reality_dbs if d != spec_db) if spec_db else []
|
|
336
|
+
if spec_db and contradicting and spec_db not in reality_dbs:
|
|
307
337
|
findings.append({
|
|
308
338
|
"id": "oracle-datastore-conflict",
|
|
309
339
|
"dimension": "spec_vs_reality",
|
|
@@ -321,14 +351,292 @@ if contradicting and spec_db not in reality_dbs:
|
|
|
321
351
|
% (spec_db, ", ".join(contradicting), spec_db)),
|
|
322
352
|
})
|
|
323
353
|
|
|
354
|
+
# ===========================================================================
|
|
355
|
+
# Leg 2: HTTP ROUTE grounding -- spec endpoints must map to real handlers.
|
|
356
|
+
# ===========================================================================
|
|
357
|
+
# Same finding shape as oracle-datastore-conflict (dimension=spec_vs_reality,
|
|
358
|
+
# severity=high). FP-avoidance: this leg fires on ABSENCE, which is more FP-prone
|
|
359
|
+
# than the datastore contradiction. Two hard guards keep it zero-FP:
|
|
360
|
+
# (a) only run when source yields a RECOGNIZABLE route set (>=1 real route);
|
|
361
|
+
# zero parsed routes -> greenfield / unknown framework -> fail-open.
|
|
362
|
+
# (b) normalize path params across frameworks so /orders/:id, /orders/{id} and
|
|
363
|
+
# /orders/<id> all compare equal; only a genuinely absent path fires.
|
|
364
|
+
|
|
365
|
+
# Normalize an HTTP path for cross-framework comparison: lowercase, strip a
|
|
366
|
+
# trailing slash, collapse any param segment (:id, {id}, <id>, <int:id>) to "*".
|
|
367
|
+
# Trailing sentence punctuation is stripped per-segment so a spec path written
|
|
368
|
+
# mid-prose ("GET /healthz.") normalizes identically to the source "/healthz".
|
|
369
|
+
_param_seg = re.compile(r"^(:[^/]+|\{[^/}]+\}|<[^/>]+>)$")
|
|
370
|
+
# Strip ONLY sentence punctuation prose glues onto a path. Deliberately EXCLUDES
|
|
371
|
+
# the param close characters } > ) ] so that "{id}." -> "{id}" (still a param),
|
|
372
|
+
# never "{id" (broken). Quotes/backticks are stripped (markdown code spans).
|
|
373
|
+
_trailing_punct = ".,;:!?'\"`"
|
|
374
|
+
def norm_path(p):
|
|
375
|
+
p = p.strip().strip('"').strip("'").strip("`")
|
|
376
|
+
# keep only the path portion (drop querystring / trailing text)
|
|
377
|
+
p = p.split("?")[0].split("#")[0]
|
|
378
|
+
if not p.startswith("/"):
|
|
379
|
+
return None
|
|
380
|
+
segs = [s for s in p.split("/") if s != ""]
|
|
381
|
+
out = []
|
|
382
|
+
for s in segs:
|
|
383
|
+
# strip trailing sentence punctuation ("/healthz." -> "healthz",
|
|
384
|
+
# "{id}." -> "{id}") without touching a param's close brace.
|
|
385
|
+
s = s.rstrip(_trailing_punct)
|
|
386
|
+
if not s:
|
|
387
|
+
continue
|
|
388
|
+
out.append("*" if _param_seg.match(s) else s.lower())
|
|
389
|
+
return "/" + "/".join(out)
|
|
390
|
+
|
|
391
|
+
# Spec endpoints: match METHOD + PATH pairs the spec explicitly names, e.g.
|
|
392
|
+
# "GET /orders", "POST /users/{id}". Requiring a leading HTTP verb keeps this
|
|
393
|
+
# from grabbing arbitrary slashes in prose (file paths, URLs).
|
|
394
|
+
SPEC_ENDPOINT_RE = re.compile(
|
|
395
|
+
r"(?i)\b(GET|POST|PUT|PATCH|DELETE)\s+(/[A-Za-z0-9_\-/:{}<>*.]*)")
|
|
396
|
+
spec_endpoints = set()
|
|
397
|
+
for m in SPEC_ENDPOINT_RE.finditer(spec_text):
|
|
398
|
+
np = norm_path(m.group(2))
|
|
399
|
+
if np:
|
|
400
|
+
spec_endpoints.add(np)
|
|
401
|
+
|
|
402
|
+
# Discover real routes in source across common web frameworks. Each pattern
|
|
403
|
+
# captures the path string literal. We scan the same bounded source set as the
|
|
404
|
+
# datastore leg (SKIP_DIRS, cap files) to stay fast.
|
|
405
|
+
ROUTE_PATTERNS = [
|
|
406
|
+
# express / fastify / koa-router / node: app.get("/x"), router.post('/x')
|
|
407
|
+
re.compile(r"""(?i)\b(?:app|router|route|api|server|fastify)\s*\.\s*(?:get|post|put|patch|delete|all|route)\s*\(\s*['"`]([^'"`]+)['"`]"""),
|
|
408
|
+
# flask: @app.route("/x"), @bp.route('/x')
|
|
409
|
+
re.compile(r"""(?i)@\s*\w+\.route\s*\(\s*['"]([^'"]+)['"]"""),
|
|
410
|
+
# fastapi / starlette: @app.get("/x"), @router.post('/x')
|
|
411
|
+
re.compile(r"""(?i)@\s*\w+\.(?:get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]"""),
|
|
412
|
+
# django path()/re_path()/url(): path("orders/", ...) (may be relative)
|
|
413
|
+
re.compile(r"""(?i)\b(?:re_)?path\s*\(\s*r?['"]([^'"]*)['"]"""),
|
|
414
|
+
re.compile(r"""(?i)\burl\s*\(\s*r?['"]\^?([^'"$]*)['"]"""),
|
|
415
|
+
# spring / jax-rs annotations: @GetMapping("/x") @RequestMapping("/x") @Path("/x")
|
|
416
|
+
re.compile(r"""(?i)@(?:Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*(?:value\s*=\s*)?['"]([^'"]+)['"]"""),
|
|
417
|
+
re.compile(r"""(?i)@Path\s*\(\s*['"]([^'"]+)['"]"""),
|
|
418
|
+
# go: mux.HandleFunc("/x", ...), r.Get("/x", ...), e.GET("/x", ...)
|
|
419
|
+
re.compile(r"""(?i)\.\s*(?:HandleFunc|Handle|Get|Post|Put|Patch|Delete|GET|POST|PUT|PATCH|DELETE)\s*\(\s*['"]([^'"]+)['"]"""),
|
|
420
|
+
# ruby on rails routes.rb: get "/x", post 'x'
|
|
421
|
+
re.compile(r"""(?i)^\s*(?:get|post|put|patch|delete)\s+['"]([^'"]+)['"]"""),
|
|
422
|
+
]
|
|
423
|
+
SOURCE_EXTS = (".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".go",
|
|
424
|
+
".rb", ".java", ".kt", ".php", ".rs")
|
|
425
|
+
|
|
426
|
+
def discover_routes(cap_files=800):
|
|
427
|
+
found = set()
|
|
428
|
+
seen = 0
|
|
429
|
+
for root, dirs, files in os.walk(project):
|
|
430
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
431
|
+
for fn in files:
|
|
432
|
+
if not (fn.endswith(SOURCE_EXTS) or fn == "routes.rb" or fn == "urls.py"):
|
|
433
|
+
continue
|
|
434
|
+
seen += 1
|
|
435
|
+
if seen > cap_files:
|
|
436
|
+
break
|
|
437
|
+
txt = read_text(os.path.join(root, fn), 120000)
|
|
438
|
+
for pat in ROUTE_PATTERNS:
|
|
439
|
+
for mm in pat.finditer(txt):
|
|
440
|
+
raw = mm.group(1)
|
|
441
|
+
if not raw:
|
|
442
|
+
continue
|
|
443
|
+
# django paths are often relative ("orders/"); normalize by
|
|
444
|
+
# prefixing a slash so they compare on the same axis.
|
|
445
|
+
cand = raw if raw.startswith("/") else "/" + raw
|
|
446
|
+
np = norm_path(cand)
|
|
447
|
+
if np:
|
|
448
|
+
found.add(np)
|
|
449
|
+
else:
|
|
450
|
+
continue
|
|
451
|
+
break
|
|
452
|
+
return found
|
|
453
|
+
|
|
454
|
+
if spec_endpoints:
|
|
455
|
+
real_routes = discover_routes()
|
|
456
|
+
# Guard (a): only fire when source yields a recognizable route set. Zero
|
|
457
|
+
# discovered routes means we could not parse this framework -> fail-open.
|
|
458
|
+
if real_routes:
|
|
459
|
+
missing = sorted(e for e in spec_endpoints if e not in real_routes)
|
|
460
|
+
if missing:
|
|
461
|
+
findings.append({
|
|
462
|
+
"id": "oracle-route-missing",
|
|
463
|
+
"dimension": "spec_vs_reality",
|
|
464
|
+
"axis": "http_route",
|
|
465
|
+
"severity": "high",
|
|
466
|
+
"spec_asserts": missing,
|
|
467
|
+
"codebase_reality": sorted(real_routes)[:20],
|
|
468
|
+
"title": "Spec names endpoint(s) with no handler in source: %s" % (
|
|
469
|
+
", ".join(missing)),
|
|
470
|
+
"detail": ("The spec/PRD names the HTTP endpoint(s) %s, but no "
|
|
471
|
+
"matching route/handler was discovered in the codebase "
|
|
472
|
+
"(source declares %d route(s), none matching after path-"
|
|
473
|
+
"param normalization). Acceptance criteria derived from "
|
|
474
|
+
"the spec alone would pass while the endpoint is not "
|
|
475
|
+
"actually served. Wire the handler or correct the spec "
|
|
476
|
+
"before completion."
|
|
477
|
+
% (", ".join(missing), len(real_routes))),
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
# ===========================================================================
|
|
481
|
+
# Leg 4: DOMAIN INVARIANT -- plaintext password storage detection.
|
|
482
|
+
# ===========================================================================
|
|
483
|
+
# Deterministic, positive-evidence-only: fire High only when source shows a
|
|
484
|
+
# password value being PERSISTED (assigned into a stored record / column / insert)
|
|
485
|
+
# with NO hashing function anywhere nearby. Presence of a known hash primitive in
|
|
486
|
+
# the same file suppresses the finding (the app hashes -> not plaintext).
|
|
487
|
+
HASH_MARKERS = re.compile(
|
|
488
|
+
r"(?i)\b(bcrypt|scrypt|argon2|argon2id|pbkdf2|"
|
|
489
|
+
r"passlib|hashpw|generate_password_hash|password_hash|"
|
|
490
|
+
r"crypto\.(?:pbkdf2|scrypt)|sha256_crypt|make_password)\b")
|
|
491
|
+
# A password being written into a persisted structure. Kept tight to avoid FPs:
|
|
492
|
+
# require the word "password" (NOT password_hash / hashed_password / passwordHash)
|
|
493
|
+
# as an assigned key/column/value, near a persistence verb or a column decl.
|
|
494
|
+
# The negative lookahead/behind on _hash / hashed_ is what keeps hashed-password
|
|
495
|
+
# code (the correct case) from tripping the leg.
|
|
496
|
+
PW_STORE_RE = re.compile(
|
|
497
|
+
r"(?i)("
|
|
498
|
+
r"(?<!_)(?<!hashed_)password(?!_hash)(?!_digest)\s*[:=]\s*[^\n,;)]+|" # password = value
|
|
499
|
+
r"['\"](?<!_)password(?!_hash)['\"]\s*:\s*[^\n,}]+|" # "password": value
|
|
500
|
+
r"(?<!_)password(?!_hash)\s+(?:VARCHAR|TEXT|CHAR|STRING)\b|" # SQL column decl
|
|
501
|
+
r"\.\s*(?<!_)password(?!_hash)\s*=\s*[^\n;]+|" # obj.password = value
|
|
502
|
+
r"INSERT\s+INTO[^;]*?\(\s*[^)]*\bpassword\b[^)]*\)|" # INSERT (... password ...)
|
|
503
|
+
r"CREATE\s+TABLE[^;]*?\bpassword\b" # CREATE TABLE ... password
|
|
504
|
+
r")")
|
|
505
|
+
PERSIST_VERB = re.compile(
|
|
506
|
+
r"(?i)\b(INSERT\s+INTO|\.save\(|\.create\(|\.insert\(|"
|
|
507
|
+
r"session\.add\(|db\.|repository\.|\.execute\(|CREATE\s+TABLE|"
|
|
508
|
+
r"VALUES\s*\()")
|
|
509
|
+
|
|
510
|
+
def detect_plaintext_password(cap_files=800):
|
|
511
|
+
seen = 0
|
|
512
|
+
for root, dirs, files in os.walk(project):
|
|
513
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
514
|
+
for fn in files:
|
|
515
|
+
if not (fn.endswith(SOURCE_EXTS) or fn.endswith(".sql")):
|
|
516
|
+
continue
|
|
517
|
+
seen += 1
|
|
518
|
+
if seen > cap_files:
|
|
519
|
+
return None
|
|
520
|
+
path = os.path.join(root, fn)
|
|
521
|
+
txt = read_text(path, 120000)
|
|
522
|
+
if not PW_STORE_RE.search(txt):
|
|
523
|
+
continue
|
|
524
|
+
# Suppress when the same file hashes passwords (positive evidence the
|
|
525
|
+
# app is not storing plaintext).
|
|
526
|
+
if HASH_MARKERS.search(txt):
|
|
527
|
+
continue
|
|
528
|
+
# Require a persistence signal in the same file so we only flag
|
|
529
|
+
# STORED passwords, not e.g. a login form field or a request parse.
|
|
530
|
+
if PERSIST_VERB.search(txt):
|
|
531
|
+
rel = os.path.relpath(path, project)
|
|
532
|
+
return rel
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
# Only run the invariant when the spec is about something that stores passwords
|
|
536
|
+
# (auth / users / login / register / credential). This keeps the leg from
|
|
537
|
+
# scanning unrelated projects and firing on incidental matches.
|
|
538
|
+
if re.search(r"(?i)\b(password|auth|login|register|credential|sign[\s-]?up|user account)\b", spec_text):
|
|
539
|
+
pw_file = detect_plaintext_password()
|
|
540
|
+
if pw_file:
|
|
541
|
+
findings.append({
|
|
542
|
+
"id": "oracle-invariant-plaintext-password",
|
|
543
|
+
"dimension": "domain_invariant",
|
|
544
|
+
"axis": "auth_password_hashing",
|
|
545
|
+
"severity": "high",
|
|
546
|
+
"spec_asserts": "passwords stored securely (hashed)",
|
|
547
|
+
"codebase_reality": [pw_file],
|
|
548
|
+
"title": "Password appears stored without hashing in %s" % pw_file,
|
|
549
|
+
"detail": ("Source in %s persists a password value with no hashing "
|
|
550
|
+
"primitive (bcrypt/scrypt/argon2/pbkdf2/passlib/...) present "
|
|
551
|
+
"in that file. Storing plaintext (or reversibly-encrypted) "
|
|
552
|
+
"passwords is a domain-invariant violation the spec's prose "
|
|
553
|
+
"acceptance checks would not catch. Hash passwords before "
|
|
554
|
+
"persistence."
|
|
555
|
+
% pw_file),
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
# ===========================================================================
|
|
559
|
+
# Leg 3: PUBLIC API SYMBOL existence via the LSP proxy (NOT LLM grep).
|
|
560
|
+
# ===========================================================================
|
|
561
|
+
# The spec may name public API symbols ("exports parseConfig", "public function
|
|
562
|
+
# createOrder"). We verify each EXISTS in the workspace via the LSP proxy's
|
|
563
|
+
# one-shot --check-symbols subcommand (same cwd/--root contract as the LSP-
|
|
564
|
+
# diagnostics quality gate). This is deterministic ground truth, not an LLM
|
|
565
|
+
# guessing from the same prose. HONEST DEGRADATION: when no language server is
|
|
566
|
+
# reachable, the probe returns available=false and every verdict is null; we
|
|
567
|
+
# record the leg as INCONCLUSIVE and emit NO finding (never a fabricated pass,
|
|
568
|
+
# never a false High). Symbols with a definite exists=false verdict -> High.
|
|
569
|
+
symbol_leg = {"ran": False, "available": None, "checked": [], "missing": []}
|
|
570
|
+
|
|
571
|
+
# Extract candidate public-API symbol names the spec explicitly calls out. We
|
|
572
|
+
# require an anchoring keyword so we only probe names the spec asserts as public
|
|
573
|
+
# API, not every identifier in prose.
|
|
574
|
+
SPEC_SYMBOL_RE = re.compile(
|
|
575
|
+
r"(?i)\b(?:public|export(?:s|ed)?|expose[sd]?|api|function|method|class|endpoint symbol)\b"
|
|
576
|
+
r"[^\n`]*?`?\b([A-Za-z_][A-Za-z0-9_]{2,})`?\s*\(")
|
|
577
|
+
spec_symbols = []
|
|
578
|
+
for m in SPEC_SYMBOL_RE.finditer(spec_text):
|
|
579
|
+
nm = m.group(1)
|
|
580
|
+
if nm and nm.lower() not in ("the", "this", "that", "public", "export", "function", "method", "class") and nm not in spec_symbols:
|
|
581
|
+
spec_symbols.append(nm)
|
|
582
|
+
spec_symbols = spec_symbols[:25] # bound the probe
|
|
583
|
+
|
|
584
|
+
if spec_symbols:
|
|
585
|
+
install_dir = os.environ.get("_ORACLE_INSTALL_DIR", "")
|
|
586
|
+
# Only attempt the probe when we know the loki install dir (so `-m
|
|
587
|
+
# mcp.lsp_proxy` imports). Without it we cannot reach the proxy -> honest
|
|
588
|
+
# inconclusive, no finding.
|
|
589
|
+
if install_dir and os.path.isdir(os.path.join(install_dir, "mcp")):
|
|
590
|
+
import subprocess
|
|
591
|
+
try:
|
|
592
|
+
proc = subprocess.run(
|
|
593
|
+
[sys.executable, "-m", "mcp.lsp_proxy",
|
|
594
|
+
"--check-symbols", "--root", project],
|
|
595
|
+
input="\n".join(spec_symbols),
|
|
596
|
+
cwd=install_dir,
|
|
597
|
+
capture_output=True, text=True, timeout=90,
|
|
598
|
+
)
|
|
599
|
+
probe = json.loads((proc.stdout or "").strip().splitlines()[-1]) if proc.stdout.strip() else {}
|
|
600
|
+
except Exception:
|
|
601
|
+
probe = {}
|
|
602
|
+
symbol_leg["available"] = bool(probe.get("available"))
|
|
603
|
+
symbol_leg["ran"] = True
|
|
604
|
+
res = probe.get("results", {}) or {}
|
|
605
|
+
symbol_leg["checked"] = sorted(res.keys())
|
|
606
|
+
if symbol_leg["available"]:
|
|
607
|
+
# Only a DEFINITE exists=false (not null) is a miss.
|
|
608
|
+
miss = sorted(k for k, v in res.items() if v is False)
|
|
609
|
+
symbol_leg["missing"] = miss
|
|
610
|
+
if miss:
|
|
611
|
+
findings.append({
|
|
612
|
+
"id": "oracle-symbol-missing",
|
|
613
|
+
"dimension": "spec_vs_reality",
|
|
614
|
+
"axis": "public_api_symbol",
|
|
615
|
+
"severity": "high",
|
|
616
|
+
"spec_asserts": miss,
|
|
617
|
+
"codebase_reality": "symbol(s) not found by LSP workspace search",
|
|
618
|
+
"title": "Spec names public API symbol(s) the LSP cannot find: %s" % (
|
|
619
|
+
", ".join(miss)),
|
|
620
|
+
"detail": ("The spec/PRD names %s as public API, but the LSP "
|
|
621
|
+
"workspace symbol search (ground truth, not an LLM "
|
|
622
|
+
"grep) reports they do not exist. Acceptance criteria "
|
|
623
|
+
"derived from the spec alone would pass against a "
|
|
624
|
+
"missing symbol. Implement the symbol or correct the "
|
|
625
|
+
"spec before completion."
|
|
626
|
+
% ", ".join(miss)),
|
|
627
|
+
})
|
|
628
|
+
|
|
324
629
|
result = {
|
|
325
|
-
"version":
|
|
630
|
+
"version": 2,
|
|
326
631
|
"spec_path": spec_path,
|
|
327
632
|
"spec_datastore": spec_db,
|
|
328
633
|
"reality_datastores": sorted(reality_dbs),
|
|
634
|
+
"spec_endpoints": sorted(spec_endpoints),
|
|
635
|
+
"spec_symbols": spec_symbols,
|
|
636
|
+
"symbol_leg": symbol_leg,
|
|
329
637
|
"findings": findings,
|
|
330
638
|
"deferred": [
|
|
331
|
-
"domain
|
|
639
|
+
"additional domain invariants (money-not-float, PII-encryption)",
|
|
332
640
|
"additional stack axes (web framework, language runtime)",
|
|
333
641
|
],
|
|
334
642
|
}
|
package/autonomy/run.sh
CHANGED
|
@@ -10490,6 +10490,23 @@ _severity_is_blocking() {
|
|
|
10490
10490
|
grep -qiE '(\[(critical|high)\])|(\*\*[[:space:]]*(critical|high)[[:space:]]*\*\*)|(severity:?[[:space:]]*(critical|high))|(^[[:space:]]*[-*][[:space:]]+(critical|high)([[:space:]:.,*]|$))' "$file"
|
|
10491
10491
|
}
|
|
10492
10492
|
|
|
10493
|
+
# 7.114.0 (rank 9): count non-blocking (Medium/Low) findings in a reviewer file.
|
|
10494
|
+
# Feeds the weighted mergeability quality score. Mirrors the severity-token
|
|
10495
|
+
# tolerance of _severity_is_blocking (bracketed, bold, 'Severity:', or bullet).
|
|
10496
|
+
# Echoes "<medium_count> <low_count>" so the caller can weight them differently.
|
|
10497
|
+
# Parity-locked with countNonBlockingFindings() in loki-ts/src/runner/quality_gates.ts.
|
|
10498
|
+
_count_nonblocking_findings() {
|
|
10499
|
+
local file="$1"
|
|
10500
|
+
if [ ! -f "$file" ]; then
|
|
10501
|
+
echo "0 0"
|
|
10502
|
+
return 0
|
|
10503
|
+
fi
|
|
10504
|
+
local med low
|
|
10505
|
+
med=$(grep -icE '(\[medium\])|(\*\*[[:space:]]*medium[[:space:]]*\*\*)|(severity:?[[:space:]]*medium)|(^[[:space:]]*[-*][[:space:]]+medium([[:space:]:.,*]|$))' "$file")
|
|
10506
|
+
low=$(grep -icE '(\[low\])|(\*\*[[:space:]]*low[[:space:]]*\*\*)|(severity:?[[:space:]]*low)|(^[[:space:]]*[-*][[:space:]]+low([[:space:]:.,*]|$))' "$file")
|
|
10507
|
+
echo "${med:-0} ${low:-0}"
|
|
10508
|
+
}
|
|
10509
|
+
|
|
10493
10510
|
run_code_review() {
|
|
10494
10511
|
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
10495
10512
|
local review_dir="$loki_dir/quality/reviews"
|
|
@@ -10638,7 +10655,7 @@ MANAGED_SELECTION
|
|
|
10638
10655
|
log_warn "Managed review council unavailable; falling back to CLI fan-out"
|
|
10639
10656
|
fi
|
|
10640
10657
|
|
|
10641
|
-
log_info "Selecting
|
|
10658
|
+
log_info "Selecting reviewers (architecture-strategist + maintainer-mergeability always on, plus keyword-scored specialists)..."
|
|
10642
10659
|
|
|
10643
10660
|
# Write diff/files to temp files for python to read (avoid env var size limits)
|
|
10644
10661
|
# Use printf to prevent shell variable expansion in diff content (#78)
|
|
@@ -10770,13 +10787,25 @@ if all(s == 0 for s in scores.values()):
|
|
|
10770
10787
|
else:
|
|
10771
10788
|
selected = ranked[:2]
|
|
10772
10789
|
|
|
10773
|
-
# Output JSON: architecture-strategist always first
|
|
10790
|
+
# Output JSON: architecture-strategist + maintainer-mergeability always first
|
|
10791
|
+
# (both carry a mandate no keyword-selected specialist does), then the 2 selected.
|
|
10792
|
+
# 7.114.0 (rank 9): maintainer-mergeability is the "would a real maintainer merge
|
|
10793
|
+
# this PR" reviewer. It covers scope creep, dead/duplicated code, and conformance
|
|
10794
|
+
# to the surrounding code's conventions -- the tech-lead axes the security/test/
|
|
10795
|
+
# perf/dependency/architecture pool misses. Its findings feed the SAME
|
|
10796
|
+
# Critical/High=block, Medium/Low=non-blocking mechanism and additionally the
|
|
10797
|
+
# weighted quality score in aggregate.json (rank 9).
|
|
10774
10798
|
result = {
|
|
10775
10799
|
"reviewers": [
|
|
10776
10800
|
{
|
|
10777
10801
|
"name": "architecture-strategist",
|
|
10778
10802
|
"focus": "SOLID, coupling, cohesion, patterns, abstraction, dependency direction",
|
|
10779
10803
|
"checks": "SOLID violations, excessive coupling, wrong patterns, missing abstractions, dependency direction issues, god classes/functions"
|
|
10804
|
+
},
|
|
10805
|
+
{
|
|
10806
|
+
"name": "maintainer-mergeability",
|
|
10807
|
+
"focus": "Would a maintainer merge this PR as-is: scope discipline, dead/duplicated code, convention conformance",
|
|
10808
|
+
"checks": "scope creep (changes unrelated to the stated task, drive-by edits, unrequested refactors), dead code (unreachable, unused, commented-out, leftover debug), duplicated logic that should reuse an existing helper, non-conformance to the surrounding code's conventions (naming, error handling, structure, formatting), and anything a careful human reviewer would ask to be changed before merging"
|
|
10780
10809
|
}
|
|
10781
10810
|
] + [
|
|
10782
10811
|
{
|
|
@@ -10812,7 +10841,9 @@ SPECIALIST_SELECT
|
|
|
10812
10841
|
"reviewers=$reviewer_names" \
|
|
10813
10842
|
"iteration=$ITERATION_COUNT"
|
|
10814
10843
|
|
|
10815
|
-
# Dispatch
|
|
10844
|
+
# Dispatch all selected reviewers as parallel blind reviews (provider-specific
|
|
10845
|
+
# invocation). Count is dynamic: 2 always-on (architecture-strategist,
|
|
10846
|
+
# maintainer-mergeability) + up to 2 keyword-scored specialists.
|
|
10816
10847
|
local pids=()
|
|
10817
10848
|
local reviewer_count
|
|
10818
10849
|
reviewer_count=$(echo "$selected_specialists" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['reviewers']))")
|
|
@@ -10906,6 +10937,10 @@ BUILD_PROMPT
|
|
|
10906
10937
|
# Such a review proves nothing, so we treat it as INCONCLUSIVE -> blocking.
|
|
10907
10938
|
local real_verdict_count=0
|
|
10908
10939
|
local no_output_count=0
|
|
10940
|
+
# 7.114.0 (rank 9): accumulate non-blocking (Medium/Low) findings across all
|
|
10941
|
+
# reviewers to compute the weighted mergeability quality score below.
|
|
10942
|
+
local nonblocking_medium=0
|
|
10943
|
+
local nonblocking_low=0
|
|
10909
10944
|
|
|
10910
10945
|
for i in $(seq 0 $((reviewer_count - 1))); do
|
|
10911
10946
|
local reviewer_name
|
|
@@ -10945,6 +10980,16 @@ BUILD_PROMPT
|
|
|
10945
10980
|
continue
|
|
10946
10981
|
fi
|
|
10947
10982
|
((real_verdict_count++))
|
|
10983
|
+
# 7.114.0 (rank 9): tally this reviewer's non-blocking (Medium/Low)
|
|
10984
|
+
# findings for the weighted quality score. Counted for BOTH PASS and FAIL
|
|
10985
|
+
# reviewers -- a PASS verdict can still list Low-severity nits that should
|
|
10986
|
+
# lower the mergeability score without blocking the gate.
|
|
10987
|
+
local _nb_counts _nb_med _nb_low
|
|
10988
|
+
_nb_counts=$(_count_nonblocking_findings "$review_output")
|
|
10989
|
+
_nb_med=${_nb_counts%% *}
|
|
10990
|
+
_nb_low=${_nb_counts##* }
|
|
10991
|
+
nonblocking_medium=$((nonblocking_medium + _nb_med))
|
|
10992
|
+
nonblocking_low=$((nonblocking_low + _nb_low))
|
|
10948
10993
|
if [ "$verdict" = "FAIL" ]; then
|
|
10949
10994
|
((fail_count++))
|
|
10950
10995
|
# Check for Critical/High severity findings (bracketed OR unbracketed
|
|
@@ -10988,6 +11033,23 @@ BUILD_PROMPT
|
|
|
10988
11033
|
fi
|
|
10989
11034
|
fi
|
|
10990
11035
|
|
|
11036
|
+
# 7.114.0 (rank 9): weighted mergeability quality score (FrontierCode-style).
|
|
11037
|
+
# A SCORE reported alongside the binary block verdict, NOT a new hard gate
|
|
11038
|
+
# (surfaced only; opt-in enforcement via LOKI_REVIEW_MERGEABILITY_MIN below).
|
|
11039
|
+
# Deterministic rubric: start at 100; any blocker (Critical/High) => 0; else
|
|
11040
|
+
# subtract 5 per Medium finding and 2 per Low finding, floored at 0. This
|
|
11041
|
+
# separates a tight, mergeable change (high score, no blocker) from a
|
|
11042
|
+
# sprawling one whose scope/dead-code/convention findings pile up.
|
|
11043
|
+
# Parity-locked with computeMergeabilityScore() in loki-ts/src/runner/quality_gates.ts.
|
|
11044
|
+
local quality_score
|
|
11045
|
+
if [ "$has_blocking" = "true" ]; then
|
|
11046
|
+
quality_score=0
|
|
11047
|
+
else
|
|
11048
|
+
quality_score=$((100 - (nonblocking_medium * 5) - (nonblocking_low * 2)))
|
|
11049
|
+
[ "$quality_score" -lt 0 ] && quality_score=0
|
|
11050
|
+
fi
|
|
11051
|
+
log_info "Mergeability quality score: ${quality_score}/100 (medium=${nonblocking_medium}, low=${nonblocking_low}, blocker=${has_blocking})"
|
|
11052
|
+
|
|
10991
11053
|
# Save aggregate results via python3 + env vars (no shell interpolation in JSON)
|
|
10992
11054
|
export LOKI_REVIEW_AGG_FILE="$review_dir/$review_id/aggregate.json"
|
|
10993
11055
|
export LOKI_REVIEW_AGG_ID="$review_id"
|
|
@@ -10998,6 +11060,9 @@ BUILD_PROMPT
|
|
|
10998
11060
|
export LOKI_REVIEW_AGG_VERDICTS="$verdicts_summary"
|
|
10999
11061
|
export LOKI_REVIEW_AGG_REAL="$real_verdict_count"
|
|
11000
11062
|
export LOKI_REVIEW_AGG_INCONCLUSIVE="$review_inconclusive"
|
|
11063
|
+
export LOKI_REVIEW_AGG_QSCORE="$quality_score"
|
|
11064
|
+
export LOKI_REVIEW_AGG_QMED="$nonblocking_medium"
|
|
11065
|
+
export LOKI_REVIEW_AGG_QLOW="$nonblocking_low"
|
|
11001
11066
|
python3 << 'AGG_SCRIPT'
|
|
11002
11067
|
import json, os
|
|
11003
11068
|
result = {
|
|
@@ -11008,7 +11073,12 @@ result = {
|
|
|
11008
11073
|
"has_blocking": os.environ["LOKI_REVIEW_AGG_BLOCKING"] == "true",
|
|
11009
11074
|
"real_verdict_count": int(os.environ["LOKI_REVIEW_AGG_REAL"]),
|
|
11010
11075
|
"inconclusive": os.environ["LOKI_REVIEW_AGG_INCONCLUSIVE"] == "true",
|
|
11011
|
-
"verdicts": os.environ["LOKI_REVIEW_AGG_VERDICTS"].strip()
|
|
11076
|
+
"verdicts": os.environ["LOKI_REVIEW_AGG_VERDICTS"].strip(),
|
|
11077
|
+
# rank 9: weighted mergeability quality score (0 if any blocker, else
|
|
11078
|
+
# 100 - 5*medium - 2*low, floored at 0). Reported metric, not a hard gate.
|
|
11079
|
+
"quality_score": int(os.environ["LOKI_REVIEW_AGG_QSCORE"]),
|
|
11080
|
+
"nonblocking_medium": int(os.environ["LOKI_REVIEW_AGG_QMED"]),
|
|
11081
|
+
"nonblocking_low": int(os.environ["LOKI_REVIEW_AGG_QLOW"])
|
|
11012
11082
|
}
|
|
11013
11083
|
with open(os.environ["LOKI_REVIEW_AGG_FILE"], "w") as f:
|
|
11014
11084
|
json.dump(result, f, indent=2)
|
|
@@ -11016,6 +11086,7 @@ AGG_SCRIPT
|
|
|
11016
11086
|
unset LOKI_REVIEW_AGG_FILE LOKI_REVIEW_AGG_ID LOKI_REVIEW_AGG_ITER
|
|
11017
11087
|
unset LOKI_REVIEW_AGG_PASS LOKI_REVIEW_AGG_FAIL LOKI_REVIEW_AGG_BLOCKING LOKI_REVIEW_AGG_VERDICTS
|
|
11018
11088
|
unset LOKI_REVIEW_AGG_REAL LOKI_REVIEW_AGG_INCONCLUSIVE
|
|
11089
|
+
unset LOKI_REVIEW_AGG_QSCORE LOKI_REVIEW_AGG_QMED LOKI_REVIEW_AGG_QLOW
|
|
11019
11090
|
|
|
11020
11091
|
emit_event_json "code_review_complete" \
|
|
11021
11092
|
"review_id=$review_id" \
|
|
@@ -11141,6 +11212,18 @@ DA_AGG_PATCH
|
|
|
11141
11212
|
return 1
|
|
11142
11213
|
fi
|
|
11143
11214
|
|
|
11215
|
+
# 7.114.0 (rank 9): OPT-IN mergeability-score gate. Default OFF -- the score
|
|
11216
|
+
# is reported in aggregate.json regardless, but it only BLOCKS when the
|
|
11217
|
+
# operator explicitly sets a floor via LOKI_REVIEW_MERGEABILITY_MIN. This
|
|
11218
|
+
# keeps the existing Critical/High=block, Medium/Low=non-blocking contract
|
|
11219
|
+
# unchanged by default while giving teams a knob to demand a minimum
|
|
11220
|
+
# mergeability score. Never fabricates a pass; a low score only ever blocks.
|
|
11221
|
+
if [ -n "${LOKI_REVIEW_MERGEABILITY_MIN:-}" ] && [ "$quality_score" -lt "${LOKI_REVIEW_MERGEABILITY_MIN}" ]; then
|
|
11222
|
+
log_error "CODE REVIEW BLOCKED: mergeability quality score ${quality_score} < floor ${LOKI_REVIEW_MERGEABILITY_MIN} (LOKI_REVIEW_MERGEABILITY_MIN)"
|
|
11223
|
+
log_error " Review details: $review_dir/$review_id/ ; unset LOKI_REVIEW_MERGEABILITY_MIN to disable this gate"
|
|
11224
|
+
return 1
|
|
11225
|
+
fi
|
|
11226
|
+
|
|
11144
11227
|
# Finding #596 FIX A2 + WAVE8 FIX run.sh-F3: an inconclusive review (fewer
|
|
11145
11228
|
# usable verdicts than reviewers, retry already exhausted or disabled) blocks
|
|
11146
11229
|
# unless explicitly opted out. This is the 'verified before done' promise: a
|
|
@@ -13956,7 +14039,24 @@ build_prompt() {
|
|
|
13956
14039
|
phases="${phases%,}" # Remove trailing comma
|
|
13957
14040
|
|
|
13958
14041
|
# Ralph Wiggum Mode - Reason-Act-Reflect-VERIFY cycle with self-verification loop (Boris Cherny pattern)
|
|
13959
|
-
|
|
14042
|
+
# 7.114.0 (rank 8): rarv_instruction is now mode-aware. The never-finished
|
|
14043
|
+
# tail is a self-contradiction in a finite PRD/checkpoint run (which has an
|
|
14044
|
+
# auto-derived COMPLETION_PROMISE and was switched out of perpetual). Gate the
|
|
14045
|
+
# never-finished tail on the SAME split that completion_instruction uses
|
|
14046
|
+
# (perpetual OR no completion promise). Finite runs instead get a concise
|
|
14047
|
+
# "stop when verified-done, the completion gates are the authority" directive.
|
|
14048
|
+
# The unverifiable "2-3x quality improvement" clause is removed from ALL modes.
|
|
14049
|
+
# Parity-locked with rarvInstruction() in loki-ts/src/runner/build_prompt.ts.
|
|
14050
|
+
local _rarv_perpetual="false"
|
|
14051
|
+
if [ "$AUTONOMY_MODE" = "perpetual" ] || [ "$PERPETUAL_MODE" = "true" ] || [ -z "$COMPLETION_PROMISE" ]; then
|
|
14052
|
+
_rarv_perpetual="true"
|
|
14053
|
+
fi
|
|
14054
|
+
local rarv_instruction="RALPH WIGGUM MODE ACTIVE. Use Reason-Act-Reflect-VERIFY cycle: 1) REASON - READ .loki/CONTINUITY.md including 'Mistakes & Learnings' section to avoid past errors. CHECK .loki/state/relevant-learnings.json for cross-project learnings from previous projects (mistakes to avoid, patterns to apply). Check .loki/state/ and .loki/queue/, identify next task. CHECK .loki/state/resources.json for system resource warnings - if CPU or memory is high, reduce parallel agent spawning or pause non-critical tasks. Limit to MAX_PARALLEL_AGENTS=${MAX_PARALLEL_AGENTS}. If queue empty, find new improvements. 2) ACT - Execute task, write code, commit changes atomically (git checkpoint). 3) REFLECT - Update .loki/CONTINUITY.md with progress, update state, identify NEXT improvement. Save valuable learnings for future projects. 4) VERIFY - Run automated tests (unit, integration, E2E), check compilation/build, verify against spec. IF VERIFICATION FAILS: a) Capture error details (stack trace, logs), b) Analyze root cause, c) UPDATE 'Mistakes & Learnings' in CONTINUITY.md with what failed, why, and how to prevent, d) Rollback to last good git checkpoint if needed, e) Apply learning and RETRY from REASON. If verification passes, mark task complete and continue."
|
|
14055
|
+
if [ "$_rarv_perpetual" = "true" ]; then
|
|
14056
|
+
rarv_instruction="${rarv_instruction} CRITICAL: There is NEVER a 'finished' state - always find the next improvement, optimization, test, or feature."
|
|
14057
|
+
else
|
|
14058
|
+
rarv_instruction="${rarv_instruction} When the PRD requirements are implemented and completion gates pass, claim done via loki_complete_task and STOP; do not add unrequested improvements. Verify once -- the completion gates (tests, checklist, evidence) are the authority on done; do not re-verify redundantly."
|
|
14059
|
+
fi
|
|
13960
14060
|
|
|
13961
14061
|
# Completion instruction (S0.2 -- structured tool call).
|
|
13962
14062
|
# When PRD requirements are implemented, tests pass, and the checklist is
|
|
@@ -14049,7 +14149,7 @@ build_prompt() {
|
|
|
14049
14149
|
# writing any reference to a symbol the agent has not already read with
|
|
14050
14150
|
# the Read tool, prefer mcp__loki-mode-lsp-proxy__lsp_check_exists. This
|
|
14051
14151
|
# is the single most leveraged grounding primitive per OpenCode research.
|
|
14052
|
-
local lsp_grounding_instruction="LSP_GROUNDING: When the loki-mode-lsp-proxy MCP server is available, prefer LSP tools for symbol verification BEFORE writing code that references those symbols. Workflow: (1) Need to call \`foo.bar()\` you have not already read? -> mcp__loki-mode-lsp-proxy__lsp_check_exists with symbol='bar' (sub-200ms when cached). If exists:false, do NOT write the call -- use mcp__loki-mode-lsp-proxy__lsp_workspace_symbols with the concept name to find the real symbol, or use Read to see the actual API. (2) Just edited a file? -> mcp__loki-mode-lsp-proxy__lsp_get_diagnostics on that file to see new errors before the next iteration. (3) Need to jump to a definition by name (no file:line known)? -> mcp__loki-mode-lsp-proxy__lsp_find_definition_by_name. Skip these tools silently when the server is not available -- check the tool list, do not retry on errors. Goal: eliminate hallucinated API calls before they ship."
|
|
14152
|
+
local lsp_grounding_instruction="LSP_GROUNDING: When the loki-mode-lsp-proxy MCP server is available, prefer LSP tools for symbol verification BEFORE writing code that references those symbols. Workflow: (1) Need to call \`foo.bar()\` you have not already read? -> mcp__loki-mode-lsp-proxy__lsp_check_exists with symbol='bar' (sub-200ms when cached). If exists:false, do NOT write the call -- use mcp__loki-mode-lsp-proxy__lsp_workspace_symbols with the concept name to find the real symbol, or use Read to see the actual API. (2) Just edited a file? -> mcp__loki-mode-lsp-proxy__lsp_get_diagnostics on that file to see new errors before the next iteration. (3) Need to jump to a definition by name (no file:line known)? -> mcp__loki-mode-lsp-proxy__lsp_find_definition_by_name. Skip these tools silently when the server is not available -- check the tool list, do not retry on errors. Goal: eliminate hallucinated API calls before they ship. PARALLEL_TOOL_CALLS: When issuing independent read-only operations (reads, greps, file lookups, LSP checks) that do not depend on each other, issue them in a single message so they run in parallel; do not serialize independent lookups."
|
|
14053
14153
|
|
|
14054
14154
|
# AGENTS.md instruction (agents.md standard: plain Markdown at repo root,
|
|
14055
14155
|
# nearest-file-wins, read natively by Claude Code/Codex/etc.). Loki prefers
|
package/dashboard/__init__.py
CHANGED
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.
|
|
5
|
+
**Version:** v7.114.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.
|
|
2
|
+
var q8=Object.defineProperty;var J8=($)=>$;function V8($,Q){this[$]=J8.bind(null,Q)}var b=($,Q)=>{for(var Z in Q)q8($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:V8.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var J$=import.meta.require;var m1={};b(m1,{lokiDir:()=>j,homeLokiDir:()=>T$,findRepoRootForVersion:()=>$1,REPO_ROOT:()=>h});import{resolve as t,dirname as e$}from"path";import{fileURLToPath as W8}from"url";import{existsSync as N$}from"fs";import{homedir as H8}from"os";function G8(){let $=v1;for(let Q=0;Q<6;Q++){if(N$(t($,"VERSION"))&&N$(t($,"autonomy/run.sh")))return $;let Z=e$($);if(Z===$)break;$=Z}return t(v1,"..","..","..")}function $1($){let Q=$;for(let Z=0;Z<6;Z++){if(N$(t(Q,"VERSION"))&&N$(t(Q,"autonomy/run.sh")))return Q;let z=e$(Q);if(z===Q)break;Q=z}return t($,"..","..","..")}function j(){return process.env.LOKI_DIR??t(process.cwd(),".loki")}function T$(){return t(H8(),".loki")}var v1,h;var C=P(()=>{v1=e$(W8(import.meta.url));h=G8()});import{readFileSync as U8}from"fs";import{resolve as Y8,dirname as B8}from"path";import{fileURLToPath as M8}from"url";function S$(){if(Q$!==null)return Q$;let $="7.114.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=B8(M8(import.meta.url)),Z=$1(Q);Q$=U8(Y8(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var Q1=P(()=>{C()});var p1={};b(p1,{runOrThrow:()=>S8,run:()=>R,readStreamCapped:()=>c1,commandVersion:()=>C8,commandExists:()=>u,ShellError:()=>Z1,MAX_STDOUT_BYTES:()=>u1});async function c1($,Q=u1){let Z=$.getReader(),z=new TextDecoder,X="",q=0;try{while(q<Q){let{done:K,value:W}=await Z.read();if(K)break;if(!W)continue;if(q+=W.byteLength,q>Q){let V=W.byteLength-(q-Q);X+=z.decode(W.subarray(0,V),{stream:!0});break}X+=z.decode(W,{stream:!0})}X+=z.decode()}finally{try{await Z.cancel()}catch{}Z.releaseLock()}return X}async function R($,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[q,K,W]=await Promise.all([c1(Z.stdout),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function S8($,Q={}){let Z=await R($,Q);if(Z.exitCode!==0)throw new Z1(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function u($){let Q=D8($),Z=await R(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function D8($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function C8($,Q="--version"){if(!await u($))return null;let z=await R([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var u1=16777216,Z1;var o=P(()=>{Z1=class Z1 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 r($){return b8?"":$}var b8,M,S,_,tZ,L,k,y,J;var p=P(()=>{b8=(process.env.NO_COLOR??"").length>0;M=r("\x1B[0;31m"),S=r("\x1B[0;32m"),_=r("\x1B[1;33m"),tZ=r("\x1B[0;34m"),L=r("\x1B[0;36m"),k=r("\x1B[1m"),y=r("\x1B[2m"),J=r("\x1B[0m")});import{existsSync as l8}from"fs";async function Z$(){if(A$!==void 0)return A$;let $="/opt/homebrew/bin/python3.12";if(l8($))return A$=$,$;let Q=await u("python3.12");if(Q)return A$=Q,Q;let Z=await u("python3");return A$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return R([Z,"-c",$],Q)}var A$;var V$=P(()=>{o()});var V0={};b(V0,{runStatus:()=>U3});import{existsSync as v,readFileSync as H$,readdirSync as $0,statSync as Q0}from"fs";import{resolve as D,basename as z3}from"path";import{homedir as X3}from"os";function Z0($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function z0($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*C$/Q);if(X>C$)X=C$;let q=C$-X,K=S;if(z>=80)K=M;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),V=Z0($),H=Z0(Q);return` ${k}${Z}${J} ${K}[${W}]${J} ${z}% (${V} / ${H})`}async function q3(){if(await u("jq"))return!0;return process.stdout.write(`${M}Error: jq is required but not installed.${J}
|
|
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)
|
|
@@ -814,4 +814,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
|
|
|
814
814
|
`),2}default:return process.stderr.write(`Unknown command: ${Q}
|
|
815
815
|
`),process.stderr.write(K8),2}}e1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var SZ=await NZ(Bun.argv.slice(2));process.exit(SZ);
|
|
816
816
|
|
|
817
|
-
//# debugId=
|
|
817
|
+
//# debugId=0888AC629271567B64756E2164756E21
|
package/mcp/__init__.py
CHANGED
package/mcp/lsp_proxy.py
CHANGED
|
@@ -1595,8 +1595,49 @@ def main() -> None:
|
|
|
1595
1595
|
'install dir -- the install dir is only the import path for '
|
|
1596
1596
|
'`-m mcp.lsp_proxy`.',
|
|
1597
1597
|
)
|
|
1598
|
+
parser.add_argument(
|
|
1599
|
+
'--check-symbols', action='store_true',
|
|
1600
|
+
help='One-shot: read a newline-delimited list of symbol names on stdin '
|
|
1601
|
+
'and, for each, run lsp_check_exists against the workspace at '
|
|
1602
|
+
'--root. Emits JSON {"available": bool, "results": {sym: '
|
|
1603
|
+
'true|false|null}} on stdout. available=false / null verdicts mean '
|
|
1604
|
+
'no language server was reachable (honest inconclusive, never a '
|
|
1605
|
+
'fabricated exists). Same cwd/--root contract as --write-diagnostics: '
|
|
1606
|
+
'cwd is the install dir so `-m mcp.lsp_proxy` imports; --root is the '
|
|
1607
|
+
'TARGET project the symbols must exist in.',
|
|
1608
|
+
)
|
|
1598
1609
|
args = parser.parse_args()
|
|
1599
1610
|
|
|
1611
|
+
if args.check_symbols:
|
|
1612
|
+
# One-shot symbol-existence probe for the acceptance-oracle (source-
|
|
1613
|
+
# grounded checklist). Chdir into --root so the in-process LSP client's
|
|
1614
|
+
# _resolve_workspace_root()/_pick_language_for_workspace() see the TARGET
|
|
1615
|
+
# project (the process cwd is the install dir for the import). Always
|
|
1616
|
+
# cleans up spawned language-server subprocesses via the finally arm.
|
|
1617
|
+
target = os.path.abspath(args.root or os.getcwd())
|
|
1618
|
+
symbols = [ln.strip() for ln in sys.stdin.read().splitlines() if ln.strip()]
|
|
1619
|
+
results: Dict[str, Any] = {}
|
|
1620
|
+
available = False
|
|
1621
|
+
try:
|
|
1622
|
+
if os.path.isdir(target):
|
|
1623
|
+
os.chdir(target)
|
|
1624
|
+
for sym in symbols:
|
|
1625
|
+
try:
|
|
1626
|
+
raw = _lsp_check_exists_blocking(sym)
|
|
1627
|
+
parsed = json.loads(raw)
|
|
1628
|
+
except (OSError, ValueError):
|
|
1629
|
+
results[sym] = None
|
|
1630
|
+
continue
|
|
1631
|
+
exists = parsed.get('exists', None)
|
|
1632
|
+
results[sym] = exists
|
|
1633
|
+
# A non-null verdict on ANY symbol means the server answered.
|
|
1634
|
+
if exists is not None:
|
|
1635
|
+
available = True
|
|
1636
|
+
finally:
|
|
1637
|
+
_cleanup_all_clients()
|
|
1638
|
+
print(json.dumps({'available': available, 'results': results}))
|
|
1639
|
+
return
|
|
1640
|
+
|
|
1600
1641
|
if args.write_diagnostics:
|
|
1601
1642
|
# One-shot writer mode -- no MCP server, no event loop. Always cleans
|
|
1602
1643
|
# up any spawned LSP clients before exit. `--root` is the TARGET
|
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.
|
|
4
|
+
"version": "7.114.0",
|
|
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 8 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.
|
|
5
|
+
"version": "7.114.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 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",
|