loki-mode 7.7.9 → 7.7.11

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: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.7.9
6
+ # Loki Mode v7.7.11
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -381,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
381
 
382
382
  ---
383
383
 
384
- **v7.7.9 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
384
+ **v7.7.11 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.7.9
1
+ 7.7.11
package/autonomy/loki CHANGED
@@ -1470,6 +1470,26 @@ cmd_start() {
1470
1470
  # Record session start time for efficiency tracking
1471
1471
  record_session_start
1472
1472
 
1473
+ # UT2-13: Write cli-provider marker when --provider was explicitly passed.
1474
+ # This lets cmd_status_json report provider_source="cli" for the lifetime
1475
+ # of this session (24-hour heuristic). Atomic write via .tmp + mv.
1476
+ # v7.7.11 (council fix): validate provider against canonical set BEFORE
1477
+ # writing so a typo like `--provider xyz` does not pollute status output
1478
+ # with bogus state for 24h. Also write current PID alongside so the reader
1479
+ # can drop the marker when the originating process is no longer alive
1480
+ # (SIGKILL / crash paths that skip our cleanup hooks).
1481
+ case "$provider" in
1482
+ claude|codex|cline|aider)
1483
+ mkdir -p "$LOKI_DIR/state" 2>/dev/null || true
1484
+ printf '%s:%s:%s\n' "$provider" "$(date +%s)" "$$" \
1485
+ > "$LOKI_DIR/state/.cli-provider.tmp" 2>/dev/null \
1486
+ && mv "$LOKI_DIR/state/.cli-provider.tmp" \
1487
+ "$LOKI_DIR/state/cli-provider" 2>/dev/null || true
1488
+ ;;
1489
+ '') : ;; # no --provider passed
1490
+ *) log_warn "UT2-13: skipped cli-provider marker (unknown provider '$provider')" ;;
1491
+ esac
1492
+
1473
1493
  # Emit learning signal: user preference for provider selection (SYN-018)
1474
1494
  if [ -n "$provider" ]; then
1475
1495
  # User explicitly chose a provider
@@ -2272,6 +2292,41 @@ dashboard_port = sys.argv[3]
2272
2292
  env_provider = sys.argv[4]
2273
2293
  result = {}
2274
2294
 
2295
+ # UT2-13: Helper -- read cli-provider marker written by --provider flag dispatch.
2296
+ # v7.7.11 (council fix): file format is now '<provider>:<unix_ts>:<pid>' so we
2297
+ # can drop the marker when the originating process is gone (SIGKILL / crash
2298
+ # paths that bypassed our cleanup hooks). Also validates provider name against
2299
+ # canonical set so a typo writing the file can't pollute status output.
2300
+ # Legacy format '<provider>:<unix_ts>' still parses (no PID liveness check).
2301
+ # Returns (provider_name, True) when valid; (None, False) otherwise.
2302
+ def _read_cli_provider(loki_dir):
2303
+ cli_file = os.path.join(loki_dir, 'state', 'cli-provider')
2304
+ if not os.path.isfile(cli_file):
2305
+ return None, False
2306
+ try:
2307
+ content = open(cli_file).read().strip()
2308
+ parts = content.split(':')
2309
+ if len(parts) < 2:
2310
+ return None, False
2311
+ prov = parts[0]
2312
+ ts = int(parts[1])
2313
+ if prov not in ('claude', 'codex', 'cline', 'aider'):
2314
+ return None, False
2315
+ if time.time() - ts > 86400:
2316
+ return None, False
2317
+ # If a PID is present, drop the marker when that process is gone.
2318
+ if len(parts) >= 3:
2319
+ try:
2320
+ pid = int(parts[2])
2321
+ if pid > 0:
2322
+ os.kill(pid, 0)
2323
+ except (ValueError, ProcessLookupError, PermissionError):
2324
+ # PID missing/dead/cross-uid -> treat as stale.
2325
+ return None, False
2326
+ return prov, True
2327
+ except Exception:
2328
+ return None, False
2329
+
2275
2330
  # Version
2276
2331
  version_file = os.path.join(skill_dir, 'VERSION')
2277
2332
  if os.path.isfile(version_file):
@@ -2285,8 +2340,17 @@ if not os.path.isdir(loki_dir):
2285
2340
  result['status'] = 'inactive'
2286
2341
  result['phase'] = None
2287
2342
  result['iteration'] = 0
2288
- result['provider'] = env_provider if os.environ.get('LOKI_PROVIDER', '') else 'claude'
2289
- result['provider_source'] = 'env' if os.environ.get('LOKI_PROVIDER', '') else 'default'
2343
+ # UT2-13: CLI check first (inactive path -- loki_dir may not exist yet).
2344
+ _cli_prov, _cli_set = _read_cli_provider(loki_dir)
2345
+ if _cli_set:
2346
+ result['provider'] = _cli_prov
2347
+ result['provider_source'] = 'cli'
2348
+ elif os.environ.get('LOKI_PROVIDER', ''):
2349
+ result['provider'] = env_provider
2350
+ result['provider_source'] = 'env'
2351
+ else:
2352
+ result['provider'] = 'claude'
2353
+ result['provider_source'] = 'default'
2290
2354
  result['dashboard_url'] = None
2291
2355
  result['pid'] = None
2292
2356
  result['elapsed_time'] = 0
@@ -2345,7 +2409,9 @@ else:
2345
2409
  result['iteration'] = 0
2346
2410
 
2347
2411
  # Provider + provider_source (v7.7.2 B-5 clarity: surface WHY this
2348
- # value won the resolution race: 'saved' vs 'env' vs 'default').
2412
+ # value won the resolution race: 'cli' > 'saved' > 'env' > 'default').
2413
+ # UT2-13: cli source is checked FIRST -- overrides saved/env/default.
2414
+ _cli_prov, _cli_set = _read_cli_provider(loki_dir)
2349
2415
  provider_file = os.path.join(loki_dir, 'state', 'provider')
2350
2416
  if os.path.isfile(provider_file):
2351
2417
  try:
@@ -2356,7 +2422,10 @@ if os.path.isfile(provider_file):
2356
2422
  else:
2357
2423
  saved = ''
2358
2424
  env_set = os.environ.get('LOKI_PROVIDER', '') != ''
2359
- if saved:
2425
+ if _cli_set:
2426
+ result['provider'] = _cli_prov
2427
+ result['provider_source'] = 'cli'
2428
+ elif saved:
2360
2429
  result['provider'] = saved
2361
2430
  result['provider_source'] = 'saved'
2362
2431
  elif env_set:
@@ -4736,6 +4805,18 @@ cmd_run() {
4736
4805
  if [[ -n "${2:-}" ]]; then
4737
4806
  provider_override="$2"
4738
4807
  start_args+=("--provider" "$2")
4808
+ # UT2-13: Record CLI provider at parse time (atomic write).
4809
+ # v7.7.11 (council fix): validate against canonical set; include
4810
+ # PID so reader can drop the marker when originating process is gone.
4811
+ case "$2" in
4812
+ claude|codex|cline|aider)
4813
+ mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
4814
+ printf '%s:%s:%s\n' "$2" "$(date +%s)" "$$" \
4815
+ > "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
4816
+ && mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
4817
+ "${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
4818
+ ;;
4819
+ esac
4739
4820
  shift 2
4740
4821
  else
4741
4822
  echo -e "${RED}--provider requires a value${NC}"
@@ -4745,6 +4826,17 @@ cmd_run() {
4745
4826
  --provider=*)
4746
4827
  provider_override="${1#*=}"
4747
4828
  start_args+=("--provider" "$provider_override")
4829
+ # UT2-13: Record CLI provider at parse time (atomic write).
4830
+ # v7.7.11 (council fix): validate + include PID.
4831
+ case "$provider_override" in
4832
+ claude|codex|cline|aider)
4833
+ mkdir -p "${LOKI_DIR:-.loki}/state" 2>/dev/null || true
4834
+ printf '%s:%s:%s\n' "$provider_override" "$(date +%s)" "$$" \
4835
+ > "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" 2>/dev/null \
4836
+ && mv "${LOKI_DIR:-.loki}/state/.cli-provider.tmp" \
4837
+ "${LOKI_DIR:-.loki}/state/cli-provider" 2>/dev/null || true
4838
+ ;;
4839
+ esac
4748
4840
  shift
4749
4841
  ;;
4750
4842
  --parallel|--bg|--background|--simple|--complex|--no-dashboard|--sandbox|--no-plan)
package/autonomy/run.sh CHANGED
@@ -8755,10 +8755,68 @@ _intelligent_usage_regen() {
8755
8755
  _manifests="${_manifests}=== $f ===\n$(head -50 "$target_dir/$f" 2>/dev/null)\n\n"
8756
8756
  fi
8757
8757
  done
8758
+ # v7.7.10 F-3 fix: include entrypoint file content so the model can read
8759
+ # the ACTUAL port / host bindings instead of guessing from package.json
8760
+ # scripts (which often imply port 3000 by convention but server.js may
8761
+ # bind a different port like 3001). Without this the regen wrote
8762
+ # "curl http://localhost:3000" for projects where the server bound 3001.
8763
+ local _entrypoints=""
8764
+ local _ep_candidates=""
8765
+ # Detect entrypoint from package.json main field if present
8766
+ if [ -f "$target_dir/package.json" ]; then
8767
+ local _pkg_main
8768
+ _pkg_main=$(python3 -c "import json,sys; d=json.load(open('$target_dir/package.json'));print(d.get('main') or '')" 2>/dev/null)
8769
+ [ -n "$_pkg_main" ] && _ep_candidates="$_ep_candidates $_pkg_main"
8770
+ # Extract files referenced in `scripts.start` and `scripts.dev`
8771
+ local _pkg_scripts
8772
+ _pkg_scripts=$(python3 -c "import json,re,sys; d=json.load(open('$target_dir/package.json'));s=d.get('scripts',{});c=' '.join([s.get('start',''),s.get('dev','')]);[print(t) for t in re.findall(r'[\\w/.-]+\\.(?:js|mjs|cjs|ts|mts|cts|py)\\b',c)]" 2>/dev/null)
8773
+ [ -n "$_pkg_scripts" ] && _ep_candidates="$_ep_candidates $_pkg_scripts"
8774
+ fi
8775
+ # Fallback convention names for common stacks
8776
+ for _ep in server.js server.ts server.mjs index.js index.ts app.js app.ts \
8777
+ main.py app.py server.py manage.py wsgi.py asgi.py \
8778
+ main.go cmd/server/main.go src/main.rs src/index.ts dist/server.js \
8779
+ build/server.js; do
8780
+ _ep_candidates="$_ep_candidates $_ep"
8781
+ done
8782
+ # Read first 80 lines of up to 3 unique existing candidates, scrubbing
8783
+ # common secret-bearing lines before they ship to the haiku endpoint.
8784
+ # v7.7.10 privacy guard: replaces lines matching API_KEY/SECRET/PASSWORD/
8785
+ # TOKEN/PRIVATE_KEY/AUTH/CREDENTIAL/BEARER assignments with [REDACTED]
8786
+ # so default-on regen does not exfiltrate hardcoded secrets. Port-binding
8787
+ # lines (listen/run/ListenAndServe with numeric literals) are preserved.
8788
+ # Opt out entirely with LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE=0.
8789
+ local _include_source="${LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE:-1}"
8790
+ local _seen="" _count=0
8791
+ for _ep in $_ep_candidates; do
8792
+ # Skip duplicates and non-existent files
8793
+ case " $_seen " in *" $_ep "*) continue ;; esac
8794
+ _seen="$_seen $_ep"
8795
+ if [ -f "$target_dir/$_ep" ]; then
8796
+ local _ep_body
8797
+ if [ "$_include_source" = "0" ]; then
8798
+ _ep_body="(entrypoint source omitted: LOKI_INTELLIGENT_USAGE_INCLUDE_SOURCE=0)"
8799
+ else
8800
+ # Scrub: any line whose text contains a credential keyword
8801
+ # has its value (everything after the first `:` or `=`)
8802
+ # replaced with [REDACTED]. Then any literal high-entropy
8803
+ # token shape (stripe sk-, github ghp_/ghs_, slack xox, GCP
8804
+ # AIza, AWS AKIA) is replaced inline. Port-binding lines
8805
+ # (no credential keyword) pass through unchanged.
8806
+ _ep_body=$(head -80 "$target_dir/$_ep" 2>/dev/null \
8807
+ | sed -E \
8808
+ -e '/[Aa][Pp][Ii][_-]?[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Tt][Oo][Kk][Ee][Nn]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee][_-]?[Kk][Ee][Yy]|[Cc][Rr][Ee][Dd][Ee][Nn][Tt][Ii][Aa][Ll]|[Bb][Ee][Aa][Rr][Ee][Rr]/ s/[:=].*$/= [REDACTED]/' \
8809
+ -e 's/(sk-[A-Za-z0-9_-]{16,}|pk_[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{16,}|ghs_[A-Za-z0-9]{16,}|xox[bpoa]-[A-Za-z0-9-]{16,}|AIza[A-Za-z0-9_-]{32,}|AKIA[A-Z0-9]{12,})/[REDACTED]/g')
8810
+ fi
8811
+ _entrypoints="${_entrypoints}=== Entrypoint: $_ep ===\n${_ep_body}\n\n"
8812
+ _count=$((_count + 1))
8813
+ [ "$_count" -ge 3 ] && break
8814
+ fi
8815
+ done
8758
8816
  _commits=$(cd "$target_dir" && git log --oneline -10 2>/dev/null || true)
8759
8817
 
8760
8818
  log_info "Regenerating USAGE.md from final project state (intelligent mode)..."
8761
- local _ic_prompt="You are writing a USAGE.md for the project below. Detect the stack from the manifest files; emit a concise (under 100 lines) Markdown doc with sections: ## Prerequisites, ## Install, ## Start, ## Verify (2-3 copy-paste curl/browser/CLI commands with expected output), ## Stop. Use the ACTUAL command names from package.json scripts or pyproject entry points -- never generic placeholders. Output ONLY the Markdown body (no code-fence wrapper, no preamble).
8819
+ local _ic_prompt="You are writing a USAGE.md for the project below. Detect the stack from the manifest files; emit a concise (under 100 lines) Markdown doc with sections: ## Prerequisites, ## Install, ## Start, ## Verify (2-3 copy-paste curl/browser/CLI commands with expected output), ## Stop. Use the ACTUAL command names from package.json scripts or pyproject entry points -- never generic placeholders. For ports, read the ENTRYPOINT file contents below (server.listen / app.listen / app.run / http.ListenAndServe / uvicorn.run port arg) -- do NOT infer port from script names or convention. If the entrypoint reads from process.env.PORT with a literal default, use the literal default. Output ONLY the Markdown body (no code-fence wrapper, no preamble).
8762
8820
 
8763
8821
  === Project tree (max 30 files, 3 levels deep) ===
8764
8822
  ${_tree}
@@ -8766,6 +8824,9 @@ ${_tree}
8766
8824
  === Manifest files ===
8767
8825
  ${_manifests}
8768
8826
 
8827
+ === Entrypoint file contents (port bindings live here, NOT in package.json) ===
8828
+ ${_entrypoints}
8829
+
8769
8830
  === Last 10 commits ===
8770
8831
  ${_commits}"
8771
8832
 
@@ -12136,6 +12197,8 @@ cleanup() {
12136
12197
  # v7.5.12: Kill any running provider pipeline first, before slow cleanup.
12137
12198
  kill_provider_child 2>/dev/null || true
12138
12199
  rm -f "$loki_dir/STOP" "$loki_dir/PAUSE" "$loki_dir/PAUSED.md" 2>/dev/null
12200
+ # UT2-13: Clear cli-provider marker on session end.
12201
+ rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
12139
12202
  if type app_runner_cleanup &>/dev/null; then
12140
12203
  app_runner_cleanup
12141
12204
  fi
@@ -12183,6 +12246,8 @@ except (json.JSONDecodeError, OSError): pass
12183
12246
  stop_status_monitor
12184
12247
  kill_all_registered
12185
12248
  rm -f "$loki_dir/loki.pid" "$loki_dir/PAUSE" 2>/dev/null
12249
+ # UT2-13: Clear cli-provider marker on session end.
12250
+ rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
12186
12251
  # Clean up per-session PID file if running with session ID
12187
12252
  if [ -n "${LOKI_SESSION_ID:-}" ]; then
12188
12253
  rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
@@ -12789,6 +12854,8 @@ main() {
12789
12854
  stop_status_monitor
12790
12855
  local loki_dir="${TARGET_DIR:-.}/.loki"
12791
12856
  rm -f "$loki_dir/loki.pid" 2>/dev/null
12857
+ # UT2-13: Clear cli-provider marker on normal session end.
12858
+ rm -f "$loki_dir/state/cli-provider" 2>/dev/null || true
12792
12859
  # Clean up per-session PID file if running with session ID
12793
12860
  if [ -n "${LOKI_SESSION_ID:-}" ]; then
12794
12861
  rm -f "$loki_dir/sessions/${LOKI_SESSION_ID}/loki.pid" 2>/dev/null
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.7.9"
10
+ __version__ = "7.7.11"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -492,6 +492,24 @@
492
492
  font-size: 13px;
493
493
  color: var(--loki-text-secondary);
494
494
  }
495
+
496
+ /* USAGE.md markdown render styles */
497
+ .usage-md { max-height: 480px; overflow: auto; padding: 12px; background: var(--loki-bg-secondary, #111); border-radius: 4px; color: var(--loki-text-primary, #eee); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.6; }
498
+ .usage-md h1 { font-size: 1.35rem; font-weight: 600; margin: 0.75em 0 0.4em; border-bottom: 1px solid var(--loki-border, #333); padding-bottom: 4px; }
499
+ .usage-md h2 { font-size: 1.15rem; font-weight: 600; margin: 0.75em 0 0.4em; }
500
+ .usage-md h3 { font-size: 1rem; font-weight: 600; margin: 0.65em 0 0.3em; }
501
+ .usage-md p { margin: 0.4em 0; }
502
+ .usage-md ul, .usage-md ol { margin: 0.4em 0 0.4em 1.5em; padding: 0; }
503
+ .usage-md li { margin: 0.15em 0; }
504
+ .usage-md pre { background: var(--loki-bg-tertiary, #0a0a0a); border: 1px solid var(--loki-border, #333); border-radius: 4px; padding: 10px 12px; overflow-x: auto; margin: 0.5em 0; }
505
+ .usage-md pre code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11.5px; background: none; padding: 0; white-space: pre; }
506
+ .usage-md code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 11.5px; background: var(--loki-bg-tertiary, #0a0a0a); padding: 1px 4px; border-radius: 3px; }
507
+ .usage-md blockquote { border-left: 3px solid var(--loki-accent, #553de9); margin: 0.5em 0; padding: 4px 12px; color: var(--loki-text-secondary, #888); }
508
+ .usage-md hr { border: none; border-top: 1px solid var(--loki-border, #333); margin: 0.75em 0; }
509
+ .usage-md strong { font-weight: 600; }
510
+ .usage-md em { font-style: italic; }
511
+ .usage-md a { color: var(--loki-accent, #553de9); text-decoration: none; }
512
+ .usage-md a:hover { text-decoration: underline; }
495
513
  </style>
496
514
  </head>
497
515
  <body>
@@ -707,10 +725,148 @@
707
725
  <h3 style="font-family: 'DM Serif Display', Georgia, serif; font-size: 1.15rem; font-weight: 400; color: var(--loki-text-primary); margin-bottom: 12px;">How to Run (USAGE.md)</h3>
708
726
  <div id="usage-doc-panel" style="background: var(--loki-bg-card, #1a1a1a); border: 1px solid var(--loki-border, #333); border-radius: 5px; padding: 12px;">
709
727
  <div id="usage-doc-meta" style="font-size: 11px; color: var(--loki-text-muted, #888); margin-bottom: 8px;">Loading...</div>
710
- <pre id="usage-doc-content" style="margin: 0; padding: 12px; background: var(--loki-bg-secondary, #111); border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 12px; white-space: pre-wrap; word-break: break-word; color: var(--loki-text-primary, #eee); max-height: 480px; overflow: auto;"></pre>
728
+ <div id="usage-doc-content" class="usage-md"></div>
711
729
  </div>
712
730
  <script>
713
731
  (function(){
732
+ // Minimal markdown-to-HTML converter for USAGE.md content.
733
+ // Handles: fenced code blocks, headings, horizontal rules,
734
+ // blockquotes, unordered/ordered lists, inline bold/italic/code,
735
+ // and paragraphs. Script tags are stripped from non-code text.
736
+ function renderUsageMarkdown(md) {
737
+ if (!md) return '';
738
+ var lines = md.split('
739
+ ');
740
+ var html = '';
741
+ var i = 0;
742
+ var inList = null; // 'ul' or 'ol'
743
+
744
+ function escapeHtml(s) {
745
+ return s.replace(/&/g, '&amp;')
746
+ .replace(/</g, '&lt;')
747
+ .replace(/>/g, '&gt;')
748
+ .replace(/"/g, '&quot;');
749
+ }
750
+
751
+ function closeList() {
752
+ if (inList) { html += '</' + inList + '>'; inList = null; }
753
+ }
754
+
755
+ function inlineFormat(s) {
756
+ // Escape HTML first (outside code spans)
757
+ // Process inline code spans before escaping the rest
758
+ var BT = '`'; // backtick -- avoid literal backtick inside template literal
759
+ var result = '';
760
+ var codeSpanRe = new RegExp('(' + BT + '[^' + BT + ']+' + BT + ')');
761
+ var chunks = s.split(codeSpanRe);
762
+ for (var ci = 0; ci < chunks.length; ci++) {
763
+ var ch = chunks[ci];
764
+ if (ch.charAt(0) === BT && ch.charAt(ch.length - 1) === BT && ch.length > 2) {
765
+ result += '<code>' + escapeHtml(ch.slice(1, -1)) + '</code>';
766
+ } else {
767
+ var esc = escapeHtml(ch);
768
+ // strip any residual <script tags that survived escaping (defensive)
769
+ esc = esc.replace(/&lt;script/gi, '&lt;sc​ript');
770
+ esc = esc.replace(/**([^*]+)**/g, '<strong>$1</strong>');
771
+ esc = esc.replace(/__([^_]+)__/g, '<strong>$1</strong>');
772
+ esc = esc.replace(/*([^*]+)*/g, '<em>$1</em>');
773
+ esc = esc.replace(/_([^_]+)_/g, '<em>$1</em>');
774
+ // v7.7.11 XSS guard (Opus 1 council): only allow http/https/mailto/anchor/relative href.
775
+ // javascript: / data: / vbscript: are stripped to a plain code span so the URL stays visible
776
+ // but is not clickable. USAGE.md absorbs agent output + PRD text; treat as untrusted.
777
+ esc = esc.replace(/[([^]]+)](([^)]+))/g, function(_m, label, url){
778
+ var safe = /^(https?://|mailto:|#|/(?!/))/.test(url);
779
+ if (safe) return '<a href="' + url + '" rel="noopener noreferrer">' + label + '</a>';
780
+ return '<code>' + label + ' (' + url + ')</code>';
781
+ });
782
+ result += esc;
783
+ }
784
+ }
785
+ return result;
786
+ }
787
+
788
+ while (i < lines.length) {
789
+ var line = lines[i];
790
+
791
+ // Fenced code block (BT3 = three backticks)
792
+ var BT3 = '```';
793
+ if (line.slice(0, 3) === BT3) {
794
+ closeList();
795
+ var lang = line.slice(3).trim();
796
+ var codeLines = [];
797
+ i++;
798
+ while (i < lines.length && lines[i].slice(0, 3) !== BT3) {
799
+ codeLines.push(lines[i]);
800
+ i++;
801
+ }
802
+ var codeContent = escapeHtml(codeLines.join('
803
+ '));
804
+ html += '<pre><code' + (lang ? ' class="language-' + escapeHtml(lang) + '"' : '') + '>' + codeContent + '</code></pre>';
805
+ i++;
806
+ continue;
807
+ }
808
+
809
+ // Headings
810
+ var hMatch = line.match(/^(#{1,6})s+(.*)/);
811
+ if (hMatch) {
812
+ closeList();
813
+ var level = hMatch[1].length;
814
+ html += '<h' + level + '>' + inlineFormat(hMatch[2]) + '</h' + level + '>';
815
+ i++;
816
+ continue;
817
+ }
818
+
819
+ // Horizontal rule
820
+ if (/^(-{3,}|*{3,}|_{3,})$/.test(line.trim())) {
821
+ closeList();
822
+ html += '<hr>';
823
+ i++;
824
+ continue;
825
+ }
826
+
827
+ // Blockquote
828
+ if (/^>/.test(line)) {
829
+ closeList();
830
+ html += '<blockquote>' + inlineFormat(line.replace(/^>s?/, '')) + '</blockquote>';
831
+ i++;
832
+ continue;
833
+ }
834
+
835
+ // Unordered list
836
+ var ulMatch = line.match(/^(s*[-*+])s+(.*)/);
837
+ if (ulMatch) {
838
+ if (inList !== 'ul') { closeList(); html += '<ul>'; inList = 'ul'; }
839
+ html += '<li>' + inlineFormat(ulMatch[2]) + '</li>';
840
+ i++;
841
+ continue;
842
+ }
843
+
844
+ // Ordered list
845
+ var olMatch = line.match(/^s*d+.s+(.*)/);
846
+ if (olMatch) {
847
+ if (inList !== 'ol') { closeList(); html += '<ol>'; inList = 'ol'; }
848
+ html += '<li>' + inlineFormat(olMatch[1]) + '</li>';
849
+ i++;
850
+ continue;
851
+ }
852
+
853
+ // Blank line
854
+ if (line.trim() === '') {
855
+ closeList();
856
+ i++;
857
+ continue;
858
+ }
859
+
860
+ // Paragraph
861
+ closeList();
862
+ html += '<p>' + inlineFormat(line) + '</p>';
863
+ i++;
864
+ }
865
+
866
+ closeList();
867
+ return html;
868
+ }
869
+
714
870
  function loadUsage(){
715
871
  fetch('/api/usage').then(function(r){ return r.json(); }).then(function(j){
716
872
  var meta = document.getElementById('usage-doc-meta');
@@ -718,13 +874,13 @@
718
874
  if (!meta || !body) return;
719
875
  if (!j || !j.exists){
720
876
  meta.textContent = 'USAGE.md not generated yet. Loki writes it at the end of each session.';
721
- body.textContent = '';
877
+ body.innerHTML = '';
722
878
  return;
723
879
  }
724
880
  var size = j.size > 1024 ? (j.size/1024).toFixed(1) + ' KB' : j.size + ' B';
725
881
  var when = j.mtime ? new Date(j.mtime*1000).toLocaleString() : '';
726
882
  meta.textContent = j.path + ' (' + size + (j.truncated ? ', truncated' : '') + ', ' + when + ')';
727
- body.textContent = j.content || '';
883
+ body.innerHTML = renderUsageMarkdown(j.content || '');
728
884
  }).catch(function(e){
729
885
  var meta = document.getElementById('usage-doc-meta');
730
886
  if (meta) meta.textContent = 'Failed to load USAGE.md: ' + (e && e.message ? e.message : 'unknown');
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.7.9
5
+ **Version:** v7.7.11
6
6
 
7
7
  ---
8
8