loki-mode 9.48.2 → 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 +137 -12
  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.2
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.2
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.2 | [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.2
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,7 +17412,78 @@ if manifests:
17358
17412
  echo -e "${RED}Error: No repositories found matching: ${multi_repo}${NC}"
17359
17413
  return 1
17360
17414
  fi
17361
- # HONESTY: this flag DISCOVERS repositories; it does not migrate them.
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.
17362
17487
  # `repos[]` is populated here and never referenced again -- everything
17363
17488
  # below operates on the single "$codebase_path". The old message said
17364
17489
  # "Multi-repo migration (3 repositories)" and listed all three, then
@@ -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
@@ -703,6 +703,175 @@ PY
703
703
  return 0
704
704
  }
705
705
 
706
+ # _qs_is_rejection <answer>: true when the user explicitly declined the offered
707
+ # templates. Before this existed the picker's `*)` arm mapped ANY unrecognized
708
+ # answer -- "none", "no", "skip", "q" -- onto top3[0], so an explicit rejection
709
+ # was silently converted into a selection and quickstart built a todo app for a
710
+ # user who had just said none of them fit. An explicit no must never become a
711
+ # yes; these answers route to the brief-as-spec path instead.
712
+ _qs_is_rejection() {
713
+ local a
714
+ a=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')
715
+ case "$a" in
716
+ none|no|n|skip|q|quit|neither|nope|"none of these"|"none of them") return 0;;
717
+ esac
718
+ return 1
719
+ }
720
+
721
+ # _qs_detect_brownfield: print a short human reason when the CWD is an existing
722
+ # project, and return 0. Return 1 (printing nothing) for a greenfield directory.
723
+ #
724
+ # Signals are EXPLICIT MANIFESTS plus a git repo with at least one commit. A
725
+ # loose "any source file present" check was deliberately rejected: the test
726
+ # harnesses write helper scripts and stray .md files into their working
727
+ # directories, so a broad check would classify an empty scratch dir as an
728
+ # existing project and silently reroute the greenfield flow. A manifest or a
729
+ # commit is a deliberate artifact of a real project, which is the claim being
730
+ # made on screen.
731
+ #
732
+ # bash 3.2 safe: no associative arrays, no mapfile.
733
+ _qs_detect_brownfield() {
734
+ local reasons="" m
735
+ for m in package.json pyproject.toml go.mod Cargo.toml pom.xml Gemfile composer.json build.gradle setup.py; do
736
+ if [ -f "./$m" ]; then
737
+ if [ -z "$reasons" ]; then reasons="$m"; else reasons="$reasons, $m"; fi
738
+ fi
739
+ done
740
+ # A git repo counts only WITH a commit: `git init` alone in an empty folder
741
+ # is where a brand-new greenfield build legitimately starts, and treating
742
+ # that as "existing code" would break the first-run flow.
743
+ if git rev-parse --git-dir >/dev/null 2>&1 && git rev-parse HEAD >/dev/null 2>&1; then
744
+ if [ -z "$reasons" ]; then reasons="git repo"; else reasons="$reasons, git repo"; fi
745
+ fi
746
+ [ -n "$reasons" ] || return 1
747
+ printf '%s' "$reasons"
748
+ return 0
749
+ }
750
+
751
+ # _qs_classify_invoke <prompt>: the single model-call primitive for intent
752
+ # classification. Its own function so tests can override it, mirroring
753
+ # _loki_prd_enrich_invoke (autonomy/lib/prd-enrich.sh:43).
754
+ #
755
+ # Calls `claude -p` directly rather than the provider_invoke shell function
756
+ # because `timeout` execs a real command, not a function -- the same documented
757
+ # reason prd-enrich gives. No HTTP: this repo is provider-agnostic over CLIs and
758
+ # has zero chat/completions calls in source.
759
+ #
760
+ # The timeout is deliberately short. Quickstart is the first thing a new user
761
+ # runs, so a stalled provider must degrade to the offline scorer in seconds
762
+ # rather than inherit prd-enrich's 120s budget.
763
+ : "${LOKI_QUICKSTART_CLASSIFY_TIMEOUT:=15}"
764
+ _qs_classify_invoke() {
765
+ local prompt="$1"
766
+ command -v claude >/dev/null 2>&1 || return 1
767
+ local rc=0 out=""
768
+ local to=""
769
+ if command -v timeout >/dev/null 2>&1; then to="timeout"
770
+ elif command -v gtimeout >/dev/null 2>&1; then to="gtimeout"; fi
771
+ if [ -n "$to" ]; then
772
+ out=$(CAVEMAN_DEFAULT_MODE=off "$to" "${LOKI_QUICKSTART_CLASSIFY_TIMEOUT}" \
773
+ claude --dangerously-skip-permissions -p "$prompt" 2>/dev/null) || rc=$?
774
+ else
775
+ out=$(CAVEMAN_DEFAULT_MODE=off \
776
+ claude --dangerously-skip-permissions -p "$prompt" 2>/dev/null) || rc=$?
777
+ fi
778
+ [ "$rc" -ne 0 ] && return 1
779
+ [ -z "$out" ] && return 1
780
+ printf '%s' "$out"
781
+ return 0
782
+ }
783
+
784
+ # _qs_classify_intent <brief>: print new_project | change_to_existing_code when
785
+ # the model answers cleanly, and return 0. Return 1 on ANY miss.
786
+ #
787
+ # FAIL-CLOSED IS LOAD-BEARING. No provider, no network, a slow call, a non-zero
788
+ # exit, an empty body, or an unrecognized word all return 1, and every caller
789
+ # falls back to the offline keyword ranking that shipped before this existed.
790
+ # Quickstart must never hang or hard-fail on the model path.
791
+ _qs_classify_intent() {
792
+ local brief="$1"
793
+ [ -n "$brief" ] || return 1
794
+ local out=""
795
+ out=$(_qs_classify_invoke "Classify this software request as exactly one word.
796
+
797
+ Answer new_project if the user wants a brand-new application built from scratch.
798
+ Answer change_to_existing_code if the user wants to modify, update, fix, restyle,
799
+ refactor, or add to code that already exists.
800
+ Answer unclear if you genuinely cannot tell.
801
+
802
+ Reply with ONE word and nothing else: new_project, change_to_existing_code, or unclear.
803
+
804
+ Request: $brief") || return 1
805
+ # Take the first bare word; a chatty model must not defeat the match.
806
+ out=$(printf '%s' "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z_' ' ')
807
+ case " $out " in
808
+ *" change_to_existing_code "*) printf 'change_to_existing_code'; return 0;;
809
+ *" new_project "*) printf 'new_project'; return 0;;
810
+ esac
811
+ return 1
812
+ }
813
+
814
+ # _qs_write_change_prd <path> <brief> <mode>: render a spec from the user's own
815
+ # brief. <mode> is "change" (a change against an existing codebase) or "new" (a
816
+ # brand-new project).
817
+ #
818
+ # The mode argument is load-bearing and must never be inferred from
819
+ # $brownfield_reason. That variable stays populated after the classifier flips a
820
+ # brownfield verdict back to a new build, so a user in an existing repo who asks
821
+ # for "a brand new X" and then rejects the template would be handed
822
+ # existing-codebase framing for a project that does not exist yet. Both callers
823
+ # set the mode explicitly at the point they decide what the spec IS.
824
+ #
825
+ # Why not reuse cmd_start's existing seams: `--brief` runs synthesize_brief_prd
826
+ # (loki:15759), which says "Build the smallest working version of the above" and
827
+ # stamps "Mode: Brief (zero-config first run)" -- greenfield framing that would
828
+ # reproduce the original bug one layer down. `loki start <dir>` runs
829
+ # synthesize_repo_prd (loki:15802), a quoted heredoc carrying NO caller text: it
830
+ # tells the agent to pick its own improvement and would discard what the user
831
+ # actually asked for. Neither carries a user-specified change against existing
832
+ # code, so this writes that spec and hands it to the normal PRD path.
833
+ #
834
+ # The brief is written via printf argument, never expanded into the heredoc, so
835
+ # no shell metacharacter in user input is interpreted.
836
+ _qs_write_change_prd() {
837
+ local out_file="$1" brief="$2" mode="${3:-change}"
838
+ {
839
+ if [ "$mode" = "new" ]; then
840
+ printf '# Build Request\n\n'
841
+ printf '## Goal\n'
842
+ printf '%s\n\n' "$brief"
843
+ printf '## Context\n'
844
+ printf 'This is a NEW project. Build it from scratch in the working directory.\n'
845
+ printf 'No starting template was chosen, so the Goal above is the whole spec.\n\n'
846
+ printf '## Requirements\n'
847
+ printf -- '- Scaffold the new application described in the Goal above.\n'
848
+ printf -- '- Build the smallest version that genuinely works end to end.\n'
849
+ printf -- '- Choose a conventional, well-supported stack for this kind of app.\n\n'
850
+ printf '## Success Criteria\n'
851
+ printf -- '- The application described in the Goal runs and is usable.\n'
852
+ printf -- '- Everything the Goal explicitly asks for is present.\n\n'
853
+ else
854
+ printf '# Change Request\n\n'
855
+ printf '## Goal\n'
856
+ printf '%s\n\n' "$brief"
857
+ printf '## Context\n'
858
+ printf 'This is an EXISTING codebase, not a new project. The working directory\n'
859
+ printf 'already contains the application this change applies to.\n\n'
860
+ printf '## Requirements\n'
861
+ printf -- '- Read the existing code first and follow its established conventions.\n'
862
+ printf -- '- Make the change described in the Goal above, and only that change.\n'
863
+ printf -- '- Do NOT scaffold a new project, and do NOT replace the existing app.\n'
864
+ printf -- '- Preserve all unrelated existing behavior and files.\n\n'
865
+ printf '## Success Criteria\n'
866
+ printf -- '- The change described in the Goal is visible when the app runs.\n'
867
+ printf -- '- Existing functionality still works; nothing unrelated regressed.\n\n'
868
+ fi
869
+ printf '## Constraints\n'
870
+ printf -- '- Keep the diff bounded and easy to review.\n'
871
+ printf -- '- No emojis, no em dashes in code, comments, or docs.\n'
872
+ } > "$out_file"
873
+ }
874
+
706
875
  # _qs_help: concise usage for `loki quickstart --help`.
707
876
  _qs_help() {
708
877
  printf '%sloki quickstart%s - guided first build (setup, idea, template, plan, go)\n' "$_QS_BOLD" "$_QS_NC"
@@ -749,8 +918,18 @@ _qs_help() {
749
918
  printf ' 3. Template Pick the closest starting template (offline keyword match)\n'
750
919
  printf ' 4. Plan Review the honest cost/time estimate, then confirm\n'
751
920
  printf '\n'
921
+ printf 'Existing projects:\n'
922
+ printf ' If the current directory is already a project (package.json,\n'
923
+ printf ' pyproject.toml, go.mod, Cargo.toml, a git repo with commits, and\n'
924
+ printf ' similar), quickstart treats your description as a CHANGE to that\n'
925
+ printf ' code instead of a new build. No template is offered and no file is\n'
926
+ printf ' written into your project directory.\n'
927
+ printf ' At the template prompt you can always answer "none" (or skip/no/q)\n'
928
+ printf ' to use your description directly as the spec.\n'
929
+ printf '\n'
752
930
  printf 'The PRD is written to ./prd.md in the current directory (or the next\n'
753
931
  printf 'free prd-quickstart*.md name if that exists), then the build starts.\n'
932
+ printf 'The change-request path above writes no PRD into your project.\n'
754
933
  return 0
755
934
  }
756
935
 
@@ -1122,7 +1301,50 @@ cmd_quickstart() {
1122
1301
  fi
1123
1302
 
1124
1303
  # ----- Step 3 of 4: Pick a template (skipped if a PRD path was given) ----
1125
- if [ -z "$prd_source" ]; then
1304
+ #
1305
+ # BROWNFIELD FIRST. Offering greenfield templates inside an existing project
1306
+ # is how "update the color theme of this app" became a brand-new todo app.
1307
+ # When the CWD is already a project, a brief is a CHANGE REQUEST against the
1308
+ # code that is here, so the template picker is skipped entirely.
1309
+ #
1310
+ # Gating:
1311
+ # - dry_run is excluded: preview must stay the deterministic template/plan
1312
+ # preview its JSON schema promises, and must run no provider code.
1313
+ # - an explicit --template is excluded: the user was specific, and an
1314
+ # explicit choice outranks detection.
1315
+ # spec_mode records WHY the brief became the spec, decided at each site that
1316
+ # decides it. It is not derived from $brownfield_reason: that stays set
1317
+ # after the classifier flip below, and reusing it would frame a brand-new
1318
+ # build as a change to code that does not exist.
1319
+ local brownfield_reason="" use_brief_as_spec=false spec_mode="change"
1320
+ if [ -z "$prd_source" ] && [ -z "$template_override" ] && [ "$dry_run" != true ] && [ -n "$brief" ]; then
1321
+ brownfield_reason="$(_qs_detect_brownfield)" || brownfield_reason=""
1322
+ if [ -n "$brownfield_reason" ]; then
1323
+ use_brief_as_spec=true
1324
+ spec_mode="change"
1325
+ # The model gets the final say when a provider CLI is available, so
1326
+ # "build me a brand new dashboard" typed inside an existing repo is
1327
+ # still treated as a new build. Fail-closed: any miss keeps the
1328
+ # filesystem verdict, which is deterministic and offline.
1329
+ local _qs_intent=""
1330
+ _qs_intent="$(_qs_classify_intent "$brief")" || _qs_intent=""
1331
+ if [ "$_qs_intent" = "new_project" ]; then
1332
+ use_brief_as_spec=false
1333
+ # This path falls through to the template picker and can reach
1334
+ # the rejection arm below, so the mode must not stay "change".
1335
+ spec_mode="new"
1336
+ fi
1337
+ fi
1338
+ fi
1339
+
1340
+ if [ "$use_brief_as_spec" = true ]; then
1341
+ printf '%sStep 3 of 4: Existing project detected%s\n' "$_QS_BOLD" "$_QS_NC"
1342
+ printf ' Detected an existing project (%s). Treating this as a change\n' "$brownfield_reason"
1343
+ printf ' to your code, not a new build.\n'
1344
+ printf ' No starting template is being used.\n\n'
1345
+ input_kind="change"
1346
+ template_name="change-request"
1347
+ elif [ -z "$prd_source" ]; then
1126
1348
  if [ -n "$template_override" ]; then
1127
1349
  template_name="$template_override"
1128
1350
  printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
@@ -1174,25 +1396,85 @@ cmd_quickstart() {
1174
1396
  printf ' Selected %s (top match) for "%s".\n\n' "${top3[0]}" "$brief"
1175
1397
  fi
1176
1398
 
1399
+ # An explicit rejection is honored, never converted into a selection.
1400
+ # This arm used to be absent, so "none" fell through to `*)` and became
1401
+ # top3[0] -- the founder-reported bug where typing "none" still built a
1402
+ # todo app. With no template, the user's own brief becomes the spec.
1403
+ if _qs_is_rejection "$pick"; then
1404
+ if [ -z "$brief" ]; then
1405
+ # Nothing to build from: no template AND no description. Refuse
1406
+ # rather than invent a spec or silently fall back to a template.
1407
+ printf '%sNo template selected and no description given; nothing to build.%s\n' "$_QS_RED" "$_QS_NC" >&2
1408
+ printf 'Re-run: loki quickstart "<what you want>"\n' >&2
1409
+ exit 2
1410
+ fi
1411
+ use_brief_as_spec=true
1412
+ # Reaching the picker at all means this is NOT an existing codebase
1413
+ # being changed: the brownfield arm skips the picker entirely, and
1414
+ # the classifier-override arm above already set "new". Rejecting a
1415
+ # template asks for a new project with no starting template.
1416
+ spec_mode="new"
1417
+ input_kind="change"
1418
+ template_name="no-template"
1419
+ printf ' No template. Using your description as the spec.\n\n'
1420
+ else
1177
1421
  case "$pick" in
1178
1422
  ""|1) template_name="${top3[0]}";;
1179
1423
  2) template_name="${top3[1]:-${top3[0]}}";;
1180
1424
  3) template_name="${top3[2]:-${top3[0]}}";;
1181
- *) template_name="${top3[0]}";; # any unexpected input -> the default
1425
+ *) template_name="${top3[0]}";; # any unrecognized NON-rejection input
1182
1426
  esac
1427
+ fi
1183
1428
 
1184
1429
  fi
1185
1430
 
1186
- local tdir; tdir="$(_qs_templates_dir)"
1187
- prd_source="$tdir/$template_name.md"
1188
- if [ ! -f "$prd_source" ]; then
1189
- printf '%sTemplate file not found: %s%s\n' "$_QS_RED" "$prd_source" "$_QS_NC" >&2
1190
- exit 1
1431
+ if [ "$use_brief_as_spec" != true ]; then
1432
+ local tdir; tdir="$(_qs_templates_dir)"
1433
+ prd_source="$tdir/$template_name.md"
1434
+ if [ ! -f "$prd_source" ]; then
1435
+ printf '%sTemplate file not found: %s%s\n' "$_QS_RED" "$prd_source" "$_QS_NC" >&2
1436
+ exit 1
1437
+ fi
1191
1438
  fi
1192
1439
  else
1193
1440
  template_name="$(basename "$prd_source")"
1194
1441
  fi
1195
1442
 
1443
+ # The change-request spec is staged OUTSIDE the working tree. It is derived
1444
+ # from what the user typed, not authored by them, so landing it in the CWD
1445
+ # would drop an unrequested file into an existing project -- and the ./prd.md
1446
+ # overwrite prompt below exists precisely to keep quickstart from writing
1447
+ # over a file the user already has. Staging in a temp file keeps the
1448
+ # brownfield path write-free in the user's directory.
1449
+ if [ "$use_brief_as_spec" = true ]; then
1450
+ # The .md suffix is load-bearing, not cosmetic. cmd_start classifies a
1451
+ # positional via detect_arg_type (loki:1675): an extensionless path is
1452
+ # only reached by the late `[ -e "$arg" ]` branch, and BOTH PRD
1453
+ # existence guards (loki:2962, loki:2985) are `case` arms matching
1454
+ # *.md|*.json|*.txt|*.yaml|*.yml, so an extensionless spec silently
1455
+ # skips the fail-fast checks. Naming it .md puts classification on the
1456
+ # explicit extension branch and under those guards.
1457
+ # BSD mktemp (stock macOS) does NOT substitute the X-run when a suffix
1458
+ # follows it: `mktemp foo.XXXXXX.md` creates the LITERAL file
1459
+ # "foo.XXXXXX.md". That is a fixed name, so a second concurrent
1460
+ # quickstart would fail to create it and the build would never start.
1461
+ # Create with a bare X-run (portable, unique), then rename to add .md.
1462
+ local _qs_stage=""
1463
+ _qs_stage="$(mktemp "${TMPDIR:-/tmp}/loki-quickstart-change.XXXXXX")" || {
1464
+ printf '%sCould not stage the change request.%s\n' "$_QS_RED" "$_QS_NC" >&2
1465
+ exit 1
1466
+ }
1467
+ if mv "$_qs_stage" "$_qs_stage.md" 2>/dev/null; then
1468
+ prd_source="$_qs_stage.md"
1469
+ else
1470
+ prd_source="$_qs_stage"
1471
+ fi
1472
+ # Not trap-cleaned on purpose: cmd_start EXECS the runner and never
1473
+ # returns here, and the runner must still be able to read this spec.
1474
+ # It lives in TMPDIR under a mktemp name, so the OS reaps it.
1475
+ _qs_write_change_prd "$prd_source" "$brief" "$spec_mode"
1476
+ fi
1477
+
1196
1478
  # ----- Step 4 of 4: Review the plan (reuse the slice-A estimator) --------
1197
1479
  printf '%sStep 4 of 4: Review the plan%s\n' "$_QS_BOLD" "$_QS_NC"
1198
1480
  # The plan is ALWAYS rendered, on both paths, from the same real estimator.
@@ -1280,8 +1562,14 @@ cmd_quickstart() {
1280
1562
  fi
1281
1563
 
1282
1564
  # ----- Land the PRD at ./prd.md (design 3.6) ----------------------------
1565
+ # The change-request path deliberately lands NOTHING in the working tree.
1566
+ # The user asked for a change to code that already exists; dropping a
1567
+ # generated prd.md into their project is the unrequested write that made the
1568
+ # original bug worse. The staged spec is handed to cmd_start directly.
1283
1569
  local target="./prd.md"
1284
- if [ -e "$target" ]; then
1570
+ if [ "$use_brief_as_spec" = true ]; then
1571
+ target="$prd_source"
1572
+ elif [ -e "$target" ]; then
1285
1573
  local overwrite=""
1286
1574
  # Non-interactive never asks and never overwrites. Leaving $overwrite
1287
1575
  # empty falls into the existing suffix walk below, which is already the
@@ -1338,6 +1626,10 @@ cmd_quickstart() {
1338
1626
  return 2
1339
1627
  fi
1340
1628
  rm -f "$preview_stage"
1629
+ elif [ "$use_brief_as_spec" = true ]; then
1630
+ # $target IS $prd_source here (the staged change request); copying a
1631
+ # file onto itself would truncate it.
1632
+ :
1341
1633
  elif ! cp "$prd_source" "$target" 2>/dev/null; then
1342
1634
  printf '%sCould not write the PRD to the current directory. Try a writable directory.%s\n' "$_QS_RED" "$_QS_NC" >&2
1343
1635
  exit 1
@@ -1345,7 +1637,12 @@ cmd_quickstart() {
1345
1637
 
1346
1638
  printf '\n'
1347
1639
  printf 'Starting your build. Progress streams here in the terminal.\n'
1348
- printf ' PRD saved to: %s\n' "$target"
1640
+ if [ "$use_brief_as_spec" = true ]; then
1641
+ printf ' Change request: %s\n' "$brief"
1642
+ printf ' No file was written to your project directory.\n'
1643
+ else
1644
+ printf ' PRD saved to: %s\n' "$target"
1645
+ fi
1349
1646
  # `loki dashboard` with no subcommand prints help and exits -- it does not
1350
1647
  # start. Naming the bare command here sent every first-run user to a help
1351
1648
  # screen at the exact moment they wanted to watch their build.