loki-mode 10.6.13 → 10.7.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 (110) hide show
  1. package/README.md +8 -5
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/autonomy/acp-agent.py +128 -0
  5. package/autonomy/completion-council.sh +3 -3
  6. package/autonomy/docker-run.sh +8 -0
  7. package/autonomy/issue-parser.sh +8 -7
  8. package/autonomy/issue-providers.sh +159 -1
  9. package/autonomy/lib/engine10-push.sh +4 -1
  10. package/autonomy/lib/review_risk.py +91 -0
  11. package/autonomy/loki +225 -31
  12. package/autonomy/mobile-verify.sh +66 -0
  13. package/autonomy/quickstart.sh +44 -4
  14. package/autonomy/run.sh +44 -2
  15. package/completions/_loki +2 -0
  16. package/completions/loki.bash +1 -1
  17. package/dashboard/__init__.py +1 -1
  18. package/dashboard/api_runs_v1.py +80 -0
  19. package/dashboard/server.py +2 -0
  20. package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +3 -3
  21. package/docs/INSTALLATION.md +5 -1
  22. package/docs/acp-editors.md +29 -0
  23. package/docs/api-runs-v1.md +12 -0
  24. package/docs/architecture/ADR-001-runtime-migration.md +13 -13
  25. package/docs/dev/architecture-reference.md +6 -2
  26. package/docs/merge-queue.md +13 -0
  27. package/docs/mobile-verify.md +11 -0
  28. package/docs/sentry-intake.md +13 -0
  29. package/docs/v10/BOARD.md +66 -12
  30. package/docs/v10/CP-ENTERPRISE-UI.md +276 -0
  31. package/docs/v10/D61-SPEED.md +4 -0
  32. package/docs/v10/DECISIONS.md +85 -0
  33. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  34. package/docs/v10/GAP-RESEARCH-1003.md +32 -0
  35. package/docs/v10/MORNING-BRIEF-1003.md +39 -0
  36. package/docs/v10/PROGRESS.md +107 -0
  37. package/docs/v10/SWARM.md +32 -0
  38. package/docs/visual-evidence.md +5 -0
  39. package/loki-ts/dist/loki.js +766 -624
  40. package/mcp/__init__.py +1 -1
  41. package/package.json +1 -1
  42. package/packages/control-plane/dist/server.js +263 -9
  43. package/packages/control-plane/src/server/actions.ts +60 -0
  44. package/packages/control-plane/src/server/app.ts +39 -3
  45. package/packages/control-plane/src/shipper/watch.ts +64 -0
  46. package/packages/control-plane/ui/dist/assets/index-CUQ65vJY.css +1 -0
  47. package/packages/control-plane/ui/dist/assets/index-CwwGfp6E.js +129 -0
  48. package/packages/control-plane/ui/dist/index.html +3 -3
  49. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  50. package/skills/healing.md +2 -2
  51. package/tools/proof-passport.py +1 -1
  52. package/web-app/dist/assets/{AdminPage-CVEdde3t.js → AdminPage-BQPb5rc5.js} +1 -1
  53. package/web-app/dist/assets/{Avatar-D1r8n_Da.js → Avatar-Cttco6FC.js} +1 -1
  54. package/web-app/dist/assets/{Badge-BAPyJ97M.js → Badge-CPb6kpbH.js} +1 -1
  55. package/web-app/dist/assets/{Button-C-GCctTq.js → Button-caTL28a4.js} +1 -1
  56. package/web-app/dist/assets/{CockpitPage-CV9MhyuV.js → CockpitPage-bEbLx6ph.js} +1 -1
  57. package/web-app/dist/assets/{ComparePage-D1R5Lxmy.js → ComparePage-DNtJSij-.js} +1 -1
  58. package/web-app/dist/assets/{ErrorBoundary-CFvJx4vV.js → ErrorBoundary-BrgC3K44.js} +1 -1
  59. package/web-app/dist/assets/{EvidenceReceiptPanel-9I7dfPz0.js → EvidenceReceiptPanel-Q-mSrUUQ.js} +1 -1
  60. package/web-app/dist/assets/{GitHubIssuesPanel-8YH6WkTh.js → GitHubIssuesPanel-DbmYl_Er.js} +1 -1
  61. package/web-app/dist/assets/{GitHubPRsPanel-BZvEHBTF.js → GitHubPRsPanel-C6PCHM4B.js} +1 -1
  62. package/web-app/dist/assets/{HomePage-DhjWTo8R.js → HomePage-7zsL_k_e.js} +2 -2
  63. package/web-app/dist/assets/{LoginPage-BwNGyWxP.js → LoginPage-Cl4ON8RQ.js} +1 -1
  64. package/web-app/dist/assets/{MagicPage-Dg9z7deq.js → MagicPage-BNEAhTqH.js} +1 -1
  65. package/web-app/dist/assets/{MetricsPage-DLF1F6XC.js → MetricsPage-DyBH-88q.js} +1 -1
  66. package/web-app/dist/assets/{NotFoundPage-CZClMI3s.js → NotFoundPage-CIVr21yL.js} +1 -1
  67. package/web-app/dist/assets/{ProjectPage-CnAvyHDJ.js → ProjectPage-BMeVYocF.js} +3 -3
  68. package/web-app/dist/assets/{ProjectsPage-D33VGyVL.js → ProjectsPage-D-H2WRVU.js} +1 -1
  69. package/web-app/dist/assets/{SettingsPage-DQtlpXPe.js → SettingsPage-o0kR9RAt.js} +1 -1
  70. package/web-app/dist/assets/{ShowcasePage-CAitzJlY.js → ShowcasePage-DtfMDMjp.js} +1 -1
  71. package/web-app/dist/assets/{SystemSettingsPage-CV49UjwU.js → SystemSettingsPage-CkXdpZ_T.js} +1 -1
  72. package/web-app/dist/assets/{TeamsPage-5EKrbkN3.js → TeamsPage-Cu5H9cQ0.js} +1 -1
  73. package/web-app/dist/assets/{TemplatesPage-BRsrm00y.js → TemplatesPage-3wdbVwil.js} +1 -1
  74. package/web-app/dist/assets/{TerminalOutput-DgEnMwnQ.js → TerminalOutput-CWn301xh.js} +1 -1
  75. package/web-app/dist/assets/{activity-hXTYu4Zb.js → activity-YGaAt4zJ.js} +1 -1
  76. package/web-app/dist/assets/{bell-D5QkmMjg.js → bell-C28q0GO7.js} +1 -1
  77. package/web-app/dist/assets/{bot-CGOzPami.js → bot-DLcQGI92.js} +1 -1
  78. package/web-app/dist/assets/{check-HxPvzPXm.js → check-w0sBNlcm.js} +1 -1
  79. package/web-app/dist/assets/{chevron-left-D7HYUp4T.js → chevron-left-W7O0vC_m.js} +1 -1
  80. package/web-app/dist/assets/{circle-alert-BdufMh8P.js → circle-alert-Cxjq9fxB.js} +1 -1
  81. package/web-app/dist/assets/{clock-DMcS0xjR.js → clock-C8EqzmNl.js} +1 -1
  82. package/web-app/dist/assets/{cloud-Bt-0bh9m.js → cloud-xNgp4g3Y.js} +1 -1
  83. package/web-app/dist/assets/{code-xml-DRtDKt4_.js → code-xml-B-lyNDLJ.js} +1 -1
  84. package/web-app/dist/assets/{database-B0ChKZQ4.js → database-DoW5lmWc.js} +1 -1
  85. package/web-app/dist/assets/{dollar-sign-st_8sF_M.js → dollar-sign-BcARA5sC.js} +1 -1
  86. package/web-app/dist/assets/{file-code-corner-UdiLz5N6.js → file-code-corner-CehcII0J.js} +1 -1
  87. package/web-app/dist/assets/{file-plus-De1dimur.js → file-plus-DI9Zo5hP.js} +1 -1
  88. package/web-app/dist/assets/{globe-rrbXmytb.js → globe-CiZWBk3H.js} +1 -1
  89. package/web-app/dist/assets/{hammer-CBtNZi-S.js → hammer-3qv_T2N-.js} +1 -1
  90. package/web-app/dist/assets/{index-D3YvZaMV.js → index-gF9grAz-.js} +3 -3
  91. package/web-app/dist/assets/{layers-BmthCdkO.js → layers-CN7d0By9.js} +1 -1
  92. package/web-app/dist/assets/{loader-circle-C_sXFJUa.js → loader-circle-DywcM-im.js} +1 -1
  93. package/web-app/dist/assets/{lock-DT6V3D97.js → lock-_hdSmkU4.js} +1 -1
  94. package/web-app/dist/assets/{package-D86K1oU6.js → package-BV5zgqxJ.js} +1 -1
  95. package/web-app/dist/assets/{plus-Dwkssum8.js → plus-CU_Hh4-x.js} +1 -1
  96. package/web-app/dist/assets/{refresh-cw-DhRlqf1F.js → refresh-cw-DMK-fNl-.js} +1 -1
  97. package/web-app/dist/assets/{rotate-ccw-EvA5ZJVL.js → rotate-ccw-CEB3_v5_.js} +1 -1
  98. package/web-app/dist/assets/{scroll-text-D4ABo43E.js → scroll-text-BSdXxcRN.js} +1 -1
  99. package/web-app/dist/assets/{server-BAAdr_fD.js → server-CAMlP1yJ.js} +1 -1
  100. package/web-app/dist/assets/{shield-alert-DJPLy9t8.js → shield-alert-B-r1Dt3h.js} +1 -1
  101. package/web-app/dist/assets/{thumbs-up-D07JqmoS.js → thumbs-up-CYfxw9o4.js} +1 -1
  102. package/web-app/dist/assets/{trash-2-BJYwlHzl.js → trash-2-DX3nGPS8.js} +1 -1
  103. package/web-app/dist/assets/{trending-down-Bo8txoOw.js → trending-down-Bt-hSntd.js} +1 -1
  104. package/web-app/dist/assets/{trending-up-Bb13tkln.js → trending-up-DVSlvQp8.js} +1 -1
  105. package/web-app/dist/assets/{upload-D60DiVNZ.js → upload-D45RRHAP.js} +1 -1
  106. package/web-app/dist/assets/{usePolling-DJmgDiio.js → usePolling-fq_Xj8iw.js} +1 -1
  107. package/web-app/dist/assets/{user-BmXCbSJm.js → user-BWcJkLlr.js} +1 -1
  108. package/web-app/dist/index.html +1 -1
  109. package/packages/control-plane/ui/dist/assets/index-CFfrqCx2.css +0 -1
  110. package/packages/control-plane/ui/dist/assets/index-Jt31nrK-.js +0 -114
package/autonomy/loki CHANGED
@@ -1693,6 +1693,7 @@ show_help() {
1693
1693
  echo "Observe:"
1694
1694
  echo " dashboard [cmd] Operations UI server (start|stop|status|url|open)"
1695
1695
  echo " mcp Launch the MCP server (Model Context Protocol, stdio)"
1696
+ echo " acp Run Loki as an ACP agent over stdio (VS Code, JetBrains, Zed)"
1696
1697
  echo ""
1697
1698
  echo "Report:"
1698
1699
  echo " report [cmd] Reporting: session|metrics|cost|export|share|dogfood"
@@ -1812,19 +1813,19 @@ show_help() {
1812
1813
  # missing from this list, so a new command cannot ship invisible.
1813
1814
  echo ""
1814
1815
  echo "All commands:"
1815
- echo " agent analyze api assets audit backlog bench checkpoint (cp) ci cleanup"
1816
+ echo " acp agent analyze api assets audit backlog bench checkpoint (cp) ci cleanup"
1816
1817
  echo " cluster cockpit code compliance completions compound config context (ctx)"
1817
1818
  echo " cost council crash dashboard demo deploy docker docs doctor dogfood"
1818
1819
  echo " enterprise estimate explain export failover github grill heal help"
1819
1820
  echo " import init intent"
1820
- echo " issue kpis logs magic mcp memory metrics migrate modernize monitor"
1821
+ echo " issue keys kpis logs magic mcp memory metrics migrate modernize monitor"
1821
1822
  echo " next notify onboard open optimize otel outcomes own (handoff) pause plan preview"
1822
1823
  echo " projects proof (receipt) provider quick quickstart rc remote report reset"
1823
- echo " resume review rollback run sandbox secrets secure self-update sentrux"
1824
+ echo " merge resume review rollback run sandbox secrets secure self-update sentrux"
1824
1825
  echo " serve setup-skill share ship spec start state stats status steer stop"
1825
1826
  echo " syslog telemetry template test tour trigger trust trust-metrics"
1826
1827
  echo " ultracode update verify version voice watch watchdog web welcome why"
1827
- echo " wiki worktree (wt)"
1828
+ echo " wiki workspace worktree (wt)"
1828
1829
  echo " control"
1829
1830
  echo ""
1830
1831
  echo "Any command: loki <command> --help"
@@ -2007,6 +2008,9 @@ _loki_control_ui() {
2007
2008
 
2008
2009
  cmd_ui() {
2009
2010
  _loki_control_ui "$@" && return 0
2011
+ # CP-LEGACY: callers that must never start the classic server (run.sh
2012
+ # auto-open) set LOKI_UI_NO_CLASSIC=1.
2013
+ [ "${LOKI_UI_NO_CLASSIC:-0}" = "1" ] && return 1
2010
2014
  local port="" pid=""
2011
2015
  if [ -f "$DASHBOARD_PID_FILE" ]; then
2012
2016
  pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)
@@ -2084,6 +2088,18 @@ detect_arg_type() {
2084
2088
  return 0
2085
2089
  fi
2086
2090
 
2091
+ # Sentry intake: an https Sentry issue URL, or an exported Sentry JSON file
2092
+ # on disk, is an issue (checked before the .json -> prd rule below).
2093
+ if [[ "$arg" =~ ^https://[^/]*sentry[^/]*/.*issues/[0-9]+ ]]; then
2094
+ echo "issue"
2095
+ return 0
2096
+ fi
2097
+ if [[ "$arg" == *.json ]] && [ -f "$arg" ] \
2098
+ && head -c 2000000 "$arg" 2>/dev/null | grep -Eq '"(shortId|culprit)"[[:space:]]*:|"exception"[[:space:]]*:|"type"[[:space:]]*:[[:space:]]*"exception"'; then
2099
+ echo "issue"
2100
+ return 0
2101
+ fi
2102
+
2087
2103
  # Known PRD extensions - treat as PRD first (highest priority).
2088
2104
  # This handles edge case where a file is literally named "123.md" or
2089
2105
  # sits in a dir that matches another pattern.
@@ -7441,14 +7457,28 @@ _loki_dashboard_bind_is_safe() {
7441
7457
  return 1
7442
7458
  }
7443
7459
 
7460
+ _loki_dashboard_args_need_classic() {
7461
+ local a
7462
+ for a in "$@"; do
7463
+ case "$a" in
7464
+ --host|--host=*|--tls-cert|--tls-cert=*|--tls-key|--tls-key=*) return 0 ;;
7465
+ esac
7466
+ done
7467
+ return 1
7468
+ }
7469
+
7444
7470
  cmd_dashboard() {
7445
7471
  local subcommand="${1:-}"
7446
7472
 
7447
- # The Control Plane is the default UI; LOKI_CONTROL_DEFAULT=0 restores the classic dashboard.
7448
- # Only the bare forms are taken over; `start --port ...` and friends keep the classic server.
7473
+ # CP-LEGACY (D57): the Control Plane is the default UI for `dashboard`,
7474
+ # `dashboard start` and `dashboard open`, with or without flags. The legacy
7475
+ # Python dashboard stays reachable only by explicit opt-in:
7476
+ # LOKI_LEGACY_DASHBOARD=1 or LOKI_CONTROL_DEFAULT=0. Enterprise bind/TLS
7477
+ # flags (--host, --tls-cert, --tls-key) are classic-server concepts and
7478
+ # keep the classic server.
7449
7479
  case "$subcommand" in
7450
7480
  ""|start|open)
7451
- if [ $# -le 1 ] || { [ $# -eq 2 ] && [ "$2" = "--no-open" ]; }; then
7481
+ if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ] && ! _loki_dashboard_args_need_classic "${@:2}"; then
7452
7482
  _loki_control_ui "${@:2}" && return 0
7453
7483
  fi
7454
7484
  ;;
@@ -8307,6 +8337,11 @@ cmd_web_redirect_to_dashboard() {
8307
8337
  echo " it watches runs that are already going. To build from that spec:" >&2
8308
8338
  echo " loki start '${_web_dropped_prd}'" >&2
8309
8339
  fi
8340
+ # Flags such as --port must reach the classic validator, so only the
8341
+ # bare form (optionally --no-open) is routed to the Control Plane.
8342
+ if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ] && [ ${#dash_args[@]} -eq 0 ] && _loki_control_ui; then
8343
+ return 0
8344
+ fi
8310
8345
  cmd_dashboard_start ${dash_args[@]+"${dash_args[@]}"}
8311
8346
  }
8312
8347
 
@@ -19270,7 +19305,7 @@ LANG_BY_EXT = {
19270
19305
  }
19271
19306
 
19272
19307
  TEST_HINT = re.compile(r'(^|[/_.-])(test|tests|spec|specs|__tests__)([/_.-]|s?$|\.)', re.I)
19273
- TODO_RE = re.compile(r'\b(TODO|FIXME|HACK|XXX)\b')
19308
+ TODO_RE = re.compile(r'\b(TODO|FIXME|HACK|XXX)s?\b')
19274
19309
 
19275
19310
  # import/require line -> the referenced module basename(s). Deliberately cheap:
19276
19311
  # we only need a proxy for how many OTHER files reference a given file, to rank
@@ -19291,6 +19326,7 @@ total_files = 0
19291
19326
  todo_count = 0
19292
19327
  has_tests = False
19293
19328
  big_files = [] # (loc, relpath)
19329
+ max_loc = 0 # largest file by LOC, across every file
19294
19330
  # module reference tallies: how many files import a given basename (no-ext)
19295
19331
  ref_counts = {}
19296
19332
  file_records = {} # relpath -> record
@@ -19340,6 +19376,8 @@ for dirpath, dirnames, filenames in os.walk(root):
19340
19376
  }
19341
19377
  files.append(rec)
19342
19378
  file_records[rel] = rec
19379
+ if loc > max_loc:
19380
+ max_loc = loc
19343
19381
  if loc >= 500:
19344
19382
  big_files.append((loc, rel))
19345
19383
  # tally inbound references toward the imported basenames
@@ -19352,7 +19390,7 @@ for rec in files:
19352
19390
 
19353
19391
  # --- Technical-debt / maintenance-risk signals (all deterministic) ---
19354
19392
  big_files.sort(reverse=True)
19355
- largest_loc = big_files[0][0] if big_files else 0
19393
+ largest_loc = max_loc
19356
19394
  # dependency-manifest presence (a real, cheap signal; NOT a staleness figure).
19357
19395
  manifest_names = {
19358
19396
  'package.json', 'requirements.txt', 'Pipfile', 'pyproject.toml', 'go.mod',
@@ -21761,8 +21799,10 @@ ultracode council demo dogfood heal modernize import issue github init template
21761
21799
  status stop pause resume monitor watch watchdog dashboard web open preview share \
21762
21800
  memory context ctx checkpoint state report kpis cost metrics stats logs otel \
21763
21801
  syslog telemetry crash config provider doctor setup-skill onboard quickstart \
21764
- welcome update self-update rollback failover cluster enterprise remote deploy docker \
21802
+ welcome update self-update rollback merge failover cluster enterprise remote deploy docker \
21765
21803
  sandbox api mcp magic compound assets export wiki control docs explain why audit compliance \
21804
+ welcome update self-update rollback failover cluster enterprise remote deploy docker \
21805
+ sandbox api mcp acp magic compound assets export wiki control docs explain why audit compliance \
21766
21806
  secrets analyze optimize bench ci reset cleanup notify trigger voice sentrux \
21767
21807
  worktree wt projects cp rc trust-metrics serve agent code self_update test help version"
21768
21808
  local best
@@ -22094,6 +22134,9 @@ main() {
22094
22134
  rollback)
22095
22135
  cmd_rollback "$@"
22096
22136
  ;;
22137
+ merge)
22138
+ cmd_merge "$@"
22139
+ ;;
22097
22140
  council)
22098
22141
  cmd_council "$@"
22099
22142
  ;;
@@ -22174,6 +22217,10 @@ main() {
22174
22217
  mcp)
22175
22218
  cmd_mcp "$@"
22176
22219
  ;;
22220
+ acp)
22221
+ # ACP agent over stdio; stdout is protocol-only. See docs/acp-editors.md.
22222
+ LOKI_ACP_LOKI="${LOKI_ACP_LOKI:-$_LOKI_SCRIPT_DIR/loki}" exec python3 "$_LOKI_SCRIPT_DIR/acp-agent.py"
22223
+ ;;
22177
22224
  migrate)
22178
22225
  # CLI consolidation (Phase B): 'migrate' -> 'modernize migrate'.
22179
22226
  _deprecated_alias migrate "modernize migrate" "$@"
@@ -22706,6 +22753,7 @@ cmd_review() {
22706
22753
  local review_ultra=false
22707
22754
  local review_assume_yes=false
22708
22755
  local review_timeout=""
22756
+ local review_risk=false
22709
22757
 
22710
22758
  while [[ $# -gt 0 ]]; do
22711
22759
  case "$1" in
@@ -22733,6 +22781,10 @@ cmd_review() {
22733
22781
  echo " skip the prompt. Optional [file-or-dir] is"
22734
22782
  echo " passed as the ultrareview target (branch / PR"
22735
22783
  echo " number / base branch)."
22784
+ echo " --risk Deterministic 0-100 diff risk score with per-factor"
22785
+ echo " breakdown (files, sensitive paths, size, test delta,"
22786
+ echo " deleted tests). Works with --pr/--staged/--since;"
22787
+ echo " 'loki review <pr-number> --risk' also works. Use --json"
22736
22788
  echo " --yes, -y Skip the --ultra confirmation prompt"
22737
22789
  echo " --timeout <minutes> --ultra: max minutes to wait (default 30)"
22738
22790
  echo " --help, -h Show this help"
@@ -22756,6 +22808,8 @@ cmd_review() {
22756
22808
  return 0
22757
22809
  ;;
22758
22810
  --ultra) review_ultra=true; shift ;;
22811
+ --risk) review_risk=true; shift ;;
22812
+ --json) review_format="json"; shift ;;
22759
22813
  --yes|-y) review_assume_yes=true; shift ;;
22760
22814
  --timeout)
22761
22815
  shift
@@ -22811,6 +22865,29 @@ cmd_review() {
22811
22865
  return $?
22812
22866
  fi
22813
22867
 
22868
+ if [ "$review_risk" = true ]; then
22869
+ if [ -z "$review_pr" ] && [ -z "$review_since" ] && [[ "$review_target" =~ ^[0-9]+$ ]] && [ ! -e "$review_target" ]; then
22870
+ review_pr="$review_target"
22871
+ fi
22872
+ local risk_diff risk_src="uncommitted changes"
22873
+ if [ -n "$review_pr" ]; then
22874
+ command -v gh &>/dev/null || { echo "Error: gh CLI required for PR risk" >&2; return 1; }
22875
+ risk_diff=$(gh pr diff "$review_pr" 2>&1) || { echo "Error: Could not fetch PR #${review_pr}: $risk_diff" >&2; return 1; }
22876
+ risk_src="PR #${review_pr}"
22877
+ elif [ -n "$review_since" ]; then
22878
+ risk_diff=$(git diff "$review_since" 2>&1) || { echo "Error: Invalid commit reference: $review_since" >&2; return 1; }
22879
+ risk_src="changes since $review_since"
22880
+ elif [ "$review_staged" = true ]; then
22881
+ risk_diff=$(git diff --cached 2>/dev/null); risk_src="staged changes"
22882
+ else
22883
+ risk_diff=$(git diff 2>/dev/null)
22884
+ fi
22885
+ local risk_args=()
22886
+ [ "$review_format" = "json" ] && risk_args+=(--json)
22887
+ printf '%s\n' "$risk_diff" | python3 "$_LOKI_SCRIPT_DIR/lib/review_risk.py" ${risk_args[@]+"${risk_args[@]}"} "$risk_src"
22888
+ return $?
22889
+ fi
22890
+
22814
22891
  # Severity level ordering (higher number = more severe)
22815
22892
  _review_sev_level() {
22816
22893
  case "$1" in
@@ -24647,16 +24724,17 @@ if total_removed > 0:
24647
24724
  # Show or rebuild the memory index layer
24648
24725
  if [ "${2:-}" = "rebuild" ]; then
24649
24726
  PYTHONPATH="${SKILL_DIR}${PYTHONPATH:+:$PYTHONPATH}" python3 -c "
24727
+ import sys
24650
24728
  try:
24651
- from memory.layers import IndexLayer
24652
- layer = IndexLayer('.loki/memory')
24653
- layer.update([]) # Will scan and rebuild
24654
- print('Index rebuilt')
24655
- except ImportError:
24656
- print('Error: memory.layers module not found')
24729
+ from memory.engine import MemoryEngine
24730
+ engine = MemoryEngine(base_path='.loki/memory')
24731
+ engine.rebuild_index()
24732
+ total = (engine.get_index() or {}).get('total_memories', 0)
24657
24733
  except Exception as e:
24658
- print(f'Error: {e}')
24659
- " 2>/dev/null
24734
+ print(f'Error: index rebuild failed: {type(e).__name__}: {e}', file=sys.stderr)
24735
+ sys.exit(1)
24736
+ print(f'Index rebuilt: {total} memories indexed')
24737
+ " || return 1
24660
24738
  else
24661
24739
  cat .loki/memory/index.json 2>/dev/null | python3 -m json.tool 2>/dev/null || echo "No index found"
24662
24740
  fi
@@ -26290,6 +26368,93 @@ SHOW_EOF
26290
26368
  esac
26291
26369
  }
26292
26370
 
26371
+ # loki merge: serial PR merge queue. Queue lives in .loki/merge-queue (one PR
26372
+ # number per line). A PR merges only when `gh pr checks` exits 0 (all green);
26373
+ # after each merge the next PR is rebased onto the new base (gh pr update-branch
26374
+ # --rebase) and must go green again before it merges. Never forces anything.
26375
+ cmd_merge() {
26376
+ local sub="${1:-help}"
26377
+ shift 2>/dev/null || true
26378
+ local qfile=".loki/merge-queue"
26379
+ case "$sub" in
26380
+ add)
26381
+ [ $# -gt 0 ] || { echo "Usage: loki merge add <pr-number>..." >&2; return 2; }
26382
+ mkdir -p .loki || return 1
26383
+ local n
26384
+ for n in "$@"; do
26385
+ case "$n" in ''|*[!0-9]*) echo "Invalid PR number: $n" >&2; return 2 ;; esac
26386
+ if [ -f "$qfile" ] && grep -qx "$n" "$qfile"; then
26387
+ echo "PR #$n already queued"
26388
+ else
26389
+ echo "$n" >> "$qfile"
26390
+ echo "Queued PR #$n"
26391
+ fi
26392
+ done
26393
+ ;;
26394
+ list|ls)
26395
+ if [ -s "$qfile" ]; then cat "$qfile" | sed 's/^/#/'; else echo "Merge queue is empty"; fi
26396
+ ;;
26397
+ clear)
26398
+ rm -f "$qfile"; echo "Merge queue cleared"
26399
+ ;;
26400
+ run)
26401
+ local dry=false
26402
+ [ "${1:-}" = "--dry-run" ] && dry=true
26403
+ if [ ! -s "$qfile" ]; then echo "Merge queue is empty"; return 0; fi
26404
+ command -v gh >/dev/null 2>&1 || { echo "gh CLI is required" >&2; return 1; }
26405
+ local poll="${LOKI_MERGE_POLL_S:-20}" tries="${LOKI_MERGE_MAX_POLLS:-90}"
26406
+ local first=true n rc i remaining="" merged=0 blocked=0
26407
+ while IFS= read -r n; do
26408
+ [ -n "$n" ] || continue
26409
+ if $dry; then
26410
+ rc=0; gh pr checks "$n" >/dev/null 2>&1 || rc=$?
26411
+ case "$rc" in
26412
+ 0) echo "#$n: checks green, would merge (squash)" ;;
26413
+ 8) echo "#$n: checks pending, would wait" ;;
26414
+ *) echo "#$n: checks not green, would skip" ;;
26415
+ esac
26416
+ $first || echo "#$n: would rebase onto new base first"
26417
+ first=false
26418
+ continue
26419
+ fi
26420
+ if ! $first; then
26421
+ gh pr update-branch "$n" --rebase >/dev/null 2>&1 \
26422
+ || { echo "#$n: rebase onto new base failed, left in queue"; remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue; }
26423
+ echo "#$n: rebased onto new base"
26424
+ fi
26425
+ rc=8; i=0
26426
+ while [ "$rc" = "8" ] && [ "$i" -lt "$tries" ]; do
26427
+ rc=0; gh pr checks "$n" >/dev/null 2>&1 || rc=$?
26428
+ [ "$rc" = "8" ] && { sleep "$poll"; i=$((i+1)); }
26429
+ done
26430
+ if [ "$rc" != "0" ]; then
26431
+ echo "#$n: checks not green (rc=$rc), left in queue, not merged"
26432
+ remaining="$remaining$n"$'\n'; blocked=$((blocked+1)); continue
26433
+ fi
26434
+ if gh pr merge "$n" --squash >/dev/null 2>&1; then
26435
+ echo "#$n: merged"; merged=$((merged+1)); first=false
26436
+ else
26437
+ echo "#$n: merge failed, left in queue"
26438
+ remaining="$remaining$n"$'\n'; blocked=$((blocked+1))
26439
+ fi
26440
+ done < "$qfile"
26441
+ if $dry; then echo "Dry run: nothing changed"; return 0; fi
26442
+ printf '%s' "$remaining" > "$qfile"
26443
+ echo "Merged: $merged, left in queue: $blocked"
26444
+ [ "$blocked" -eq 0 ]
26445
+ ;;
26446
+ help|--help|-h|*)
26447
+ echo "Usage: loki merge <add|list|run|clear>"
26448
+ echo " add <pr>... Queue pull requests"
26449
+ echo " list Show the queue"
26450
+ echo " run [--dry-run] Merge one at a time, only when checks are green;"
26451
+ echo " each next PR is rebased onto the new base first."
26452
+ echo " --dry-run lists what would happen, changes nothing."
26453
+ [ "$sub" = "help" ] || [ "$sub" = "--help" ] || [ "$sub" = "-h" ] || return 2
26454
+ ;;
26455
+ esac
26456
+ }
26457
+
26293
26458
  # R6: one-command rollback. Top-level, obvious entry point that mirrors the Bun
26294
26459
  # `loki rollback` subcommands (list/show/to/latest) so both routes are at parity.
26295
26460
  # Restore is destructive on .loki/ state, so it ALWAYS captures a forced
@@ -38105,18 +38270,28 @@ cmd_docker() {
38105
38270
  # the port-in-use check, so the picked port is ignored on reuse). Run in
38106
38271
  # a subshell so its internal `exit` paths (already-running -> exit 0,
38107
38272
  # errors -> exit 1) never abort the blocking docker run.
38108
- local _dash_port="57374"
38109
- if declare -F loki_docker_pick_host_port >/dev/null 2>&1; then
38110
- _dash_port="$(loki_docker_pick_host_port 2>/dev/null || echo 57374)"
38111
- fi
38112
- ( cmd_dashboard_start --port "$_dash_port" ) || true
38273
+ # CP-LEGACY (D57): open the Control Plane, never the classic server,
38274
+ # unless LOKI_LEGACY_DASHBOARD=1 opts back in. Same open gate as before.
38275
+ if [ "${LOKI_LEGACY_DASHBOARD:-0}" != "1" ]; then
38276
+ local _cp_args=()
38277
+ if [ ! -t 1 ] || [ "$_docker_bg" = "1" ] || [ "${LOKI_NO_AUTO_OPEN:-0}" = "1" ]; then
38278
+ _cp_args=(--no-open)
38279
+ fi
38280
+ ( _loki_control_ui ${_cp_args[@]+"${_cp_args[@]}"} ) >&2 || true
38281
+ else
38282
+ local _dash_port="57374"
38283
+ if declare -F loki_docker_pick_host_port >/dev/null 2>&1; then
38284
+ _dash_port="$(loki_docker_pick_host_port 2>/dev/null || echo 57374)"
38285
+ fi
38286
+ ( cmd_dashboard_start --port "$_dash_port" ) || true
38113
38287
 
38114
- # Auto-open the dashboard in the browser, gated EXACTLY like
38115
- # run.sh:~10088: a TTY on stdout, not background, and not opted out via
38116
- # LOKI_NO_AUTO_OPEN=1. cmd_dashboard_open reads the persisted port file,
38117
- # so the URL is correct even when an existing dashboard was reused.
38118
- if [ -t 1 ] && [ "$_docker_bg" != "1" ] && [ "${LOKI_NO_AUTO_OPEN:-0}" != "1" ]; then
38119
- ( cmd_dashboard_open ) || true
38288
+ # Auto-open the dashboard in the browser, gated EXACTLY like
38289
+ # run.sh:~10088: a TTY on stdout, not background, and not opted out via
38290
+ # LOKI_NO_AUTO_OPEN=1. cmd_dashboard_open reads the persisted port file,
38291
+ # so the URL is correct even when an existing dashboard was reused.
38292
+ if [ -t 1 ] && [ "$_docker_bg" != "1" ] && [ "${LOKI_NO_AUTO_OPEN:-0}" != "1" ]; then
38293
+ ( cmd_dashboard_open ) || true
38294
+ fi
38120
38295
  fi
38121
38296
 
38122
38297
  # F4 FIX-DOCKER-STOP (write side): record this run's container/state so
@@ -38135,8 +38310,27 @@ cmd_docker() {
38135
38310
  # its live status from the bind-mounted .loki/session.json. Best-effort and
38136
38311
  # failure-swallowed: registry bookkeeping must never block a build.
38137
38312
  _loki_docker_register_host running
38138
- "${docker_argv[@]}"
38139
- local rc=$?
38313
+ local rc _errlog=""
38314
+ # Capture output for a failure summary only when no TTY is attached (a TTY
38315
+ # run already shows the live output and must not be piped).
38316
+ if [ ! -t 1 ] || [ ! -t 0 ]; then
38317
+ _errlog="$(mktemp "${TMPDIR:-/tmp}/loki-docker-out.XXXXXX" 2>/dev/null)" || _errlog=""
38318
+ fi
38319
+ if [ -n "$_errlog" ]; then
38320
+ "${docker_argv[@]}" 2>&1 | tee "$_errlog"
38321
+ rc=${PIPESTATUS[0]}
38322
+ else
38323
+ "${docker_argv[@]}"
38324
+ rc=$?
38325
+ fi
38326
+ if [ "$rc" -ne 0 ]; then
38327
+ local _last=""
38328
+ [ -n "$_errlog" ] && _last="$(grep -v '^[[:space:]]*$' "$_errlog" 2>/dev/null | tail -n 1)"
38329
+ echo -e "${RED}loki docker: container exited with code ${rc}.${NC}" >&2
38330
+ [ -n "$_last" ] && echo "Last error: ${_last}" >&2
38331
+ echo "Fix hint: for PRs set GH_TOKEN=\$(gh auth token), and ensure ANTHROPIC_API_KEY or a Claude login is available, then rerun." >&2
38332
+ fi
38333
+ [ -n "$_errlog" ] && rm -f "$_errlog" 2>/dev/null
38140
38334
  _loki_docker_register_host stopped
38141
38335
  # F4 FIX-DOCKER-STOP (clear side): runs unconditionally after the blocking
38142
38336
  # call (not gated on rc), same as register_host stopped.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Mobile emulator tests: run a mobile project's tests on an available emulator
3
+ # or simulator. Never reports PASS without a run.
4
+ # Usage: mobile-verify.sh [project_dir]
5
+ # Env: LOKI_MOBILE_TEST_CMD override the test command
6
+ # LOKI_MOBILE_TIMEOUT seconds (default 600)
7
+ # Exit: 0 = ran and passed, or not a mobile project (stated)
8
+ # 1 = ran and failed
9
+ # 3 = NOT VERIFIED (no emulator/simulator, or no runnable test command)
10
+ set -u
11
+ dir="${1:-.}"
12
+ cd "$dir" || { echo "mobile tests: NOT VERIFIED (cannot enter $dir)"; exit 3; }
13
+
14
+ kind=""
15
+ if [ -f pubspec.yaml ] && grep -q "flutter" pubspec.yaml 2>/dev/null; then
16
+ kind="flutter"
17
+ elif [ -f package.json ] && grep -qE '"(expo|react-native)"[[:space:]]*:' package.json 2>/dev/null; then
18
+ kind="react-native"
19
+ fi
20
+ if [ -z "$kind" ]; then
21
+ echo "mobile tests: not applicable (no React Native, Expo or Flutter project)"
22
+ exit 0
23
+ fi
24
+
25
+ device=""
26
+ if command -v adb >/dev/null 2>&1; then
27
+ d="$(adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1; exit}')"
28
+ [ -n "$d" ] && device="android:$d"
29
+ fi
30
+ if [ -z "$device" ] && command -v xcrun >/dev/null 2>&1; then
31
+ d="$(xcrun simctl list devices booted 2>/dev/null | awk '/\(Booted\)/ {print "ios-simulator"; exit}')"
32
+ [ -n "$d" ] && device="$d"
33
+ fi
34
+ if [ -z "$device" ]; then
35
+ echo "mobile tests: NOT VERIFIED ($kind project, no booted Android emulator or iOS simulator found; tests were not run)"
36
+ exit 3
37
+ fi
38
+
39
+ cmd="${LOKI_MOBILE_TEST_CMD:-}"
40
+ if [ -z "$cmd" ]; then
41
+ if [ "$kind" = "flutter" ]; then
42
+ if [ -d integration_test ]; then cmd="flutter test integration_test"; else cmd="flutter test"; fi
43
+ elif grep -q '"test:e2e"' package.json 2>/dev/null; then
44
+ cmd="npm run test:e2e"
45
+ elif grep -q '"e2e"' package.json 2>/dev/null; then
46
+ cmd="npm run e2e"
47
+ fi
48
+ fi
49
+ if [ -z "$cmd" ]; then
50
+ echo "mobile tests: NOT VERIFIED ($kind project on $device, but no e2e test script (test:e2e or e2e) to run)"
51
+ exit 3
52
+ fi
53
+
54
+ timeout_s="${LOKI_MOBILE_TIMEOUT:-600}"
55
+ if command -v timeout >/dev/null 2>&1; then
56
+ timeout -k 10 "$timeout_s" bash -c "$cmd"
57
+ else
58
+ bash -c "$cmd"
59
+ fi
60
+ rc=$?
61
+ if [ "$rc" -eq 0 ]; then
62
+ echo "mobile tests: PASS ($kind on $device, ran: $cmd)"
63
+ exit 0
64
+ fi
65
+ echo "mobile tests: FAIL ($kind on $device, ran: $cmd, exit $rc)"
66
+ exit 1
@@ -152,6 +152,12 @@ landing:static-landing-page:3
152
152
  static:static-landing-page:3
153
153
  marketing:static-landing-page:3
154
154
  fullstack:full-stack-demo:3
155
+ shortener:rest-api:3
156
+ url:rest-api:2
157
+ urls:rest-api:2
158
+ stats:dashboard:3
159
+ statistics:dashboard:3
160
+ metrics:dashboard:3
155
161
  MAP
156
162
  }
157
163
 
@@ -171,6 +177,10 @@ _qs_is_stopword() {
171
177
  # plus the curated keyword weights. No network, no provider, no LLM.
172
178
  _qs_score_templates() {
173
179
  local brief="$1"
180
+ # --best: print only the strongest real score (baseline excluded) so callers
181
+ # can tell a genuine match from simple-todo-app winning by default.
182
+ local best_mode=false
183
+ [ "${2:-}" = "--best" ] && best_mode=true
174
184
  local tdir; tdir="$(_qs_templates_dir)"
175
185
  local brief_lc; brief_lc=$(printf '%s' "$brief" | tr '[:upper:]' '[:lower:]')
176
186
 
@@ -198,7 +208,7 @@ _qs_score_templates() {
198
208
 
199
209
  # No templates resolvable: fall back to the guaranteed default only.
200
210
  if [ -z "$scores" ]; then
201
- printf 'simple-todo-app\n'
211
+ if [ "$best_mode" = true ]; then printf '0\n'; else printf 'simple-todo-app\n'; fi
202
212
  return 0
203
213
  fi
204
214
 
@@ -230,7 +240,7 @@ _qs_score_templates() {
230
240
  done
231
241
 
232
242
  # Guaranteed default baseline: simple-todo-app gets +1 and wins exact ties.
233
- deltas="${deltas}simple-todo-app\t1\n"
243
+ [ "$best_mode" = true ] || deltas="${deltas}simple-todo-app\t1\n"
234
244
 
235
245
  # Fold: keep only names that are REAL templates (a keyword map entry naming
236
246
  # a template that does not exist must not invent one), sum the deltas, then
@@ -250,7 +260,20 @@ _qs_score_templates() {
250
260
  printf "%d\t%d\t%s\n", sum[n], prio, n
251
261
  }
252
262
  }
253
- ' | sort -t"$(printf '\t')" -k1,1nr -k2,2n -k3,3 | head -3 | cut -f3
263
+ ' | sort -t"$(printf '\t')" -k1,1nr -k2,2n -k3,3 | {
264
+ if [ "$best_mode" = true ]; then
265
+ head -1 | cut -f1 | awk '{ print ($1 == "" ? 0 : $1) }'
266
+ else
267
+ head -3 | cut -f3
268
+ fi
269
+ }
270
+ }
271
+
272
+ # _qs_match_strength <brief>: the best real (non-baseline) template score. 0
273
+ # means no template matched the brief at all; >= 2 is a real match (one
274
+ # filename token or one curated keyword).
275
+ _qs_match_strength() {
276
+ _qs_score_templates "$1" --best
254
277
  }
255
278
 
256
279
  # _qs_template_summary <name>: a short one-line description for the picker.
@@ -1363,6 +1386,10 @@ cmd_quickstart() {
1363
1386
  [ -n "$line" ] && top3+=("$line")
1364
1387
  done < <(_qs_score_templates "$brief")
1365
1388
 
1389
+ local match_strength=0
1390
+ match_strength="$(_qs_match_strength "$brief")"
1391
+ case "$match_strength" in ''|*[!0-9]*) match_strength=0;; esac
1392
+
1366
1393
  # Defensive: guarantee a default if scoring produced nothing.
1367
1394
  if [ "${#top3[@]}" -eq 0 ]; then
1368
1395
  top3=("simple-todo-app")
@@ -1388,6 +1415,9 @@ cmd_quickstart() {
1388
1415
  printf ' %d) %-18s %s%s\n' "$i" "$t" "$(_qs_template_summary "$t")" "$suffix"
1389
1416
  i=$((i + 1))
1390
1417
  done
1418
+ if [ "$match_strength" -lt 2 ] && [ -n "$brief" ]; then
1419
+ printf ' No strong match (score %s); these are weak guesses. Type "none" to build from your description.\n' "$match_strength"
1420
+ fi
1391
1421
  printf ' Choose 1-%d, or press Enter for 1.\n' "${#top3[@]}"
1392
1422
  printf '> '
1393
1423
  read -r pick 2>/dev/null || pick=""
@@ -1400,7 +1430,17 @@ cmd_quickstart() {
1400
1430
  # non-interactive choice is exactly the one an operator pressing
1401
1431
  # Enter would get. No picker, no prompt, no second code path.
1402
1432
  printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
1403
- printf ' Selected %s (top match) for "%s".\n\n' "${top3[0]}" "$brief"
1433
+ if [ "$match_strength" -ge 2 ]; then
1434
+ printf ' Selected %s (top match, score %s) for "%s".\n\n' "${top3[0]}" "$match_strength" "$brief"
1435
+ elif [ -n "$brief" ]; then
1436
+ # No template matched: building a todo app for an unrelated
1437
+ # brief is wrong (issue #217). Use the brief as the spec.
1438
+ pick="none"
1439
+ printf ' No strong template match for "%s" (score %s).\n' "$brief" "$match_strength"
1440
+ printf ' Falling back to a plain-spec build from your description.\n\n'
1441
+ else
1442
+ printf ' Selected %s (default) for the sample Todo app.\n\n' "${top3[0]}"
1443
+ fi
1404
1444
  fi
1405
1445
 
1406
1446
  # An explicit rejection is honored, never converted into a selection.
package/autonomy/run.sh CHANGED
@@ -11804,6 +11804,26 @@ result = {
11804
11804
  "successes": filter_relevant(successes, context)
11805
11805
  }
11806
11806
 
11807
+ # Project memory: learnings and decisions persisted by earlier runs in THIS
11808
+ # project, newest first, bounded by entry count and total characters.
11809
+ if os.environ.get("LOKI_PROJECT_MEMORY", "1") != "0":
11810
+ try:
11811
+ max_chars = int(os.environ.get("LOKI_PROJECT_MEMORY_MAX_CHARS", "2000"))
11812
+ except ValueError:
11813
+ max_chars = 2000
11814
+ mem, used = [], 0
11815
+ for kind in ("mistakes", "patterns", "successes"):
11816
+ for e in reversed(load_jsonl(f".loki/memory/learnings/project-{kind}.jsonl")):
11817
+ d = e.get("description", "").strip()[:300]
11818
+ if not d or used + len(d) > max_chars:
11819
+ continue
11820
+ used += len(d)
11821
+ mem.append({"kind": kind[:-1] if kind != "successes" else "success", "description": d})
11822
+ if len(mem) >= 15:
11823
+ break
11824
+ if mem:
11825
+ result["project_memory"] = mem
11826
+
11807
11827
  with open(".loki/state/relevant-learnings.json", 'w') as f:
11808
11828
  json.dump(result, f, indent=2)
11809
11829
  LEARNINGS_SCRIPT
@@ -11863,7 +11883,23 @@ def get_existing_hashes(filepath):
11863
11883
  return hashes
11864
11884
 
11865
11885
  def save_entries(filepath, entries, category):
11866
- """Save entries avoiding duplicates (case-insensitive)"""
11886
+ """Save entries avoiding duplicates (case-insensitive).
11887
+
11888
+ Also mirrors into the project-scoped memory (.loki/memory/learnings/
11889
+ project-<kind>.jsonl) so the next run in this project reads them back.
11890
+ """
11891
+ if os.environ.get("LOKI_PROJECT_MEMORY", "1") != "0":
11892
+ try:
11893
+ pdir = ".loki/memory/learnings"
11894
+ os.makedirs(pdir, exist_ok=True)
11895
+ pfile = os.path.join(pdir, "project-" + os.path.basename(filepath))
11896
+ if pfile != filepath:
11897
+ save_entries_to(pfile, entries, category)
11898
+ except Exception:
11899
+ pass
11900
+ return save_entries_to(filepath, entries, category)
11901
+
11902
+ def save_entries_to(filepath, entries, category):
11867
11903
  existing = get_existing_hashes(filepath)
11868
11904
  saved = 0
11869
11905
  with open(filepath, 'a') as f:
@@ -19522,8 +19558,14 @@ start_dashboard() {
19522
19558
  # Auto-open the dashboard in the browser, but ONLY for an interactive
19523
19559
  # foreground session. loki_open_url (lib/browser-open.sh) refuses on
19524
19560
  # no TTY, CI, test runners, LOKI_NO_BROWSER=1 or LOKI_NO_AUTO_OPEN=1.
19561
+ # CP-LEGACY (D57): open the Control Plane, not this legacy server,
19562
+ # unless LOKI_LEGACY_DASHBOARD=1 opts back in.
19525
19563
  if [ "${BACKGROUND_MODE:-false}" != "true" ]; then
19526
- loki_open_url "${url_scheme}://127.0.0.1:$DASHBOARD_PORT/" 2>/dev/null || true
19564
+ if [ "${LOKI_LEGACY_DASHBOARD:-0}" = "1" ]; then
19565
+ loki_open_url "${url_scheme}://127.0.0.1:$DASHBOARD_PORT/" 2>/dev/null || true
19566
+ elif [ -t 1 ] && [ "${LOKI_NO_AUTO_OPEN:-0}" != "1" ] && [ -x "${skill_dir}/autonomy/loki" ]; then
19567
+ LOKI_UI_NO_CLASSIC=1 "${skill_dir}/autonomy/loki" ui >/dev/null 2>&1 || true
19568
+ fi
19527
19569
  fi
19528
19570
  return 0
19529
19571
  else
package/completions/_loki CHANGED
@@ -188,6 +188,8 @@ function _loki_commands {
188
188
  'deploy:Advisory deploy command (print-only)'
189
189
  'docker:Run Loki in a Docker container'
190
190
  'mcp:MCP server commands'
191
+ 'merge:Green-only serial merge queue'
192
+ 'acp:Agent Client Protocol agent over stdio'
191
193
  'magic:Spec-driven component generation'
192
194
  'assets:Export/import shareable team assets'
193
195
  'spec:Living spec drift detection'