kward 0.80.0 → 0.80.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +2 -2
- data/doc/skills.md +2 -0
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/prompt_interface.rb +37 -1
- data/lib/kward/cli/runtime_helpers.rb +1 -0
- data/lib/kward/cli/slash_commands.rb +1 -1
- data/lib/kward/cli/tabs.rb +1 -0
- data/lib/kward/cli.rb +2 -0
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +25 -9
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/plugin_registry.rb +20 -13
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +35 -10
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/skills/registry.rb +16 -11
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dceefb01256daa642d6fe1d06d2e332cac1154c93072524b4aa5e98779a26807
|
|
4
|
+
data.tar.gz: cc8b3c0f226c0b58ea5704a613f84a39d93c5da3ae4d4b9769c891aad2d704d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e433643946a04c43fc833edb1a7a550d4333a4184e5c68e5a54b8ab8f24317821587176fc85337a5682fee8caa87cc0f404cf2717490338fc3d9f4988c6e3c4
|
|
7
|
+
data.tar.gz: 3ea451ab6978d7e726270de4d0e6bc14e1c5a5b6792bb00de551cc137d0e1875579da03437eb2687d8e1d8d93e099ba87a1bfd42fdd18070ddf215fc7ad88ed7
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kward will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.80.1] - 2026-07-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Avoided repeated full-buffer syntax scans while rendering generic-language editor lines, keeping Python and other supported languages responsive during cursor movement.
|
|
10
|
+
- Routed interactive configuration, plugin, skill, compaction, memory, and logging warnings through synchronized `Runtime>` output so diagnostics cannot corrupt the terminal layout.
|
|
11
|
+
|
|
5
12
|
## [0.80.0] - 2026-07-27
|
|
6
13
|
|
|
7
14
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kward (0.80.
|
|
4
|
+
kward (0.80.1)
|
|
5
5
|
base64
|
|
6
6
|
nokogiri
|
|
7
7
|
tiktoken_ruby
|
|
@@ -146,7 +146,7 @@ CHECKSUMS
|
|
|
146
146
|
html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd
|
|
147
147
|
io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
|
|
148
148
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
149
|
-
kward (0.80.
|
|
149
|
+
kward (0.80.1)
|
|
150
150
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
151
151
|
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
|
152
152
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
data/doc/skills.md
CHANGED
|
@@ -92,6 +92,8 @@ Or set it manually:
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
In the interactive TUI, skipped or malformed skill diagnostics are shown as synchronized `Runtime>` messages so they do not interrupt screen rendering. Other interactive warnings use the same channel; non-interactive commands continue to report diagnostics on stderr.
|
|
96
|
+
|
|
95
97
|
## How Kward uses skills
|
|
96
98
|
|
|
97
99
|
Kward uses progressive loading:
|
data/lib/kward/agent.rb
CHANGED
|
@@ -28,11 +28,12 @@ module Kward
|
|
|
28
28
|
# lowest layer that owns the behavior, and use `Agent` only for cross-step turn
|
|
29
29
|
# coordination.
|
|
30
30
|
class Agent
|
|
31
|
-
def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger:
|
|
31
|
+
def initialize(client:, tool_registry: ToolRegistry.new, conversation: Conversation.new, telemetry_logger: nil, warning_sink: nil, hook_manager: nil, hook_context: nil)
|
|
32
32
|
@client = client
|
|
33
33
|
@tool_registry = tool_registry
|
|
34
34
|
@conversation = conversation
|
|
35
|
-
@
|
|
35
|
+
@warning_sink = warning_sink
|
|
36
|
+
@telemetry_logger = telemetry_logger || TelemetryLogger.new(warning_sink: warning_sink)
|
|
36
37
|
@hook_manager = hook_manager
|
|
37
38
|
@hook_context = hook_context
|
|
38
39
|
end
|
|
@@ -191,9 +192,9 @@ module Kward
|
|
|
191
192
|
|
|
192
193
|
def auto_compact_if_needed
|
|
193
194
|
context_window = @client.current_context_window if @client.respond_to?(:current_context_window)
|
|
194
|
-
Compactor.new(conversation: @conversation, client: @client).auto_compact_if_needed(context_window: context_window)
|
|
195
|
+
Compactor.new(conversation: @conversation, client: @client, warning_sink: @warning_sink).auto_compact_if_needed(context_window: context_window)
|
|
195
196
|
rescue StandardError => e
|
|
196
|
-
|
|
197
|
+
emit_warning "Auto-compaction failed: #{e.message}"
|
|
197
198
|
nil
|
|
198
199
|
end
|
|
199
200
|
|
|
@@ -201,14 +202,18 @@ module Kward
|
|
|
201
202
|
settings = Compaction::Settings.from_config
|
|
202
203
|
return nil unless settings.enabled
|
|
203
204
|
|
|
204
|
-
Compactor.new(conversation: @conversation, client: @client, settings: settings).compact(
|
|
205
|
+
Compactor.new(conversation: @conversation, client: @client, settings: settings, warning_sink: @warning_sink).compact(
|
|
205
206
|
custom_instructions: "The previous model request exceeded the context window. Preserve the current task state and critical details needed to retry."
|
|
206
207
|
)
|
|
207
208
|
rescue Compaction::NothingToCompact, Compaction::AlreadyCompacted, StandardError => compaction_error
|
|
208
|
-
|
|
209
|
+
emit_warning "Context overflow recovery failed: #{compaction_error.message}; original error: #{error.message}"
|
|
209
210
|
nil
|
|
210
211
|
end
|
|
211
212
|
|
|
213
|
+
def emit_warning(message)
|
|
214
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
215
|
+
end
|
|
216
|
+
|
|
212
217
|
def chat(on_reasoning_delta: nil, on_retry: nil, cancellation: nil, steering: nil, options: {}, tool_registry: nil)
|
|
213
218
|
reasoning_delta = lambda do |delta|
|
|
214
219
|
cancellation&.raise_if_cancelled!
|
|
@@ -104,7 +104,7 @@ module Kward
|
|
|
104
104
|
def prepare_memory_context(conversation, input)
|
|
105
105
|
Memory::TurnContext.apply(conversation: conversation, input: input)
|
|
106
106
|
rescue StandardError => e
|
|
107
|
-
|
|
107
|
+
emit_warning "Memory retrieval failed: #{e.message}"
|
|
108
108
|
nil
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -120,7 +120,7 @@ module Kward
|
|
|
120
120
|
|
|
121
121
|
summarize_memory(conversation, manager: manager)
|
|
122
122
|
rescue StandardError => e
|
|
123
|
-
|
|
123
|
+
emit_warning "Memory auto-summary failed: #{e.message}"
|
|
124
124
|
nil
|
|
125
125
|
end
|
|
126
126
|
|
|
@@ -15,6 +15,8 @@ module Kward
|
|
|
15
15
|
prompt_interface = load_prompt_interface
|
|
16
16
|
return unless prompt_interface
|
|
17
17
|
|
|
18
|
+
@interactive_warning_sink_active = true
|
|
19
|
+
ConfigFiles.warning_sink = interactive_warning_sink
|
|
18
20
|
@prompt = prompt_interface.new(
|
|
19
21
|
slash_commands: slash_command_entries,
|
|
20
22
|
overlay_settings: ConfigFiles.overlay_settings,
|
|
@@ -48,6 +50,28 @@ module Kward
|
|
|
48
50
|
else
|
|
49
51
|
@prompt.start
|
|
50
52
|
end
|
|
53
|
+
flush_interactive_warnings
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def interactive_warning_sink
|
|
57
|
+
@interactive_warning_sink ||= lambda do |message|
|
|
58
|
+
unless @interactive_warning_sink_active
|
|
59
|
+
warn message
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if prompt_interface?
|
|
64
|
+
runtime_output(message)
|
|
65
|
+
else
|
|
66
|
+
(@pending_interactive_warnings ||= []) << message
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def flush_interactive_warnings
|
|
72
|
+
warnings = @pending_interactive_warnings
|
|
73
|
+
@pending_interactive_warnings = []
|
|
74
|
+
Array(warnings).each { |message| runtime_output(message) }
|
|
51
75
|
end
|
|
52
76
|
|
|
53
77
|
def load_prompt_interface
|
|
@@ -68,6 +92,18 @@ module Kward
|
|
|
68
92
|
@prompt.respond_to?(:start_stream_block) && @prompt.respond_to?(:write_delta)
|
|
69
93
|
end
|
|
70
94
|
|
|
95
|
+
def emit_warning(message)
|
|
96
|
+
sink = ConfigFiles.warning_sink
|
|
97
|
+
sink ? sink.call(message) : warn(message)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def clear_interactive_warning_sink
|
|
101
|
+
ConfigFiles.warning_sink = nil
|
|
102
|
+
@interactive_warning_sink_active = false
|
|
103
|
+
@pending_interactive_warnings = nil
|
|
104
|
+
@interactive_warning_sink = nil
|
|
105
|
+
end
|
|
106
|
+
|
|
71
107
|
def update_prompt_workspace_root(root)
|
|
72
108
|
return unless @prompt.respond_to?(:update_workspace_root)
|
|
73
109
|
|
|
@@ -179,7 +215,7 @@ module Kward
|
|
|
179
215
|
context = plugin_context(current_footer_conversation, "")
|
|
180
216
|
renderer.call(context).to_s
|
|
181
217
|
rescue StandardError => e
|
|
182
|
-
|
|
218
|
+
emit_warning "Warning: Kward plugin footer error: #{e.message}"
|
|
183
219
|
""
|
|
184
220
|
end
|
|
185
221
|
end
|
|
@@ -345,7 +345,7 @@ module Kward
|
|
|
345
345
|
percent = ((reserve_tokens.to_f / context_window.to_i) * 100).round(1)
|
|
346
346
|
"Auto-compaction reserve: #{reserve_tokens} tokens (#{percent}% of #{context_window})"
|
|
347
347
|
rescue StandardError => e
|
|
348
|
-
|
|
348
|
+
emit_warning "Auto-compaction status unavailable: #{e.message}"
|
|
349
349
|
nil
|
|
350
350
|
end
|
|
351
351
|
|
data/lib/kward/cli/tabs.rb
CHANGED
data/lib/kward/cli.rb
CHANGED
|
@@ -333,6 +333,7 @@ module Kward
|
|
|
333
333
|
@prompt.edit_file(path, base_dir: Dir.pwd, allow_new: true)
|
|
334
334
|
ensure
|
|
335
335
|
@prompt.close if @prompt.respond_to?(:close) && prompt_interface?
|
|
336
|
+
clear_interactive_warning_sink if respond_to?(:clear_interactive_warning_sink, true)
|
|
336
337
|
end
|
|
337
338
|
|
|
338
339
|
def run_prompt_or_interactive
|
|
@@ -546,6 +547,7 @@ module Kward
|
|
|
546
547
|
stop_tabs if respond_to?(:stop_tabs, true)
|
|
547
548
|
@prompt.close if prompt_interface?
|
|
548
549
|
ensure
|
|
550
|
+
clear_interactive_warning_sink if respond_to?(:clear_interactive_warning_sink, true)
|
|
549
551
|
cleanup_unused_sessions
|
|
550
552
|
remember_active_session(session_store)
|
|
551
553
|
end
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -734,9 +734,10 @@ module Kward
|
|
|
734
734
|
AUTO_COMPACTION_EXTRA_GUARD_FLOOR = 12_000
|
|
735
735
|
|
|
736
736
|
# Creates an object for conversation compaction.
|
|
737
|
-
def initialize(conversation:, client:, tool_result_summarizer: nil, settings: nil, summarizer: nil)
|
|
737
|
+
def initialize(conversation:, client:, tool_result_summarizer: nil, settings: nil, summarizer: nil, warning_sink: nil)
|
|
738
738
|
@conversation = conversation
|
|
739
739
|
@client = client
|
|
740
|
+
@warning_sink = warning_sink
|
|
740
741
|
@settings = settings || Compaction::Settings.from_config
|
|
741
742
|
@prompt_builder = Compaction::PromptBuilder.new(
|
|
742
743
|
serializer: Compaction::ConversationSerializer.new(tool_result_summarizer: tool_result_summarizer)
|
|
@@ -791,7 +792,7 @@ module Kward
|
|
|
791
792
|
rescue Compaction::NothingToCompact, Compaction::AlreadyCompacted
|
|
792
793
|
nil
|
|
793
794
|
rescue StandardError => e
|
|
794
|
-
|
|
795
|
+
emit_warning "Auto-compaction failed: #{e.message}"
|
|
795
796
|
nil
|
|
796
797
|
end
|
|
797
798
|
|
|
@@ -814,6 +815,10 @@ module Kward
|
|
|
814
815
|
|
|
815
816
|
private
|
|
816
817
|
|
|
818
|
+
def emit_warning(message)
|
|
819
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
820
|
+
end
|
|
821
|
+
|
|
817
822
|
def prepare
|
|
818
823
|
@conversation.refresh_system_message_if_workspace_agents_changed!
|
|
819
824
|
Compaction::Preparation.new(conversation: @conversation, settings: @settings).call
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -66,6 +66,7 @@ module Kward
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
@skip_config = false
|
|
69
|
+
@warning_sink = nil
|
|
69
70
|
|
|
70
71
|
module_function
|
|
71
72
|
|
|
@@ -73,6 +74,14 @@ module Kward
|
|
|
73
74
|
@skip_config = value
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def warning_sink=(sink)
|
|
78
|
+
@warning_sink = sink
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def warning_sink
|
|
82
|
+
@warning_sink
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
def skip_config?
|
|
77
86
|
@skip_config == true
|
|
78
87
|
end
|
|
@@ -791,13 +800,13 @@ module Kward
|
|
|
791
800
|
|
|
792
801
|
size = File.size(path)
|
|
793
802
|
if size > MAX_PROMPT_FILE_BYTES
|
|
794
|
-
|
|
803
|
+
emit_warning "Warning: skipping #{label} #{path}: file too large (#{size} bytes; limit is #{MAX_PROMPT_FILE_BYTES} bytes)"
|
|
795
804
|
return nil
|
|
796
805
|
end
|
|
797
806
|
|
|
798
807
|
File.read(path)
|
|
799
808
|
rescue StandardError => e
|
|
800
|
-
|
|
809
|
+
emit_warning "Warning: skipping #{label} #{path}: #{e.message}"
|
|
801
810
|
nil
|
|
802
811
|
end
|
|
803
812
|
|
|
@@ -908,8 +917,8 @@ module Kward
|
|
|
908
917
|
# Lists configured skills discovered under the config directory.
|
|
909
918
|
#
|
|
910
919
|
# @return [Array<Skill>] skill metadata available to the model
|
|
911
|
-
def skills(workspace_root: Dir.pwd)
|
|
912
|
-
skills_registry(workspace_root: workspace_root).skills
|
|
920
|
+
def skills(workspace_root: Dir.pwd, warning_sink: nil)
|
|
921
|
+
skills_registry(workspace_root: workspace_root, warning_sink: warning_sink).skills
|
|
913
922
|
end
|
|
914
923
|
|
|
915
924
|
# @return [String] trusted user plugin directory
|
|
@@ -923,7 +932,7 @@ module Kward
|
|
|
923
932
|
# workspace or custom `KWARD_CONFIG_PATH` directory.
|
|
924
933
|
#
|
|
925
934
|
# @return [Array<String>] sorted plugin file paths
|
|
926
|
-
def plugin_paths
|
|
935
|
+
def plugin_paths(warning_sink: nil)
|
|
927
936
|
plugins_root = plugin_dir
|
|
928
937
|
return [] unless Dir.exist?(plugins_root)
|
|
929
938
|
|
|
@@ -931,7 +940,7 @@ module Kward
|
|
|
931
940
|
paths.concat(Dir.glob(File.join(plugins_root, "*", "plugin.rb")))
|
|
932
941
|
paths.sort
|
|
933
942
|
rescue StandardError => e
|
|
934
|
-
|
|
943
|
+
emit_warning("Warning: skipping Kward plugins in #{plugins_root}: #{e.message}", warning_sink: warning_sink)
|
|
935
944
|
[]
|
|
936
945
|
end
|
|
937
946
|
|
|
@@ -952,7 +961,7 @@ module Kward
|
|
|
952
961
|
skills_registry(workspace_root: workspace_root).read_skill_file(name, relative_path)
|
|
953
962
|
end
|
|
954
963
|
|
|
955
|
-
def skills_registry(workspace_root: Dir.pwd)
|
|
964
|
+
def skills_registry(workspace_root: Dir.pwd, warning_sink: nil)
|
|
956
965
|
Skills::Registry.new(
|
|
957
966
|
config_dir: config_dir,
|
|
958
967
|
workspace_root: workspace_root,
|
|
@@ -960,7 +969,8 @@ module Kward
|
|
|
960
969
|
skill_class: Skill,
|
|
961
970
|
max_file_bytes: MAX_SKILL_FILE_BYTES,
|
|
962
971
|
markdown_parser: ->(path) { Frontmatter.markdown_parts(path, lenient: true) },
|
|
963
|
-
inside_directory: method(:inside_directory?)
|
|
972
|
+
inside_directory: method(:inside_directory?),
|
|
973
|
+
warning_sink: warning_sink || @warning_sink
|
|
964
974
|
)
|
|
965
975
|
end
|
|
966
976
|
|
|
@@ -968,10 +978,16 @@ module Kward
|
|
|
968
978
|
Prompts::Templates.new(
|
|
969
979
|
config_dir: config_dir,
|
|
970
980
|
template_class: PromptTemplate,
|
|
971
|
-
markdown_parser: ->(path) { Frontmatter.markdown_parts(path) }
|
|
981
|
+
markdown_parser: ->(path) { Frontmatter.markdown_parts(path) },
|
|
982
|
+
warning_sink: @warning_sink
|
|
972
983
|
)
|
|
973
984
|
end
|
|
974
985
|
|
|
986
|
+
def emit_warning(message, warning_sink: nil)
|
|
987
|
+
sink = warning_sink || @warning_sink
|
|
988
|
+
sink ? sink.call(message) : warn(message)
|
|
989
|
+
end
|
|
990
|
+
|
|
975
991
|
def inside_directory?(path, base)
|
|
976
992
|
PathGuard.inside?(path, base)
|
|
977
993
|
end
|
|
@@ -11,13 +11,14 @@ module Kward
|
|
|
11
11
|
class AuditLog
|
|
12
12
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
|
13
13
|
|
|
14
|
-
def initialize(path: nil, config_path: ConfigFiles.config_path, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr)
|
|
14
|
+
def initialize(path: nil, config_path: ConfigFiles.config_path, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr, warning_sink: nil)
|
|
15
15
|
@path = path
|
|
16
16
|
@config_path = config_path
|
|
17
17
|
@max_bytes = max_bytes.to_i.positive? ? max_bytes.to_i : DEFAULT_MAX_BYTES
|
|
18
18
|
@clock = clock
|
|
19
19
|
@monotonic_clock = monotonic_clock
|
|
20
20
|
@error_output = error_output
|
|
21
|
+
@warning_sink = warning_sink
|
|
21
22
|
@mutex = Mutex.new
|
|
22
23
|
@warned = false
|
|
23
24
|
end
|
|
@@ -112,7 +113,9 @@ module Kward
|
|
|
112
113
|
return if @warned
|
|
113
114
|
|
|
114
115
|
@warned = true
|
|
115
|
-
|
|
116
|
+
message = "Warning: hook audit logging failed: #{error.message}"
|
|
117
|
+
sink = @warning_sink || ConfigFiles.warning_sink
|
|
118
|
+
sink ? sink.call(message) : @error_output&.puts(message)
|
|
116
119
|
rescue StandardError
|
|
117
120
|
nil
|
|
118
121
|
end
|
|
@@ -313,16 +313,19 @@ module Kward
|
|
|
313
313
|
class << self
|
|
314
314
|
attr_accessor :loading_registry, :loading_path
|
|
315
315
|
|
|
316
|
-
def load(paths:
|
|
317
|
-
|
|
316
|
+
def load(paths: nil, reserved_commands: [], warning_sink: nil)
|
|
317
|
+
warning_sink ||= ConfigFiles.warning_sink
|
|
318
|
+
paths ||= ConfigFiles.plugin_paths(warning_sink: warning_sink)
|
|
319
|
+
registry = new(reserved_commands: reserved_commands, warning_sink: warning_sink)
|
|
318
320
|
paths.each { |path| registry.load_file(path) }
|
|
319
321
|
registry
|
|
320
322
|
end
|
|
321
323
|
end
|
|
322
324
|
|
|
323
325
|
# Creates an object for trusted plugin loading and dispatch.
|
|
324
|
-
def initialize(reserved_commands: [])
|
|
326
|
+
def initialize(reserved_commands: [], warning_sink: nil)
|
|
325
327
|
@reserved_commands = reserved_commands.map(&:to_s)
|
|
328
|
+
@warning_sink = warning_sink
|
|
326
329
|
@commands = {}
|
|
327
330
|
@interactive_commands = {}
|
|
328
331
|
@tab_types = {}
|
|
@@ -419,7 +422,7 @@ module Kward
|
|
|
419
422
|
rendered = entry[:renderer].call(context)
|
|
420
423
|
parts << rendered.to_s unless rendered.to_s.empty?
|
|
421
424
|
rescue StandardError => e
|
|
422
|
-
|
|
425
|
+
emit_warning "Warning: Kward plugin prompt context error in #{entry[:path]}: #{e.message}"
|
|
423
426
|
end
|
|
424
427
|
parts.empty? ? nil : parts.join("\n\n")
|
|
425
428
|
end
|
|
@@ -431,7 +434,7 @@ module Kward
|
|
|
431
434
|
@transcript_event_handlers.each do |entry|
|
|
432
435
|
entry[:handler].call(transcript_event, context)
|
|
433
436
|
rescue StandardError => e
|
|
434
|
-
|
|
437
|
+
emit_warning "Warning: Kward plugin transcript event error in #{entry[:path]}: #{e.message}"
|
|
435
438
|
end
|
|
436
439
|
nil
|
|
437
440
|
end
|
|
@@ -444,7 +447,7 @@ module Kward
|
|
|
444
447
|
Kernel.load(path, true)
|
|
445
448
|
@paths << path
|
|
446
449
|
rescue StandardError => e
|
|
447
|
-
|
|
450
|
+
emit_warning "Warning: skipping Kward plugin #{path}: #{e.message}"
|
|
448
451
|
ensure
|
|
449
452
|
self.class.loading_registry = previous_registry
|
|
450
453
|
self.class.loading_path = previous_path
|
|
@@ -462,11 +465,11 @@ module Kward
|
|
|
462
465
|
raise "Plugin command /#{name} requires a handler" unless handler
|
|
463
466
|
|
|
464
467
|
if @reserved_commands.include?(name)
|
|
465
|
-
|
|
468
|
+
emit_warning "Warning: skipping Kward plugin command /#{name}: reserved command"
|
|
466
469
|
return nil
|
|
467
470
|
end
|
|
468
471
|
if @commands.key?(name)
|
|
469
|
-
|
|
472
|
+
emit_warning "Warning: skipping duplicate Kward plugin command /#{name}: #{path}"
|
|
470
473
|
return nil
|
|
471
474
|
end
|
|
472
475
|
|
|
@@ -485,11 +488,11 @@ module Kward
|
|
|
485
488
|
raise "Interactive command /#{name} requires a handler" unless handler
|
|
486
489
|
|
|
487
490
|
if @reserved_commands.include?(name) || @commands.key?(name)
|
|
488
|
-
|
|
491
|
+
emit_warning "Warning: skipping Kward interactive command /#{name}: reserved command"
|
|
489
492
|
return nil
|
|
490
493
|
end
|
|
491
494
|
if @interactive_commands.key?(name)
|
|
492
|
-
|
|
495
|
+
emit_warning "Warning: skipping duplicate Kward interactive command /#{name}: #{path}"
|
|
493
496
|
return nil
|
|
494
497
|
end
|
|
495
498
|
|
|
@@ -512,7 +515,7 @@ module Kward
|
|
|
512
515
|
raise "Plugin tab type #{name} requires a handler" unless handler
|
|
513
516
|
|
|
514
517
|
if @tab_types.key?(name) || @tab_types_by_id.key?(id)
|
|
515
|
-
|
|
518
|
+
emit_warning "Warning: skipping duplicate Kward plugin tab type #{id}: #{path}"
|
|
516
519
|
return nil
|
|
517
520
|
end
|
|
518
521
|
|
|
@@ -529,7 +532,7 @@ module Kward
|
|
|
529
532
|
raise "Plugin transport #{name} requires a handler" unless handler
|
|
530
533
|
|
|
531
534
|
if @transports.key?(name) || @transports_by_id.key?(id)
|
|
532
|
-
|
|
535
|
+
emit_warning "Warning: skipping duplicate Kward plugin transport #{id}: #{path}"
|
|
533
536
|
return nil
|
|
534
537
|
end
|
|
535
538
|
|
|
@@ -543,11 +546,15 @@ module Kward
|
|
|
543
546
|
def register_footer(path: nil, &renderer)
|
|
544
547
|
raise "Plugin footer requires a renderer" unless renderer
|
|
545
548
|
|
|
546
|
-
|
|
549
|
+
emit_warning "Warning: replacing Kward plugin footer from #{@footer_path}: #{path}" if @footer
|
|
547
550
|
@footer = renderer
|
|
548
551
|
@footer_path = path
|
|
549
552
|
end
|
|
550
553
|
|
|
554
|
+
def emit_warning(message)
|
|
555
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
556
|
+
end
|
|
557
|
+
|
|
551
558
|
def register_transcript_event(path: nil, &handler)
|
|
552
559
|
raise "Plugin transcript event requires a handler" unless handler
|
|
553
560
|
|
|
@@ -23,10 +23,12 @@ module Kward
|
|
|
23
23
|
LANGUAGE_DEFINITIONS = {
|
|
24
24
|
javascript: {
|
|
25
25
|
extensions: %w[.js .jsx .mjs .cjs],
|
|
26
|
+
block_comment: true,
|
|
26
27
|
keywords: %w[async await break case catch class const continue debugger default delete do else export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield]
|
|
27
28
|
},
|
|
28
29
|
typescript: {
|
|
29
30
|
extensions: %w[.ts .tsx],
|
|
31
|
+
block_comment: true,
|
|
30
32
|
keywords: %w[abstract any as async await boolean break case catch class const constructor continue debugger declare default delete do else enum export extends false finally for from function if implements import in infer instanceof interface is keyof let module namespace never new null number object of private protected public readonly return static string super switch symbol this throw true try type typeof undefined unknown var void while with yield]
|
|
31
33
|
},
|
|
32
34
|
shell: {
|
|
@@ -62,34 +64,42 @@ module Kward
|
|
|
62
64
|
},
|
|
63
65
|
go: {
|
|
64
66
|
extensions: %w[.go],
|
|
67
|
+
block_comment: true,
|
|
65
68
|
keywords: %w[break case chan const continue default defer else fallthrough false for func go goto if import interface map nil package range return select struct switch true type var]
|
|
66
69
|
},
|
|
67
70
|
rust: {
|
|
68
71
|
extensions: %w[.rs],
|
|
72
|
+
block_comment: true,
|
|
69
73
|
keywords: %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while]
|
|
70
74
|
},
|
|
71
75
|
java: {
|
|
72
76
|
extensions: %w[.java],
|
|
77
|
+
block_comment: true,
|
|
73
78
|
keywords: %w[abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient true try void volatile while]
|
|
74
79
|
},
|
|
75
80
|
csharp: {
|
|
76
81
|
extensions: %w[.cs],
|
|
82
|
+
block_comment: true,
|
|
77
83
|
keywords: %w[abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while var async await]
|
|
78
84
|
},
|
|
79
85
|
c: {
|
|
80
86
|
extensions: %w[.c .h],
|
|
87
|
+
block_comment: true,
|
|
81
88
|
keywords: %w[auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while]
|
|
82
89
|
},
|
|
83
90
|
cpp: {
|
|
84
91
|
extensions: %w[.cc .cpp .cxx .hpp .hh .hxx],
|
|
92
|
+
block_comment: true,
|
|
85
93
|
keywords: %w[alignas alignof and asm auto bool break case catch char char16_t char32_t class const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept nullptr operator or private protected public register reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while]
|
|
86
94
|
},
|
|
87
95
|
swift: {
|
|
88
96
|
extensions: %w[.swift],
|
|
97
|
+
block_comment: true,
|
|
89
98
|
keywords: %w[as associatedtype break case catch class continue default defer deinit do else enum extension false fileprivate for func guard if import in init inout internal is let nil open operator private protocol public repeat rethrows return self Self static struct subscript super switch throw throws true try typealias var where while]
|
|
90
99
|
},
|
|
91
100
|
kotlin: {
|
|
92
101
|
extensions: %w[.kt .kts],
|
|
102
|
+
block_comment: true,
|
|
93
103
|
keywords: %w[as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while by catch constructor delegate dynamic field file finally get import init param property receiver set setparam where actual abstract annotation companion const crossinline data enum expect external final infix inline inner internal lateinit noinline open operator out override private protected public reified sealed suspend tailrec vararg]
|
|
94
104
|
},
|
|
95
105
|
lua: {
|
|
@@ -296,8 +306,7 @@ module Kward
|
|
|
296
306
|
return line.to_s unless definition
|
|
297
307
|
|
|
298
308
|
text = line.to_s
|
|
299
|
-
return colored(text, :gray) if editor_c_style_block_comment_line?(line_index)
|
|
300
|
-
|
|
309
|
+
return colored(text, :gray) if definition[:block_comment] && editor_c_style_block_comment_line?(line_index)
|
|
301
310
|
marker = definition[:line_comment] || "//"
|
|
302
311
|
comment_index = editor_comment_index(text, marker)
|
|
303
312
|
return editor_highlight_generic_code(text, definition[:keywords]) unless comment_index
|
|
@@ -313,7 +322,8 @@ module Kward
|
|
|
313
322
|
end
|
|
314
323
|
|
|
315
324
|
def editor_generic_pattern(keywords)
|
|
316
|
-
|
|
325
|
+
@editor_generic_patterns ||= {}
|
|
326
|
+
@editor_generic_patterns[keywords] ||= /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(keywords)})\b)/
|
|
317
327
|
end
|
|
318
328
|
|
|
319
329
|
def editor_highlight_generic_token(token, keywords)
|
|
@@ -333,16 +343,31 @@ module Kward
|
|
|
333
343
|
def editor_c_style_block_comment_line?(line_index)
|
|
334
344
|
return false unless line_index && @editor_state
|
|
335
345
|
|
|
336
|
-
|
|
337
|
-
@
|
|
346
|
+
lines = @editor_state.lines
|
|
347
|
+
unless @editor_block_comment_lines.equal?(lines)
|
|
348
|
+
@editor_block_comment_lines = lines
|
|
349
|
+
@editor_block_comment_states = []
|
|
350
|
+
@editor_block_comment_in_comment = false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
target = line_index.to_i
|
|
354
|
+
return false if target.negative?
|
|
355
|
+
return @editor_block_comment_states[target] if target < @editor_block_comment_states.length
|
|
356
|
+
|
|
357
|
+
(@editor_block_comment_states.length..target).each do |index|
|
|
358
|
+
line = lines[index].to_s
|
|
338
359
|
starts_block = editor_comment_index(line, "/*")
|
|
339
|
-
ends_block =
|
|
340
|
-
|
|
360
|
+
ends_block = @editor_block_comment_in_comment && line.include?("*/")
|
|
361
|
+
@editor_block_comment_states << (@editor_block_comment_in_comment || !starts_block.nil?)
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
363
|
+
if starts_block && !line[starts_block..].to_s.include?("*/")
|
|
364
|
+
@editor_block_comment_in_comment = true
|
|
365
|
+
elsif ends_block
|
|
366
|
+
@editor_block_comment_in_comment = false
|
|
367
|
+
end
|
|
344
368
|
end
|
|
345
|
-
|
|
369
|
+
|
|
370
|
+
@editor_block_comment_states[target]
|
|
346
371
|
end
|
|
347
372
|
|
|
348
373
|
def editor_comment_index(line, marker)
|
|
@@ -4,10 +4,11 @@ module Kward
|
|
|
4
4
|
module Prompts
|
|
5
5
|
# Parsed prompt template loaded from disk.
|
|
6
6
|
class Templates
|
|
7
|
-
def initialize(config_dir:, template_class:, markdown_parser:)
|
|
7
|
+
def initialize(config_dir:, template_class:, markdown_parser:, warning_sink: nil)
|
|
8
8
|
@config_dir = config_dir
|
|
9
9
|
@template_class = template_class
|
|
10
10
|
@markdown_parser = markdown_parser
|
|
11
|
+
@warning_sink = warning_sink
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def prompt_templates(reserved_commands: [])
|
|
@@ -21,11 +22,11 @@ module Kward
|
|
|
21
22
|
next unless template
|
|
22
23
|
|
|
23
24
|
if reserved.include?(template.command)
|
|
24
|
-
|
|
25
|
+
emit_warning "Warning: skipping Kward prompt command /#{template.command}: reserved command"
|
|
25
26
|
next
|
|
26
27
|
end
|
|
27
28
|
if seen[template.command]
|
|
28
|
-
|
|
29
|
+
emit_warning "Warning: skipping duplicate Kward prompt command /#{template.command}: #{path}"
|
|
29
30
|
next
|
|
30
31
|
end
|
|
31
32
|
|
|
@@ -33,7 +34,7 @@ module Kward
|
|
|
33
34
|
template
|
|
34
35
|
end
|
|
35
36
|
rescue StandardError => e
|
|
36
|
-
|
|
37
|
+
emit_warning "Warning: skipping Kward prompt templates in #{prompts_root}: #{e.message}"
|
|
37
38
|
[]
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -42,7 +43,7 @@ module Kward
|
|
|
42
43
|
def parse_prompt_template(path)
|
|
43
44
|
command = File.basename(path, ".md")
|
|
44
45
|
unless command.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
45
|
-
|
|
46
|
+
emit_warning "Warning: skipping Kward prompt template #{path}: invalid command name"
|
|
46
47
|
return nil
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -55,9 +56,13 @@ module Kward
|
|
|
55
56
|
path: path
|
|
56
57
|
)
|
|
57
58
|
rescue StandardError => e
|
|
58
|
-
|
|
59
|
+
emit_warning "Warning: skipping Kward prompt template #{path}: #{e.message}"
|
|
59
60
|
nil
|
|
60
61
|
end
|
|
62
|
+
|
|
63
|
+
def emit_warning(message)
|
|
64
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
65
|
+
end
|
|
61
66
|
end
|
|
62
67
|
end
|
|
63
68
|
end
|
|
@@ -14,7 +14,7 @@ module Kward
|
|
|
14
14
|
class Registry
|
|
15
15
|
SkillSource = Struct.new(:root, :label, :scope, :precedence, keyword_init: true)
|
|
16
16
|
|
|
17
|
-
def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:)
|
|
17
|
+
def initialize(config_dir:, workspace_root:, project_skills_trusted:, skill_class:, max_file_bytes:, markdown_parser:, inside_directory:, warning_sink: nil)
|
|
18
18
|
@config_dir = config_dir
|
|
19
19
|
@workspace_root = workspace_root
|
|
20
20
|
@project_skills_trusted = project_skills_trusted
|
|
@@ -22,6 +22,7 @@ module Kward
|
|
|
22
22
|
@max_file_bytes = max_file_bytes
|
|
23
23
|
@markdown_parser = markdown_parser
|
|
24
24
|
@inside_directory = inside_directory
|
|
25
|
+
@warning_sink = warning_sink
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Returns discovered, validated skills in precedence order.
|
|
@@ -36,7 +37,7 @@ module Kward
|
|
|
36
37
|
next unless skill
|
|
37
38
|
|
|
38
39
|
if seen[skill.name]
|
|
39
|
-
|
|
40
|
+
emit_warning "Warning: skipping duplicate Kward skill #{skill.name.inspect}: #{path}"
|
|
40
41
|
next
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -45,7 +46,7 @@ module Kward
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
rescue StandardError => e
|
|
48
|
-
|
|
49
|
+
emit_warning "Warning: skipping Kward skills: #{e.message}"
|
|
49
50
|
[]
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -99,13 +100,13 @@ module Kward
|
|
|
99
100
|
def scan_source(source)
|
|
100
101
|
return [] unless Dir.exist?(source.root)
|
|
101
102
|
if source.scope == :project && !@project_skills_trusted
|
|
102
|
-
|
|
103
|
+
emit_warning "Warning: skipping #{source.label} in #{source.root}: project skills are not trusted"
|
|
103
104
|
return []
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
Dir.glob(File.join(source.root, "*", "SKILL.md")).sort
|
|
107
108
|
rescue StandardError => e
|
|
108
|
-
|
|
109
|
+
emit_warning "Warning: skipping #{source.label} in #{source.root}: #{e.message}"
|
|
109
110
|
[]
|
|
110
111
|
end
|
|
111
112
|
|
|
@@ -151,12 +152,12 @@ module Kward
|
|
|
151
152
|
return warn_skip(path, "missing description") if description.empty?
|
|
152
153
|
return warn_skip(path, "description exceeds 1024 characters") if description.length > 1024
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
emit_warning "Warning: Kward skill #{path}: name does not match parent directory" if name != File.basename(File.dirname(path))
|
|
156
|
+
emit_warning "Warning: Kward skill #{path}: name exceeds 64 characters" if name.length > 64
|
|
157
|
+
emit_warning "Warning: Kward skill #{path}: name contains invalid characters" unless valid_name?(name)
|
|
157
158
|
|
|
158
159
|
compatibility = optional_text(frontmatter["compatibility"])
|
|
159
|
-
|
|
160
|
+
emit_warning "Warning: Kward skill #{path}: compatibility exceeds 500 characters" if compatibility && compatibility.length > 500
|
|
160
161
|
|
|
161
162
|
@skill_class.new(
|
|
162
163
|
name: name,
|
|
@@ -169,7 +170,7 @@ module Kward
|
|
|
169
170
|
allowed_tools: optional_text(frontmatter["allowed-tools"])
|
|
170
171
|
)
|
|
171
172
|
rescue StandardError => e
|
|
172
|
-
|
|
173
|
+
emit_warning "Warning: skipping Kward skill #{path}: #{e.message}"
|
|
173
174
|
nil
|
|
174
175
|
end
|
|
175
176
|
|
|
@@ -183,9 +184,13 @@ module Kward
|
|
|
183
184
|
end
|
|
184
185
|
|
|
185
186
|
def warn_skip(path, reason)
|
|
186
|
-
|
|
187
|
+
emit_warning "Warning: skipping Kward skill #{path}: #{reason}"
|
|
187
188
|
nil
|
|
188
189
|
end
|
|
190
|
+
|
|
191
|
+
def emit_warning(message)
|
|
192
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
193
|
+
end
|
|
189
194
|
end
|
|
190
195
|
end
|
|
191
196
|
end
|
|
@@ -20,13 +20,14 @@ module Kward
|
|
|
20
20
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
|
21
21
|
|
|
22
22
|
# Creates an object for telemetry event logging.
|
|
23
|
-
def initialize(config_path: ConfigFiles.config_path, log_dir: nil, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr)
|
|
23
|
+
def initialize(config_path: ConfigFiles.config_path, log_dir: nil, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr, warning_sink: nil)
|
|
24
24
|
@config_path = config_path
|
|
25
25
|
@log_dir = log_dir
|
|
26
26
|
@max_bytes = max_bytes.to_i.positive? ? max_bytes.to_i : DEFAULT_MAX_BYTES
|
|
27
27
|
@clock = clock
|
|
28
28
|
@monotonic_clock = monotonic_clock
|
|
29
29
|
@error_output = error_output
|
|
30
|
+
@warning_sink = warning_sink
|
|
30
31
|
@mutex = Mutex.new
|
|
31
32
|
@warned = false
|
|
32
33
|
end
|
|
@@ -192,7 +193,9 @@ module Kward
|
|
|
192
193
|
return if @warned
|
|
193
194
|
|
|
194
195
|
@warned = true
|
|
195
|
-
|
|
196
|
+
message = "Warning: telemetry logging failed: #{error.message}"
|
|
197
|
+
sink = @warning_sink || ConfigFiles.warning_sink
|
|
198
|
+
sink ? sink.call(message) : @error_output&.puts(message)
|
|
196
199
|
rescue StandardError
|
|
197
200
|
nil
|
|
198
201
|
end
|
data/lib/kward/version.rb
CHANGED