kward 0.78.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -2
  3. data/.github/workflows/pages.yml +1 -1
  4. data/CHANGELOG.md +82 -1
  5. data/Gemfile.lock +2 -2
  6. data/README.md +33 -38
  7. data/doc/agent-tools.md +10 -11
  8. data/doc/api.md +3 -3
  9. data/doc/authentication.md +79 -110
  10. data/doc/code-search.md +1 -1
  11. data/doc/composer.md +1 -1
  12. data/doc/configuration.md +136 -27
  13. data/doc/context-budgeting.md +6 -6
  14. data/doc/context-tools.md +2 -2
  15. data/doc/editor.md +7 -7
  16. data/doc/extensibility.md +9 -29
  17. data/doc/files.md +6 -6
  18. data/doc/getting-started.md +3 -3
  19. data/doc/git.md +8 -2
  20. data/doc/lifecycle-hooks.md +5 -4
  21. data/doc/local-models.md +130 -0
  22. data/doc/mcp.md +5 -5
  23. data/doc/permissions.md +15 -5
  24. data/doc/plugins.md +39 -4
  25. data/doc/prompt-templates.md +105 -0
  26. data/doc/providers.md +140 -0
  27. data/doc/releasing.md +11 -3
  28. data/doc/rpc.md +65 -31
  29. data/doc/sandboxing.md +142 -0
  30. data/doc/security.md +31 -10
  31. data/doc/session-management.md +2 -4
  32. data/doc/skills.md +10 -0
  33. data/doc/tabs.md +37 -0
  34. data/doc/telegram.md +138 -0
  35. data/doc/transports.md +209 -0
  36. data/doc/usage.md +11 -3
  37. data/doc/web-search.md +3 -3
  38. data/doc/workspace-tools.md +4 -4
  39. data/examples/plugins/stardate_footer.rb +10 -0
  40. data/examples/plugins/telegram/plugin.rb +39 -0
  41. data/examples/plugins/telegram/telegram_api.rb +136 -0
  42. data/examples/plugins/telegram/telegram_transport.rb +304 -0
  43. data/kward.gemspec +1 -1
  44. data/lib/kward/ansi.rb +1 -0
  45. data/lib/kward/auth/api_key_store.rb +99 -0
  46. data/lib/kward/cli/auth_commands.rb +72 -13
  47. data/lib/kward/cli/commands.rb +7 -0
  48. data/lib/kward/cli/doctor.rb +21 -0
  49. data/lib/kward/cli/git.rb +31 -8
  50. data/lib/kward/cli/plugins.rb +1 -0
  51. data/lib/kward/cli/prompt_interface.rb +7 -0
  52. data/lib/kward/cli/rendering.rb +6 -2
  53. data/lib/kward/cli/runtime_helpers.rb +4 -2
  54. data/lib/kward/cli/sessions.rb +1 -1
  55. data/lib/kward/cli/settings.rb +162 -36
  56. data/lib/kward/cli/slash_commands.rb +109 -1
  57. data/lib/kward/cli/tabs.rb +102 -37
  58. data/lib/kward/cli/transports.rb +67 -0
  59. data/lib/kward/cli/worktrees.rb +360 -0
  60. data/lib/kward/cli.rb +22 -1
  61. data/lib/kward/config_files.rb +106 -4
  62. data/lib/kward/conversation.rb +27 -8
  63. data/lib/kward/git_worktree_manager.rb +291 -0
  64. data/lib/kward/local_command_runner.rb +2 -2
  65. data/lib/kward/model/azure_openai_config.rb +54 -0
  66. data/lib/kward/model/catalog.rb +113 -0
  67. data/lib/kward/model/client.rb +364 -23
  68. data/lib/kward/model/model_info.rb +60 -1
  69. data/lib/kward/model/payloads.rb +64 -4
  70. data/lib/kward/model/provider_catalog.rb +114 -0
  71. data/lib/kward/model/sources.rb +70 -0
  72. data/lib/kward/model/stream_parser.rb +141 -39
  73. data/lib/kward/pan/index.html.erb +50 -0
  74. data/lib/kward/pan/server.rb +49 -2
  75. data/lib/kward/permissions/policy.rb +18 -4
  76. data/lib/kward/plugin_chat_runtime.rb +374 -0
  77. data/lib/kward/plugin_registry.rb +81 -8
  78. data/lib/kward/private_file.rb +9 -3
  79. data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
  80. data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
  81. data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
  82. data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
  83. data/lib/kward/prompt_interface/editor/controller.rb +43 -10
  84. data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
  85. data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
  86. data/lib/kward/prompt_interface/editor/state.rb +3 -9
  87. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
  88. data/lib/kward/prompt_interface/file_overlay.rb +2 -2
  89. data/lib/kward/prompt_interface/git_prompt.rb +7 -3
  90. data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
  91. data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
  92. data/lib/kward/prompt_interface/interactive/state.rb +2 -2
  93. data/lib/kward/prompt_interface/project_browser.rb +25 -6
  94. data/lib/kward/prompt_interface/runtime_state.rb +22 -6
  95. data/lib/kward/prompt_interface/screen.rb +3 -0
  96. data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
  97. data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
  98. data/lib/kward/prompt_interface.rb +71 -12
  99. data/lib/kward/prompts/commands.rb +3 -1
  100. data/lib/kward/prompts.rb +22 -10
  101. data/lib/kward/rpc/auth_manager.rb +112 -152
  102. data/lib/kward/rpc/config_manager.rb +46 -6
  103. data/lib/kward/rpc/plugin_chat_manager.rb +56 -194
  104. data/lib/kward/rpc/prompt_bridge.rb +8 -4
  105. data/lib/kward/rpc/redactor.rb +1 -1
  106. data/lib/kward/rpc/server.rb +102 -14
  107. data/lib/kward/rpc/session_manager.rb +151 -38
  108. data/lib/kward/sandbox/capabilities.rb +39 -0
  109. data/lib/kward/sandbox/command_runner.rb +28 -0
  110. data/lib/kward/sandbox/environment.rb +24 -0
  111. data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
  112. data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
  113. data/lib/kward/sandbox/passthrough_runner.rb +13 -0
  114. data/lib/kward/sandbox/policy.rb +74 -0
  115. data/lib/kward/sandbox/runner_factory.rb +55 -0
  116. data/lib/kward/sandbox/unavailable_runner.rb +21 -0
  117. data/lib/kward/sandbox.rb +9 -0
  118. data/lib/kward/session_store.rb +110 -24
  119. data/lib/kward/skills/capture.rb +144 -0
  120. data/lib/kward/tab_driver.rb +6 -3
  121. data/lib/kward/tools/git_commit.rb +39 -0
  122. data/lib/kward/tools/registry.rb +26 -6
  123. data/lib/kward/tools/tool_call.rb +1 -0
  124. data/lib/kward/transport/gateway.rb +253 -0
  125. data/lib/kward/transport/host.rb +343 -0
  126. data/lib/kward/transport/manager.rb +179 -0
  127. data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
  128. data/lib/kward/transport/runtime.rb +41 -0
  129. data/lib/kward/transport/store.rb +101 -0
  130. data/lib/kward/transport.rb +204 -0
  131. data/lib/kward/version.rb +1 -1
  132. data/lib/kward/workspace.rb +18 -3
  133. data/lib/kward/workspace_factory.rb +34 -0
  134. data/templates/default/fulldoc/html/js/kward.js +3 -0
  135. data/templates/default/kward_navigation.rb +8 -1
  136. data/templates/default/layout/html/setup.rb +5 -0
  137. metadata +41 -2
@@ -40,14 +40,30 @@ module Kward
40
40
 
41
41
  def tick_footer_locked
42
42
  return false unless @footer && @started && @asking
43
+ return false unless footer_refresh_due?
43
44
 
44
- now = monotonic_now
45
- elapsed = now - @last_footer_refresh
46
- return false if elapsed < FOOTER_REFRESH_INTERVAL
45
+ previous_text = @cached_footer_text
46
+ refresh_footer_text_locked
47
+ previous_text != @cached_footer_text
48
+ end
47
49
 
48
- steps = (elapsed / FOOTER_REFRESH_INTERVAL).floor
49
- @last_footer_refresh += steps * FOOTER_REFRESH_INTERVAL
50
- true
50
+ def cached_footer_text
51
+ return "" unless @footer
52
+
53
+ refresh_footer_text_locked if footer_refresh_due?
54
+ @cached_footer_text.to_s
55
+ end
56
+
57
+ def footer_refresh_due?
58
+ @last_footer_refresh.nil? || monotonic_now - @last_footer_refresh >= FOOTER_REFRESH_INTERVAL
59
+ end
60
+
61
+ def refresh_footer_text_locked
62
+ @cached_footer_text = @footer.call.to_s.gsub(/\s+/, " ").strip
63
+ rescue StandardError
64
+ @cached_footer_text = ""
65
+ ensure
66
+ @last_footer_refresh = monotonic_now
51
67
  end
52
68
 
53
69
  def cached_composer_status_text
@@ -114,6 +114,9 @@ module Kward
114
114
  @reserved_rows = 0
115
115
  @last_width = current_width
116
116
  @last_height = current_height
117
+ if interactive_active_locked?
118
+ @interactive_state[:controller].resize(width: interactive_canvas_width(current_width))
119
+ end
117
120
  redraw_screen_locked(width: current_width, height: current_height)
118
121
  true
119
122
  end
@@ -626,7 +626,13 @@ module Kward
626
626
 
627
627
  def selection_matches
628
628
  choices = @select_state ? @select_state[:choices] : []
629
- filter = (select_search_active? || !composer_input.strip.empty?) ? composer_input.downcase.strip : ""
629
+ filter = if select_input_active?
630
+ ""
631
+ elsif select_search_active? || !composer_input.strip.empty?
632
+ composer_input.downcase.strip
633
+ else
634
+ ""
635
+ end
630
636
  matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
631
637
  clamp_selection_index(matches.length)
632
638
  matches
@@ -4,10 +4,14 @@ module Kward
4
4
  class PromptInterface
5
5
  # Bounded in-memory transcript buffer used by the prompt interface.
6
6
  class TranscriptBuffer
7
+ TRIM_RATIO = 0.9
8
+ TRIM_BOUNDARY_SEARCH = 1_024
9
+
7
10
  attr_reader :text
8
11
 
9
12
  def initialize(limit:)
10
- @limit = limit
13
+ @limit = [limit.to_i, 1].max
14
+ @trim_target = [(@limit * TRIM_RATIO).floor, 1].max
11
15
  @text = +""
12
16
  @display_rows_cache_width = nil
13
17
  @display_rows_cache = nil
@@ -42,7 +46,7 @@ module Kward
42
46
 
43
47
  def append(text)
44
48
  @text << ANSI.sanitize_transcript(text)
45
- @text = @text[-@limit, @limit] if @text.length > @limit
49
+ trim_to_limit if @text.length > @limit
46
50
  invalidate_display_rows_cache
47
51
  @text
48
52
  end
@@ -77,6 +81,24 @@ module Kward
77
81
  @display_rows_cache_width = nil
78
82
  @display_rows_cache = nil
79
83
  end
84
+
85
+ private
86
+
87
+ def trim_to_limit
88
+ cutoff = @text.length - @trim_target
89
+ newline = @text.index("\n", cutoff)
90
+ cutoff = newline + 1 if newline && newline - cutoff <= TRIM_BOUNDARY_SEARCH
91
+ cutoff = safe_escape_boundary(cutoff)
92
+ @text = @text[cutoff..].to_s
93
+ end
94
+
95
+ def safe_escape_boundary(cutoff)
96
+ escape_start = @text.rindex("\e", cutoff)
97
+ return cutoff unless escape_start
98
+
99
+ escape = ANSI.escape_sequence_at(@text, escape_start)
100
+ escape && escape_start + escape.length > cutoff ? escape_start + escape.length : cutoff
101
+ end
80
102
  end
81
103
  end
82
104
  end
@@ -128,12 +128,13 @@ module Kward
128
128
  end
129
129
  end
130
130
 
131
- def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil)
131
+ def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil)
132
132
  @input_io = input
133
133
  @output_io = output
134
134
  @reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
135
135
  @mutex = Mutex.new
136
136
  @prompt_history = prompt_history
137
+ @workspace_root = File.expand_path(workspace_root.to_s.empty? ? Dir.pwd : workspace_root)
137
138
  @composer = ComposerState.new
138
139
  load_history(@prompt_history.values) if @prompt_history
139
140
  self.composer_input = @composer.input
@@ -146,7 +147,8 @@ module Kward
146
147
  @steered_count = 0
147
148
  @spinner_frame_index = 0
148
149
  @last_spinner_tick = monotonic_now
149
- @last_footer_refresh = monotonic_now
150
+ @last_footer_refresh = nil
151
+ @cached_footer_text = nil
150
152
  @last_composer_status_refresh = 0.0
151
153
  @cached_composer_status_text = nil
152
154
  @prompt_label = "You>"
@@ -340,10 +342,23 @@ module Kward
340
342
  @mutex.synchronize { editor_active? }
341
343
  end
342
344
 
343
- def edit_file(path, base_dir: Dir.pwd, allow_new: true)
345
+ def update_workspace_root(root, prompt_history: nil)
346
+ @mutex.synchronize do
347
+ expanded_root = File.expand_path(root.to_s.empty? ? Dir.pwd : root)
348
+ return false if expanded_root == @workspace_root
349
+
350
+ @workspace_root = expanded_root
351
+ @prompt_history = prompt_history if prompt_history
352
+ load_history(@prompt_history.values) if @prompt_history
353
+ reset_workspace_file_state_locked
354
+ true
355
+ end
356
+ end
357
+
358
+ def edit_file(path, base_dir: nil, allow_new: true)
344
359
  start(render: false)
345
360
  opened = @mutex.synchronize do
346
- open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
361
+ open_editor(path, allow_new: allow_new, base_dir: base_dir || prompt_workspace_root, restrict_to_workspace: false).tap do
347
362
  render_prompt_locked
348
363
  end
349
364
  end
@@ -364,6 +379,24 @@ module Kward
364
379
  run_editor
365
380
  end
366
381
 
382
+ # Opens an in-memory document editor. The callback receives edited content
383
+ # when the user saves; return an error message to keep the editor open.
384
+ def review_document(title:, content:, &on_save)
385
+ raise ArgumentError, "review_document requires a save callback" unless on_save
386
+
387
+ start(render: false)
388
+ opened = @mutex.synchronize do
389
+ @editor_save_callback = on_save
390
+ open_scratchpad(:markdown, content: content).tap do
391
+ @editor_state.status = "#{title} · Ctrl+S save · Ctrl+Q cancel"
392
+ render_prompt_locked
393
+ end
394
+ end
395
+ return false unless opened
396
+
397
+ run_editor
398
+ end
399
+
367
400
  def run_editor
368
401
  loop do
369
402
  key = read_key(nonblock: true)
@@ -592,9 +625,14 @@ module Kward
592
625
 
593
626
  def update_tabs(labels:, active_index: 0)
594
627
  @mutex.synchronize do
595
- @tabs = Array(labels).map { |label| normalize_tab_label(label) }
596
- @active_tab_index = active_index.to_i
628
+ normalized_labels = Array(labels).map { |label| normalize_tab_label(label) }
629
+ normalized_index = active_index.to_i
630
+ return false if @tabs == normalized_labels && @active_tab_index == normalized_index
631
+
632
+ @tabs = normalized_labels
633
+ @active_tab_index = normalized_index
597
634
  render_prompt_locked if @started && @asking
635
+ true
598
636
  end
599
637
  end
600
638
 
@@ -607,16 +645,19 @@ module Kward
607
645
  end
608
646
  end
609
647
 
610
- def tab_view_snapshot
648
+ def tab_view_snapshot(include_transcript: true)
611
649
  @mutex.synchronize do
612
- {
650
+ snapshot = {
613
651
  composer: @composer.dup,
614
652
  prompt_label: @prompt_label.dup,
615
- editor_state: @editor_state&.dup,
616
- transcript_buffer: @transcript_buffer.dup,
617
- transcript_viewport_rows: @transcript_viewport_rows,
618
- stream_state: @stream_state.dup
653
+ editor_state: @editor_state&.dup
619
654
  }
655
+ if include_transcript
656
+ snapshot[:transcript_buffer] = @transcript_buffer.dup
657
+ snapshot[:transcript_viewport_rows] = @transcript_viewport_rows
658
+ snapshot[:stream_state] = @stream_state.dup
659
+ end
660
+ snapshot
620
661
  end
621
662
  end
622
663
 
@@ -841,6 +882,8 @@ module Kward
841
882
 
842
883
  def refresh_composer_status
843
884
  @mutex.synchronize do
885
+ @cached_footer_text = nil
886
+ @last_footer_refresh = nil
844
887
  @cached_composer_status_text = nil
845
888
  @last_composer_status_refresh = 0.0
846
889
  render_prompt_locked if @started && @asking
@@ -861,6 +904,22 @@ module Kward
861
904
 
862
905
  private
863
906
 
907
+ def prompt_workspace_root
908
+ @workspace_root
909
+ end
910
+
911
+ def reset_workspace_file_state_locked
912
+ @file_mention_paths = nil
913
+ @file_mention_path_entries_paths = nil
914
+ @file_mention_path_entries = nil
915
+ @project_browser_state = nil
916
+ @project_browser_tree_paths = nil
917
+ @project_browser_tree = nil
918
+ @file_overlay_dismissed_token = nil
919
+ @file_open_dismissed_token = nil
920
+ @file_editor_open_status = nil
921
+ end
922
+
864
923
  def modal_active_locked?
865
924
  @question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
866
925
  end
@@ -30,10 +30,12 @@ module Kward
30
30
  { name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
31
31
  { name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
32
32
  { name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
33
- { name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
33
+ { name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
34
34
  { name: "status", description: "Show the current status message.", argument_hint: "" },
35
35
  { name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
36
+ { name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
36
37
  { name: "hooks", description: "Inspect lifecycle hooks.", argument_hint: "[list|events|logs|doctor|trust|untrust]" },
38
+ { name: "skill", description: "Activate a skill or capture one from a saved session.", argument_hint: "<name>|capture" },
37
39
  { name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
38
40
  ].freeze
39
41
  BUILTIN_RESERVED_COMMAND_NAMES = BUILTIN_COMMANDS.map { |command| command[:name] }.freeze
data/lib/kward/prompts.rb CHANGED
@@ -22,23 +22,26 @@ module Kward
22
22
  # @param plugin_context [String, nil] trusted plugin-provided instructions
23
23
  # @return [Hash] role/content system message
24
24
  # @api public
25
- def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
25
+ def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
26
26
  {
27
27
  role: "system",
28
- content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context).compact.join("\n\n")
28
+ content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).compact.join("\n\n")
29
29
  }
30
30
  end
31
31
 
32
- def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
33
- prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context).map { |section| section[:content] }
32
+ def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
33
+ prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).map { |section| section[:content] }
34
34
  end
35
35
 
36
36
  # Returns labeled prompt sections for inspection by CLI and RPC frontends.
37
37
  #
38
38
  # @return [Array<Hash>] section hashes with label, content, and optional source
39
39
  # @api public
40
- def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
40
+ def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
41
+ return replacement_prompt_sections if ConfigFiles.replacement_system_prompt?
42
+
41
43
  sections = [prompt_section("Built-in system prompt", base_prompt)]
44
+ sections << prompt_section("Execution profile", execution_profile_context) unless execution_profile_context.to_s.empty?
42
45
  sections << prompt_section(config_agents_prompt_label, config_agents_prompt, source: config_agents_prompt_source)
43
46
  sections << prompt_section("Memory context", memory_context) unless memory_context.to_s.empty?
44
47
  if include_workspace_personality
@@ -53,11 +56,11 @@ module Kward
53
56
 
54
57
  def base_prompt
55
58
  <<~PROMPT.strip
56
- You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
59
+ You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Only call tools advertised for the current turn. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
57
60
 
58
- For web research, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on.
61
+ When web tools are available, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on. Use code_search for package, GitHub repository, and source-code research.
59
62
 
60
- Manage code context deliberately. Prefer context_for_task, summarize_file_structure, and read_file mode="outline"/"preview" before broad reads. Escalate to read_file mode="range" for exact lines, and use mode="full" only when focused context is insufficient. Use context_budget_stats when asked about context savings.
63
+ Manage code context deliberately. Prefer context_for_task, summarize_file_structure, and read_file mode="outline"/"preview" before broad reads. Escalate to read_file mode="range" for exact lines, and use mode="full" only when focused context is insufficient. If output is compacted or replaced with a duplicate reference, use retrieve_tool_output to inspect the needed original detail. Use context_budget_stats when asked about context savings. When a material requirement is ambiguous, use ask_user_question when it is available; otherwise state the assumption.
61
64
  PROMPT
62
65
  end
63
66
 
@@ -73,6 +76,7 @@ module Kward
73
76
  end
74
77
 
75
78
  def config_agents_prompt_source
79
+ return nil unless ConfigFiles.include_config_principles?
76
80
  return ConfigFiles.config_principles_path if File.exist?(ConfigFiles.config_principles_path)
77
81
  return ConfigFiles.config_agents_path if File.exist?(ConfigFiles.config_agents_path)
78
82
 
@@ -112,6 +116,14 @@ module Kward
112
116
  "Workspace AGENTS.md hint"
113
117
  end
114
118
 
119
+ def replacement_prompt_sections
120
+ path = ConfigFiles.system_prompt_file_path
121
+ content = ConfigFiles.system_prompt_file
122
+ return [] if content.to_s.empty?
123
+
124
+ [prompt_section("Custom system prompt (replacement)", content, source: path)]
125
+ end
126
+
115
127
  def prompt_section(label, content, source: nil)
116
128
  return nil if content.to_s.empty?
117
129
 
@@ -123,8 +135,8 @@ module Kward
123
135
 
124
136
  lines = [
125
137
  "Agent Skills are available.",
126
- "When a task matches a skill's description, use read_skill to load its instructions before proceeding.",
127
- "Use read_skill with a relative path to read referenced files inside a skill directory.",
138
+ "When read_skill is available and a task matches a skill's description, use it to load the instructions before proceeding.",
139
+ "When using read_skill, use a relative path to read referenced files inside a skill directory.",
128
140
  "Available skills:"
129
141
  ]
130
142
  skills.each do |skill|