loki-mode 9.48.2 → 9.49.3

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 (88) 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/lib/claude-flags.sh +18 -2
  5. package/autonomy/loki +137 -12
  6. package/autonomy/notification-checker.py +45 -17
  7. package/autonomy/provider-offer.sh +47 -0
  8. package/autonomy/quickstart.sh +306 -9
  9. package/autonomy/run.sh +43 -4
  10. package/dashboard/__init__.py +1 -1
  11. package/dashboard/static/index.html +7 -1
  12. package/docs/INSTALLATION.md +1 -1
  13. package/docs/KNOWN-ISSUES.md +42 -0
  14. package/docs/cost-controls.md +1 -1
  15. package/docs/environment-variables.md +1 -1
  16. package/loki-ts/dist/loki.js +317 -317
  17. package/mcp/__init__.py +1 -1
  18. package/package.json +1 -1
  19. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  20. package/web-app/dist/assets/{AdminPage-PyyVXg4b.js → AdminPage-BA6etdwL.js} +1 -1
  21. package/web-app/dist/assets/{Avatar-CTUlr_Xf.js → Avatar-CAtbcD23.js} +1 -1
  22. package/web-app/dist/assets/{Badge-BQeXvmg8.js → Badge-sb_JbKl0.js} +1 -1
  23. package/web-app/dist/assets/{Button-Dc6-P6pz.js → Button-BamXbIWa.js} +1 -1
  24. package/web-app/dist/assets/{CockpitPage-BspOJ3Fv.js → CockpitPage-CUYizBtG.js} +4 -4
  25. package/web-app/dist/assets/{ComparePage-CFXIM--Z.js → ComparePage-BnmNTggM.js} +1 -1
  26. package/web-app/dist/assets/{ErrorBoundary-C7vQYDW9.js → ErrorBoundary-BjX4_KOW.js} +1 -1
  27. package/web-app/dist/assets/{EvidenceReceiptPanel-CqjsjTOs.js → EvidenceReceiptPanel-Bq6pmNUw.js} +1 -1
  28. package/web-app/dist/assets/{GitHubIssuesPanel-DyuNDMgD.js → GitHubIssuesPanel-DD8c6RVj.js} +1 -1
  29. package/web-app/dist/assets/{GitHubPRsPanel-BtAq20xF.js → GitHubPRsPanel-4NawTXRl.js} +2 -2
  30. package/web-app/dist/assets/{HomePage-BuVFOrO8.js → HomePage-DTREi8OE.js} +1 -1
  31. package/web-app/dist/assets/{LoginPage-CuhhrxGb.js → LoginPage-DavfeQxC.js} +1 -1
  32. package/web-app/dist/assets/{MagicPage-D2KZPX3F.js → MagicPage-BIbTFlwh.js} +1 -1
  33. package/web-app/dist/assets/MetricsPage-Cn3UPKpU.js +6 -0
  34. package/web-app/dist/assets/{NotFoundPage-CTnnaWlL.js → NotFoundPage-BVs_nuBv.js} +1 -1
  35. package/web-app/dist/assets/{ProjectPage-uJFaoM8-.js → ProjectPage-CLplvOHe.js} +25 -25
  36. package/web-app/dist/assets/{ProjectsPage-8lKAOBIW.js → ProjectsPage-BBFzBBG_.js} +2 -2
  37. package/web-app/dist/assets/{SettingsPage-D1BBd5qy.js → SettingsPage-WPsJ_K9r.js} +1 -1
  38. package/web-app/dist/assets/{ShowcasePage-CmQ0n8iO.js → ShowcasePage-ZaLaJl5J.js} +1 -1
  39. package/web-app/dist/assets/SystemSettingsPage-CCORP-uG.js +1 -0
  40. package/web-app/dist/assets/{TeamsPage-BS0TtVUa.js → TeamsPage-B6IGT_K2.js} +1 -1
  41. package/web-app/dist/assets/{TemplatesPage-C2zPnnf2.js → TemplatesPage-RrYYC3CM.js} +1 -1
  42. package/web-app/dist/assets/{TerminalOutput-Dle1klqq.js → TerminalOutput-CeTvKnkl.js} +1 -1
  43. package/web-app/dist/assets/{activity-DQp5U9f9.js → activity-CySdOdDP.js} +1 -1
  44. package/web-app/dist/assets/{bell-BqkTmgL7.js → bell-BHdr5yat.js} +1 -1
  45. package/web-app/dist/assets/{bot-DILhaIGH.js → bot-BaB4q_4m.js} +1 -1
  46. package/web-app/dist/assets/{check-BC9x7oPm.js → check-BAgbzVrK.js} +1 -1
  47. package/web-app/dist/assets/{chevron-left--yqvAfjx.js → chevron-left-CrVUY29C.js} +1 -1
  48. package/web-app/dist/assets/{circle-alert-DOH6PcgZ.js → circle-alert-YrWgH49w.js} +1 -1
  49. package/web-app/dist/assets/{clock-DKSDFsE6.js → clock-DN8LAN-0.js} +1 -1
  50. package/web-app/dist/assets/{cloud-DRBU3JYs.js → cloud-D6C4b1KY.js} +1 -1
  51. package/web-app/dist/assets/{code-xml-D2LqszR9.js → code-xml-xBWyKfux.js} +1 -1
  52. package/web-app/dist/assets/{database-D7ceedlh.js → database-BYqcS6kn.js} +1 -1
  53. package/web-app/dist/assets/{dollar-sign-D0A47egY.js → dollar-sign--WOh4k_m.js} +1 -1
  54. package/web-app/dist/assets/{file-code-corner-CH_Qbtjo.js → file-code-corner-B0rLPjW1.js} +1 -1
  55. package/web-app/dist/assets/{file-plus-CUNMjHk9.js → file-plus-BNhUQ1TQ.js} +1 -1
  56. package/web-app/dist/assets/folder-open-DliiOgFl.js +11 -0
  57. package/web-app/dist/assets/{globe-gZnaGg8N.js → globe-BV-hx0u_.js} +1 -1
  58. package/web-app/dist/assets/{hammer-BhJC5DMK.js → hammer-DhmP4GLw.js} +1 -1
  59. package/web-app/dist/assets/index-D98GzaW8.css +1 -0
  60. package/web-app/dist/assets/{index-BMmYYws0.js → index-x0TESFYr.js} +3 -3
  61. package/web-app/dist/assets/{layers-glXeoUbJ.js → layers-DXWDMSYj.js} +1 -1
  62. package/web-app/dist/assets/{lightbulb-gXn8H0Lf.js → lightbulb-PBw6hhLS.js} +1 -1
  63. package/web-app/dist/assets/{loader-circle-Do2ke8xm.js → loader-circle-B7KiGAVU.js} +1 -1
  64. package/web-app/dist/assets/{lock-Bgplb1_n.js → lock-BUTRYdDQ.js} +1 -1
  65. package/web-app/dist/assets/{mail-j4Lo8hfB.js → mail-DaLiNrPb.js} +1 -1
  66. package/web-app/dist/assets/{minus-dhrzKSK5.js → minus-DB_hlGEk.js} +1 -1
  67. package/web-app/dist/assets/{package-CInWQQ97.js → package-CymmXoiQ.js} +1 -1
  68. package/web-app/dist/assets/{plus-BpYvoLlZ.js → plus-Cp4AO4I2.js} +1 -1
  69. package/web-app/dist/assets/{refresh-cw-F6koqEgF.js → refresh-cw-ygYTmZTE.js} +1 -1
  70. package/web-app/dist/assets/{rotate-ccw-CQ2W5S8f.js → rotate-ccw-Ba3QxM9K.js} +1 -1
  71. package/web-app/dist/assets/{save-DMCYGm06.js → save-DPm96BnR.js} +1 -1
  72. package/web-app/dist/assets/{server-Bj8EX4Z1.js → server-BapOz_vf.js} +1 -1
  73. package/web-app/dist/assets/{shield-alert-CiYuulCU.js → shield-alert-DYzfVqWS.js} +1 -1
  74. package/web-app/dist/assets/{thumbs-up-BqEh1puP.js → thumbs-up-DxOTplej.js} +11 -6
  75. package/web-app/dist/assets/{trash-2-CCwouZJJ.js → trash-2-Br8dh_6B.js} +1 -1
  76. package/web-app/dist/assets/{trending-down-Cx1vhE4o.js → trending-down-CxSP6MU3.js} +1 -1
  77. package/web-app/dist/assets/{trending-up-BUhVYJ2T.js → trending-up-CXx0ywZb.js} +1 -1
  78. package/web-app/dist/assets/{upload-D5IsZeFM.js → upload-CpEO3qNc.js} +1 -1
  79. package/web-app/dist/assets/{usePolling-CJbfhFcH.js → usePolling-BuzXwjmC.js} +1 -1
  80. package/web-app/dist/assets/{user-BRLhezsD.js → user-DUUYT47B.js} +1 -1
  81. package/web-app/dist/index.html +2 -2
  82. package/web-app/server.py +121 -1
  83. package/web-app/dist/assets/MetricsPage-DFEYJl9z.js +0 -6
  84. package/web-app/dist/assets/SystemSettingsPage-CSHaVyuD.js +0 -6
  85. package/web-app/dist/assets/copy-BKW4U4Qr.js +0 -6
  86. package/web-app/dist/assets/folder-open-CQ_SVq94.js +0 -6
  87. package/web-app/dist/assets/git-commit-horizontal-DQUBe0oD.js +0 -6
  88. package/web-app/dist/assets/index-84JUN_aA.css +0 -1
@@ -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.
package/autonomy/run.sh CHANGED
@@ -1181,10 +1181,12 @@ PERPETUAL_MODE=${LOKI_PERPETUAL_MODE:-false}
1181
1181
  #
1182
1182
  # MEASURED, not chosen. Real per-iteration wall clock across every recorded run
1183
1183
  # on this machine: median 718s, max 1746s. So the 1000 default is an 8.3-DAY
1184
- # ceiling -- and it is the ONLY backstop, because the other two valves ship
1185
- # disabled: LOKI_BUDGET_LIMIT defaults to "" (check_budget_limit returns
1186
- # immediately) and LOKI_MAX_DURATION defaults to 0 (check_max_duration returns
1187
- # "never stop").
1184
+ # ceiling. When this analysis was written it was also the ONLY backstop,
1185
+ # because the other two valves shipped disabled. That is no longer true of the
1186
+ # budget: since v9.42.0 LOKI_BUDGET_LIMIT defaults to "100.00" (see :527 --
1187
+ # note `-` not `:-`, so only an EXPLICIT "" means unlimited), and breaching it
1188
+ # pauses the run. LOKI_MAX_DURATION does still default to 0 (check_max_duration
1189
+ # returns "never stop" at :18278).
1188
1190
  #
1189
1191
  # It also contradicts our own documentation. SETUP.md tells users to RAISE the
1190
1192
  # budget for large work with LOKI_MAX_ITERATIONS=40, and the demo uses 10 -- so
@@ -17860,6 +17862,43 @@ BUDGETUPD_EOF
17860
17862
  return 0
17861
17863
  fi
17862
17864
 
17865
+ # LIVE COST VISIBILITY. The loop has always computed cumulative spend here,
17866
+ # every iteration, and printed NOTHING until 80% of the cap. That silence is
17867
+ # the "predatory / un-capped" feeling users report about agentic tools: the
17868
+ # enforcement was never missing (the cap pauses at 100.00 by default), only
17869
+ # the visibility was, so spend was discoverable solely after the fact.
17870
+ #
17871
+ # Reuses the $current_cost computed above; deliberately NOT a second reader.
17872
+ # Two readers of one number disagreeing is the exact defect class fixed in
17873
+ # 405da630, and a per-iteration cost line is not worth reintroducing it.
17874
+ #
17875
+ # Safe on stdout: this function has exactly two non-comment references in
17876
+ # the file -- its own definition, and the single loop call site, which
17877
+ # tests the exit code and never captures output. Verified with a
17878
+ # comment-excluding grep plus a positive control.
17879
+ #
17880
+ # This comment deliberately does NOT spell out the call-site expression.
17881
+ # tests/test-max-duration.sh:151 counts that exact string across this file
17882
+ # and asserts there is exactly ONE, so quoting it here made the count 2 and
17883
+ # turned a green suite red -- a text guard firing on prose written to
17884
+ # explain it. Describe the call site; never reproduce it.
17885
+ #
17886
+ # log_info is additionally gated by _loki_log_enabled, so quiet modes stay
17887
+ # quiet.
17888
+ # The zero test must be NUMERIC. `[ "$current_cost" != "0" ]` is a string
17889
+ # compare and the python above emits "0.0", so a run with nothing recorded
17890
+ # would announce "Cost so far: $0.0" -- claiming a measurement we do not
17891
+ # have, which is the exact fabrication this codebase deletes on sight.
17892
+ #
17893
+ # No else-branch: BUDGET_LIMIT empty means unlimited, and check_budget_limit
17894
+ # returns at its first line in that case, so any "no cap set" arm here would
17895
+ # be unreachable code advertising a message that can never print. The
17896
+ # no-cap disclosure already lives at run-start (show_run_start_estimate).
17897
+ if [ -n "$current_cost" ] \
17898
+ && awk -v c="$current_cost" 'BEGIN{exit !(c+0 > 0)}' 2>/dev/null; then
17899
+ log_info "Cost so far: \$${current_cost} of \$${BUDGET_LIMIT} cap (iteration ${ITERATION_COUNT:-0})."
17900
+ fi
17901
+
17863
17902
  # Update budget.json with current usage (not exceeded)
17864
17903
  if [ -n "$current_cost" ] && [ "$current_cost" != "0" ]; then
17865
17904
  cat > ".loki/metrics/budget.json" << BUDGETUPD_EOF
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.48.2"
10
+ __version__ = "9.49.3"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2444,7 +2444,11 @@ var LokiDashboard=(()=>{var Ie=Object.defineProperty;var zt=Object.getOwnPropert
2444
2444
  PASSED
2445
2445
  </div>
2446
2446
  </div>
2447
- `}_renderJourney(){if(this._journeyState==="unavailable")return'<div class="journey-state" role="status">Issue-to-PR evidence is unavailable. No readiness claim can be made.</div>';if(this._journeyState!=="ready"||!this._journeyProof)return'<div class="journey-state" role="status">No issue-to-PR receipt yet. Start an issue run to record its first result, proof, and PR state.</div>';let e=this._journeyProof,t=e?.facts?.journey;if(!t||!t.issue)return'<div class="journey-state" role="status">Latest receipt is not an issue run. Issue-to-PR readiness is not applicable.</div>';let i=t.time_to_first_result_sec,a=typeof i=="number"&&i>=0,s=t.first_result_kind==="code_change"?"verified patch":t.first_result_kind==="proposed_solution_plan"?"proposed solution":"result",r=a?`${Math.round(i)}s to ${s}`:"Not measured",o=typeof e?.honesty?.headline=="string"?e.honesty.headline:null,n=Array.isArray(e?.honesty?.degraded)?e.honesty.degraded.length:null,l=o||"Not evaluated",c=n===null?"Uncertainty not measured":n===0?"No recorded gaps":`${n} recorded gap${n===1?"":"s"}`,p=t.pull_request,u="Not prepared",m="";p&&p.state&&(u=String(p.state).replace(/_/g," "),this._isSafeWebUrl(p.url)&&(m=`<a class="journey-link" href="${this._escapeHtml(p.url)}" target="_blank" rel="noopener noreferrer">Open PR</a>`));let g=this._data.phase||(this._data.status==="running"?"In progress":"Run complete");return`
2447
+ `}_renderDisconnected(){return`<div class="journey-state" role="status">
2448
+ Not connected to the Loki API. No session numbers can be shown until it
2449
+ reconnects. Start it with <code>loki dashboard start</code>, or check the
2450
+ API URL in Settings.
2451
+ </div>`}_renderJourney(){if(this._journeyState==="unavailable")return'<div class="journey-state" role="status">Issue-to-PR evidence is unavailable. No readiness claim can be made.</div>';if(this._journeyState!=="ready"||!this._journeyProof)return'<div class="journey-state" role="status">No issue-to-PR receipt yet. Start an issue run to record its first result, proof, and PR state.</div>';let e=this._journeyProof,t=e?.facts?.journey;if(!t||!t.issue)return'<div class="journey-state" role="status">Latest receipt is not an issue run. Issue-to-PR readiness is not applicable.</div>';let i=t.time_to_first_result_sec,a=typeof i=="number"&&i>=0,s=t.first_result_kind==="code_change"?"verified patch":t.first_result_kind==="proposed_solution_plan"?"proposed solution":"result",r=a?`${Math.round(i)}s to ${s}`:"Not measured",o=typeof e?.honesty?.headline=="string"?e.honesty.headline:null,n=Array.isArray(e?.honesty?.degraded)?e.honesty.degraded.length:null,l=o||"Not evaluated",c=n===null?"Uncertainty not measured":n===0?"No recorded gaps":`${n} recorded gap${n===1?"":"s"}`,p=t.pull_request,u="Not prepared",m="";p&&p.state&&(u=String(p.state).replace(/_/g," "),this._isSafeWebUrl(p.url)&&(m=`<a class="journey-link" href="${this._escapeHtml(p.url)}" target="_blank" rel="noopener noreferrer">Open PR</a>`));let g=this._data.phase||(this._data.status==="running"?"In progress":"Run complete");return`
2448
2452
  <section class="journey" aria-labelledby="journey-heading">
2449
2453
  <div class="journey-heading" id="journey-heading">Issue to PR</div>
2450
2454
  <div class="journey-steps">
@@ -2633,6 +2637,7 @@ var LokiDashboard=(()=>{var Ie=Object.defineProperty;var zt=Object.getOwnPropert
2633
2637
  <span class="overview-title">Overview</span>
2634
2638
  </div>
2635
2639
 
2640
+ ${this._data.connected?`
2636
2641
  ${this._renderJourney()}
2637
2642
 
2638
2643
  <div class="overview-grid">
@@ -2687,6 +2692,7 @@ var LokiDashboard=(()=>{var Ie=Object.defineProperty;var zt=Object.getOwnPropert
2687
2692
  <div class="card-value small-text">${p}</div>
2688
2693
  </div>
2689
2694
  </div>
2695
+ `:this._renderDisconnected()}
2690
2696
  </div>
2691
2697
  `}};customElements.get("loki-overview")||customElements.define("loki-overview",X);var Ft=[{id:"pending",label:"Pending",status:"pending",color:"var(--loki-text-muted)"},{id:"in_progress",label:"In Progress",status:"in_progress",color:"var(--loki-blue)"},{id:"review",label:"In Review",status:"review",color:"var(--loki-purple)"},{id:"done",label:"Completed",status:"done",color:"var(--loki-green)"}],Z=class d extends v{static get observedAttributes(){return["api-url","project-id","theme","readonly"]}constructor(){super(),this._tasks=[],this._loading=!0,this._error=null,this._draggedTask=null,this._selectedTask=null,this._expandedCards=new Set,this._selectedTasks=new Set,this._bulkMode=!1,this._activeFilter="all",this._searchQuery="",this._visibleCounts={},this._api=null,this._state=N()}static get PAGE_SIZE(){return 10}_getVisibleCount(e){let t=this._visibleCounts[e];return typeof t=="number"&&t>0?t:d.PAGE_SIZE}_showMore(e){this._visibleCounts[e]=this._getVisibleCount(e)+d.PAGE_SIZE,this.render()}_columnIcon(e){switch(e){case"pending":return'<circle cx="12" cy="12" r="10"/>';case"in_progress":return'<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>';case"review":return'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';case"done":return'<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>';default:return'<circle cx="12" cy="12" r="10"/>'}}connectedCallback(){super.connectedCallback(),this._setupApi(),this._loadTasks()}disconnectedCallback(){super.disconnectedCallback(),this._teardownApiListeners()}_teardownApiListeners(){this._api&&this._onTaskEvent&&(this._api.removeEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.removeEventListener(f.TASK_DELETED,this._onTaskEvent))}attributeChangedCallback(e,t,i){t!==i&&(e==="api-url"&&this._api&&(this._teardownApiListeners(),this._setupApi(),this._loadTasks()),e==="project-id"&&this._loadTasks(),e==="theme"&&this._applyTheme())}_setupApi(){let e=this.getAttribute("api-url")||window.location.origin;this._api=h({baseUrl:e}),this._onTaskEvent=()=>this._loadTasks(),this._api.addEventListener(f.TASK_CREATED,this._onTaskEvent),this._api.addEventListener(f.TASK_UPDATED,this._onTaskEvent),this._api.addEventListener(f.TASK_DELETED,this._onTaskEvent)}async _loadTasks(){let e=this._api;this._loading=!0,this._error=null,this.render();try{let t=this.getAttribute("project-id"),i=t?{projectId:parseInt(t)}:{},a=await e.listTasks(i);if(e!==this._api)return;this._tasks=a;let s=this._state.get("localTasks")||[];s.length>0&&(this._tasks=[...this._tasks,...s.map(r=>({...r,isLocal:!0}))]),this._state.update({"cache.tasks":this._tasks},!1)}catch(t){if(e!==this._api)return;this._error=t.message,this._tasks=(this._state.get("localTasks")||[]).map(i=>({...i,isLocal:!0}))}this._loading=!1,this.render()}_getTasksByStatus(e){return this._getFilteredTasks().filter(i=>i.status?.toLowerCase().replace(/-/g,"_")===e)}_handleDragStart(e,t){this.hasAttribute("readonly")||(this._draggedTask=t,e.target.classList.add("dragging"),e.dataTransfer.effectAllowed="move",e.dataTransfer.setData("text/plain",t.id.toString()))}_handleDragEnd(e){e.target.classList.remove("dragging"),this._draggedTask=null,this.shadowRoot.querySelectorAll(".kanban-tasks").forEach(t=>{t.classList.remove("drag-over")})}_handleDragOver(e){e.preventDefault(),e.dataTransfer.dropEffect="move"}_handleDragEnter(e){e.preventDefault(),e.currentTarget.classList.add("drag-over")}_handleDragLeave(e){e.currentTarget.contains(e.relatedTarget)||e.currentTarget.classList.remove("drag-over")}async _handleDrop(e,t){if(e.preventDefault(),e.currentTarget.classList.remove("drag-over"),!this._draggedTask||this.hasAttribute("readonly"))return;let i=this._draggedTask.id,a=this._tasks.find(r=>r.id===i);if(!a)return;let s=a.status;if(s!==t){a.status=t,this.render();try{a.isLocal?this._state.moveLocalTask(i,t):await this._api.moveTask(i,t,0),this.dispatchEvent(new CustomEvent("task-moved",{detail:{taskId:i,oldStatus:s,newStatus:t}}))}catch(r){a.status=s,this.render(),console.error("Failed to move task:",r)}}}_toggleCardExpand(e){this._expandedCards.has(e)?this._expandedCards.delete(e):this._expandedCards.add(e),this.render()}_toggleTaskSelection(e,t){t&&t.stopPropagation(),this._selectedTasks.has(e)?this._selectedTasks.delete(e):this._selectedTasks.add(e),this.render()}_toggleBulkMode(){this._bulkMode=!this._bulkMode,this._bulkMode||this._selectedTasks.clear(),this.render()}async _bulkMove(e){let t=[...this._selectedTasks];for(let i of t){let a=this._tasks.find(s=>String(s.id)===String(i));if(a&&a.status!==e)try{a.isLocal?this._state.moveLocalTask(i,e):await this._api.moveTask(i,e,0),a.status=e}catch(s){console.error("Failed to bulk move task:",i,s)}}this._selectedTasks.clear(),this._bulkMode=!1,this.render(),this._loadTasks()}async _bulkDelete(){let e=[...this._selectedTasks];for(let t of e)try{await this._api.deleteTask(t)}catch(i){console.error("Failed to delete task:",t,i)}this._selectedTasks.clear(),this._bulkMode=!1,this._loadTasks()}_setFilter(e){this._activeFilter=e,this._visibleCounts={},this.render()}_setSearch(e){this._searchQuery=e||"",this._visibleCounts={},this._renderTaskRegion();let t=this.shadowRoot.getElementById("task-search");if(t){t.focus();let i=t.value.length;try{t.setSelectionRange(i,i)}catch{}}}_getFilteredTasks(){let e=[...this._tasks],t=new Date,i=new Date(t.getFullYear(),t.getMonth(),t.getDate()),a=new Date(i.getTime()-7*24*60*60*1e3);switch(this._activeFilter){case"today":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=i});break;case"this-week":e=e.filter(r=>{let o=r.created_at?new Date(r.created_at):null;return o&&o>=a});break;case"running":e=e.filter(r=>r.status==="in_progress");break;case"failed":e=e.filter(r=>r.status==="failed"||r.status==="error");break;default:break}let s=this._searchQuery.trim().toLowerCase();return s&&(e=e.filter(r=>[r.id,r.title,r.description,r.type].map(n=>String(n??"").toLowerCase()).join(" ").includes(s))),e}_openAddTaskModal(e="pending"){this.dispatchEvent(new CustomEvent("add-task",{detail:{status:e}}))}_openTaskDetail(e){this._selectedTask=e,this.render(),this.dispatchEvent(new CustomEvent("task-click",{detail:{task:e}}))}_closeTaskDetail(){this._selectedTask=null,this.render()}_renderMarkdown(e){return C(e)}_formatTimestamp(e){if(!e)return"";try{let t=new Date(e);return isNaN(t.getTime())?this._escapeHtml(String(e)):t.toLocaleString()}catch{return this._escapeHtml(String(e))}}_phaseClass(e){let t=String(e||"").toLowerCase();return["reason","plan","planning"].includes(t)?"phase-reason":["act","execute","execution","implement"].includes(t)?"phase-act":["reflect","review"].includes(t)?"phase-reflect":["verify","test","gate"].includes(t)?"phase-verify":"phase-default"}_logLevelClass(e){let t=String(e||"info").toLowerCase();return t==="error"||t==="fatal"?"log-error":t==="warn"||t==="warning"?"log-warn":t==="debug"||t==="trace"?"log-debug":"log-info"}_renderTaskDetailModal(e){if(!e)return"";let t=(e.priority||"medium").toLowerCase(),i=t.charAt(0).toUpperCase()+t.slice(1),a=e.status||"pending",s=a.replace(/_/g," ").replace(/\b\w/g,g=>g.toUpperCase()),r=e.metadata||{},o=Array.isArray(e.acceptance_criteria)?e.acceptance_criteria:[],n=Array.isArray(e.context_files)?e.context_files:[],l=e.specification||"",c=e.description||"",p=Array.isArray(e.notes)?e.notes:[],u=Array.isArray(e.logs)?e.logs:[],m=e.full_content||"";return`
2692
2698
  <div class="modal-overlay" id="task-detail-overlay">
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v9.48.2
5
+ **Version:** v9.49.3
6
6
 
7
7
  ---
8
8
 
@@ -0,0 +1,42 @@
1
+
2
+ ## doctor --json writes 108 bytes to stderr on a provider-less host
3
+
4
+ `bash autonomy/loki doctor --json` on a host with NO provider CLI emits:
5
+
6
+ ```
7
+ autonomy/loki: line 14066: required: command not found
8
+ autonomy/loki: line 14066: status: command not found
9
+ ```
10
+
11
+ **Not user-affecting, and not a JSON defect.** stdout is 5176 bytes of valid
12
+ JSON; only stderr is polluted. Every `doctor --json` assertion in the repo
13
+ discards stderr (`tests/test-airgap-commands.sh:50`,
14
+ `tests/test-doctor-blocker-parity.sh:129,132`,
15
+ `tests/test-doctor-ci-gateable.sh:46,84,86`).
16
+ `tests/test-e2e-features.sh:104` was the lone outlier using `2>&1` and has been
17
+ brought into line.
18
+
19
+ **Attribution:** introduced this session. `git show 0416810d:autonomy/loki`
20
+ (v9.48.2) emits 0 bytes on the same host; HEAD emits 108. The region was last
21
+ touched by `56e03374 feat(doctor): show provider availability`.
22
+
23
+ **Six hypotheses tested and REFUTED, so the next person does not repeat them:**
24
+
25
+ 1. Word-splitting of `$_provider_availability` at `:14066` -- the assignment is
26
+ correctly quoted; reproduced in isolation as OK.
27
+ 2. `provider_availability_json` emitting to stderr -- 0 bytes, clean 353-byte
28
+ JSON in isolation.
29
+ 3. Its callees `auto_detect_provider` / `check_provider_installed` -- both
30
+ silent.
31
+ 4. The JSON value's content -- `required` and `status` appear 0 times in it.
32
+ 5. The value being multi-line and breaking the env-assignment chain -- forcing
33
+ `LOKI_PROVIDER_AVAILABILITY=""` as a literal STILL reproduces.
34
+ 6. `render_provider_availability` -- silent in isolation, and disabling the call
35
+ still reproduces.
36
+
37
+ **Also known:** `bash -n` parses the file clean; sourcing the file and calling
38
+ `cmd_doctor_json` directly emits 0 bytes, while executing the file emits 108.
39
+ That difference (streaming read vs single parse) is the strongest remaining
40
+ lead. The words `required` and `status` are python identifiers at `:14085-14098`
41
+ inside the `python3 -c "` string that opens at `:14069`.
42
+
@@ -25,7 +25,7 @@ They bound different things, and a run that stalls needs all three.
25
25
 
26
26
  | Cap | Bounds | Default |
27
27
  |---|---|---|
28
- | `LOKI_BUDGET_LIMIT` | Total spend, in USD | unset (no cap) |
28
+ | `LOKI_BUDGET_LIMIT` | Total spend, in USD | `100.00` (breaching pauses the run) |
29
29
  | `LOKI_MAX_ITERATIONS` | Number of iterations | 1000 |
30
30
  | `LOKI_MAX_DURATION` | Wall-clock time | unset (no cap) |
31
31
 
@@ -23,7 +23,7 @@ with nowhere to look them up.
23
23
  |---|---|---|
24
24
  | `LOKI_PRD_FILE` | none | Path to a spec file, as an alternative to the positional argument. |
25
25
  | `LOKI_MAX_ITERATIONS` | `1000` | Hard cap on iterations. Reaching it is a deterministic terminal failure, not a success. Also in `config schema`. |
26
- | `LOKI_BUDGET_LIMIT` | unset (no cap) | Spend cap in USD. On exhaustion the run stops and reports a terminal failure -- see [exit codes](./exit-codes.md). |
26
+ | `LOKI_BUDGET_LIMIT` | `100.00` | Spend cap in USD. Breaching it **pauses** the run (writes `.loki/PAUSE` and saves state); it never kills work or discards a deliverable. Remove `.loki/PAUSE` to resume, or raise the cap. Set to `""` for no cap. A warning is logged at 80%. |
27
27
  | `LOKI_MAX_DURATION` | unset (no cap) | Wall-clock cap in seconds. Stops cleanly at the next iteration boundary with a `max_duration_reached` terminal status. `loki start --max-duration` also accepts `90m` / `2h`. |
28
28
  | `LOKI_AUTO_CONFIRM` | unset | `true`/`false` to control prompts. Takes precedence over `CI`. |
29
29
  | `LOKI_CONFIG_DUMP` | `0` | `1` prints the resolved configuration and exits **without starting a run or spending anything**. |