loki-mode 9.48.1 → 9.49.2

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 (83) hide show
  1. package/.github/workflows/loki-issue-to-pr.yml +1 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/loki +166 -13
  5. package/autonomy/provider-offer.sh +47 -0
  6. package/autonomy/quickstart.sh +306 -9
  7. package/dashboard/__init__.py +1 -1
  8. package/dashboard/static/index.html +7 -1
  9. package/docs/INSTALLATION.md +1 -1
  10. package/docs/KNOWN-ISSUES.md +42 -0
  11. package/loki-ts/dist/loki.js +317 -317
  12. package/mcp/__init__.py +1 -1
  13. package/package.json +1 -1
  14. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  15. package/web-app/dist/assets/{AdminPage-PyyVXg4b.js → AdminPage-DpV2vW3y.js} +1 -1
  16. package/web-app/dist/assets/{Avatar-CTUlr_Xf.js → Avatar-BXeMYK1_.js} +1 -1
  17. package/web-app/dist/assets/{Badge-BQeXvmg8.js → Badge-D8WddQk1.js} +1 -1
  18. package/web-app/dist/assets/{Button-Dc6-P6pz.js → Button-CFzTTVeJ.js} +1 -1
  19. package/web-app/dist/assets/{CockpitPage-BspOJ3Fv.js → CockpitPage-D1eQe9QF.js} +4 -4
  20. package/web-app/dist/assets/{ComparePage-CFXIM--Z.js → ComparePage-uX3OPphr.js} +1 -1
  21. package/web-app/dist/assets/{ErrorBoundary-C7vQYDW9.js → ErrorBoundary-wE3oxaG6.js} +1 -1
  22. package/web-app/dist/assets/{EvidenceReceiptPanel-CqjsjTOs.js → EvidenceReceiptPanel-B_Wouz6N.js} +1 -1
  23. package/web-app/dist/assets/{GitHubIssuesPanel-DyuNDMgD.js → GitHubIssuesPanel-DRlEeFuw.js} +1 -1
  24. package/web-app/dist/assets/{GitHubPRsPanel-BtAq20xF.js → GitHubPRsPanel-CCv6_I74.js} +2 -2
  25. package/web-app/dist/assets/{HomePage-BuVFOrO8.js → HomePage-EG-jq8nR.js} +1 -1
  26. package/web-app/dist/assets/{LoginPage-CuhhrxGb.js → LoginPage-CEOnBICH.js} +1 -1
  27. package/web-app/dist/assets/{MagicPage-D2KZPX3F.js → MagicPage-DeWaFdsG.js} +1 -1
  28. package/web-app/dist/assets/MetricsPage-CG8CuSSJ.js +6 -0
  29. package/web-app/dist/assets/{NotFoundPage-CTnnaWlL.js → NotFoundPage-Mz5rOat6.js} +1 -1
  30. package/web-app/dist/assets/{ProjectPage-uJFaoM8-.js → ProjectPage-D2Fh6tVw.js} +8 -8
  31. package/web-app/dist/assets/{ProjectsPage-8lKAOBIW.js → ProjectsPage-B8cRWkWa.js} +2 -2
  32. package/web-app/dist/assets/{SettingsPage-D1BBd5qy.js → SettingsPage-DKpsst6x.js} +1 -1
  33. package/web-app/dist/assets/{ShowcasePage-CmQ0n8iO.js → ShowcasePage-qab065PM.js} +1 -1
  34. package/web-app/dist/assets/SystemSettingsPage-9qmEH2Y6.js +1 -0
  35. package/web-app/dist/assets/{TeamsPage-BS0TtVUa.js → TeamsPage-CW6lvW-0.js} +1 -1
  36. package/web-app/dist/assets/{TemplatesPage-C2zPnnf2.js → TemplatesPage-B-a61T6a.js} +1 -1
  37. package/web-app/dist/assets/{TerminalOutput-Dle1klqq.js → TerminalOutput-CJy13rAn.js} +1 -1
  38. package/web-app/dist/assets/{activity-DQp5U9f9.js → activity-CA4moIvC.js} +1 -1
  39. package/web-app/dist/assets/{bell-BqkTmgL7.js → bell-JyOeobLN.js} +1 -1
  40. package/web-app/dist/assets/{bot-DILhaIGH.js → bot-Cu4UwP5w.js} +1 -1
  41. package/web-app/dist/assets/{check-BC9x7oPm.js → check-Drzd8-9c.js} +1 -1
  42. package/web-app/dist/assets/{chevron-left--yqvAfjx.js → chevron-left-ClvdXm3Y.js} +1 -1
  43. package/web-app/dist/assets/{circle-alert-DOH6PcgZ.js → circle-alert-CstyZfKG.js} +1 -1
  44. package/web-app/dist/assets/{clock-DKSDFsE6.js → clock-Bnz6tlQN.js} +1 -1
  45. package/web-app/dist/assets/{cloud-DRBU3JYs.js → cloud-IPIgE4P3.js} +1 -1
  46. package/web-app/dist/assets/{code-xml-D2LqszR9.js → code-xml-BOZbQiOt.js} +1 -1
  47. package/web-app/dist/assets/{database-D7ceedlh.js → database-CHclPjog.js} +1 -1
  48. package/web-app/dist/assets/{dollar-sign-D0A47egY.js → dollar-sign-D6RFNgdZ.js} +1 -1
  49. package/web-app/dist/assets/{file-code-corner-CH_Qbtjo.js → file-code-corner-CNayCvOY.js} +1 -1
  50. package/web-app/dist/assets/{file-plus-CUNMjHk9.js → file-plus-BmVfFwsX.js} +1 -1
  51. package/web-app/dist/assets/folder-open-ChzGfLRr.js +11 -0
  52. package/web-app/dist/assets/{globe-gZnaGg8N.js → globe-DtXgCG3Z.js} +1 -1
  53. package/web-app/dist/assets/{hammer-BhJC5DMK.js → hammer-DCa0I_7V.js} +1 -1
  54. package/web-app/dist/assets/{index-BMmYYws0.js → index-Cu-njJ8o.js} +3 -3
  55. package/web-app/dist/assets/index-D98GzaW8.css +1 -0
  56. package/web-app/dist/assets/{layers-glXeoUbJ.js → layers-DxIhn7NZ.js} +1 -1
  57. package/web-app/dist/assets/{lightbulb-gXn8H0Lf.js → lightbulb-D7Pzhd06.js} +1 -1
  58. package/web-app/dist/assets/{loader-circle-Do2ke8xm.js → loader-circle-DXgc_u5I.js} +1 -1
  59. package/web-app/dist/assets/{lock-Bgplb1_n.js → lock-qRAVU53b.js} +1 -1
  60. package/web-app/dist/assets/{mail-j4Lo8hfB.js → mail-CHe3KyCw.js} +1 -1
  61. package/web-app/dist/assets/{minus-dhrzKSK5.js → minus-B8p0FoYZ.js} +1 -1
  62. package/web-app/dist/assets/{package-CInWQQ97.js → package-C5LDn8SI.js} +1 -1
  63. package/web-app/dist/assets/{plus-BpYvoLlZ.js → plus-CRd_FHOA.js} +1 -1
  64. package/web-app/dist/assets/{refresh-cw-F6koqEgF.js → refresh-cw-DKoSN8s3.js} +1 -1
  65. package/web-app/dist/assets/{rotate-ccw-CQ2W5S8f.js → rotate-ccw-Nf8zB8G0.js} +1 -1
  66. package/web-app/dist/assets/{save-DMCYGm06.js → save-B8ytQLgW.js} +1 -1
  67. package/web-app/dist/assets/{server-Bj8EX4Z1.js → server-D_VRDkaD.js} +1 -1
  68. package/web-app/dist/assets/{shield-alert-CiYuulCU.js → shield-alert-PsDkSyFD.js} +1 -1
  69. package/web-app/dist/assets/{thumbs-up-BqEh1puP.js → thumbs-up-Da1QQhqo.js} +11 -6
  70. package/web-app/dist/assets/{trash-2-CCwouZJJ.js → trash-2-ChoylpGQ.js} +1 -1
  71. package/web-app/dist/assets/{trending-down-Cx1vhE4o.js → trending-down-CX2fUeho.js} +1 -1
  72. package/web-app/dist/assets/{trending-up-BUhVYJ2T.js → trending-up-CGyYgZH7.js} +1 -1
  73. package/web-app/dist/assets/{upload-D5IsZeFM.js → upload-KkeVyg67.js} +1 -1
  74. package/web-app/dist/assets/{usePolling-CJbfhFcH.js → usePolling-C5f4xrXD.js} +1 -1
  75. package/web-app/dist/assets/{user-BRLhezsD.js → user-WCGccIST.js} +1 -1
  76. package/web-app/dist/index.html +2 -2
  77. package/web-app/server.py +121 -1
  78. package/web-app/dist/assets/MetricsPage-DFEYJl9z.js +0 -6
  79. package/web-app/dist/assets/SystemSettingsPage-CSHaVyuD.js +0 -6
  80. package/web-app/dist/assets/copy-BKW4U4Qr.js +0 -6
  81. package/web-app/dist/assets/folder-open-CQ_SVq94.js +0 -6
  82. package/web-app/dist/assets/git-commit-horizontal-DQUBe0oD.js +0 -6
  83. package/web-app/dist/assets/index-84JUN_aA.css +0 -1
@@ -61,7 +61,7 @@ jobs:
61
61
  # Pinned to a TAG, not @main. A moving ref would hand every user
62
62
  # whatever main holds at the moment their issue is labeled, including a
63
63
  # half-landed release. Bump this line deliberately.
64
- uses: asklokesh/loki-mode/.github/actions/issue-to-pr@v9.48.1
64
+ uses: asklokesh/loki-mode/.github/actions/issue-to-pr@v9.49.2
65
65
  env:
66
66
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
67
67
  with:
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v9.48.1
6
+ # Loki Mode v9.49.2
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.48.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.49.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.48.1
1
+ 9.49.2
package/autonomy/loki CHANGED
@@ -13538,10 +13538,34 @@ except Exception:
13538
13538
  "$HOME/.cline/skills/loki-mode:Cline CLI"
13539
13539
  "$HOME/.aider/skills/loki-mode:Aider CLI"
13540
13540
  )
13541
+ # SEVERITY DEPENDS ON WHOSE SKILL IT IS (v9.48.x). A dangling skill symlink
13542
+ # used to block unconditionally, so a stale link left by an old checkout
13543
+ # path exited 1 on a HEALTHY machine -- while the Provider Availability
13544
+ # section three lines above said that same provider was "not installed".
13545
+ # Doctor contradicted itself on one screen and told the user to fix a tool
13546
+ # they do not have.
13547
+ #
13548
+ # The link only blocks when it belongs to the provider a build would
13549
+ # actually launch. Anything else is inert state: still reported, with the
13550
+ # dangling target and the fix, but not a reason to refuse to start.
13551
+ #
13552
+ # `|| true` is mandatory under `set -euo pipefail`: the accessor returns
13553
+ # non-zero when no provider resolves (a bare call would abort doctor), and
13554
+ # an empty id then makes every entry non-blocking -- the same direction the
13555
+ # No-AI-provider blocker above already covers.
13556
+ local _selected_provider=""
13557
+ if declare -f effective_provider_id >/dev/null 2>&1; then
13558
+ _selected_provider="$(effective_provider_id 2>/dev/null || true)"
13559
+ fi
13541
13560
  for entry in "${skill_dirs[@]}"; do
13542
13561
  local sdir="${entry%%:*}"
13543
13562
  local sname="${entry#*:}"
13544
13563
  local short_path="${sdir/$HOME/~}"
13564
+ # Provider id from the directory, never a parallel list: `.claude` ->
13565
+ # `claude`. A fifth hand-maintained list is how the lists in this repo
13566
+ # have drifted from auto_detect_provider before (v8.76.0, v8.82.0).
13567
+ local _sid="${sdir#"$HOME"/.}"
13568
+ _sid="${_sid%%/*}"
13545
13569
  if [ -f "$sdir/SKILL.md" ]; then
13546
13570
  echo -e " ${GREEN}PASS${NC} $sname ${DIM}$short_path${NC}"
13547
13571
  pass_count=$((pass_count + 1))
@@ -13560,10 +13584,22 @@ except Exception:
13560
13584
  _target=$(ls -ld "$sdir" 2>/dev/null | sed -e 's/^[^>]*-> //')
13561
13585
  [ -n "$_target" ] || _target="unknown"
13562
13586
  fi
13563
- echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
13564
- echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
13565
- _doctor_block "$sname is a broken symlink. Fix: loki setup-skill"
13566
- fail_count=$((fail_count + 1))
13587
+ # Blocking ONLY for the provider a build would actually use. The
13588
+ # non-blocking arm is a WARN rather than a non-counted FAIL on
13589
+ # purpose: a red FAIL badge that never reaches `N failed` or the
13590
+ # `Blocking (N)` list makes the summary disagree with the screen.
13591
+ # Either way the dangling target and the fix are still printed --
13592
+ # the stale link is reported, never silently swallowed.
13593
+ if [ -n "$_selected_provider" ] && [ "$_sid" = "$_selected_provider" ]; then
13594
+ echo -e " ${RED}FAIL${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
13595
+ echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
13596
+ _doctor_block "$sname is a broken symlink. Fix: loki setup-skill"
13597
+ fail_count=$((fail_count + 1))
13598
+ else
13599
+ echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(broken symlink -> $_target)${NC}"
13600
+ echo -e " ${YELLOW}Fix: loki setup-skill${NC}"
13601
+ warn_count=$((warn_count + 1))
13602
+ fi
13567
13603
  else
13568
13604
  echo -e " ${YELLOW}WARN${NC} $sname ${DIM}(not found - run 'loki setup-skill')${NC}"
13569
13605
  warn_count=$((warn_count + 1))
@@ -14183,14 +14219,26 @@ memory = {
14183
14219
  #
14184
14220
  # CAUTION: this block lives inside a DOUBLE-QUOTED python3 -c program, so no
14185
14221
  # apostrophes and no double quotes anywhere here, comments included.
14222
+ #
14223
+ # SEVERITY MIRRORS THE TEXT PATH: a dangling link is a failure only for the
14224
+ # provider a build would actually use. Read from the SAME payload the text
14225
+ # route uses (LOKI_PROVIDER_AVAILABILITY, set by cmd_doctor_json), parsed here
14226
+ # rather than via a second env var so the two cannot disagree. LOKI_PROVIDER
14227
+ # wins for the same reason effective_provider_id honors it.
14186
14228
  _SKILL_ENTRIES = (
14187
- ('Claude Code', '.claude/skills/loki-mode'),
14188
- ('Codex CLI', '.codex/skills/loki-mode'),
14189
- ('Cline CLI', '.cline/skills/loki-mode'),
14190
- ('Aider CLI', '.aider/skills/loki-mode'),
14229
+ ('Claude Code', '.claude/skills/loki-mode', 'claude'),
14230
+ ('Codex CLI', '.codex/skills/loki-mode', 'codex'),
14231
+ ('Cline CLI', '.cline/skills/loki-mode', 'cline'),
14232
+ ('Aider CLI', '.aider/skills/loki-mode', 'aider'),
14191
14233
  )
14234
+ try:
14235
+ _sk_pa = os.environ.get('LOKI_PROVIDER_AVAILABILITY', '').strip()
14236
+ _sk_selected = (json.loads(_sk_pa).get('selected') or '') if _sk_pa else ''
14237
+ except Exception:
14238
+ _sk_selected = ''
14239
+ _sk_selected = os.environ.get('LOKI_PROVIDER', '') or _sk_selected
14192
14240
  skills = []
14193
- for _sk_name, _sk_rel in _SKILL_ENTRIES:
14241
+ for _sk_name, _sk_rel, _sk_id in _SKILL_ENTRIES:
14194
14242
  _sk_dir = os.path.join(os.path.expanduser('~'), _sk_rel)
14195
14243
  # JSON is commonly archived or forwarded to automation. Keep the useful
14196
14244
  # user-relative location without exposing the machine-specific home path.
@@ -14200,8 +14248,14 @@ for _sk_name, _sk_rel in _SKILL_ENTRIES:
14200
14248
  'detail': None, 'required': 'required'})
14201
14249
  elif os.path.islink(_sk_dir):
14202
14250
  # islink is true for a DANGLING link (lstat-based), which is exactly
14203
- # the broken case the text path reports as FAIL.
14204
- skills.append({'name': _sk_name, 'path': _sk_json_path, 'status': 'fail',
14251
+ # the broken case the text path reports. fail only for the effective
14252
+ # provider; otherwise warn, matching the text path badge for badge.
14253
+ # `required` stays 'required' in both arms: an absent link has always
14254
+ # been warn+required, so the warn arm introduces no new shape, and a
14255
+ # consumer keys on `status`.
14256
+ _sk_blocking = bool(_sk_selected) and _sk_id == _sk_selected
14257
+ skills.append({'name': _sk_name, 'path': _sk_json_path,
14258
+ 'status': 'fail' if _sk_blocking else 'warn',
14205
14259
  'detail': 'broken symlink. Fix: loki setup-skill',
14206
14260
  'required': 'required'})
14207
14261
  else:
@@ -17358,12 +17412,111 @@ if manifests:
17358
17412
  echo -e "${RED}Error: No repositories found matching: ${multi_repo}${NC}"
17359
17413
  return 1
17360
17414
  fi
17361
- echo -e "${BOLD}Multi-repo migration${NC} ($repo_count repositories)"
17415
+ # ORCHESTRATION (v9.49.0): --multi-repo now MIGRATES each discovered
17416
+ # repository, one at a time, instead of only listing them.
17417
+ #
17418
+ # WHY A WRAPPER AND NOT SURGERY INSIDE THIS FUNCTION: cmd_migrate_start
17419
+ # is self-contained per codebase_path (manifest, secret scan, phases)
17420
+ # and was measured to have 20 `return`s and ZERO `exit`s, so it is
17421
+ # safe to call in a loop -- a failing repo returns a code instead of
17422
+ # killing the run. Recursing once per repo reuses every existing gate
17423
+ # rather than reimplementing the pipeline.
17424
+ #
17425
+ # PARTIAL-FAILURE SEMANTICS: continue-and-report, decided on the
17426
+ # codebase's own precedent, not preference:
17427
+ # - the task schema already has `blocked` as a first-class state
17428
+ # alongside `completed` (mcp/server.py:888), so "some units did not
17429
+ # finish" is already expressible
17430
+ # - run.sh carries 562 best-effort sites: the house style is degrade
17431
+ # honestly, do not abort
17432
+ # - fail-closed is reserved for DEPENDENCY violations (a migrate phase
17433
+ # refusing to run without its predecessor's artifacts, :17575), and
17434
+ # independent repositories have no such dependency on each other
17435
+ # One repo failing therefore must not deny the other four their result.
17436
+ # The per-repo status table plus a non-zero aggregate exit code is what
17437
+ # makes that honest rather than silent.
17438
+ #
17439
+ # LOKI_MULTI_REPO_SEQUENTIAL=0 is NOT offered: these share a migrations
17440
+ # dir and a dashboard port, and parallel runs would interleave output
17441
+ # into an unreadable report.
17442
+ if [ "$repo_count" -gt 1 ] && [ "${_LOKI_MR_CHILD:-0}" != "1" ]; then
17443
+ echo -e "${BOLD}Multi-repo migration${NC} ($repo_count repositories, sequential)"
17444
+ echo "---"
17445
+ for repo in "${repos[@]}"; do echo " - $repo"; done
17446
+ echo ""
17447
+
17448
+ local _mr_ok=0 _mr_bad=0
17449
+ local _mr_rows=""
17450
+ local _mr_repo _mr_rc
17451
+ for _mr_repo in "${repos[@]}"; do
17452
+ echo ""
17453
+ echo -e "${CYAN}=== [$((_mr_ok + _mr_bad + 1))/$repo_count] $_mr_repo ===${NC}"
17454
+ _mr_rc=0
17455
+ # _LOKI_MR_CHILD guards the recursion: the child sees the same
17456
+ # --multi-repo argument and must NOT re-enter this block.
17457
+ _LOKI_MR_CHILD=1 cmd_migrate_start \
17458
+ "$_mr_repo" "$target" "$plan_only" "$phase" "$parallel" \
17459
+ "$compliance" "$dry_run" "$resume" "" "$export_report" \
17460
+ "$no_dashboard" "$no_docs" || _mr_rc=$?
17461
+ if [ "$_mr_rc" -eq 0 ]; then
17462
+ _mr_ok=$((_mr_ok + 1))
17463
+ _mr_rows="${_mr_rows} OK ${_mr_repo}"$'\n'
17464
+ else
17465
+ _mr_bad=$((_mr_bad + 1))
17466
+ _mr_rows="${_mr_rows} BLOCKED ${_mr_repo} (exit ${_mr_rc})"$'\n'
17467
+ fi
17468
+ done
17469
+
17470
+ echo ""
17471
+ echo -e "${BOLD}Multi-repo result${NC}"
17472
+ echo "---"
17473
+ printf '%s' "$_mr_rows"
17474
+ echo ""
17475
+ echo " ${_mr_ok} of ${repo_count} migrated, ${_mr_bad} blocked."
17476
+ if [ "$_mr_bad" -gt 0 ]; then
17477
+ # A non-zero exit is what keeps "continue" from becoming
17478
+ # "silently tolerate": CI and `loki next` both branch on it.
17479
+ echo -e "${YELLOW}Some repositories did not complete. Re-run just those, or inspect them with 'loki why'.${NC}"
17480
+ return 1
17481
+ fi
17482
+ return 0
17483
+ fi
17484
+
17485
+ # HONESTY (single-repo, or a --multi-repo glob that matched exactly one):
17486
+ # this flag DISCOVERS repositories; it does not migrate them.
17487
+ # `repos[]` is populated here and never referenced again -- everything
17488
+ # below operates on the single "$codebase_path". The old message said
17489
+ # "Multi-repo migration (3 repositories)" and listed all three, then
17490
+ # created one migration scoped to the first. A user reading that
17491
+ # believed three repos were migrated when one was.
17492
+ #
17493
+ # Orchestrating a real migration across repositories is a separate piece
17494
+ # of work (dependency order, per-repo gates, per-repo receipts). Until
17495
+ # that exists, this says exactly what it does and hands the user the
17496
+ # commands that cover the rest, rather than implying coverage it has not
17497
+ # delivered.
17498
+ local _mr_others=$((repo_count - 1))
17499
+ echo -e "${BOLD}Repositories found${NC} ($repo_count)"
17362
17500
  echo "---"
17363
17501
  for repo in "${repos[@]}"; do
17364
- echo " - $repo"
17502
+ if [ "$repo" = "$codebase_path" ]; then
17503
+ echo " - $repo (migrating now)"
17504
+ else
17505
+ echo " - $repo"
17506
+ fi
17365
17507
  done
17366
17508
  echo ""
17509
+ if [ "$_mr_others" -gt 0 ]; then
17510
+ echo -e "${YELLOW}--multi-repo discovers repositories; it does not migrate them together.${NC}"
17511
+ echo "This run migrates only: $codebase_path"
17512
+ echo ""
17513
+ echo "To cover the others, run each one:"
17514
+ for repo in "${repos[@]}"; do
17515
+ [ "$repo" = "$codebase_path" ] && continue
17516
+ echo " loki modernize migrate \"$repo\" --target \"$target\""
17517
+ done
17518
+ echo ""
17519
+ fi
17367
17520
  fi
17368
17521
 
17369
17522
  local migration_dir
@@ -464,6 +464,49 @@ render_provider_availability() {
464
464
  ) || return 1
465
465
  }
466
466
 
467
+ # effective_provider_id: print the provider id a build would ACTUALLY use, or
468
+ # nothing when none is resolvable. Exit status is 0 only when an id was printed.
469
+ #
470
+ # WHY IT EXISTS. Doctor grades a dangling skill symlink, and the severity of
471
+ # that depends on WHOSE skill it is. A stale link for a provider the user does
472
+ # not use is inert state; the same link for the provider a build will launch is
473
+ # a real blocker. Both the text route and the Bun route need that one id, and
474
+ # both previously re-derived it inside their own subshells (the two callers
475
+ # below). Deriving it a third and fourth time is exactly the drift this file's
476
+ # header exists to prevent, so it is extracted here once.
477
+ #
478
+ # LOKI_PROVIDER WINS. render_provider_availability prints "override with
479
+ # LOKI_PROVIDER" two lines above, so honoring auto-detection alone would grade
480
+ # a skill link against a provider the very same screen says is not in use.
481
+ # The override is NOT validated against installed state on purpose: an explicit
482
+ # LOKI_PROVIDER is the user's stated intent, and a build would carry it through.
483
+ #
484
+ # DEGRADES SILENTLY, like its two siblings: a missing or unsourceable loader
485
+ # prints nothing and returns non-zero, and the caller falls back to treating
486
+ # every entry as non-blocking rather than erroring.
487
+ effective_provider_id() {
488
+ if [ -n "${LOKI_PROVIDER:-}" ]; then
489
+ printf '%s\n' "$LOKI_PROVIDER"
490
+ return 0
491
+ fi
492
+
493
+ local _po_here
494
+ _po_here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)" || return 1
495
+ local _loader="$_po_here/providers/loader.sh"
496
+ [ -f "$_loader" ] || return 1
497
+
498
+ local _sel
499
+ _sel="$(
500
+ # shellcheck source=/dev/null
501
+ source "$_loader" >/dev/null 2>&1 || exit 1
502
+ declare -f auto_detect_provider >/dev/null 2>&1 || exit 1
503
+ auto_detect_provider 2>/dev/null
504
+ )" || return 1
505
+
506
+ [ -n "$_sel" ] || return 1
507
+ printf '%s\n' "$_sel"
508
+ }
509
+
467
510
  # provider_availability_json: the same data as one JSON object, for doctor --json.
468
511
  # Separate from the renderer so neither format has to parse the other. Same
469
512
  # degrade rule: no loader means no output and a non-zero status.
@@ -517,6 +560,10 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
517
560
  # routes call these so the two renderings cannot drift apart.
518
561
  providers) render_provider_availability ;;
519
562
  providers-json) provider_availability_json ;;
563
+ # The provider id a build would actually use, for the Bun doctor's
564
+ # skill-severity decision. Same one-implementation rule as the two
565
+ # above: doctor.ts reads this instead of re-deriving the priority list.
566
+ effective-provider) effective_provider_id ;;
520
567
  *) offer_provider_install report ;;
521
568
  esac
522
569
  fi