loki-mode 7.112.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 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.112.0
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.112.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
411
+ **v7.114.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.112.0
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 [ "$circuit_triggered" = "true" ]; then
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
- elif [ $((ITERATION_COUNT % COUNCIL_CHECK_INTERVAL)) -eq 0 ]; then
3547
+ fi
3548
+ if _council_should_check_now "$circuit_triggered" "$completion_claimed"; then
3423
3549
  should_check=true
3424
3550
  fi
3425
3551
 
@@ -405,9 +405,19 @@ for friction in data.get('frictions', []):
405
405
  loc = friction.get('location', '')
406
406
  if matches(file_path, loc):
407
407
  cls = friction.get('classification', 'unknown')
408
- safe = friction.get('safe_to_remove', False)
409
- if cls in ('business_rule', 'unknown') and not safe:
410
- print(f'BLOCKED: Friction {friction.get(\"id\", \"?\")} in {loc} classified as {cls}')
408
+ # Fail-safe clearance: a friction is removable ONLY when it is EXPLICITLY
409
+ # marked safe with a real boolean True. A JSON string \"false\"/\"true\", an
410
+ # int 1, or None are all NOT safe -- 'is True' rejects them (a string is
411
+ # truthy, so 'not \"false\"' would have wrongly cleared it). This is a
412
+ # whitelist: removal is blocked unless explicitly cleared, rather than a
413
+ # blacklist of two hardcoded labels. The archaeology prompt defines no
414
+ # closed classification taxonomy, so the LLM invents labels freely
415
+ # (workaround, performance_hack, magic_value, ...); no label may serve as
416
+ # a clearance signal (including 'true_bug'), or a mislabel would bypass
417
+ # the gate. safe_to_remove is True is the sole clearance.
418
+ safe = (friction.get('safe_to_remove') is True)
419
+ if not safe:
420
+ print(f'BLOCKED: Friction {friction.get(\"id\", \"?\")} in {loc} ({cls}) not marked safe_to_remove=true')
411
421
  sys.exit(0)
412
422
  if strict and cls != 'true_bug':
413
423
  print(f'BLOCKED (strict): Friction {friction.get(\"id\", \"?\")} in {loc} - strict mode requires explicit approval')
@@ -417,7 +427,7 @@ print('OK')
417
427
 
418
428
  if [[ "$blocked" == BLOCKED* ]]; then
419
429
  echo "HOOK_BLOCKED: $blocked"
420
- echo "To proceed: Update friction-map.json to classify this friction or set safe_to_remove=true"
430
+ echo "To proceed: Update friction-map.json to set safe_to_remove=true (must be a JSON boolean true, not the string \"true\") for this friction once it is characterized and cleared for removal."
421
431
  return 1
422
432
  fi
423
433
  fi
@@ -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 (v7.x, P2-3 first slice):
124
- # IMPLEMENTED: the spec-vs-reality CONFLICT dimension for the database/datastore
125
- # choice, which is the highest-signal, lowest-false-positive triangulation
126
- # (the choices are mutually exclusive and have unambiguous dependency markers).
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): the third leg of full
129
- # triangulation -- DOMAIN INVARIANTS (auth must hash, money must not be float,
130
- # PII must be encrypted) -- and additional stack axes (web framework, language
131
- # runtime). These need an invariant catalog plus per-language AST-aware
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) != 1:
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
- contradicting = sorted(d for d in reality_dbs if d != spec_db)
306
- if contradicting and spec_db not in reality_dbs:
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": 1,
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-invariant oracle (auth-hashing, money-not-float, PII-encryption)",
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 3 specialist reviewers from pool..."
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, then the 2 selected
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 3 parallel blind reviews using provider-specific invocation
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
- 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. This self-verification loop achieves 2-3x quality improvement. CRITICAL: There is NEVER a 'finished' state - always find the next improvement, optimization, test, or feature."
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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.112.0"
10
+ __version__ = "7.114.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2295,7 +2295,7 @@ var LokiDashboard=(()=>{var Re=Object.defineProperty;var _t=Object.getOwnPropert
2295
2295
  }
2296
2296
 
2297
2297
  ${V}
2298
- `}getAriaPattern(e){return Ie[e]||{}}applyAriaPattern(e,t){let i=this.getAriaPattern(t);for(let[a,s]of Object.entries(i))if(a==="role")e.setAttribute("role",s);else{let r=a.replace(/([A-Z])/g,"-$1").toLowerCase();e.setAttribute(r,s)}}render(){}};var I={realtime:1e3,normal:2e3,background:5e3,offline:1e4},et={vscode:I.normal,browser:I.realtime,cli:I.background},tt={baseUrl:typeof window<"u"?window.location.origin:"http://localhost:57374",wsUrl:typeof window<"u"?`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`:"ws://localhost:57374/ws",pollInterval:2e3,timeout:1e4,retryAttempts:3,retryDelay:1e3},f={CONNECTED:"api:connected",DISCONNECTED:"api:disconnected",ERROR:"api:error",STATUS_UPDATE:"api:status-update",TASK_CREATED:"api:task-created",TASK_UPDATED:"api:task-updated",TASK_DELETED:"api:task-deleted",PROJECT_CREATED:"api:project-created",PROJECT_UPDATED:"api:project-updated",AGENT_UPDATE:"api:agent-update",LOG_MESSAGE:"api:log-message",MEMORY_UPDATE:"api:memory-update",CHECKLIST_UPDATE:"api:checklist-update"},D=class D extends EventTarget{static getInstance(e={}){let t=e.baseUrl||tt.baseUrl;return D._instances.has(t)||D._instances.set(t,new D(e)),D._instances.get(t)}static clearInstances(){D._instances.forEach(e=>e.disconnect()),D._instances.clear()}constructor(e={}){super(),this.config={...tt,...e},this._ws=null,this._connected=!1,this._pollInterval=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._maxReconnectAttempts=20,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=et[this._context]||I.normal,this._visibilityChangeHandler=null,this._messageHandler=null,this._setupAdaptivePolling(),this._setupVSCodeBridge()}_detectContext(){return typeof acquireVsCodeApi<"u"?"vscode":typeof window<"u"&&window.location?"browser":"cli"}get context(){return this._context}static get POLL_INTERVALS(){return I}_setupAdaptivePolling(){typeof document>"u"||(this._visibilityChangeHandler=()=>{document.hidden?this._setPollInterval(I.background):this._setPollInterval(et[this._context]||I.normal)},document.addEventListener("visibilitychange",this._visibilityChangeHandler))}_setPollInterval(e){this._currentPollInterval=e,this._pollInterval&&(this.stopPolling(),this.startPolling(null,e))}setPollMode(e){let t=I[e];t&&this._setPollInterval(t)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}this._messageHandler=e=>{let t=e.data;if(!(!t||!t.type))switch(t.type){case"updateStatus":this._emit(f.STATUS_UPDATE,t.data);break;case"updateTasks":this._emit(f.TASK_UPDATED,t.data);break;case"taskCreated":this._emit(f.TASK_CREATED,t.data);break;case"taskDeleted":this._emit(f.TASK_DELETED,t.data);break;case"projectCreated":this._emit(f.PROJECT_CREATED,t.data);break;case"projectUpdated":this._emit(f.PROJECT_UPDATED,t.data);break;case"agentUpdate":this._emit(f.AGENT_UPDATE,t.data);break;case"logMessage":this._emit(f.LOG_MESSAGE,t.data);break;case"memoryUpdate":this._emit(f.MEMORY_UPDATE,t.data);break;case"connected":this._connected=!0,this._emit(f.CONNECTED,t.data);break;case"disconnected":this._connected=!1,this._emit(f.DISCONNECTED,t.data);break;case"error":this._emit(f.ERROR,t.data);break;case"setPollMode":this.setPollMode(t.data.mode);break;default:this._emit(`api:${t.type}`,t.data)}},window.addEventListener("message",this._messageHandler)}}get isVSCode(){return this._context==="vscode"}postToVSCode(e,t={}){this._vscodeApi&&this._vscodeApi.postMessage({type:e,data:t})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(e,t={}){this.postToVSCode("userAction",{action:e,...t})}get baseUrl(){return this.config.baseUrl}set baseUrl(e){this.config.baseUrl=e,this.config.wsUrl=e.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((e,t)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._reconnectAttempts=0,this._emit(f.CONNECTED),e()},this._ws.onclose=()=>{this._connected=!1,this._emit(f.DISCONNECTED),this._scheduleReconnect()},this._ws.onerror=i=>{this._emit(f.ERROR,{error:i}),t(i)},this._ws.onmessage=i=>{try{let a=JSON.parse(i.data);this._handleMessage(a)}catch(a){console.error("Failed to parse WebSocket message:",a)}}}catch(i){t(i)}})}disconnect(){this._ws&&(this._ws.close(),this._ws=null),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null),this._connected=!1,this._cleanupGlobalListeners()}_cleanupGlobalListeners(){this._visibilityChangeHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this._visibilityChangeHandler),this._visibilityChangeHandler=null),this._messageHandler&&typeof window<"u"&&(window.removeEventListener("message",this._messageHandler),this._messageHandler=null)}destroy(){this.disconnect()}_scheduleReconnect(){if(this._reconnectTimeout)return;if(this._reconnectAttempts>=this._maxReconnectAttempts){console.warn("WebSocket max reconnect attempts reached, giving up"),this._emit(f.ERROR,{error:"Max reconnect attempts reached"});return}let e=Math.min(this.config.retryDelay*Math.pow(2,this._reconnectAttempts),3e4);this._reconnectAttempts++,this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},e)}_handleMessage(e){if(e.type==="ping"){this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"pong"}));return}let i={connected:f.CONNECTED,status_update:f.STATUS_UPDATE,task_created:f.TASK_CREATED,task_updated:f.TASK_UPDATED,task_deleted:f.TASK_DELETED,task_moved:f.TASK_UPDATED,project_created:f.PROJECT_CREATED,project_updated:f.PROJECT_UPDATED,agent_update:f.AGENT_UPDATE,log:f.LOG_MESSAGE}[e.type]||`api:${e.type}`;this._emit(i,e.data)}_emit(e,t={}){this.dispatchEvent(new CustomEvent(e,{detail:t}))}async _request(e,t={}){let i=`${this.config.baseUrl}${e}`,a=new AbortController,s=t&&typeof t.timeout=="number"?t.timeout:this.config.timeout,r=setTimeout(()=>a.abort(),s);try{let o=await fetch(i,{...t,signal:a.signal,credentials:"include",headers:{"Content-Type":"application/json",...t.headers}});if(clearTimeout(r),!o.ok){let n=await o.text().catch(()=>""),l=o.statusText||`HTTP ${o.status}`;if(n)try{let c=JSON.parse(n);l=c.detail||c.error||c.message||l}catch{l=n.length>200?n.slice(0,200)+"...":n}throw new Error(l)}return o.status===204?null:await o.json()}catch(o){throw clearTimeout(r),o.name==="AbortError"?new Error("Request timeout"):o}}async _get(e,t=!1){if(t&&this._cache.has(e)){let a=this._cache.get(e);if(Date.now()-a.timestamp<this._cacheTimeout)return a.data}let i=await this._request(e);return t&&this._cache.set(e,{data:i,timestamp:Date.now()}),i}async _post(e,t,i={}){return this._request(e,{method:"POST",body:JSON.stringify(t),...i})}async _put(e,t){return this._request(e,{method:"PUT",body:JSON.stringify(t)})}async _delete(e){return this._request(e,{method:"DELETE"})}async get(e){return this._get(e)}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(e=null){let t=e?`?status=${e}`:"";return this._get(`/api/projects${t}`)}async getProject(e){return this._get(`/api/projects/${e}`)}async createProject(e){return this._post("/api/projects",e)}async updateProject(e,t){return this._put(`/api/projects/${e}`,t)}async deleteProject(e){return this._delete(`/api/projects/${e}`)}async listTasks(e={}){let t=new URLSearchParams;e.projectId&&t.append("project_id",e.projectId),e.status&&t.append("status",e.status),e.priority&&t.append("priority",e.priority);let i=t.toString()?`?${t}`:"";return this._get(`/api/tasks${i}`)}async getTask(e){return this._get(`/api/tasks/${e}`)}async createTask(e){return this._post("/api/tasks",e)}async updateTask(e,t){return this._put(`/api/tasks/${e}`,t)}async moveTask(e,t,i){return this._post(`/api/tasks/${e}/move`,{status:t,position:i})}async deleteTask(e){return this._delete(`/api/tasks/${e}`)}async getMemorySummary(){return this._get("/api/memory/summary",!0)}async getMemoryIndex(){return this._get("/api/memory/index",!0)}async getMemoryTimeline(){return this._get("/api/memory/timeline")}async listEpisodes(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/episodes${t?"?"+t:""}`)}async getEpisode(e){return this._get(`/api/memory/episodes/${e}`)}async listPatterns(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/patterns${t?"?"+t:""}`)}async getPattern(e){return this._get(`/api/memory/patterns/${e}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(e){return this._get(`/api/memory/skills/${e}`)}async retrieveMemories(e,t=null,i=5){return this._post("/api/memory/retrieve",{query:e,taskType:t,topK:i},{timeout:3e4})}async consolidateMemory(e=24){return this._post("/api/memory/consolidate",{sinceHours:e},{timeout:12e4})}async getTokenEconomics(){return this._get("/api/memory/economics")}async searchMemory(e,t="all",i=20){let a=new URLSearchParams({q:e,collection:t,limit:String(i)});return this._get(`/api/memory/search?${a}`)}async getMemoryStats(){return this._get("/api/memory/stats",!0)}async listRegisteredProjects(e=!1){return this._get(`/api/registry/projects?include_inactive=${e}`)}async registerProject(e,t=null,i=null){return this._post("/api/registry/projects",{path:e,name:t,alias:i})}async discoverProjects(e=3){return this._get(`/api/registry/discover?max_depth=${e}`)}async syncRegistry(){return this._post("/api/registry/sync",{},{timeout:45e3})}async getCrossProjectTasks(e=null){let t=e?`?project_ids=${e.join(",")}`:"";return this._get(`/api/registry/tasks${t}`)}async getLearningMetrics(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/metrics${i}`)}async getLearningTrends(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/trends${i}`)}async getLearningSignals(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source),e.limit&&t.append("limit",String(e.limit)),e.offset&&t.append("offset",String(e.offset));let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/signals${i}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(e={}){return this._post("/api/learning/aggregate",e,{timeout:6e4})}async getAggregatedPreferences(e=20){return this._get(`/api/learning/preferences?limit=${e}`)}async getAggregatedErrors(e=20){return this._get(`/api/learning/errors?limit=${e}`)}async getAggregatedSuccessPatterns(e=20){return this._get(`/api/learning/success?limit=${e}`)}async getToolEfficiency(e=20){return this._get(`/api/learning/tools?limit=${e}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getCouncilState(){return this._get("/api/council/state")}async getCouncilVerdicts(e=20){return this._get(`/api/council/verdicts?limit=${e}`)}async getCouncilConvergence(){return this._get("/api/council/convergence")}async getCouncilReport(){return this._get("/api/council/report")}async forceCouncilReview(){return this._post("/api/council/force-review",{})}async getContext(){return this._get("/api/context")}async getNotifications(e,t){let i=new URLSearchParams;e&&i.set("severity",e),t&&i.set("unread_only","true");let a=i.toString();return this._get("/api/notifications"+(a?"?"+a:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(e){return this._put("/api/notifications/triggers",{triggers:e})}async acknowledgeNotification(e){return this._post("/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{})}async startSession(e,t={}){let i={provider:t.provider||"claude",parallel:!!t.parallel};return t.prdPath?i.prd_path=t.prdPath:i.prd_text=e||"",t.model&&(i.model=t.model),t.advisorModel&&(i.advisor_model=t.advisorModel),this._post("/api/control/start",i)}async pauseSession(){return this._post("/api/control/pause",{})}async resumeSession(){return this._post("/api/control/resume",{})}async stopSession(){return this._post("/api/control/stop",{})}async getSessionModel(){return this._get("/api/session/model")}async setSessionModel(e){return this._post("/api/session/model",{model:e||null})}async getLogs(e=100){return this._get(`/api/logs?lines=${e}`)}async getChecklist(){return this._get("/api/checklist")}async getChecklistSummary(){return this._get("/api/checklist/summary")}async getPrdObservations(){let e=await fetch(`${this.baseUrl}/api/prd-observations`,{credentials:"include"});if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.text()}async getChecklistWaivers(){return this._get("/api/checklist/waivers")}async addChecklistWaiver(e,t,i="dashboard"){return this._post("/api/checklist/waivers",{item_id:e,reason:t,waived_by:i})}async removeChecklistWaiver(e){return this._delete(`/api/checklist/waivers/${encodeURIComponent(e)}`)}async getCouncilGate(){return this._get("/api/council/gate")}async getAppRunnerStatus(){return this._get("/api/app-runner/status")}async getAppRunnerLogs(e=100){return this._get(`/api/app-runner/logs?lines=${e}`)}async getAppRunnerErrors(e=50){return this._get(`/api/app-runner/errors?lines=${e}`)}async restartApp(){return this._post("/api/control/app-restart",{})}async stopApp(){return this._post("/api/control/app-stop",{})}async getPlaywrightResults(){return this._get("/api/playwright/results")}async getPlaywrightScreenshot(){return this._get("/api/playwright/screenshot")}startPolling(e,t=null){if(this._pollInterval)return;this._pollCallback=e;let i=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(f.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(f.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};i();let a=t||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(i,a)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};S(D,"_instances",new Map);var N=D;function it(d={}){return new N(d)}function h(d={}){return N.getInstance(d)}var Ue="loki-state-change",je={ui:{theme:"light",sidebarCollapsed:!1,activeSection:"kanban",terminalAutoScroll:!0},session:{connected:!1,lastSync:null,mode:"offline",phase:null,iteration:null},localTasks:[],cache:{projects:[],tasks:[],agents:[],memory:null,lastFetch:null},preferences:{pollInterval:2e3,notifications:!0,soundEnabled:!1}},T=class T extends EventTarget{static getInstance(){return T._instance||(T._instance=new T),T._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let e=localStorage.getItem(T.STORAGE_KEY);if(e){let t=JSON.parse(e);return this._mergeState(je,t)}}catch(e){console.warn("Failed to load state from localStorage:",e)}return{...je}}_mergeState(e,t){let i={...e};for(let a of Object.keys(t))a in e&&typeof e[a]=="object"&&!Array.isArray(e[a])?i[a]=this._mergeState(e[a],t[a]):i[a]=t[a];return i}_saveState(){try{let e={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem(T.STORAGE_KEY,JSON.stringify(e))}catch(e){console.warn("Failed to save state to localStorage:",e)}}get(e=null){if(!e)return{...this._state};let t=e.split("."),i=this._state;for(let a of t){if(i==null)return;i=i[a]}return i}set(e,t,i=!0){let a=e.split("."),s=a.pop(),r=this._state;for(let n of a)n in r||(r[n]={}),r=r[n];let o=r[s];r[s]=t,i&&this._saveState(),this._notifyChange(e,t,o)}update(e,t=!0){let i=[];for(let[a,s]of Object.entries(e)){let r=this.get(a);this.set(a,s,!1),i.push({path:a,value:s,oldValue:r})}t&&this._saveState();for(let a of i)this._notifyChange(a.path,a.value,a.oldValue)}_notifyChange(e,t,i){this.dispatchEvent(new CustomEvent(Ue,{detail:{path:e,value:t,oldValue:i}}));let a=this._subscribers.get(e)||[];for(let r of a)try{r(t,i,e)}catch(o){console.error("State subscriber error:",o)}let s=e.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let n of o)try{n(this.get(r),null,r)}catch(l){console.error("State subscriber error:",l)}}}subscribe(e,t){return this._subscribers.has(e)||this._subscribers.set(e,[]),this._subscribers.get(e).push(t),()=>{let i=this._subscribers.get(e),a=i.indexOf(t);a>-1&&i.splice(a,1)}}reset(e=null){if(e){let t=e.split("."),i=je;for(let a of t)i=i?.[a];this.set(e,i)}else this._state={...je},this._saveState(),this.dispatchEvent(new CustomEvent(Ue,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(e){let t=this.get("localTasks")||[],i={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...e};return this.set("localTasks",[...t,i]),i}updateLocalTask(e,t){let i=this.get("localTasks")||[],a=i.findIndex(r=>r.id===e);if(a===-1)return null;let s={...i[a],...t,updatedAt:new Date().toISOString()};return i[a]=s,this.set("localTasks",[...i]),s}deleteLocalTask(e){let t=this.get("localTasks")||[];this.set("localTasks",t.filter(i=>i.id!==e))}moveLocalTask(e,t,i=null){let s=(this.get("localTasks")||[]).find(r=>r.id===e);return s?this.updateLocalTask(e,{status:t,position:i??s.position}):null}updateSession(e){this.update(Object.fromEntries(Object.entries(e).map(([t,i])=>[`session.${t}`,i])),!1)}updateCache(e){this.update({"cache.projects":e.projects??this.get("cache.projects"),"cache.tasks":e.tasks??this.get("cache.tasks"),"cache.agents":e.agents??this.get("cache.agents"),"cache.memory":e.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let e=this.get("cache.tasks")||[],i=(this.get("localTasks")||[]).map(a=>({...a,isLocal:!0}));return[...e,...i]}getTasksByStatus(e){return this.getMergedTasks().filter(t=>t.status===e)}};S(T,"STORAGE_KEY","loki-dashboard-state"),S(T,"_instance",null);var Y=T;function U(){return Y.getInstance()}function at(d){let e=U();return{get:()=>e.get(d),set:t=>e.set(d,t),subscribe:t=>e.subscribe(d,t)}}function Oe(d){return String(d??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function C(d){if(d==null||d==="")return"";let e=String(d).replace(/\r\n/g,`
2298
+ `}getAriaPattern(e){return Ie[e]||{}}applyAriaPattern(e,t){let i=this.getAriaPattern(t);for(let[a,s]of Object.entries(i))if(a==="role")e.setAttribute("role",s);else{let r=a.replace(/([A-Z])/g,"-$1").toLowerCase();e.setAttribute(r,s)}}render(){}};var I={realtime:1e3,normal:2e3,background:5e3,offline:1e4},et={vscode:I.normal,browser:I.realtime,cli:I.background},tt={baseUrl:typeof window<"u"?window.location.origin:"http://localhost:57374",wsUrl:typeof window<"u"?`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`:"ws://localhost:57374/ws",pollInterval:2e3,timeout:1e4,retryAttempts:3,retryDelay:1e3},f={CONNECTED:"api:connected",DISCONNECTED:"api:disconnected",ERROR:"api:error",STATUS_UPDATE:"api:status-update",TASK_CREATED:"api:task-created",TASK_UPDATED:"api:task-updated",TASK_DELETED:"api:task-deleted",PROJECT_CREATED:"api:project-created",PROJECT_UPDATED:"api:project-updated",AGENT_UPDATE:"api:agent-update",LOG_MESSAGE:"api:log-message",MEMORY_UPDATE:"api:memory-update",CHECKLIST_UPDATE:"api:checklist-update"},D=class D extends EventTarget{static getInstance(e={}){let t=e.baseUrl||tt.baseUrl;return D._instances.has(t)||D._instances.set(t,new D(e)),D._instances.get(t)}static clearInstances(){D._instances.forEach(e=>e.disconnect()),D._instances.clear()}constructor(e={}){super(),this.config={...tt,...e},this._ws=null,this._connected=!1,this._intentionalClose=!1,this._pollInterval=null,this._reconnectTimeout=null,this._reconnectAttempts=0,this._maxReconnectAttempts=20,this._cache=new Map,this._cacheTimeout=5e3,this._vscodeApi=null,this._context=this._detectContext(),this._currentPollInterval=et[this._context]||I.normal,this._visibilityChangeHandler=null,this._messageHandler=null,this._setupAdaptivePolling(),this._setupVSCodeBridge()}_detectContext(){return typeof acquireVsCodeApi<"u"?"vscode":typeof window<"u"&&window.location?"browser":"cli"}get context(){return this._context}static get POLL_INTERVALS(){return I}_setupAdaptivePolling(){typeof document>"u"||(this._visibilityChangeHandler=()=>{document.hidden?this._setPollInterval(I.background):this._setPollInterval(et[this._context]||I.normal)},document.addEventListener("visibilitychange",this._visibilityChangeHandler))}_setPollInterval(e){this._currentPollInterval=e,this._pollInterval&&(this.stopPolling(),this.startPolling(null,e))}setPollMode(e){let t=I[e];t&&this._setPollInterval(t)}_setupVSCodeBridge(){if(!(typeof acquireVsCodeApi>"u")){try{this._vscodeApi=acquireVsCodeApi()}catch{console.warn("VS Code API already acquired or unavailable");return}this._messageHandler=e=>{let t=e.data;if(!(!t||!t.type))switch(t.type){case"updateStatus":this._emit(f.STATUS_UPDATE,t.data);break;case"updateTasks":this._emit(f.TASK_UPDATED,t.data);break;case"taskCreated":this._emit(f.TASK_CREATED,t.data);break;case"taskDeleted":this._emit(f.TASK_DELETED,t.data);break;case"projectCreated":this._emit(f.PROJECT_CREATED,t.data);break;case"projectUpdated":this._emit(f.PROJECT_UPDATED,t.data);break;case"agentUpdate":this._emit(f.AGENT_UPDATE,t.data);break;case"logMessage":this._emit(f.LOG_MESSAGE,t.data);break;case"memoryUpdate":this._emit(f.MEMORY_UPDATE,t.data);break;case"connected":this._connected=!0,this._emit(f.CONNECTED,t.data);break;case"disconnected":this._connected=!1,this._emit(f.DISCONNECTED,t.data);break;case"error":this._emit(f.ERROR,t.data);break;case"setPollMode":this.setPollMode(t.data.mode);break;default:this._emit(`api:${t.type}`,t.data)}},window.addEventListener("message",this._messageHandler)}}get isVSCode(){return this._context==="vscode"}postToVSCode(e,t={}){this._vscodeApi&&this._vscodeApi.postMessage({type:e,data:t})}requestRefresh(){this.postToVSCode("requestRefresh")}notifyVSCode(e,t={}){this.postToVSCode("userAction",{action:e,...t})}get baseUrl(){return this.config.baseUrl}set baseUrl(e){this.config.baseUrl=e,this.config.wsUrl=e.replace(/^http/,"ws")+"/ws"}get isConnected(){return this._connected}async connect(){if(this._intentionalClose=!1,!(this._ws&&this._ws.readyState===WebSocket.OPEN))return new Promise((e,t)=>{try{this._ws=new WebSocket(this.config.wsUrl),this._ws.onopen=()=>{this._connected=!0,this._reconnectAttempts=0,this._emit(f.CONNECTED),e()},this._ws.onclose=()=>{this._connected=!1,this._emit(f.DISCONNECTED),this._intentionalClose||this._scheduleReconnect()},this._ws.onerror=i=>{this._emit(f.ERROR,{error:i}),t(i)},this._ws.onmessage=i=>{try{let a=JSON.parse(i.data);this._handleMessage(a)}catch(a){console.error("Failed to parse WebSocket message:",a)}}}catch(i){t(i)}})}disconnect(){this._intentionalClose=!0,this._ws&&(this._ws.close(),this._ws=null),this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null),this._reconnectTimeout&&(clearTimeout(this._reconnectTimeout),this._reconnectTimeout=null),this._connected=!1,this._cleanupGlobalListeners()}_cleanupGlobalListeners(){this._visibilityChangeHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this._visibilityChangeHandler),this._visibilityChangeHandler=null),this._messageHandler&&typeof window<"u"&&(window.removeEventListener("message",this._messageHandler),this._messageHandler=null)}destroy(){this.disconnect()}_scheduleReconnect(){if(this._reconnectTimeout)return;if(this._reconnectAttempts>=this._maxReconnectAttempts){console.warn("WebSocket max reconnect attempts reached, giving up"),this._emit(f.ERROR,{error:"Max reconnect attempts reached"});return}let e=Math.min(this.config.retryDelay*Math.pow(2,this._reconnectAttempts),3e4);this._reconnectAttempts++,this._reconnectTimeout=setTimeout(()=>{this._reconnectTimeout=null,this.connect().catch(()=>{})},e)}_handleMessage(e){if(e.type==="ping"){this._ws&&this._ws.readyState===WebSocket.OPEN&&this._ws.send(JSON.stringify({type:"pong"}));return}let i={connected:f.CONNECTED,status_update:f.STATUS_UPDATE,task_created:f.TASK_CREATED,task_updated:f.TASK_UPDATED,task_deleted:f.TASK_DELETED,task_moved:f.TASK_UPDATED,project_created:f.PROJECT_CREATED,project_updated:f.PROJECT_UPDATED,agent_update:f.AGENT_UPDATE,log:f.LOG_MESSAGE}[e.type]||`api:${e.type}`;this._emit(i,e.data)}_emit(e,t={}){this.dispatchEvent(new CustomEvent(e,{detail:t}))}async _request(e,t={}){let i=`${this.config.baseUrl}${e}`,a=new AbortController,s=t&&typeof t.timeout=="number"?t.timeout:this.config.timeout,r=setTimeout(()=>a.abort(),s);try{let o=await fetch(i,{...t,signal:a.signal,credentials:"include",headers:{"Content-Type":"application/json",...t.headers}});if(clearTimeout(r),!o.ok){let n=await o.text().catch(()=>""),l=o.statusText||`HTTP ${o.status}`;if(n)try{let c=JSON.parse(n);l=c.detail||c.error||c.message||l}catch{l=n.length>200?n.slice(0,200)+"...":n}throw new Error(l)}return o.status===204?null:await o.json()}catch(o){throw clearTimeout(r),o.name==="AbortError"?new Error("Request timeout"):o}}async _get(e,t=!1){if(t&&this._cache.has(e)){let a=this._cache.get(e);if(Date.now()-a.timestamp<this._cacheTimeout)return a.data}let i=await this._request(e);return t&&this._cache.set(e,{data:i,timestamp:Date.now()}),i}async _post(e,t,i={}){return this._request(e,{method:"POST",body:JSON.stringify(t),...i})}async _put(e,t){return this._request(e,{method:"PUT",body:JSON.stringify(t)})}async _delete(e){return this._request(e,{method:"DELETE"})}async get(e){return this._get(e)}async getStatus(){return this._get("/api/status")}async healthCheck(){return this._get("/health")}async listProjects(e=null){let t=e?`?status=${e}`:"";return this._get(`/api/projects${t}`)}async getProject(e){return this._get(`/api/projects/${e}`)}async createProject(e){return this._post("/api/projects",e)}async updateProject(e,t){return this._put(`/api/projects/${e}`,t)}async deleteProject(e){return this._delete(`/api/projects/${e}`)}async listTasks(e={}){let t=new URLSearchParams;e.projectId&&t.append("project_id",e.projectId),e.status&&t.append("status",e.status),e.priority&&t.append("priority",e.priority);let i=t.toString()?`?${t}`:"";return this._get(`/api/tasks${i}`)}async getTask(e){return this._get(`/api/tasks/${e}`)}async createTask(e){return this._post("/api/tasks",e)}async updateTask(e,t){return this._put(`/api/tasks/${e}`,t)}async moveTask(e,t,i){return this._post(`/api/tasks/${e}/move`,{status:t,position:i})}async deleteTask(e){return this._delete(`/api/tasks/${e}`)}async getMemorySummary(){return this._get("/api/memory/summary",!0)}async getMemoryIndex(){return this._get("/api/memory/index",!0)}async getMemoryTimeline(){return this._get("/api/memory/timeline")}async listEpisodes(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/episodes${t?"?"+t:""}`)}async getEpisode(e){return this._get(`/api/memory/episodes/${e}`)}async listPatterns(e={}){let t=new URLSearchParams(e).toString();return this._get(`/api/memory/patterns${t?"?"+t:""}`)}async getPattern(e){return this._get(`/api/memory/patterns/${e}`)}async listSkills(){return this._get("/api/memory/skills")}async getSkill(e){return this._get(`/api/memory/skills/${e}`)}async retrieveMemories(e,t=null,i=5){return this._post("/api/memory/retrieve",{query:e,taskType:t,topK:i},{timeout:3e4})}async consolidateMemory(e=24){return this._post("/api/memory/consolidate",{sinceHours:e},{timeout:12e4})}async getTokenEconomics(){return this._get("/api/memory/economics")}async searchMemory(e,t="all",i=20){let a=new URLSearchParams({q:e,collection:t,limit:String(i)});return this._get(`/api/memory/search?${a}`)}async getMemoryStats(){return this._get("/api/memory/stats",!0)}async listRegisteredProjects(e=!1){return this._get(`/api/registry/projects?include_inactive=${e}`)}async registerProject(e,t=null,i=null){return this._post("/api/registry/projects",{path:e,name:t,alias:i})}async discoverProjects(e=3){return this._get(`/api/registry/discover?max_depth=${e}`)}async syncRegistry(){return this._post("/api/registry/sync",{},{timeout:45e3})}async getCrossProjectTasks(e=null){let t=e?`?project_ids=${e.join(",")}`:"";return this._get(`/api/registry/tasks${t}`)}async getLearningMetrics(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/metrics${i}`)}async getLearningTrends(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source);let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/trends${i}`)}async getLearningSignals(e={}){let t=new URLSearchParams;e.timeRange&&t.append("timeRange",e.timeRange),e.signalType&&t.append("signalType",e.signalType),e.source&&t.append("source",e.source),e.limit&&t.append("limit",String(e.limit)),e.offset&&t.append("offset",String(e.offset));let i=t.toString()?`?${t}`:"";return this._get(`/api/learning/signals${i}`)}async getLatestAggregation(){return this._get("/api/learning/aggregation")}async triggerAggregation(e={}){return this._post("/api/learning/aggregate",e,{timeout:6e4})}async getAggregatedPreferences(e=20){return this._get(`/api/learning/preferences?limit=${e}`)}async getAggregatedErrors(e=20){return this._get(`/api/learning/errors?limit=${e}`)}async getAggregatedSuccessPatterns(e=20){return this._get(`/api/learning/success?limit=${e}`)}async getToolEfficiency(e=20){return this._get(`/api/learning/tools?limit=${e}`)}async getCost(){return this._get("/api/cost")}async getPricing(){return this._get("/api/pricing")}async getCouncilState(){return this._get("/api/council/state")}async getCouncilVerdicts(e=20){return this._get(`/api/council/verdicts?limit=${e}`)}async getCouncilConvergence(){return this._get("/api/council/convergence")}async getCouncilReport(){return this._get("/api/council/report")}async forceCouncilReview(){return this._post("/api/council/force-review",{})}async getContext(){return this._get("/api/context")}async getNotifications(e,t){let i=new URLSearchParams;e&&i.set("severity",e),t&&i.set("unread_only","true");let a=i.toString();return this._get("/api/notifications"+(a?"?"+a:""))}async getNotificationTriggers(){return this._get("/api/notifications/triggers")}async updateNotificationTriggers(e){return this._put("/api/notifications/triggers",{triggers:e})}async acknowledgeNotification(e){return this._post("/api/notifications/"+encodeURIComponent(e)+"/acknowledge",{})}async startSession(e,t={}){let i={provider:t.provider||"claude",parallel:!!t.parallel};return t.prdPath?i.prd_path=t.prdPath:i.prd_text=e||"",t.model&&(i.model=t.model),t.advisorModel&&(i.advisor_model=t.advisorModel),this._post("/api/control/start",i)}async pauseSession(){return this._post("/api/control/pause",{})}async resumeSession(){return this._post("/api/control/resume",{})}async stopSession(){return this._post("/api/control/stop",{})}async getSessionModel(){return this._get("/api/session/model")}async setSessionModel(e){return this._post("/api/session/model",{model:e||null})}async getLogs(e=100){return this._get(`/api/logs?lines=${e}`)}async getChecklist(){return this._get("/api/checklist")}async getChecklistSummary(){return this._get("/api/checklist/summary")}async getPrdObservations(){let e=await fetch(`${this.baseUrl}/api/prd-observations`,{credentials:"include"});if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.text()}async getChecklistWaivers(){return this._get("/api/checklist/waivers")}async addChecklistWaiver(e,t,i="dashboard"){return this._post("/api/checklist/waivers",{item_id:e,reason:t,waived_by:i})}async removeChecklistWaiver(e){return this._delete(`/api/checklist/waivers/${encodeURIComponent(e)}`)}async getCouncilGate(){return this._get("/api/council/gate")}async getAppRunnerStatus(){return this._get("/api/app-runner/status")}async getAppRunnerLogs(e=100){return this._get(`/api/app-runner/logs?lines=${e}`)}async getAppRunnerErrors(e=50){return this._get(`/api/app-runner/errors?lines=${e}`)}async restartApp(){return this._post("/api/control/app-restart",{})}async stopApp(){return this._post("/api/control/app-stop",{})}async getPlaywrightResults(){return this._get("/api/playwright/results")}async getPlaywrightScreenshot(){return this._get("/api/playwright/screenshot")}startPolling(e,t=null){if(this._pollInterval)return;this._pollCallback=e;let i=async()=>{try{let s=await this.getStatus();this._connected=!0,this._pollCallback&&this._pollCallback(s),this._emit(f.STATUS_UPDATE,s),this._vscodeApi&&this.postToVSCode("pollSuccess",{timestamp:Date.now()})}catch(s){this._connected=!1,this._emit(f.ERROR,{error:s}),this._vscodeApi&&this.postToVSCode("pollError",{error:s.message})}};i();let a=t||this._currentPollInterval||this.config.pollInterval;this._pollInterval=setInterval(i,a)}stopPolling(){this._pollInterval&&(clearInterval(this._pollInterval),this._pollInterval=null)}};S(D,"_instances",new Map);var N=D;function it(d={}){return new N(d)}function h(d={}){return N.getInstance(d)}var Ue="loki-state-change",je={ui:{theme:"light",sidebarCollapsed:!1,activeSection:"kanban",terminalAutoScroll:!0},session:{connected:!1,lastSync:null,mode:"offline",phase:null,iteration:null},localTasks:[],cache:{projects:[],tasks:[],agents:[],memory:null,lastFetch:null},preferences:{pollInterval:2e3,notifications:!0,soundEnabled:!1}},T=class T extends EventTarget{static getInstance(){return T._instance||(T._instance=new T),T._instance}constructor(){super(),this._state=this._loadState(),this._subscribers=new Map,this._batchUpdates=[],this._batchTimeout=null}_loadState(){try{let e=localStorage.getItem(T.STORAGE_KEY);if(e){let t=JSON.parse(e);return this._mergeState(je,t)}}catch(e){console.warn("Failed to load state from localStorage:",e)}return{...je}}_mergeState(e,t){let i={...e};for(let a of Object.keys(t))a in e&&typeof e[a]=="object"&&!Array.isArray(e[a])?i[a]=this._mergeState(e[a],t[a]):i[a]=t[a];return i}_saveState(){try{let e={ui:this._state.ui,localTasks:this._state.localTasks,preferences:this._state.preferences};localStorage.setItem(T.STORAGE_KEY,JSON.stringify(e))}catch(e){console.warn("Failed to save state to localStorage:",e)}}get(e=null){if(!e)return{...this._state};let t=e.split("."),i=this._state;for(let a of t){if(i==null)return;i=i[a]}return i}set(e,t,i=!0){let a=e.split("."),s=a.pop(),r=this._state;for(let n of a)n in r||(r[n]={}),r=r[n];let o=r[s];r[s]=t,i&&this._saveState(),this._notifyChange(e,t,o)}update(e,t=!0){let i=[];for(let[a,s]of Object.entries(e)){let r=this.get(a);this.set(a,s,!1),i.push({path:a,value:s,oldValue:r})}t&&this._saveState();for(let a of i)this._notifyChange(a.path,a.value,a.oldValue)}_notifyChange(e,t,i){this.dispatchEvent(new CustomEvent(Ue,{detail:{path:e,value:t,oldValue:i}}));let a=this._subscribers.get(e)||[];for(let r of a)try{r(t,i,e)}catch(o){console.error("State subscriber error:",o)}let s=e.split(".");for(;s.length>1;){s.pop();let r=s.join("."),o=this._subscribers.get(r)||[];for(let n of o)try{n(this.get(r),null,r)}catch(l){console.error("State subscriber error:",l)}}}subscribe(e,t){return this._subscribers.has(e)||this._subscribers.set(e,[]),this._subscribers.get(e).push(t),()=>{let i=this._subscribers.get(e),a=i.indexOf(t);a>-1&&i.splice(a,1)}}reset(e=null){if(e){let t=e.split("."),i=je;for(let a of t)i=i?.[a];this.set(e,i)}else this._state={...je},this._saveState(),this.dispatchEvent(new CustomEvent(Ue,{detail:{path:null,value:this._state,oldValue:null}}))}addLocalTask(e){let t=this.get("localTasks")||[],i={id:`local-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,createdAt:new Date().toISOString(),status:"pending",...e};return this.set("localTasks",[...t,i]),i}updateLocalTask(e,t){let i=this.get("localTasks")||[],a=i.findIndex(r=>r.id===e);if(a===-1)return null;let s={...i[a],...t,updatedAt:new Date().toISOString()};return i[a]=s,this.set("localTasks",[...i]),s}deleteLocalTask(e){let t=this.get("localTasks")||[];this.set("localTasks",t.filter(i=>i.id!==e))}moveLocalTask(e,t,i=null){let s=(this.get("localTasks")||[]).find(r=>r.id===e);return s?this.updateLocalTask(e,{status:t,position:i??s.position}):null}updateSession(e){this.update(Object.fromEntries(Object.entries(e).map(([t,i])=>[`session.${t}`,i])),!1)}updateCache(e){this.update({"cache.projects":e.projects??this.get("cache.projects"),"cache.tasks":e.tasks??this.get("cache.tasks"),"cache.agents":e.agents??this.get("cache.agents"),"cache.memory":e.memory??this.get("cache.memory"),"cache.lastFetch":new Date().toISOString()},!1)}getMergedTasks(){let e=this.get("cache.tasks")||[],i=(this.get("localTasks")||[]).map(a=>({...a,isLocal:!0}));return[...e,...i]}getTasksByStatus(e){return this.getMergedTasks().filter(t=>t.status===e)}};S(T,"STORAGE_KEY","loki-dashboard-state"),S(T,"_instance",null);var Y=T;function U(){return Y.getInstance()}function at(d){let e=U();return{get:()=>e.get(d),set:t=>e.set(d,t),subscribe:t=>e.subscribe(d,t)}}function Oe(d){return String(d??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function C(d){if(d==null||d==="")return"";let e=String(d).replace(/\r\n/g,`
2299
2299
  `),t=[];e=e.replace(/```[^\n]*\n([\s\S]*?)```/g,(u,m)=>{let g=t.length;return t.push(`<pre class="md-code"><code>${Oe(m.replace(/\n$/,""))}</code></pre>`),`\0CODEBLOCK${g}\0`});let i=Oe(e);i=i.replace(/`([^`\n]+)`/g,(u,m)=>`<code class="md-inline-code">${m}</code>`),i=i.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g,(u,m,g)=>/^(https?:|mailto:|\/|#|\.)/.test(g)?`<a class="md-link" href="${g}" target="_blank" rel="noopener noreferrer">${m}</a>`:u);let a=i.split(`
2300
2300
  `),s=[],r=[],o=[],n=()=>{r.length&&(s.push(`<p class="md-p">${r.join("<br>")}</p>`),r=[])},l=u=>{for(;o.length&&o[o.length-1].indent>=u;)s.push(`</${o.pop().tag}>`)},c=()=>{for(;o.length;)s.push(`</${o.pop().tag}>`)};for(let u of a){let m=u,g=m.trim(),x=g.match(/^CODEBLOCK(\d+)$/);if(x){n(),c(),s.push(t[Number(x[1])]);continue}if(g===""){n(),c();continue}let k=g.match(/^(#{1,6})\s+(.+)$/);if(k){n(),c();let M=Math.min(k[1].length+1,6);s.push(`<h${M} class="md-h${M}">${k[2]}</h${M}>`);continue}if(/^(-{3,}|\*{3,}|_{3,})$/.test(g)){n(),c(),s.push('<hr class="md-hr">');continue}let _=g.match(/^&gt;\s?(.*)$/);if(_){n(),c(),s.push(`<blockquote class="md-quote">${_[1]}</blockquote>`);continue}let H=m.match(/^(\s*)/)[1].replace(/\t/g," ").length,J=g.match(/^([-*+])\s+(.+)$/),G=g.match(/^(\d+)[.)]\s+(.+)$/);if(J||G){n();let M=G?"ol":"ul",xt=J?J[2]:G[2];!o.length||H>o[o.length-1].indent?(s.push(`<${M} class="md-list">`),o.push({tag:M,indent:H})):(l(H+1),o.length||(s.push(`<${M} class="md-list">`),o.push({tag:M,indent:H}))),s.push(`<li>${xt}</li>`);continue}c(),r.push(m.trim())}n(),c();let p=s.join(`
2301
2301
  `);return p=p.replace(/\*\*([^*\n]+)\*\*/g,"<strong>$1</strong>"),p=p.replace(/__([^_\n]+)__/g,"<strong>$1</strong>"),p=p.replace(/(^|[^*])\*([^*\n]+)\*/g,"$1<em>$2</em>"),p}var L=`
@@ -2314,7 +2314,7 @@ var LokiDashboard=(()=>{var Re=Object.defineProperty;var _t=Object.getOwnPropert
2314
2314
  .md-body .md-link:hover { text-decoration: underline; }
2315
2315
  .md-body .md-hr { border: 0; border-top: 1px solid var(--loki-border, #e5e1d8); margin: 16px 0; }
2316
2316
  .md-body strong { font-weight: 650; }
2317
- `;function st(d){return!d||typeof d!="string"?null:d.startsWith("page-")?d.slice(5):d}function Ne(){if(typeof document>"u")return null;let d=document.querySelector(".section-page.active");if(d&&d.id)return st(d.id);try{if(typeof localStorage<"u"){let e=localStorage.getItem("loki-active-section");if(e)return e}}catch{}return null}var Ge=class{constructor(){this._regs=new Map,this._seq=0,this._activeSection=Ne(),this._hidden=typeof document<"u"?!!document.hidden:!1,this._listenersInstalled=!1,this._sectionChangeHandler=null,this._visibilityHandler=null,this._mutationObserver=null}get _hasDom(){return typeof document<"u"}_ensureListeners(){if(!(this._listenersInstalled||!this._hasDom)){this._sectionChangeHandler=e=>{let t=e&&e.detail&&e.detail.section?e.detail.section:Ne();this._setActiveSection(t)},document.addEventListener("loki:section-change",this._sectionChangeHandler),this._visibilityHandler=()=>{let e=!!document.hidden;e!==this._hidden&&(this._hidden=e,e||(this._activeSection=Ne()),this._reevaluateAll())},document.addEventListener("visibilitychange",this._visibilityHandler);try{let e=document.getElementById("main-content")||document.body;e&&typeof MutationObserver<"u"&&(this._mutationObserver=new MutationObserver(()=>{let t=Ne();t&&t!==this._activeSection&&this._setActiveSection(t)}),this._mutationObserver.observe(e,{subtree:!0,attributes:!0,attributeFilter:["class"]}))}catch{}this._listenersInstalled=!0}}_setActiveSection(e){e!==this._activeSection&&(this._activeSection=e,this._reevaluateAll())}_resolveSection(e){if(Object.prototype.hasOwnProperty.call(e,"sectionId"))return e.sectionId===null?void 0:e.sectionId;if(e.element&&typeof e.element.closest=="function"){let t=e.element.closest(".section-page"),i=st(t&&t.id);return i===null?void 0:i}}_isAllowed(e){if(!this._hasDom)return!0;if(this._hidden)return!1;let t=this._resolveSection(e);return t===void 0?!0:t===this._activeSection}_reevaluateAll(){for(let e of this._regs.values()){let t=this._isAllowed(e);t&&!e._wasAllowed?(e._wasAllowed=!0,this._safeRun(e)):e._wasAllowed=t}}_safeRun(e){try{let t=e.loadFn();t&&typeof t.then=="function"&&t.catch(()=>{})}catch{}}register(e={}){let{loadFn:t,intervalMs:i}=e;if(typeof t!="function")throw new Error("registerPoll: loadFn must be a function");if(typeof i!="number"||!(i>0))throw new Error("registerPoll: intervalMs must be a positive number");this._ensureListeners();let a=e.id||`poll-${++this._seq}`;this._regs.has(a)&&this._stopReg(this._regs.get(a));let s={id:a,loadFn:t,intervalMs:i,element:e.element||null,_timer:null,_wasAllowed:!1};return Object.prototype.hasOwnProperty.call(e,"sectionId")&&(s.sectionId=e.sectionId),s._wasAllowed=this._isAllowed(s),s._timer=setInterval(()=>{this._isAllowed(s)?(s._wasAllowed=!0,this._safeRun(s)):s._wasAllowed=!1},i),this._regs.set(a,s),e.immediate!==!1&&s._wasAllowed&&this._safeRun(s),{id:a,stop:()=>this.unregister(a),isActive:()=>this._isAllowed(s)}}_stopReg(e){e&&e._timer&&(clearInterval(e._timer),e._timer=null)}unregister(e){let t=this._regs.get(e);return t?(this._stopReg(t),this._regs.delete(e),!0):!1}get activeSection(){return this._activeSection}get size(){return this._regs.size}destroy(){for(let e of this._regs.values())this._stopReg(e);this._regs.clear(),this._hasDom&&(this._sectionChangeHandler&&(document.removeEventListener("loki:section-change",this._sectionChangeHandler),this._sectionChangeHandler=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null)),this._listenersInstalled=!1}},Je=null;function At(){return Je||(Je=new Ge),Je}function b(d){return At().register(d)}var W=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={status:"offline",phase:null,iteration:null,provider:null,running_agents:0,pending_tasks:null,uptime_seconds:0,complexity:null,connected:!1},this._api=null,this._pollInterval=null,this._statusUpdateHandler=null,this._connectedHandler=null,this._disconnectedHandler=null,this._checklistSummary=null,this._appRunnerStatus=null,this._playwrightResults=null,this._gateStatus=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling(),this._api.connect().catch(()=>{})}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._loadAbortController&&(this._loadAbortController.abort(),this._loadAbortController=null),this._teardownApiListeners()}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._teardownApiListeners(),this._setupApi(),this._api.connect().catch(()=>{}),this._loadStatus()),e==="theme"&&this._applyTheme())}_teardownApiListeners(){this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(f.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(f.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(f.DISCONNECTED,this._disconnectedHandler))}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e}),this._statusUpdateHandler=t=>this._updateFromStatus(t.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(f.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(f.CONNECTED,this._connectedHandler),this._api.addEventListener(f.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){this._loadAbortController&&this._loadAbortController.abort(),this._loadAbortController=new AbortController;let{signal:e}=this._loadAbortController;try{let[t,i,a,s,r]=await Promise.allSettled([this._api.getStatus(),this._api.getChecklistSummary(),this._api.getAppRunnerStatus(),this._api.getPlaywrightResults(),this._api.getCouncilGate()]);if(e.aborted)return;t.status==="fulfilled"?this._updateFromStatus(t.value):(this._data.connected=!1,this._data.status="offline"),i.status==="fulfilled"&&(this._checklistSummary=i.value?.summary||null),a.status==="fulfilled"&&(this._appRunnerStatus=a.value),s.status==="fulfilled"&&(this._playwrightResults=s.value),r.status==="fulfilled"&&(this._gateStatus=r.value),this.render()}catch{if(e.aborted)return;this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(e){e&&(this._data={...this._data,connected:!0,status:e.status||"offline",phase:e.phase||null,iteration:e.iteration!=null?e.iteration:null,provider:e.provider||null,running_agents:e.running_agents||0,pending_tasks:e.pending_tasks!=null?e.pending_tasks:null,uptime_seconds:e.uptime_seconds||0,complexity:e.complexity||null})}_startPolling(){this._poll=b({loadFn:async()=>{try{await this._loadStatus()}catch{this._data.connected=!1,this._data.status="offline",this.render()}},intervalMs:5e3,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}_formatUptime(e){if(!e||e<0)return"--";let t=Math.floor(e/3600),i=Math.floor(e%3600/60),a=Math.floor(e%60);return t>0?`${t}h ${i}m`:i>0?`${i}m ${a}s`:`${a}s`}_getStatusDotClass(){switch(this._data.status){case"running":case"autonomous":return"active";case"paused":return"paused";case"stopped":return"stopped";case"error":return"error";default:return"offline"}}_escapeHtml(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_renderAppRunnerCard(){let e=this._appRunnerStatus;if(!e||e.status==="not_initialized")return`
2317
+ `;function st(d){return!d||typeof d!="string"?null:d.startsWith("page-")?d.slice(5):d}function Ne(){if(typeof document>"u")return null;let d=document.querySelector(".section-page.active");if(d&&d.id)return st(d.id);try{if(typeof localStorage<"u"){let e=localStorage.getItem("loki-active-section");if(e)return e}}catch{}return null}var Ge=class{constructor(){this._regs=new Map,this._seq=0,this._activeSection=Ne(),this._hidden=typeof document<"u"?!!document.hidden:!1,this._listenersInstalled=!1,this._sectionChangeHandler=null,this._visibilityHandler=null,this._mutationObserver=null}get _hasDom(){return typeof document<"u"}_ensureListeners(){if(!(this._listenersInstalled||!this._hasDom)){this._sectionChangeHandler=e=>{let t=e&&e.detail&&e.detail.section?e.detail.section:Ne();this._setActiveSection(t)},document.addEventListener("loki:section-change",this._sectionChangeHandler),this._visibilityHandler=()=>{let e=!!document.hidden;e!==this._hidden&&(this._hidden=e,e||(this._activeSection=Ne()),this._reevaluateAll())},document.addEventListener("visibilitychange",this._visibilityHandler);try{let e=document.getElementById("main-content")||document.body;e&&typeof MutationObserver<"u"&&(this._mutationObserver=new MutationObserver(()=>{let t=Ne();t&&t!==this._activeSection&&this._setActiveSection(t)}),this._mutationObserver.observe(e,{subtree:!0,attributes:!0,attributeFilter:["class"]}))}catch{}this._listenersInstalled=!0}}_setActiveSection(e){e!==this._activeSection&&(this._activeSection=e,this._reevaluateAll())}_resolveSection(e){if(Object.prototype.hasOwnProperty.call(e,"sectionId"))return e.sectionId===null?void 0:e.sectionId;if(e.element&&typeof e.element.closest=="function"){let t=e.element.closest(".section-page"),i=st(t&&t.id);return i===null?void 0:i}}_isAllowed(e){if(!this._hasDom)return!0;if(this._hidden)return!1;let t=this._resolveSection(e);return t===void 0?!0:t===this._activeSection}_reevaluateAll(){for(let e of this._regs.values()){let t=this._isAllowed(e);t&&!e._wasAllowed?(e._wasAllowed=!0,this._safeRun(e)):e._wasAllowed=t}}_safeRun(e){try{let t=e.loadFn();t&&typeof t.then=="function"&&t.catch(()=>{})}catch{}}register(e={}){let{loadFn:t,intervalMs:i}=e;if(typeof t!="function")throw new Error("registerPoll: loadFn must be a function");if(typeof i!="number"||!(i>0))throw new Error("registerPoll: intervalMs must be a positive number");this._ensureListeners();let a=e.id||`poll-${++this._seq}`;this._regs.has(a)&&this._stopReg(this._regs.get(a));let s={id:a,loadFn:t,intervalMs:i,element:e.element||null,_timer:null,_wasAllowed:!1};return Object.prototype.hasOwnProperty.call(e,"sectionId")&&(s.sectionId=e.sectionId),s._wasAllowed=this._isAllowed(s),s._timer=setInterval(()=>{this._isAllowed(s)?(s._wasAllowed=!0,this._safeRun(s)):s._wasAllowed=!1},i),this._regs.set(a,s),e.immediate!==!1&&s._wasAllowed&&this._safeRun(s),{id:a,stop:()=>this.unregister(a),isActive:()=>this._isAllowed(s)}}_stopReg(e){e&&e._timer&&(clearInterval(e._timer),e._timer=null)}unregister(e){let t=this._regs.get(e);return t?(this._stopReg(t),this._regs.delete(e),!0):!1}get activeSection(){return this._activeSection}get size(){return this._regs.size}destroy(){for(let e of this._regs.values())this._stopReg(e);this._regs.clear(),this._hasDom&&(this._sectionChangeHandler&&(document.removeEventListener("loki:section-change",this._sectionChangeHandler),this._sectionChangeHandler=null),this._visibilityHandler&&(document.removeEventListener("visibilitychange",this._visibilityHandler),this._visibilityHandler=null),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null)),this._listenersInstalled=!1}},Je=null;function At(){return Je||(Je=new Ge),Je}function b(d){return At().register(d)}var W=class extends v{static get observedAttributes(){return["api-url","theme"]}constructor(){super(),this._data={status:"offline",phase:null,iteration:null,provider:null,running_agents:0,pending_tasks:null,uptime_seconds:0,complexity:null,connected:!1},this._api=null,this._pollInterval=null,this._statusUpdateHandler=null,this._connectedHandler=null,this._disconnectedHandler=null,this._checklistSummary=null,this._appRunnerStatus=null,this._playwrightResults=null,this._gateStatus=null}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadStatus(),this._startPolling(),this._api.connect().catch(()=>{})}disconnectedCallback(){super.disconnectedCallback(),this._stopPolling(),this._loadAbortController&&(this._loadAbortController.abort(),this._loadAbortController=null),this._teardownApiListeners()}attributeChangedCallback(e,t,i){if(t!==i){if(e==="api-url"&&this._api){let a=this._api;this._teardownApiListeners(),this._setupApi(),a&&a!==this._api&&a.disconnect(),this._api.connect().catch(()=>{}),this._loadStatus()}e==="theme"&&this._applyTheme()}}_teardownApiListeners(){this._api&&(this._statusUpdateHandler&&this._api.removeEventListener(f.STATUS_UPDATE,this._statusUpdateHandler),this._connectedHandler&&this._api.removeEventListener(f.CONNECTED,this._connectedHandler),this._disconnectedHandler&&this._api.removeEventListener(f.DISCONNECTED,this._disconnectedHandler))}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e}),this._statusUpdateHandler=t=>this._updateFromStatus(t.detail),this._connectedHandler=()=>{this._data.connected=!0,this.render()},this._disconnectedHandler=()=>{this._data.connected=!1,this._data.status="offline",this.render()},this._api.addEventListener(f.STATUS_UPDATE,this._statusUpdateHandler),this._api.addEventListener(f.CONNECTED,this._connectedHandler),this._api.addEventListener(f.DISCONNECTED,this._disconnectedHandler)}async _loadStatus(){this._loadAbortController&&this._loadAbortController.abort(),this._loadAbortController=new AbortController;let{signal:e}=this._loadAbortController;try{let[t,i,a,s,r]=await Promise.allSettled([this._api.getStatus(),this._api.getChecklistSummary(),this._api.getAppRunnerStatus(),this._api.getPlaywrightResults(),this._api.getCouncilGate()]);if(e.aborted)return;t.status==="fulfilled"?this._updateFromStatus(t.value):(this._data.connected=!1,this._data.status="offline"),i.status==="fulfilled"&&(this._checklistSummary=i.value?.summary||null),a.status==="fulfilled"&&(this._appRunnerStatus=a.value),s.status==="fulfilled"&&(this._playwrightResults=s.value),r.status==="fulfilled"&&(this._gateStatus=r.value),this.render()}catch{if(e.aborted)return;this._data.connected=!1,this._data.status="offline",this.render()}}_updateFromStatus(e){e&&(this._data={...this._data,connected:!0,status:e.status||"offline",phase:e.phase||null,iteration:e.iteration!=null?e.iteration:null,provider:e.provider||null,running_agents:e.running_agents||0,pending_tasks:e.pending_tasks!=null?e.pending_tasks:null,uptime_seconds:e.uptime_seconds||0,complexity:e.complexity||null})}_startPolling(){this._poll=b({loadFn:async()=>{try{await this._loadStatus()}catch{this._data.connected=!1,this._data.status="offline",this.render()}},intervalMs:5e3,element:this,immediate:!1})}_stopPolling(){this._poll&&(this._poll.stop(),this._poll=null)}_formatUptime(e){if(!e||e<0)return"--";let t=Math.floor(e/3600),i=Math.floor(e%3600/60),a=Math.floor(e%60);return t>0?`${t}h ${i}m`:i>0?`${i}m ${a}s`:`${a}s`}_getStatusDotClass(){switch(this._data.status){case"running":case"autonomous":return"active";case"paused":return"paused";case"stopped":return"stopped";case"error":return"error";default:return"offline"}}_escapeHtml(e){return e?String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"):""}_renderAppRunnerCard(){let e=this._appRunnerStatus;if(!e||e.status==="not_initialized")return`
2318
2318
  <div class="overview-card">
2319
2319
  <div class="card-label">App Status</div>
2320
2320
  <div class="card-value small-text">${this._data.status==="running"||this._data.status==="autonomous"?"Starting...":"Not started"}</div>
@@ -2569,7 +2569,7 @@ var LokiDashboard=(()=>{var Re=Object.defineProperty;var _t=Object.getOwnPropert
2569
2569
  </div>
2570
2570
  </div>
2571
2571
  </div>
2572
- `}};customElements.get("loki-overview")||customElements.define("loki-overview",W);var Tt=[{id:"pending",label:"Pending",status:"pending",color:"var(--loki-text-muted)"},{id:"in_progress",label:"In Progress",status:"in_progress",color:"var(--loki-blue)"},{id:"review",label:"In Review",status:"review",color:"var(--loki-purple)"},{id:"done",label:"Completed",status:"done",color:"var(--loki-green)"}],Q=class d extends v{static get observedAttributes(){return["api-url","project-id","theme","readonly"]}constructor(){super(),this._tasks=[],this._loading=!0,this._error=null,this._draggedTask=null,this._selectedTask=null,this._expandedCards=new Set,this._selectedTasks=new Set,this._bulkMode=!1,this._activeFilter="all",this._searchQuery="",this._visibleCounts={},this._api=null,this._state=U()}static get PAGE_SIZE(){return 10}_getVisibleCount(e){let t=this._visibleCounts[e];return typeof t=="number"&&t>0?t:d.PAGE_SIZE}_showMore(e){this._visibleCounts[e]=this._getVisibleCount(e)+d.PAGE_SIZE,this.render()}_columnIcon(e){switch(e){case"pending":return'<circle cx="12" cy="12" r="10"/>';case"in_progress":return'<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>';case"review":return'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';case"done":return'<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>';default:return'<circle cx="12" cy="12" r="10"/>'}}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadTasks()}disconnectedCallback(){super.disconnectedCallback(),this._teardownApiListeners()}_teardownApiListeners(){this._api&&this._onTaskEvent&&(this._api.removeEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_DELETED,this._onTaskEvent))}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._teardownApiListeners(),this._setupApi(),this._loadTasks()),e==="project-id"&&this._loadTasks(),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e}),this._onTaskEvent=()=>this._loadTasks(),this._api.addEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.addEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.addEventListener(f.TASK_DELETED,this._onTaskEvent)}async _loadTasks(){let e=this._api;this._loading=!0,this._error=null,this.render();try{let t=this.getAttribute("project-id"),i=t?{projectId:parseInt(t)}:{},a=await e.listTasks(i);if(e!==this._api)return;this._tasks=a;let s=this._state.get("localTasks")||[];s.length>0&&(this._tasks=[...this._tasks,...s.map(r=>({...r,isLocal:!0}))]),this._state.update({"cache.tasks":this._tasks},!1)}catch(t){if(e!==this._api)return;this._error=t.message,this._tasks=(this._state.get("localTasks")||[]).map(i=>({...i,isLocal:!0}))}this._loading=!1,this.render()}_getTasksByStatus(e){return this._getFilteredTasks().filter(i=>i.status?.toLowerCase().replace(/-/g,"_")===e)}_handleDragStart(e,t){this.hasAttribute("readonly")||(this._draggedTask=t,e.target.classList.add("dragging"),e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",t.id.toString()))}_handleDragEnd(e){e.target.classList.remove("dragging"),this._draggedTask=null,this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.classList.remove("drag-over")})}_handleDragOver(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}_handleDragEnter(e){e.preventDefault(),e.currentTarget.classList.add("drag-over")}_handleDragLeave(e){e.currentTarget.contains(e.relatedTarget)||e.currentTarget.classList.remove("drag-over")}async _handleDrop(e,t){if(e.preventDefault(),e.currentTarget.classList.remove("drag-over"),!this._draggedTask||this.hasAttribute("readonly"))return;let i=this._draggedTask.id,a=this._tasks.find(r=>r.id===i);if(!a)return;let s=a.status;if(s!==t){a.status=t,this.render();try{a.isLocal?this._state.moveLocalTask(i,t):await this._api.moveTask(i,t,0),this.dispatchEvent(new CustomEvent("task-moved",{detail:{taskId:i,oldStatus:s,newStatus:t}}))}catch(r){a.status=s,this.render(),console.error("Failed to move task:",r)}}}_toggleCardExpand(e){this._expandedCards.has(e)?this._expandedCards.delete(e):this._expandedCards.add(e),this.render()}_toggleTaskSelection(e,t){t&&t.stopPropagation(),this._selectedTasks.has(e)?this._selectedTasks.delete(e):this._selectedTasks.add(e),this.render()}_toggleBulkMode(){this._bulkMode=!this._bulkMode,this._bulkMode||this._selectedTasks.clear(),this.render()}async _bulkMove(e){let t=[...this._selectedTasks];for(let i of t){let a=this._tasks.find(s=>String(s.id)===String(i));if(a&&a.status!==e)try{a.isLocal?this._state.moveLocalTask(i,e):await this._api.moveTask(i,e,0),a.status=e}catch(s){console.error("Failed to bulk move task:",i,s)}}this._selectedTasks.clear(),this._bulkMode=!1,this.render(),this._loadTasks()}async _bulkDelete(){let e=[...this._selectedTasks];for(let t of e)try{await this._api.deleteTask(t)}catch(i){console.error("Failed to delete task:",t,i)}this._selectedTasks.clear(),this._bulkMode=!1,this._loadTasks()}_setFilter(e){this._activeFilter=e,this._visibleCounts={},this.render()}_setSearch(e){this._searchQuery=e||"",this._visibleCounts={},this._renderTaskRegion();let t=this.shadowRoot.getElementById("task-search");if(t){t.focus();let i=t.value.length;try{t.setSelectionRange(i,i)}catch{}}}_getFilteredTasks(){let e=[...this._tasks],t=new Date,i=new Date(t.getFullYear(),t.getMonth(),t.getDate()),a=new Date(i.getTime()-7*24*60*60*1e3);switch(this._activeFilter){case"today":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=i});break;case"this-week":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=a});break;case"running":e=e.filter(r=>r.status==="in_progress");break;case"failed":e=e.filter(r=>r.status==="failed"||r.status==="error");break;default:break}let s=this._searchQuery.trim().toLowerCase();return s&&(e=e.filter(r=>[r.id,r.title,r.description,r.type].map(n=>String(n??"").toLowerCase()).join(" ").includes(s))),e}_openAddTaskModal(e="pending"){this.dispatchEvent(new CustomEvent("add-task",{detail:{status:e}}))}_openTaskDetail(e){this._selectedTask=e,this.render(),this.dispatchEvent(new CustomEvent("task-click",{detail:{task:e}}))}_closeTaskDetail(){this._selectedTask=null,this.render()}_renderMarkdown(e){return C(e)}_formatTimestamp(e){if(!e)return"";try{let t=new Date(e);return isNaN(t.getTime())?this._escapeHtml(String(e)):t.toLocaleString()}catch{return this._escapeHtml(String(e))}}_phaseClass(e){let t=String(e||"").toLowerCase();return["reason","plan","planning"].includes(t)?"phase-reason":["act","execute","execution","implement"].includes(t)?"phase-act":["reflect","review"].includes(t)?"phase-reflect":["verify","test","gate"].includes(t)?"phase-verify":"phase-default"}_logLevelClass(e){let t=String(e||"info").toLowerCase();return t==="error"||t==="fatal"?"log-error":t==="warn"||t==="warning"?"log-warn":t==="debug"||t==="trace"?"log-debug":"log-info"}_renderTaskDetailModal(e){if(!e)return"";let t=(e.priority||"medium").toLowerCase(),i=t.charAt(0).toUpperCase()+t.slice(1),a=e.status||"pending",s=a.replace(/_/g," ").replace(/\b\w/g,g=>g.toUpperCase()),r=e.metadata||{},o=e.acceptance_criteria||[],n=e.context_files||[],l=e.specification||"",c=e.description||"",p=Array.isArray(e.notes)?e.notes:[],u=Array.isArray(e.logs)?e.logs:[],m=e.full_content||"";return`
2572
+ `}};customElements.get("loki-overview")||customElements.define("loki-overview",W);var Tt=[{id:"pending",label:"Pending",status:"pending",color:"var(--loki-text-muted)"},{id:"in_progress",label:"In Progress",status:"in_progress",color:"var(--loki-blue)"},{id:"review",label:"In Review",status:"review",color:"var(--loki-purple)"},{id:"done",label:"Completed",status:"done",color:"var(--loki-green)"}],Q=class d extends v{static get observedAttributes(){return["api-url","project-id","theme","readonly"]}constructor(){super(),this._tasks=[],this._loading=!0,this._error=null,this._draggedTask=null,this._selectedTask=null,this._expandedCards=new Set,this._selectedTasks=new Set,this._bulkMode=!1,this._activeFilter="all",this._searchQuery="",this._visibleCounts={},this._api=null,this._state=U()}static get PAGE_SIZE(){return 10}_getVisibleCount(e){let t=this._visibleCounts[e];return typeof t=="number"&&t>0?t:d.PAGE_SIZE}_showMore(e){this._visibleCounts[e]=this._getVisibleCount(e)+d.PAGE_SIZE,this.render()}_columnIcon(e){switch(e){case"pending":return'<circle cx="12" cy="12" r="10"/>';case"in_progress":return'<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>';case"review":return'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';case"done":return'<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>';default:return'<circle cx="12" cy="12" r="10"/>'}}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadTasks()}disconnectedCallback(){super.disconnectedCallback(),this._teardownApiListeners()}_teardownApiListeners(){this._api&&this._onTaskEvent&&(this._api.removeEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_DELETED,this._onTaskEvent))}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._teardownApiListeners(),this._setupApi(),this._loadTasks()),e==="project-id"&&this._loadTasks(),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e}),this._onTaskEvent=()=>this._loadTasks(),this._api.addEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.addEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.addEventListener(f.TASK_DELETED,this._onTaskEvent)}async _loadTasks(){let e=this._api;this._loading=!0,this._error=null,this.render();try{let t=this.getAttribute("project-id"),i=t?{projectId:parseInt(t)}:{},a=await e.listTasks(i);if(e!==this._api)return;this._tasks=a;let s=this._state.get("localTasks")||[];s.length>0&&(this._tasks=[...this._tasks,...s.map(r=>({...r,isLocal:!0}))]),this._state.update({"cache.tasks":this._tasks},!1)}catch(t){if(e!==this._api)return;this._error=t.message,this._tasks=(this._state.get("localTasks")||[]).map(i=>({...i,isLocal:!0}))}this._loading=!1,this.render()}_getTasksByStatus(e){return this._getFilteredTasks().filter(i=>i.status?.toLowerCase().replace(/-/g,"_")===e)}_handleDragStart(e,t){this.hasAttribute("readonly")||(this._draggedTask=t,e.target.classList.add("dragging"),e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",t.id.toString()))}_handleDragEnd(e){e.target.classList.remove("dragging"),this._draggedTask=null,this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.classList.remove("drag-over")})}_handleDragOver(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}_handleDragEnter(e){e.preventDefault(),e.currentTarget.classList.add("drag-over")}_handleDragLeave(e){e.currentTarget.contains(e.relatedTarget)||e.currentTarget.classList.remove("drag-over")}async _handleDrop(e,t){if(e.preventDefault(),e.currentTarget.classList.remove("drag-over"),!this._draggedTask||this.hasAttribute("readonly"))return;let i=this._draggedTask.id,a=this._tasks.find(r=>r.id===i);if(!a)return;let s=a.status;if(s!==t){a.status=t,this.render();try{a.isLocal?this._state.moveLocalTask(i,t):await this._api.moveTask(i,t,0),this.dispatchEvent(new CustomEvent("task-moved",{detail:{taskId:i,oldStatus:s,newStatus:t}}))}catch(r){a.status=s,this.render(),console.error("Failed to move task:",r)}}}_toggleCardExpand(e){this._expandedCards.has(e)?this._expandedCards.delete(e):this._expandedCards.add(e),this.render()}_toggleTaskSelection(e,t){t&&t.stopPropagation(),this._selectedTasks.has(e)?this._selectedTasks.delete(e):this._selectedTasks.add(e),this.render()}_toggleBulkMode(){this._bulkMode=!this._bulkMode,this._bulkMode||this._selectedTasks.clear(),this.render()}async _bulkMove(e){let t=[...this._selectedTasks];for(let i of t){let a=this._tasks.find(s=>String(s.id)===String(i));if(a&&a.status!==e)try{a.isLocal?this._state.moveLocalTask(i,e):await this._api.moveTask(i,e,0),a.status=e}catch(s){console.error("Failed to bulk move task:",i,s)}}this._selectedTasks.clear(),this._bulkMode=!1,this.render(),this._loadTasks()}async _bulkDelete(){let e=[...this._selectedTasks];for(let t of e)try{await this._api.deleteTask(t)}catch(i){console.error("Failed to delete task:",t,i)}this._selectedTasks.clear(),this._bulkMode=!1,this._loadTasks()}_setFilter(e){this._activeFilter=e,this._visibleCounts={},this.render()}_setSearch(e){this._searchQuery=e||"",this._visibleCounts={},this._renderTaskRegion();let t=this.shadowRoot.getElementById("task-search");if(t){t.focus();let i=t.value.length;try{t.setSelectionRange(i,i)}catch{}}}_getFilteredTasks(){let e=[...this._tasks],t=new Date,i=new Date(t.getFullYear(),t.getMonth(),t.getDate()),a=new Date(i.getTime()-7*24*60*60*1e3);switch(this._activeFilter){case"today":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=i});break;case"this-week":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=a});break;case"running":e=e.filter(r=>r.status==="in_progress");break;case"failed":e=e.filter(r=>r.status==="failed"||r.status==="error");break;default:break}let s=this._searchQuery.trim().toLowerCase();return s&&(e=e.filter(r=>[r.id,r.title,r.description,r.type].map(n=>String(n??"").toLowerCase()).join(" ").includes(s))),e}_openAddTaskModal(e="pending"){this.dispatchEvent(new CustomEvent("add-task",{detail:{status:e}}))}_openTaskDetail(e){this._selectedTask=e,this.render(),this.dispatchEvent(new CustomEvent("task-click",{detail:{task:e}}))}_closeTaskDetail(){this._selectedTask=null,this.render()}_renderMarkdown(e){return C(e)}_formatTimestamp(e){if(!e)return"";try{let t=new Date(e);return isNaN(t.getTime())?this._escapeHtml(String(e)):t.toLocaleString()}catch{return this._escapeHtml(String(e))}}_phaseClass(e){let t=String(e||"").toLowerCase();return["reason","plan","planning"].includes(t)?"phase-reason":["act","execute","execution","implement"].includes(t)?"phase-act":["reflect","review"].includes(t)?"phase-reflect":["verify","test","gate"].includes(t)?"phase-verify":"phase-default"}_logLevelClass(e){let t=String(e||"info").toLowerCase();return t==="error"||t==="fatal"?"log-error":t==="warn"||t==="warning"?"log-warn":t==="debug"||t==="trace"?"log-debug":"log-info"}_renderTaskDetailModal(e){if(!e)return"";let t=(e.priority||"medium").toLowerCase(),i=t.charAt(0).toUpperCase()+t.slice(1),a=e.status||"pending",s=a.replace(/_/g," ").replace(/\b\w/g,g=>g.toUpperCase()),r=e.metadata||{},o=Array.isArray(e.acceptance_criteria)?e.acceptance_criteria:[],n=Array.isArray(e.context_files)?e.context_files:[],l=e.specification||"",c=e.description||"",p=Array.isArray(e.notes)?e.notes:[],u=Array.isArray(e.logs)?e.logs:[],m=e.full_content||"";return`
2573
2573
  <div class="modal-overlay" id="task-detail-overlay">
2574
2574
  <div class="modal-container">
2575
2575
  <div class="modal-header">
@@ -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.112.0
5
+ **Version:** v7.114.0
6
6
 
7
7
  ---
8
8
 
@@ -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.112.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}
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=647CB006182AFBC464756E2164756E21
817
+ //# debugId=0888AC629271567B64756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.112.0'
60
+ __version__ = '7.114.0'
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
@@ -1360,6 +1360,33 @@ class MemoryRetrieval:
1360
1360
  optimized = optimize_context(full_details, budget_remaining)
1361
1361
  selected_memories.extend(optimized)
1362
1362
 
1363
+ # Cross-layer dedup by id. A Layer-2 summary and a Layer-3 full record
1364
+ # can carry the SAME memory id (the summary is built from the same
1365
+ # episode/pattern/skill that retrieve_task_aware later surfaces), so
1366
+ # without this pass the same id appears twice. Every other merge path
1367
+ # dedups by id (see _merge_results seen_ids); this one must too. Prefer
1368
+ # the fuller record when both exist: keep the highest _layer for an id
1369
+ # (Layer 3 full > Layer 2 summary > Layer 1 topic). Records without an
1370
+ # id are never collapsed (each keeps its own slot), mirroring
1371
+ # _merge_results. Insertion order of first-seen ids is preserved.
1372
+ deduped: List[Dict[str, Any]] = []
1373
+ best_index_for_id: Dict[Any, int] = {}
1374
+ for item in selected_memories:
1375
+ item_id = item.get("id")
1376
+ if item_id is None:
1377
+ deduped.append(item)
1378
+ continue
1379
+ if item_id in best_index_for_id:
1380
+ existing = deduped[best_index_for_id[item_id]]
1381
+ # Higher layer = fuller record; replace the summary in place so
1382
+ # the first-seen slot keeps the richest entry for this id.
1383
+ if item.get("_layer", 0) > existing.get("_layer", 0):
1384
+ deduped[best_index_for_id[item_id]] = item
1385
+ continue
1386
+ best_index_for_id[item_id] = len(deduped)
1387
+ deduped.append(item)
1388
+ selected_memories = deduped
1389
+
1363
1390
  # Calculate final metrics
1364
1391
  total_available = self._estimate_total_available_tokens()
1365
1392
  metrics = get_context_efficiency(selected_memories, token_budget, total_available)
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.112.0",
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.112.0",
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",