loki-mode 6.74.6 → 6.75.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.
Files changed (70) hide show
  1. package/README.md +1 -53
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/loki +989 -0
  5. package/autonomy/run.sh +106 -2
  6. package/dashboard/__init__.py +1 -1
  7. package/docs/INSTALLATION.md +1 -1
  8. package/mcp/__init__.py +1 -1
  9. package/mcp/server.py +177 -0
  10. package/package.json +1 -1
  11. package/references/mcp-integration.md +59 -0
  12. package/skills/00-index.md +9 -0
  13. package/skills/documentation.md +123 -0
  14. package/skills/quality-gates.md +25 -1
  15. package/web-app/dist/assets/{AdminPage-Cwqm_kDg.js → AdminPage-D4QSV6Zi.js} +1 -1
  16. package/web-app/dist/assets/{Avatar-BgcFY2E5.js → Avatar-88MlpLO5.js} +1 -1
  17. package/web-app/dist/assets/{Badge-DeFGfZLB.js → Badge-DbGjLr4i.js} +1 -1
  18. package/web-app/dist/assets/{Button-Dg1EkPtN.js → Button-sp_FVGZj.js} +1 -1
  19. package/web-app/dist/assets/{ComparePage-D-wvMVP2.js → ComparePage-p2ENnfa7.js} +1 -1
  20. package/web-app/dist/assets/{GitHubIssuesPanel-B_Jm7CmJ.js → GitHubIssuesPanel-DBbBTG9w.js} +1 -1
  21. package/web-app/dist/assets/{GitHubPRsPanel-B5i8Q99N.js → GitHubPRsPanel-Bi_yrcAE.js} +1 -1
  22. package/web-app/dist/assets/{HomePage-CUDTdntY.js → HomePage-BB83YPiX.js} +1 -1
  23. package/web-app/dist/assets/{LoginPage-BobwVXx5.js → LoginPage-BXUudCJ9.js} +1 -1
  24. package/web-app/dist/assets/{MetricsPage-DmM--20B.js → MetricsPage-CX0Ahy-_.js} +1 -1
  25. package/web-app/dist/assets/{NotFoundPage-6_gjeogD.js → NotFoundPage-C4JqatEk.js} +1 -1
  26. package/web-app/dist/assets/{ProjectPage-C3R2wbFt.js → ProjectPage-t5J2XAJT.js} +46 -46
  27. package/web-app/dist/assets/{ProjectsPage-DQr06iBk.js → ProjectsPage-Bzpz1clk.js} +1 -1
  28. package/web-app/dist/assets/{SettingsPage-eROlGKB9.js → SettingsPage-y_yl8FvH.js} +1 -1
  29. package/web-app/dist/assets/{ShowcasePage-DEA5tT_R.js → ShowcasePage-B7d6pzMq.js} +1 -1
  30. package/web-app/dist/assets/{SystemSettingsPage-C_rIbgZg.js → SystemSettingsPage-C4tR33KU.js} +1 -1
  31. package/web-app/dist/assets/{TeamsPage-DZGoYZnD.js → TeamsPage-DIOCfZIP.js} +1 -1
  32. package/web-app/dist/assets/{TemplatesPage-DVblWpbO.js → TemplatesPage-DlKyapXX.js} +1 -1
  33. package/web-app/dist/assets/{TerminalOutput-DnESY9zk.js → TerminalOutput-Czg-ZC2k.js} +1 -1
  34. package/web-app/dist/assets/{activity-COLsZyo1.js → activity-h1wU9a0L.js} +1 -1
  35. package/web-app/dist/assets/{bell-BjLe9xXk.js → bell-Bu8lsWOp.js} +1 -1
  36. package/web-app/dist/assets/{bot-Dz62aBIi.js → bot-rWO7KjkQ.js} +1 -1
  37. package/web-app/dist/assets/{check-BQPQjkH4.js → check-BWp8L5Cy.js} +1 -1
  38. package/web-app/dist/assets/{chevron-left-BVvOVUQ8.js → chevron-left-Bw4I1yGm.js} +1 -1
  39. package/web-app/dist/assets/{circle-alert-DqoLW238.js → circle-alert-C37PKXiC.js} +1 -1
  40. package/web-app/dist/assets/{clock-Cn9fFUva.js → clock-DDScLol4.js} +1 -1
  41. package/web-app/dist/assets/{cloud-COJxbgUu.js → cloud-DaYKPLaM.js} +1 -1
  42. package/web-app/dist/assets/{copy-DOn0hVgy.js → copy-DKIRv0VK.js} +1 -1
  43. package/web-app/dist/assets/{database-Bj3Llvnk.js → database-CYZBHz51.js} +1 -1
  44. package/web-app/dist/assets/{dollar-sign-BcmncygQ.js → dollar-sign-CydJu0kl.js} +1 -1
  45. package/web-app/dist/assets/{file-code-corner-BysxoSyu.js → file-code-corner-DqZ9gpdv.js} +1 -1
  46. package/web-app/dist/assets/{file-plus-Dwi1MqSS.js → file-plus-CzeFJWp3.js} +1 -1
  47. package/web-app/dist/assets/{folder-open-WzXNCq2x.js → folder-open-4YWk08dP.js} +1 -1
  48. package/web-app/dist/assets/{git-commit-horizontal-D85UUfNF.js → git-commit-horizontal-wbqFPNID.js} +1 -1
  49. package/web-app/dist/assets/{globe-CegT0VaL.js → globe-Cby-g5Yb.js} +1 -1
  50. package/web-app/dist/assets/{hammer-ZGKvu_xy.js → hammer-BNScgGdp.js} +1 -1
  51. package/web-app/dist/assets/{index-_2iPl2nX.js → index-6Z4B0I6r.js} +74 -74
  52. package/web-app/dist/assets/{layers-B40lki5j.js → layers-XfssQc5V.js} +1 -1
  53. package/web-app/dist/assets/{lightbulb-CFSoqUsV.js → lightbulb-EhnzRw7M.js} +1 -1
  54. package/web-app/dist/assets/{loader-circle-womi7Brk.js → loader-circle-BA0QIVGA.js} +1 -1
  55. package/web-app/dist/assets/{lock-CEWBb_SL.js → lock-BABtHe6K.js} +1 -1
  56. package/web-app/dist/assets/{mail-DimGrYf8.js → mail-Dokiey5S.js} +1 -1
  57. package/web-app/dist/assets/{package-DysIuuIJ.js → package-DbJyS1Ft.js} +1 -1
  58. package/web-app/dist/assets/{plus-DG1hW27_.js → plus-BcAN8Kaj.js} +1 -1
  59. package/web-app/dist/assets/{refresh-cw-X31ig0S6.js → refresh-cw-B3dG1-Sb.js} +1 -1
  60. package/web-app/dist/assets/{rotate-ccw-CBXooICo.js → rotate-ccw-Cs1Phctm.js} +1 -1
  61. package/web-app/dist/assets/{save-DQVrWTjZ.js → save-DsrNCZrP.js} +1 -1
  62. package/web-app/dist/assets/{server-BYAMALfa.js → server-CpN2GX4G.js} +1 -1
  63. package/web-app/dist/assets/{shield-alert-B3G7vLiW.js → shield-alert-CKJ1pzCz.js} +1 -1
  64. package/web-app/dist/assets/{trash-2-ChVunC-C.js → trash-2-C9vZqTqw.js} +1 -1
  65. package/web-app/dist/assets/{trending-down-DgsuOE2t.js → trending-down-BNLTrF5P.js} +1 -1
  66. package/web-app/dist/assets/{trending-up-CZHZsGeN.js → trending-up-DmFIdVOc.js} +1 -1
  67. package/web-app/dist/assets/{usePolling-ns_dFCVn.js → usePolling-vUlY-o6P.js} +1 -1
  68. package/web-app/dist/assets/{user-FQUrWHhF.js → user-Dh00W8De.js} +1 -1
  69. package/web-app/dist/index.html +1 -1
  70. package/web-app/server.py +196 -0
package/autonomy/loki CHANGED
@@ -449,6 +449,7 @@ show_help() {
449
449
  echo " failover [cmd] Cross-provider auto-failover (status|--enable|--test|--chain)"
450
450
  echo " onboard [path] Analyze a repo and generate CLAUDE.md (structure, conventions, commands)"
451
451
  echo ' explain [path] Analyze any codebase and explain its architecture in plain English'
452
+ echo " docs [cmd] Generate, update, check project documentation"
452
453
  echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
453
454
  echo " ci [opts] CI/CD quality gate integration (--pr, --report, --github-comment)"
454
455
  echo " test [opts] AI-powered test generation (--file, --dir, --changed, --dry-run)"
@@ -11195,6 +11196,9 @@ main() {
11195
11196
  explain)
11196
11197
  cmd_explain "$@"
11197
11198
  ;;
11199
+ docs)
11200
+ cmd_docs "$@"
11201
+ ;;
11198
11202
  ci)
11199
11203
  cmd_ci "$@"
11200
11204
  ;;
@@ -18322,6 +18326,991 @@ Generated by loki explain on $(date +%Y-%m-%d)"
18322
18326
  set -euo pipefail
18323
18327
  }
18324
18328
 
18329
+ # Generate, update, check, and manage project documentation (v6.75.0)
18330
+ cmd_docs() {
18331
+ local subcmd="${1:-}"
18332
+ shift 2>/dev/null || true
18333
+
18334
+ case "$subcmd" in
18335
+ generate) _docs_generate "$@" ;;
18336
+ update) _docs_update "$@" ;;
18337
+ check) _docs_check "$@" ;;
18338
+ status) _docs_status "$@" ;;
18339
+ --help|-h|help|"")
18340
+ echo -e "${BOLD}loki docs${NC} - Generate, update, check project documentation"
18341
+ echo ""
18342
+ echo "Usage: loki docs <command> [options]"
18343
+ echo ""
18344
+ echo "Commands:"
18345
+ echo " generate [path] Generate full documentation suite in .loki/docs/"
18346
+ echo " update [path] Incremental update based on git changes since last generation"
18347
+ echo " check [path] Validate documentation coverage and staleness (exit 0=pass, 1=fail)"
18348
+ echo " status [path] Show documentation metrics (count, coverage, staleness)"
18349
+ echo ""
18350
+ echo "Options:"
18351
+ echo " --help, -h Show this help"
18352
+ echo ""
18353
+ echo "Generated documentation:"
18354
+ echo " .loki/docs/README.md Project overview, setup, usage"
18355
+ echo " .loki/docs/ARCHITECTURE.md System design, data flow, key decisions"
18356
+ echo " .loki/docs/API.md Public API reference (endpoints, functions, classes)"
18357
+ echo " .loki/docs/SETUP.md Dev environment setup, dependencies, running locally"
18358
+ echo " .loki/docs/COMPONENTS.md Per-component/module documentation"
18359
+ echo " .loki/docs/TESTING.md Test strategy, running tests, coverage"
18360
+ echo " .loki/docs/DECISIONS.md Architectural Decision Records"
18361
+ echo " .loki/docs/CLAUDE.md AI agent context file"
18362
+ echo " .loki/docs/docs-manifest.json Manifest with SHAs and timestamps"
18363
+ echo ""
18364
+ echo "Examples:"
18365
+ echo " loki docs generate # Generate docs for current project"
18366
+ echo " loki docs generate ~/projects/app # Generate docs for specific project"
18367
+ echo " loki docs update # Update only changed components"
18368
+ echo " loki docs check # Validate docs (CI-friendly)"
18369
+ echo " loki docs status # Show doc metrics"
18370
+ return 0
18371
+ ;;
18372
+ *)
18373
+ log_error "Unknown docs command: $subcmd"
18374
+ echo "Run 'loki docs --help' for usage."
18375
+ return 1
18376
+ ;;
18377
+ esac
18378
+ }
18379
+
18380
+ # --- Docs helper: scan project and build context ---
18381
+ _docs_scan_project() {
18382
+ local target_path="$1"
18383
+
18384
+ # Build file tree (excluding common noise)
18385
+ local tree_output=""
18386
+ if command -v git &>/dev/null && [ -d "$target_path/.git" ]; then
18387
+ tree_output=$(cd "$target_path" && git ls-files 2>/dev/null | head -500 || true)
18388
+ else
18389
+ tree_output=$(find "$target_path" -maxdepth 4 -type f \
18390
+ -not -path '*/node_modules/*' -not -path '*/.git/*' \
18391
+ -not -path '*/vendor/*' -not -path '*/__pycache__/*' \
18392
+ -not -path '*/dist/*' -not -path '*/build/*' \
18393
+ -not -path '*/.next/*' -not -path '*/target/*' \
18394
+ 2>/dev/null | sed "s|$target_path/||" | sort | head -500)
18395
+ fi
18396
+ echo "$tree_output"
18397
+ }
18398
+
18399
+ _docs_build_context() {
18400
+ local target_path="$1"
18401
+ local tree_output="$2"
18402
+ local context=""
18403
+
18404
+ context="PROJECT PATH: $target_path
18405
+ PROJECT NAME: $(basename "$target_path")
18406
+ "
18407
+
18408
+ # File tree summary
18409
+ local total_files src_count test_count
18410
+ total_files=$(echo "$tree_output" | { grep -c . || true; })
18411
+ src_count=$(echo "$tree_output" | { grep -cE '\.(js|ts|tsx|jsx|py|rs|go|rb|java|kt|c|cpp|h|hpp|sh|swift|cs|php)$' || true; })
18412
+ test_count=$(echo "$tree_output" | { grep -cE '(\.test\.|\.spec\.|_test\.|_spec\.|tests/|test/|__tests__/)' || true; })
18413
+ context="${context}
18414
+ FILES: $total_files total, $src_count source, $test_count test
18415
+ "
18416
+
18417
+ # Top-level directories
18418
+ local top_dirs
18419
+ top_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort | uniq -c | sort -rn | head -20)
18420
+ context="${context}
18421
+ DIRECTORY STRUCTURE:
18422
+ $top_dirs
18423
+ "
18424
+
18425
+ # File tree (first 200 files)
18426
+ context="${context}
18427
+ FILE TREE:
18428
+ $(echo "$tree_output" | head -200)
18429
+ "
18430
+
18431
+ # Key config files - read first 50 lines of each
18432
+ for cfg in package.json pyproject.toml Cargo.toml go.mod Gemfile pom.xml requirements.txt Makefile Dockerfile docker-compose.yml; do
18433
+ if [ -f "$target_path/$cfg" ]; then
18434
+ context="${context}
18435
+ --- $cfg (first 50 lines) ---
18436
+ $(head -50 "$target_path/$cfg" 2>/dev/null || true)
18437
+ "
18438
+ fi
18439
+ done
18440
+
18441
+ # README if present
18442
+ for f in README.md readme.md README.rst README; do
18443
+ if [ -f "$target_path/$f" ]; then
18444
+ context="${context}
18445
+ --- $f (first 80 lines) ---
18446
+ $(head -80 "$target_path/$f" 2>/dev/null || true)
18447
+ "
18448
+ break
18449
+ fi
18450
+ done
18451
+
18452
+ # Key entry point files (first 50 lines)
18453
+ local entry_count=0
18454
+ for candidate in \
18455
+ "src/index.ts" "src/index.js" "src/main.ts" "src/main.js" "src/app.ts" "src/app.js" \
18456
+ "index.ts" "index.js" "main.ts" "main.js" "app.ts" "app.js" "server.ts" "server.js" \
18457
+ "src/App.tsx" "src/App.jsx" "pages/index.tsx" "app/page.tsx" \
18458
+ "main.py" "app.py" "manage.py" "src/main.py" "__main__.py" \
18459
+ "main.go" "cmd/main.go" "src/main.rs" "src/lib.rs"; do
18460
+ if [ -f "$target_path/$candidate" ]; then
18461
+ context="${context}
18462
+ --- $candidate (first 50 lines) ---
18463
+ $(head -50 "$target_path/$candidate" 2>/dev/null || true)
18464
+ "
18465
+ ((++entry_count))
18466
+ [ "$entry_count" -ge 5 ] && break
18467
+ fi
18468
+ done
18469
+
18470
+ echo "$context"
18471
+ }
18472
+
18473
+ # --- Docs helper: invoke AI provider and capture output ---
18474
+ _docs_invoke_provider() {
18475
+ local prompt="$1"
18476
+ local provider="${LOKI_PROVIDER:-claude}"
18477
+ if [ -f ".loki/state/provider" ]; then
18478
+ provider=$(cat ".loki/state/provider" 2>/dev/null)
18479
+ fi
18480
+
18481
+ local result=""
18482
+ local exit_code=0
18483
+ case "$provider" in
18484
+ claude)
18485
+ result=$(claude -p "$prompt" 2>/dev/null) || exit_code=$?
18486
+ ;;
18487
+ codex)
18488
+ result=$(codex exec --full-auto "$prompt" 2>/dev/null) || exit_code=$?
18489
+ ;;
18490
+ gemini)
18491
+ result=$(gemini --approval-mode=yolo "$prompt" 2>/dev/null) || exit_code=$?
18492
+ ;;
18493
+ cline)
18494
+ result=$(cline -y "$prompt" 2>/dev/null) || exit_code=$?
18495
+ ;;
18496
+ aider)
18497
+ result=$(aider --message "$prompt" --yes-always --no-auto-commits < /dev/null 2>/dev/null) || exit_code=$?
18498
+ ;;
18499
+ *)
18500
+ log_error "Unknown provider: $provider"
18501
+ return 1
18502
+ ;;
18503
+ esac
18504
+
18505
+ if [ $exit_code -ne 0 ] && [ -z "$result" ]; then
18506
+ log_error "Provider '$provider' failed (exit $exit_code)"
18507
+ return 1
18508
+ fi
18509
+
18510
+ echo "$result"
18511
+ }
18512
+
18513
+ # --- Docs helper: write manifest ---
18514
+ _docs_write_manifest() {
18515
+ local docs_dir="$1"
18516
+ shift
18517
+ # Remaining args are file names
18518
+
18519
+ local git_sha=""
18520
+ git_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
18521
+ local generated_at
18522
+ generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
18523
+
18524
+ local files_json="{"
18525
+ local first=true
18526
+ for doc_file in "$@"; do
18527
+ local fname
18528
+ fname=$(basename "$doc_file")
18529
+ if [ -f "$docs_dir/$fname" ]; then
18530
+ local file_sha
18531
+ file_sha=$(shasum -a 256 "$docs_dir/$fname" 2>/dev/null | awk '{print $1}' || echo "unknown")
18532
+ if [ "$first" = true ]; then
18533
+ first=false
18534
+ else
18535
+ files_json="${files_json},"
18536
+ fi
18537
+ files_json="${files_json}
18538
+ \"$fname\": {\"sha256\": \"$file_sha\", \"generated_at\": \"$generated_at\"}"
18539
+ fi
18540
+ done
18541
+ files_json="${files_json}
18542
+ }"
18543
+
18544
+ cat > "$docs_dir/docs-manifest.json" <<MANIFEST_EOF
18545
+ {
18546
+ "generated_at": "$generated_at",
18547
+ "git_sha": "$git_sha",
18548
+ "files": $files_json
18549
+ }
18550
+ MANIFEST_EOF
18551
+ }
18552
+
18553
+ # --- loki docs generate ---
18554
+ _docs_generate() {
18555
+ local target_path="."
18556
+
18557
+ while [[ $# -gt 0 ]]; do
18558
+ case "$1" in
18559
+ --help|-h)
18560
+ echo "Usage: loki docs generate [path]"
18561
+ echo "Generate full documentation suite in .loki/docs/"
18562
+ return 0
18563
+ ;;
18564
+ -*)
18565
+ log_error "Unknown option: $1"
18566
+ return 1
18567
+ ;;
18568
+ *)
18569
+ target_path="$1"
18570
+ shift
18571
+ ;;
18572
+ esac
18573
+ done
18574
+
18575
+ if [ ! -d "$target_path" ]; then
18576
+ log_error "Directory not found: $target_path"
18577
+ return 1
18578
+ fi
18579
+ target_path="$(cd "$target_path" && pwd)"
18580
+
18581
+ local docs_dir="$target_path/.loki/docs"
18582
+ mkdir -p "$docs_dir"
18583
+
18584
+ log_info "Scanning project at: $target_path"
18585
+
18586
+ # Scan project
18587
+ local tree_output
18588
+ tree_output=$(_docs_scan_project "$target_path")
18589
+ local context
18590
+ context=$(_docs_build_context "$target_path" "$tree_output")
18591
+
18592
+ local project_name
18593
+ project_name="$(basename "$target_path")"
18594
+
18595
+ # Check if provider CLI is available
18596
+ local provider="${LOKI_PROVIDER:-claude}"
18597
+ if [ -f "$target_path/.loki/state/provider" ]; then
18598
+ provider=$(cat "$target_path/.loki/state/provider" 2>/dev/null)
18599
+ fi
18600
+
18601
+ local provider_available=true
18602
+ if ! command -v "$provider" &>/dev/null; then
18603
+ provider_available=false
18604
+ log_warn "Provider '$provider' not found. Generating template-based docs instead."
18605
+ fi
18606
+
18607
+ # Doc types to generate
18608
+ local doc_types="README ARCHITECTURE API SETUP COMPONENTS TESTING DECISIONS CLAUDE"
18609
+ local generated_files=""
18610
+
18611
+ for doc_type in $doc_types; do
18612
+ local doc_file="${doc_type}.md"
18613
+ local prompt=""
18614
+
18615
+ echo -e " ${CYAN}Generating${NC} $doc_file ..."
18616
+
18617
+ if [ "$provider_available" = true ]; then
18618
+ # Build a focused prompt for each doc type
18619
+ case "$doc_type" in
18620
+ README)
18621
+ prompt="Based on the following project context, generate a comprehensive README.md file. Include: project title, description, features, installation instructions, usage examples, and contribution guidelines. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18622
+
18623
+ $context"
18624
+ ;;
18625
+ ARCHITECTURE)
18626
+ prompt="Based on the following project context, generate an ARCHITECTURE.md file. Include: system overview, high-level design, component diagram (as text), data flow, directory structure explanation, key design decisions, and technology choices. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18627
+
18628
+ $context"
18629
+ ;;
18630
+ API)
18631
+ prompt="Based on the following project context, generate an API.md file. Document all public APIs: REST endpoints, exported functions, classes, and their signatures. Include parameter types, return types, and brief descriptions. If no API is detected, document the main public interfaces. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18632
+
18633
+ $context"
18634
+ ;;
18635
+ SETUP)
18636
+ prompt="Based on the following project context, generate a SETUP.md file. Include: prerequisites, installation steps, environment variables, database setup, running locally, running in Docker (if applicable), and common troubleshooting. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18637
+
18638
+ $context"
18639
+ ;;
18640
+ COMPONENTS)
18641
+ prompt="Based on the following project context, generate a COMPONENTS.md file. Document each major component/module/directory: its purpose, key files, public interface, and dependencies on other components. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18642
+
18643
+ $context"
18644
+ ;;
18645
+ TESTING)
18646
+ prompt="Based on the following project context, generate a TESTING.md file. Include: test strategy, test types (unit, integration, e2e), how to run tests, test configuration, coverage goals, and CI integration. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18647
+
18648
+ $context"
18649
+ ;;
18650
+ DECISIONS)
18651
+ prompt="Based on the following project context, generate a DECISIONS.md file with Architectural Decision Records (ADRs). Infer key decisions from the technology stack, directory structure, and configuration. Each ADR should have: title, status, context, decision, and consequences. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18652
+
18653
+ $context"
18654
+ ;;
18655
+ CLAUDE)
18656
+ prompt="Based on the following project context, generate a CLAUDE.md file for AI agent context. Include: project overview, key commands (build, test, run, lint), project structure with brief descriptions of each directory, coding conventions, important files, and any gotchas. This file helps AI coding assistants understand the project quickly. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations.
18657
+
18658
+ $context"
18659
+ ;;
18660
+ esac
18661
+
18662
+ local result=""
18663
+ result=$(_docs_invoke_provider "$prompt" 2>/dev/null) || true
18664
+
18665
+ if [ -n "$result" ]; then
18666
+ echo "$result" > "$docs_dir/$doc_file"
18667
+ generated_files="$generated_files $doc_file"
18668
+ else
18669
+ # Fallback to template
18670
+ _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
18671
+ generated_files="$generated_files $doc_file"
18672
+ fi
18673
+ else
18674
+ # Template-based generation (no AI provider)
18675
+ _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
18676
+ generated_files="$generated_files $doc_file"
18677
+ fi
18678
+
18679
+ echo -e " ${GREEN}[done]${NC} $doc_file"
18680
+ done
18681
+
18682
+ # Write manifest
18683
+ # shellcheck disable=SC2086
18684
+ _docs_write_manifest "$docs_dir" $generated_files
18685
+
18686
+ echo ""
18687
+ log_info "Documentation generated in $docs_dir/"
18688
+ log_info "Files: $(echo "$generated_files" | wc -w | tr -d ' ') docs + manifest"
18689
+ echo -e " Manifest: ${CYAN}$docs_dir/docs-manifest.json${NC}"
18690
+ }
18691
+
18692
+ # --- Docs helper: generate template-based doc (fallback when no AI provider) ---
18693
+ _docs_generate_template() {
18694
+ local doc_type="$1"
18695
+ local target_path="$2"
18696
+ local context="$3"
18697
+ local project_name
18698
+ project_name="$(basename "$target_path")"
18699
+ local timestamp
18700
+ timestamp=$(date +%Y-%m-%d)
18701
+
18702
+ # Extract basic metadata from context
18703
+ local files_line
18704
+ files_line=$(echo "$context" | grep '^FILES:' | head -1 || echo "FILES: unknown")
18705
+
18706
+ case "$doc_type" in
18707
+ README)
18708
+ cat <<EOF
18709
+ # $project_name
18710
+
18711
+ ## Overview
18712
+
18713
+ Project at \`$target_path\`.
18714
+
18715
+ $files_line
18716
+
18717
+ ## Getting Started
18718
+
18719
+ See SETUP.md for installation and development instructions.
18720
+
18721
+ ## Documentation
18722
+
18723
+ - [Architecture](ARCHITECTURE.md)
18724
+ - [API Reference](API.md)
18725
+ - [Setup Guide](SETUP.md)
18726
+ - [Components](COMPONENTS.md)
18727
+ - [Testing](TESTING.md)
18728
+ - [Decisions](DECISIONS.md)
18729
+
18730
+ ---
18731
+ Generated by loki docs generate on $timestamp
18732
+ EOF
18733
+ ;;
18734
+ ARCHITECTURE)
18735
+ cat <<EOF
18736
+ # Architecture
18737
+
18738
+ ## Overview
18739
+
18740
+ This document describes the architecture of $project_name.
18741
+
18742
+ ## Directory Structure
18743
+
18744
+ \`\`\`
18745
+ $(echo "$context" | sed -n '/^DIRECTORY STRUCTURE:/,/^$/p' | tail -n +2)
18746
+ \`\`\`
18747
+
18748
+ ## Key Components
18749
+
18750
+ See COMPONENTS.md for detailed component documentation.
18751
+
18752
+ ---
18753
+ Generated by loki docs generate on $timestamp
18754
+ EOF
18755
+ ;;
18756
+ API)
18757
+ cat <<EOF
18758
+ # API Reference
18759
+
18760
+ ## Overview
18761
+
18762
+ Public API documentation for $project_name.
18763
+
18764
+ TODO: Document public endpoints, functions, and classes.
18765
+
18766
+ ---
18767
+ Generated by loki docs generate on $timestamp
18768
+ EOF
18769
+ ;;
18770
+ SETUP)
18771
+ cat <<EOF
18772
+ # Setup Guide
18773
+
18774
+ ## Prerequisites
18775
+
18776
+ See the project configuration files for required dependencies.
18777
+
18778
+ ## Installation
18779
+
18780
+ Clone the repository and install dependencies.
18781
+
18782
+ ## Running Locally
18783
+
18784
+ Refer to the project's build and run scripts.
18785
+
18786
+ ---
18787
+ Generated by loki docs generate on $timestamp
18788
+ EOF
18789
+ ;;
18790
+ COMPONENTS)
18791
+ cat <<EOF
18792
+ # Components
18793
+
18794
+ ## Overview
18795
+
18796
+ Component documentation for $project_name.
18797
+
18798
+ ## Modules
18799
+
18800
+ $(echo "$context" | sed -n '/^DIRECTORY STRUCTURE:/,/^$/p' | tail -n +2 | awk '{print "### " $2 "\n\n" $1 " files\n"}')
18801
+
18802
+ ---
18803
+ Generated by loki docs generate on $timestamp
18804
+ EOF
18805
+ ;;
18806
+ TESTING)
18807
+ cat <<EOF
18808
+ # Testing
18809
+
18810
+ ## Overview
18811
+
18812
+ Test documentation for $project_name.
18813
+
18814
+ ## Running Tests
18815
+
18816
+ Refer to the project's test configuration for instructions.
18817
+
18818
+ ---
18819
+ Generated by loki docs generate on $timestamp
18820
+ EOF
18821
+ ;;
18822
+ DECISIONS)
18823
+ cat <<EOF
18824
+ # Architectural Decision Records
18825
+
18826
+ ## Overview
18827
+
18828
+ Key architectural decisions for $project_name.
18829
+
18830
+ ## ADR-001: [Title]
18831
+
18832
+ - **Status**: Proposed
18833
+ - **Context**: [Why this decision was needed]
18834
+ - **Decision**: [What was decided]
18835
+ - **Consequences**: [What are the trade-offs]
18836
+
18837
+ ---
18838
+ Generated by loki docs generate on $timestamp
18839
+ EOF
18840
+ ;;
18841
+ CLAUDE)
18842
+ cat <<EOF
18843
+ # $project_name
18844
+
18845
+ ## Project Overview
18846
+
18847
+ $files_line
18848
+
18849
+ ## Project Structure
18850
+
18851
+ \`\`\`
18852
+ $(echo "$context" | sed -n '/^FILE TREE:/,/^---/p' | head -50 | tail -n +2)
18853
+ \`\`\`
18854
+
18855
+ ## Key Commands
18856
+
18857
+ Refer to the project's configuration files for build, test, and run commands.
18858
+
18859
+ ---
18860
+ Generated by loki docs generate on $timestamp
18861
+ EOF
18862
+ ;;
18863
+ esac
18864
+ }
18865
+
18866
+ # --- loki docs update ---
18867
+ _docs_update() {
18868
+ local target_path="."
18869
+
18870
+ while [[ $# -gt 0 ]]; do
18871
+ case "$1" in
18872
+ --help|-h)
18873
+ echo "Usage: loki docs update [path]"
18874
+ echo "Incrementally update docs based on git changes since last generation."
18875
+ return 0
18876
+ ;;
18877
+ -*)
18878
+ log_error "Unknown option: $1"
18879
+ return 1
18880
+ ;;
18881
+ *)
18882
+ target_path="$1"
18883
+ shift
18884
+ ;;
18885
+ esac
18886
+ done
18887
+
18888
+ if [ ! -d "$target_path" ]; then
18889
+ log_error "Directory not found: $target_path"
18890
+ return 1
18891
+ fi
18892
+ target_path="$(cd "$target_path" && pwd)"
18893
+
18894
+ local docs_dir="$target_path/.loki/docs"
18895
+ local manifest="$docs_dir/docs-manifest.json"
18896
+
18897
+ if [ ! -f "$manifest" ]; then
18898
+ log_warn "No docs-manifest.json found. Running full generation instead."
18899
+ _docs_generate "$target_path"
18900
+ return $?
18901
+ fi
18902
+
18903
+ # Get the SHA from last generation
18904
+ local last_sha=""
18905
+ if command -v python3 &>/dev/null; then
18906
+ last_sha=$(python3 -c "
18907
+ import json
18908
+ try:
18909
+ d = json.load(open('$manifest'))
18910
+ print(d.get('git_sha', ''))
18911
+ except: pass
18912
+ " 2>/dev/null || true)
18913
+ fi
18914
+
18915
+ if [ -z "$last_sha" ] || [ "$last_sha" = "unknown" ]; then
18916
+ log_warn "Cannot determine last generation SHA. Running full generation."
18917
+ _docs_generate "$target_path"
18918
+ return $?
18919
+ fi
18920
+
18921
+ # Check if HEAD is the same as last generation
18922
+ local current_sha
18923
+ current_sha=$(cd "$target_path" && git rev-parse HEAD 2>/dev/null || echo "unknown")
18924
+
18925
+ if [ "$current_sha" = "$last_sha" ]; then
18926
+ log_info "Documentation is up to date (SHA: ${last_sha:0:8})"
18927
+ return 0
18928
+ fi
18929
+
18930
+ # Get changed files since last generation
18931
+ local changed_files=""
18932
+ changed_files=$(cd "$target_path" && git diff --name-only "$last_sha"..HEAD 2>/dev/null || true)
18933
+
18934
+ if [ -z "$changed_files" ]; then
18935
+ log_info "No file changes detected since last generation."
18936
+ return 0
18937
+ fi
18938
+
18939
+ local changed_count
18940
+ changed_count=$(echo "$changed_files" | wc -l | tr -d ' ')
18941
+ log_info "Found $changed_count changed files since last generation (${last_sha:0:8}..${current_sha:0:8})"
18942
+
18943
+ # Determine which doc types need updating based on changed files
18944
+ local docs_to_update=""
18945
+
18946
+ # README: always update if any significant files changed
18947
+ if echo "$changed_files" | grep -qE '\.(md|txt|json|toml|yaml|yml)$'; then
18948
+ docs_to_update="$docs_to_update README"
18949
+ fi
18950
+
18951
+ # ARCHITECTURE: update if directory structure or config changed
18952
+ if echo "$changed_files" | grep -qE '(package\.json|pyproject\.toml|Cargo\.toml|go\.mod|Dockerfile|docker-compose)'; then
18953
+ docs_to_update="$docs_to_update ARCHITECTURE"
18954
+ fi
18955
+
18956
+ # API: update if source files changed
18957
+ if echo "$changed_files" | grep -qE '\.(ts|js|py|go|rs|java|rb)$'; then
18958
+ docs_to_update="$docs_to_update API COMPONENTS"
18959
+ fi
18960
+
18961
+ # SETUP: update if config/dependency files changed
18962
+ if echo "$changed_files" | grep -qE '(package\.json|requirements\.txt|pyproject\.toml|Cargo\.toml|go\.mod|Dockerfile|\.env)'; then
18963
+ docs_to_update="$docs_to_update SETUP"
18964
+ fi
18965
+
18966
+ # TESTING: update if test files or test config changed
18967
+ if echo "$changed_files" | grep -qE '(test|spec|\.test\.|\.spec\.|jest\.config|vitest\.config|pytest|\.bats)'; then
18968
+ docs_to_update="$docs_to_update TESTING"
18969
+ fi
18970
+
18971
+ # CLAUDE: always update on any source change
18972
+ if echo "$changed_files" | grep -qE '\.(ts|js|py|go|rs|java|rb|sh)$'; then
18973
+ docs_to_update="$docs_to_update CLAUDE"
18974
+ fi
18975
+
18976
+ # Deduplicate
18977
+ docs_to_update=$(echo "$docs_to_update" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//;s/ *$//')
18978
+
18979
+ if [ -z "$docs_to_update" ]; then
18980
+ log_info "No documentation updates needed for the changed files."
18981
+ # Still update manifest SHA
18982
+ _docs_write_manifest "$docs_dir" $(ls "$docs_dir"/*.md 2>/dev/null | xargs -I{} basename {} || true)
18983
+ return 0
18984
+ fi
18985
+
18986
+ log_info "Updating docs: $docs_to_update"
18987
+
18988
+ # Re-scan and rebuild context
18989
+ local tree_output
18990
+ tree_output=$(_docs_scan_project "$target_path")
18991
+ local context
18992
+ context=$(_docs_build_context "$target_path" "$tree_output")
18993
+
18994
+ # Add changed files context
18995
+ context="${context}
18996
+ RECENTLY CHANGED FILES (since last doc generation):
18997
+ $changed_files
18998
+ "
18999
+
19000
+ local provider="${LOKI_PROVIDER:-claude}"
19001
+ if [ -f "$target_path/.loki/state/provider" ]; then
19002
+ provider=$(cat "$target_path/.loki/state/provider" 2>/dev/null)
19003
+ fi
19004
+
19005
+ local provider_available=true
19006
+ if ! command -v "$provider" &>/dev/null; then
19007
+ provider_available=false
19008
+ log_warn "Provider '$provider' not found. Generating template-based docs instead."
19009
+ fi
19010
+
19011
+ local updated_files=""
19012
+ for doc_type in $docs_to_update; do
19013
+ local doc_file="${doc_type}.md"
19014
+ echo -e " ${CYAN}Updating${NC} $doc_file ..."
19015
+
19016
+ if [ "$provider_available" = true ]; then
19017
+ local existing_content=""
19018
+ [ -f "$docs_dir/$doc_file" ] && existing_content=$(cat "$docs_dir/$doc_file" 2>/dev/null || true)
19019
+
19020
+ local prompt="Based on the following project context, update the ${doc_type}.md documentation. Here are the files that changed since the last generation:
19021
+
19022
+ $changed_files
19023
+
19024
+ Current ${doc_type}.md content:
19025
+ $existing_content
19026
+
19027
+ Project context:
19028
+ $context
19029
+
19030
+ Generate an updated ${doc_type}.md that reflects the current state of the project. Use markdown formatting. Do NOT use emojis. Output ONLY the markdown content, no explanations."
19031
+
19032
+ local result=""
19033
+ result=$(_docs_invoke_provider "$prompt" 2>/dev/null) || true
19034
+ if [ -n "$result" ]; then
19035
+ echo "$result" > "$docs_dir/$doc_file"
19036
+ fi
19037
+ else
19038
+ _docs_generate_template "$doc_type" "$target_path" "$context" > "$docs_dir/$doc_file"
19039
+ fi
19040
+
19041
+ updated_files="$updated_files $doc_file"
19042
+ echo -e " ${GREEN}[done]${NC} $doc_file"
19043
+ done
19044
+
19045
+ # Update manifest with all existing docs
19046
+ local all_doc_files=""
19047
+ for f in "$docs_dir"/*.md; do
19048
+ [ -f "$f" ] && all_doc_files="$all_doc_files $(basename "$f")"
19049
+ done
19050
+ # shellcheck disable=SC2086
19051
+ _docs_write_manifest "$docs_dir" $all_doc_files
19052
+
19053
+ echo ""
19054
+ log_info "Updated $(echo "$updated_files" | wc -w | tr -d ' ') documentation files"
19055
+ }
19056
+
19057
+ # --- loki docs check ---
19058
+ _docs_check() {
19059
+ local target_path="."
19060
+
19061
+ while [[ $# -gt 0 ]]; do
19062
+ case "$1" in
19063
+ --help|-h)
19064
+ echo "Usage: loki docs check [path]"
19065
+ echo "Validate documentation coverage and staleness. Exit 0=pass, 1=fail."
19066
+ return 0
19067
+ ;;
19068
+ -*)
19069
+ log_error "Unknown option: $1"
19070
+ return 1
19071
+ ;;
19072
+ *)
19073
+ target_path="$1"
19074
+ shift
19075
+ ;;
19076
+ esac
19077
+ done
19078
+
19079
+ if [ ! -d "$target_path" ]; then
19080
+ log_error "Directory not found: $target_path"
19081
+ return 1
19082
+ fi
19083
+ target_path="$(cd "$target_path" && pwd)"
19084
+
19085
+ local docs_dir="$target_path/.loki/docs"
19086
+ local manifest="$docs_dir/docs-manifest.json"
19087
+ local pass=true
19088
+
19089
+ echo -e "${BOLD}Documentation Check${NC}"
19090
+ echo "----------------"
19091
+
19092
+ # Check 1: docs directory exists
19093
+ if [ ! -d "$docs_dir" ]; then
19094
+ echo -e " ${RED}[FAIL]${NC} No .loki/docs/ directory found"
19095
+ echo ""
19096
+ echo "Run 'loki docs generate' to create documentation."
19097
+ return 1
19098
+ fi
19099
+
19100
+ # Check 2: README.md exists and is non-empty
19101
+ if [ -f "$docs_dir/README.md" ] && [ -s "$docs_dir/README.md" ]; then
19102
+ echo -e " ${GREEN}[PASS]${NC} README.md exists and is non-empty"
19103
+ else
19104
+ echo -e " ${RED}[FAIL]${NC} README.md missing or empty"
19105
+ pass=false
19106
+ fi
19107
+
19108
+ # Check 3: manifest exists
19109
+ if [ -f "$manifest" ]; then
19110
+ echo -e " ${GREEN}[PASS]${NC} docs-manifest.json exists"
19111
+ else
19112
+ echo -e " ${RED}[FAIL]${NC} docs-manifest.json missing"
19113
+ pass=false
19114
+ fi
19115
+
19116
+ # Check 4: expected doc files exist
19117
+ local expected_docs="README.md ARCHITECTURE.md API.md SETUP.md COMPONENTS.md TESTING.md DECISIONS.md CLAUDE.md"
19118
+ local doc_count=0
19119
+ local expected_count=0
19120
+ for doc in $expected_docs; do
19121
+ ((++expected_count))
19122
+ if [ -f "$docs_dir/$doc" ] && [ -s "$docs_dir/$doc" ]; then
19123
+ ((++doc_count))
19124
+ else
19125
+ echo -e " ${YELLOW}[WARN]${NC} Missing or empty: $doc"
19126
+ fi
19127
+ done
19128
+ local coverage_pct=0
19129
+ [ "$expected_count" -gt 0 ] && coverage_pct=$(( doc_count * 100 / expected_count ))
19130
+ echo -e " ${BOLD}Coverage:${NC} $doc_count/$expected_count docs ($coverage_pct%)"
19131
+
19132
+ # Check 5: staleness (commits since last doc generation)
19133
+ if [ -f "$manifest" ] && command -v python3 &>/dev/null; then
19134
+ local last_sha=""
19135
+ last_sha=$(python3 -c "
19136
+ import json
19137
+ try:
19138
+ d = json.load(open('$manifest'))
19139
+ print(d.get('git_sha', ''))
19140
+ except: pass
19141
+ " 2>/dev/null || true)
19142
+
19143
+ if [ -n "$last_sha" ] && [ "$last_sha" != "unknown" ]; then
19144
+ local commits_behind=0
19145
+ commits_behind=$(cd "$target_path" && git rev-list --count "$last_sha"..HEAD 2>/dev/null || echo "0")
19146
+ if [ "$commits_behind" -gt 10 ]; then
19147
+ echo -e " ${RED}[FAIL]${NC} Docs are $commits_behind commits behind HEAD (threshold: 10)"
19148
+ pass=false
19149
+ elif [ "$commits_behind" -gt 0 ]; then
19150
+ echo -e " ${YELLOW}[WARN]${NC} Docs are $commits_behind commits behind HEAD"
19151
+ else
19152
+ echo -e " ${GREEN}[PASS]${NC} Docs are up to date with HEAD"
19153
+ fi
19154
+ fi
19155
+ fi
19156
+
19157
+ # Check 6: component coverage (check if major dirs have doc entries)
19158
+ local tree_output
19159
+ tree_output=$(_docs_scan_project "$target_path")
19160
+ local major_dirs
19161
+ major_dirs=$(echo "$tree_output" | sed 's|/.*||' | sort -u | head -20)
19162
+ local dir_count=0
19163
+ local documented_dirs=0
19164
+ if [ -f "$docs_dir/COMPONENTS.md" ]; then
19165
+ local components_content
19166
+ components_content=$(cat "$docs_dir/COMPONENTS.md" 2>/dev/null || true)
19167
+ while IFS= read -r dir; do
19168
+ [ -z "$dir" ] && continue
19169
+ [ ! -d "$target_path/$dir" ] && continue
19170
+ ((++dir_count))
19171
+ if echo "$components_content" | grep -qi "$dir" 2>/dev/null; then
19172
+ ((++documented_dirs))
19173
+ fi
19174
+ done <<< "$major_dirs"
19175
+ if [ "$dir_count" -gt 0 ]; then
19176
+ local dir_pct=$(( documented_dirs * 100 / dir_count ))
19177
+ echo -e " ${BOLD}Component coverage:${NC} $documented_dirs/$dir_count directories ($dir_pct%)"
19178
+ fi
19179
+ fi
19180
+
19181
+ echo ""
19182
+ if [ "$pass" = true ] && [ "$coverage_pct" -ge 75 ]; then
19183
+ echo -e "${GREEN}Result: PASS${NC}"
19184
+ return 0
19185
+ else
19186
+ echo -e "${RED}Result: FAIL${NC}"
19187
+ [ "$coverage_pct" -lt 75 ] && echo " Doc coverage below 75%. Run 'loki docs generate' to create missing docs."
19188
+ return 1
19189
+ fi
19190
+ }
19191
+
19192
+ # --- loki docs status ---
19193
+ _docs_status() {
19194
+ local target_path="."
19195
+
19196
+ while [[ $# -gt 0 ]]; do
19197
+ case "$1" in
19198
+ --help|-h)
19199
+ echo "Usage: loki docs status [path]"
19200
+ echo "Show documentation metrics."
19201
+ return 0
19202
+ ;;
19203
+ -*)
19204
+ log_error "Unknown option: $1"
19205
+ return 1
19206
+ ;;
19207
+ *)
19208
+ target_path="$1"
19209
+ shift
19210
+ ;;
19211
+ esac
19212
+ done
19213
+
19214
+ if [ ! -d "$target_path" ]; then
19215
+ log_error "Directory not found: $target_path"
19216
+ return 1
19217
+ fi
19218
+ target_path="$(cd "$target_path" && pwd)"
19219
+
19220
+ local docs_dir="$target_path/.loki/docs"
19221
+ local manifest="$docs_dir/docs-manifest.json"
19222
+
19223
+ echo -e "${BOLD}Documentation Status${NC}"
19224
+ echo "===================="
19225
+ echo ""
19226
+
19227
+ if [ ! -d "$docs_dir" ]; then
19228
+ echo "No documentation generated yet."
19229
+ echo "Run 'loki docs generate' to create documentation."
19230
+ return 0
19231
+ fi
19232
+
19233
+ # Doc count and sizes
19234
+ local doc_count=0
19235
+ local total_size=0
19236
+ echo -e "${BOLD}Documents:${NC}"
19237
+ for f in "$docs_dir"/*.md; do
19238
+ [ ! -f "$f" ] && continue
19239
+ ((++doc_count))
19240
+ local fsize
19241
+ fsize=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
19242
+ total_size=$((total_size + fsize))
19243
+ local flines
19244
+ flines=$(wc -l < "$f" 2>/dev/null | tr -d ' ')
19245
+ local fname
19246
+ fname=$(basename "$f")
19247
+ printf " %-22s %5d lines %6s\n" "$fname" "$flines" "$(_docs_human_size "$fsize")"
19248
+ done
19249
+ echo ""
19250
+ echo " Total: $doc_count documents, $(_docs_human_size "$total_size")"
19251
+ echo ""
19252
+
19253
+ # Coverage
19254
+ local tree_output
19255
+ tree_output=$(_docs_scan_project "$target_path")
19256
+ local total_files
19257
+ total_files=$(echo "$tree_output" | { grep -c . || true; })
19258
+ local src_files
19259
+ src_files=$(echo "$tree_output" | { grep -cE '\.(js|ts|tsx|jsx|py|rs|go|rb|java|kt|c|cpp|h|hpp|sh)$' || true; })
19260
+
19261
+ local expected_docs="README.md ARCHITECTURE.md API.md SETUP.md COMPONENTS.md TESTING.md DECISIONS.md CLAUDE.md"
19262
+ local existing=0
19263
+ local expected_total=0
19264
+ for doc in $expected_docs; do
19265
+ ((++expected_total))
19266
+ [ -f "$docs_dir/$doc" ] && [ -s "$docs_dir/$doc" ] && ((++existing))
19267
+ done
19268
+ local pct=0
19269
+ [ "$expected_total" -gt 0 ] && pct=$(( existing * 100 / expected_total ))
19270
+ echo -e "${BOLD}Coverage:${NC} $existing/$expected_total doc types ($pct%)"
19271
+ echo " Project: $total_files files ($src_files source)"
19272
+ echo ""
19273
+
19274
+ # Staleness
19275
+ if [ -f "$manifest" ] && command -v python3 &>/dev/null; then
19276
+ local last_sha="" generated_at=""
19277
+ eval "$(python3 -c "
19278
+ import json
19279
+ try:
19280
+ d = json.load(open('$manifest'))
19281
+ print('last_sha=\"' + d.get('git_sha', 'unknown') + '\"')
19282
+ print('generated_at=\"' + d.get('generated_at', 'unknown') + '\"')
19283
+ except: pass
19284
+ " 2>/dev/null || true)"
19285
+
19286
+ echo -e "${BOLD}Last generated:${NC} $generated_at"
19287
+ if [ -n "$last_sha" ] && [ "$last_sha" != "unknown" ]; then
19288
+ echo " Git SHA: ${last_sha:0:12}"
19289
+ local commits_behind=0
19290
+ commits_behind=$(cd "$target_path" && git rev-list --count "$last_sha"..HEAD 2>/dev/null || echo "0")
19291
+ if [ "$commits_behind" -eq 0 ]; then
19292
+ echo -e " Staleness: ${GREEN}up to date${NC}"
19293
+ elif [ "$commits_behind" -le 10 ]; then
19294
+ echo -e " Staleness: ${YELLOW}$commits_behind commits behind${NC}"
19295
+ else
19296
+ echo -e " Staleness: ${RED}$commits_behind commits behind${NC} (consider running 'loki docs update')"
19297
+ fi
19298
+ fi
19299
+ fi
19300
+ }
19301
+
19302
+ # --- Docs helper: human-readable file size ---
19303
+ _docs_human_size() {
19304
+ local bytes="$1"
19305
+ if [ "$bytes" -ge 1048576 ]; then
19306
+ echo "$((bytes / 1048576))MB"
19307
+ elif [ "$bytes" -ge 1024 ]; then
19308
+ echo "$((bytes / 1024))KB"
19309
+ else
19310
+ echo "${bytes}B"
19311
+ fi
19312
+ }
19313
+
18325
19314
  # CI/CD quality gate integration (v6.22.0)
18326
19315
  cmd_ci() {
18327
19316
  local ci_pr=false