prompt_objects 0.1.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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +108 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +231 -0
  5. data/IMPLEMENTATION_PLAN.md +1073 -0
  6. data/LICENSE +21 -0
  7. data/README.md +73 -0
  8. data/Rakefile +27 -0
  9. data/design-doc-v2.md +1232 -0
  10. data/exe/prompt_objects +572 -0
  11. data/exe/prompt_objects_mcp +34 -0
  12. data/frontend/.gitignore +3 -0
  13. data/frontend/index.html +13 -0
  14. data/frontend/package-lock.json +4417 -0
  15. data/frontend/package.json +32 -0
  16. data/frontend/postcss.config.js +6 -0
  17. data/frontend/src/App.tsx +95 -0
  18. data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
  19. data/frontend/src/components/ChatPanel.tsx +251 -0
  20. data/frontend/src/components/Dashboard.tsx +83 -0
  21. data/frontend/src/components/Header.tsx +141 -0
  22. data/frontend/src/components/MarkdownMessage.tsx +153 -0
  23. data/frontend/src/components/MessageBus.tsx +55 -0
  24. data/frontend/src/components/ModelSelector.tsx +112 -0
  25. data/frontend/src/components/NotificationPanel.tsx +134 -0
  26. data/frontend/src/components/POCard.tsx +56 -0
  27. data/frontend/src/components/PODetail.tsx +117 -0
  28. data/frontend/src/components/PromptPanel.tsx +51 -0
  29. data/frontend/src/components/SessionsPanel.tsx +174 -0
  30. data/frontend/src/components/ThreadsSidebar.tsx +119 -0
  31. data/frontend/src/components/index.ts +11 -0
  32. data/frontend/src/hooks/useWebSocket.ts +363 -0
  33. data/frontend/src/index.css +37 -0
  34. data/frontend/src/main.tsx +10 -0
  35. data/frontend/src/store/index.ts +246 -0
  36. data/frontend/src/types/index.ts +146 -0
  37. data/frontend/tailwind.config.js +25 -0
  38. data/frontend/tsconfig.json +30 -0
  39. data/frontend/vite.config.ts +29 -0
  40. data/lib/prompt_objects/capability.rb +46 -0
  41. data/lib/prompt_objects/cli.rb +431 -0
  42. data/lib/prompt_objects/connectors/base.rb +73 -0
  43. data/lib/prompt_objects/connectors/mcp.rb +524 -0
  44. data/lib/prompt_objects/environment/exporter.rb +83 -0
  45. data/lib/prompt_objects/environment/git.rb +118 -0
  46. data/lib/prompt_objects/environment/importer.rb +159 -0
  47. data/lib/prompt_objects/environment/manager.rb +401 -0
  48. data/lib/prompt_objects/environment/manifest.rb +218 -0
  49. data/lib/prompt_objects/environment.rb +283 -0
  50. data/lib/prompt_objects/human_queue.rb +144 -0
  51. data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
  52. data/lib/prompt_objects/llm/factory.rb +84 -0
  53. data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
  54. data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
  55. data/lib/prompt_objects/llm/response.rb +61 -0
  56. data/lib/prompt_objects/loader.rb +32 -0
  57. data/lib/prompt_objects/mcp/server.rb +167 -0
  58. data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
  59. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
  60. data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
  61. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
  62. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
  63. data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
  64. data/lib/prompt_objects/message_bus.rb +97 -0
  65. data/lib/prompt_objects/primitive.rb +13 -0
  66. data/lib/prompt_objects/primitives/http_get.rb +72 -0
  67. data/lib/prompt_objects/primitives/list_files.rb +95 -0
  68. data/lib/prompt_objects/primitives/read_file.rb +81 -0
  69. data/lib/prompt_objects/primitives/write_file.rb +73 -0
  70. data/lib/prompt_objects/prompt_object.rb +415 -0
  71. data/lib/prompt_objects/registry.rb +88 -0
  72. data/lib/prompt_objects/server/api/routes.rb +297 -0
  73. data/lib/prompt_objects/server/app.rb +174 -0
  74. data/lib/prompt_objects/server/file_watcher.rb +113 -0
  75. data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
  76. data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
  77. data/lib/prompt_objects/server/public/index.html +14 -0
  78. data/lib/prompt_objects/server/websocket_handler.rb +619 -0
  79. data/lib/prompt_objects/server.rb +166 -0
  80. data/lib/prompt_objects/session/store.rb +826 -0
  81. data/lib/prompt_objects/universal/add_capability.rb +74 -0
  82. data/lib/prompt_objects/universal/add_primitive.rb +113 -0
  83. data/lib/prompt_objects/universal/ask_human.rb +109 -0
  84. data/lib/prompt_objects/universal/create_capability.rb +219 -0
  85. data/lib/prompt_objects/universal/create_primitive.rb +170 -0
  86. data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
  87. data/lib/prompt_objects/universal/list_primitives.rb +145 -0
  88. data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
  89. data/lib/prompt_objects/universal/request_primitive.rb +287 -0
  90. data/lib/prompt_objects/universal/think.rb +41 -0
  91. data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
  92. data/lib/prompt_objects.rb +62 -0
  93. data/objects/coordinator.md +48 -0
  94. data/objects/greeter.md +30 -0
  95. data/objects/reader.md +33 -0
  96. data/prompt_objects.gemspec +50 -0
  97. data/templates/basic/.gitignore +2 -0
  98. data/templates/basic/manifest.yml +7 -0
  99. data/templates/basic/objects/basic.md +32 -0
  100. data/templates/developer/.gitignore +5 -0
  101. data/templates/developer/manifest.yml +17 -0
  102. data/templates/developer/objects/code_reviewer.md +33 -0
  103. data/templates/developer/objects/coordinator.md +39 -0
  104. data/templates/developer/objects/debugger.md +35 -0
  105. data/templates/empty/.gitignore +5 -0
  106. data/templates/empty/manifest.yml +14 -0
  107. data/templates/empty/objects/.gitkeep +0 -0
  108. data/templates/empty/objects/assistant.md +41 -0
  109. data/templates/minimal/.gitignore +5 -0
  110. data/templates/minimal/manifest.yml +7 -0
  111. data/templates/minimal/objects/assistant.md +41 -0
  112. data/templates/writer/.gitignore +5 -0
  113. data/templates/writer/manifest.yml +17 -0
  114. data/templates/writer/objects/coordinator.md +33 -0
  115. data/templates/writer/objects/editor.md +33 -0
  116. data/templates/writer/objects/researcher.md +34 -0
  117. metadata +343 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to list all available capabilities in the registry.
6
+ # This helps POs discover what tools exist.
7
+ class ListCapabilities < Primitive
8
+ def name
9
+ "list_capabilities"
10
+ end
11
+
12
+ def description
13
+ "List all available capabilities (primitives and prompt objects) in the system. Useful for discovering what tools exist before creating new ones."
14
+ end
15
+
16
+ def parameters
17
+ {
18
+ type: "object",
19
+ properties: {
20
+ type: {
21
+ type: "string",
22
+ enum: ["all", "primitives", "prompt_objects"],
23
+ description: "Filter by type. Default is 'all'."
24
+ }
25
+ },
26
+ required: []
27
+ }
28
+ end
29
+
30
+ def receive(message, context:)
31
+ filter = message[:type] || message["type"] || "all"
32
+
33
+ capabilities = case filter
34
+ when "primitives"
35
+ context.env.registry.primitives
36
+ when "prompt_objects"
37
+ context.env.registry.prompt_objects
38
+ else
39
+ context.env.registry.all
40
+ end
41
+
42
+ if capabilities.empty?
43
+ return "No capabilities found."
44
+ end
45
+
46
+ lines = capabilities.map do |cap|
47
+ type_label = cap.is_a?(PromptObject) ? "[PO]" : "[Primitive]"
48
+ "- #{cap.name} #{type_label}: #{cap.description}"
49
+ end
50
+
51
+ "Available capabilities:\n#{lines.join("\n")}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to list available primitives.
6
+ # Shows stdlib primitives, custom (environment) primitives, and which ones
7
+ # the current PO has active.
8
+ class ListPrimitives < Primitive
9
+ # Names of stdlib primitives (built into the framework)
10
+ STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
11
+
12
+ def name
13
+ "list_primitives"
14
+ end
15
+
16
+ def description
17
+ "List available primitives (deterministic Ruby tools). Filter by type: stdlib (built-in), custom (environment-specific), active (on this PO), or available (all)."
18
+ end
19
+
20
+ def parameters
21
+ {
22
+ type: "object",
23
+ properties: {
24
+ filter: {
25
+ type: "string",
26
+ enum: ["available", "active", "stdlib", "custom"],
27
+ description: "Filter primitives: 'available' (all), 'active' (currently on this PO), 'stdlib' (built-in), 'custom' (environment-specific). Default: 'available'"
28
+ }
29
+ },
30
+ required: []
31
+ }
32
+ end
33
+
34
+ def receive(message, context:)
35
+ filter = (message[:filter] || message["filter"] || "available").to_s
36
+
37
+ case filter
38
+ when "stdlib"
39
+ list_stdlib(context)
40
+ when "custom"
41
+ list_custom(context)
42
+ when "active"
43
+ list_active(context)
44
+ when "available"
45
+ list_available(context)
46
+ else
47
+ "Error: Unknown filter '#{filter}'. Use: available, active, stdlib, or custom."
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def list_stdlib(context)
54
+ primitives = STDLIB_PRIMITIVES.filter_map do |name|
55
+ prim = context.env.registry.get(name)
56
+ prim if prim.is_a?(Primitive)
57
+ end
58
+
59
+ format_list("Stdlib Primitives (built-in)", primitives)
60
+ end
61
+
62
+ def list_custom(context)
63
+ primitives = context.env.registry.primitives.reject do |prim|
64
+ STDLIB_PRIMITIVES.include?(prim.name) || universal_primitive?(prim)
65
+ end
66
+
67
+ if primitives.empty?
68
+ "No custom primitives found.\nCustom primitives are stored in: #{context.env.primitives_dir}"
69
+ else
70
+ format_list("Custom Primitives (environment-specific)", primitives)
71
+ end
72
+ end
73
+
74
+ def list_active(context)
75
+ caller = context.calling_po
76
+ unless caller
77
+ return "Error: No calling PO context. This filter shows primitives active on the current PO."
78
+ end
79
+
80
+ po = context.env.registry.get(caller)
81
+ unless po.is_a?(PromptObject)
82
+ return "Error: Could not find calling PO '#{caller}'."
83
+ end
84
+
85
+ capabilities = po.config["capabilities"] || []
86
+ active_primitives = capabilities.filter_map do |cap_name|
87
+ cap = context.env.registry.get(cap_name)
88
+ cap if cap.is_a?(Primitive) && !universal_primitive?(cap)
89
+ end
90
+
91
+ if active_primitives.empty?
92
+ "No primitives currently active on #{caller}.\nUse add_primitive to add primitives to your capabilities."
93
+ else
94
+ format_list("Active Primitives on #{caller}", active_primitives)
95
+ end
96
+ end
97
+
98
+ def list_available(context)
99
+ # All registered primitives except universal ones
100
+ primitives = context.env.registry.primitives.reject { |p| universal_primitive?(p) }
101
+
102
+ # Categorize them
103
+ stdlib = primitives.select { |p| STDLIB_PRIMITIVES.include?(p.name) }
104
+ custom = primitives.reject { |p| STDLIB_PRIMITIVES.include?(p.name) }
105
+
106
+ lines = []
107
+
108
+ unless stdlib.empty?
109
+ lines << "## Stdlib Primitives (built-in)"
110
+ stdlib.each { |p| lines << format_primitive(p) }
111
+ lines << ""
112
+ end
113
+
114
+ unless custom.empty?
115
+ lines << "## Custom Primitives (environment-specific)"
116
+ custom.each { |p| lines << format_primitive(p) }
117
+ lines << ""
118
+ end
119
+
120
+ if lines.empty?
121
+ "No primitives available."
122
+ else
123
+ lines.join("\n")
124
+ end
125
+ end
126
+
127
+ def format_list(title, primitives)
128
+ return "#{title}: (none)" if primitives.empty?
129
+
130
+ lines = ["## #{title}", ""]
131
+ primitives.each { |p| lines << format_primitive(p) }
132
+ lines.join("\n")
133
+ end
134
+
135
+ def format_primitive(primitive)
136
+ "- **#{primitive.name}**: #{primitive.description}"
137
+ end
138
+
139
+ def universal_primitive?(primitive)
140
+ # Universal capabilities live in PromptObjects::Universal module
141
+ primitive.class.name&.start_with?("PromptObjects::Universal")
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module PromptObjects
6
+ module Universal
7
+ # Universal capability to modify existing primitives.
8
+ # Allows POs to fix or improve their primitives.
9
+ class ModifyPrimitive < Primitive
10
+ # Stdlib primitives cannot be modified
11
+ STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
12
+
13
+ def name
14
+ "modify_primitive"
15
+ end
16
+
17
+ def description
18
+ "Modify an existing primitive's code. Only custom (environment) primitives can be modified, not stdlib."
19
+ end
20
+
21
+ def parameters
22
+ {
23
+ type: "object",
24
+ properties: {
25
+ name: {
26
+ type: "string",
27
+ description: "Name of the primitive to modify"
28
+ },
29
+ code: {
30
+ type: "string",
31
+ description: "New Ruby code for the primitive's receive method"
32
+ },
33
+ description: {
34
+ type: "string",
35
+ description: "Optional: Update the description"
36
+ },
37
+ parameters_schema: {
38
+ type: "object",
39
+ description: "Optional: Update the parameters schema"
40
+ },
41
+ reason: {
42
+ type: "string",
43
+ description: "Brief explanation of why this change is needed (for commit message)"
44
+ }
45
+ },
46
+ required: ["name", "code"]
47
+ }
48
+ end
49
+
50
+ def receive(message, context:)
51
+ prim_name = message[:name] || message["name"]
52
+ new_code = message[:code] || message["code"]
53
+ new_description = message[:description] || message["description"]
54
+ new_params_schema = message[:parameters_schema] || message["parameters_schema"]
55
+ reason = message[:reason] || message["reason"] || "Updated primitive"
56
+
57
+ # Find the primitive
58
+ primitive = context.env.registry.get(prim_name)
59
+ unless primitive
60
+ return "Error: Primitive '#{prim_name}' not found."
61
+ end
62
+
63
+ unless primitive.is_a?(Primitive)
64
+ return "Error: '#{prim_name}' is not a primitive."
65
+ end
66
+
67
+ # Check if it's a stdlib primitive
68
+ if STDLIB_PRIMITIVES.include?(prim_name)
69
+ return "Error: Cannot modify stdlib primitive '#{prim_name}'. Stdlib primitives are built into the framework."
70
+ end
71
+
72
+ # Check if it's a universal capability
73
+ if universal_primitive?(primitive)
74
+ return "Error: Cannot modify universal capability '#{prim_name}'."
75
+ end
76
+
77
+ # Find the primitive file
78
+ path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
79
+ unless File.exist?(path)
80
+ return "Error: Cannot find primitive file at #{path}. Only custom primitives can be modified."
81
+ end
82
+
83
+ # Validate code syntax
84
+ syntax_error = validate_syntax(new_code)
85
+ return "Error: Invalid Ruby syntax - #{syntax_error}" if syntax_error
86
+
87
+ # Get current values if not provided
88
+ description = new_description || primitive.description
89
+ params_schema = new_params_schema || primitive.parameters
90
+
91
+ # Generate updated Ruby class
92
+ class_name = prim_name.split("_").map(&:capitalize).join
93
+ ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, new_code)
94
+
95
+ # Write the updated file
96
+ File.write(path, ruby_content, encoding: "UTF-8")
97
+
98
+ # Reload the primitive
99
+ begin
100
+ # Remove old constant to allow re-definition
101
+ if PromptObjects::Primitives.const_defined?(class_name)
102
+ PromptObjects::Primitives.send(:remove_const, class_name)
103
+ end
104
+
105
+ load(path)
106
+ klass = PromptObjects::Primitives.const_get(class_name)
107
+ new_instance = klass.new
108
+
109
+ # Re-register with the new instance
110
+ context.env.registry.register(new_instance)
111
+
112
+ # Auto-commit if in environment mode
113
+ if context.env.environment? && context.env.auto_commit
114
+ commit_message = "Modified primitive '#{prim_name}': #{reason}"
115
+ context.env.save(commit_message)
116
+ end
117
+
118
+ "Modified primitive '#{prim_name}'. Changes saved to #{path}."
119
+ rescue SyntaxError => e
120
+ "Error: Invalid Ruby syntax in generated code - #{e.message}"
121
+ rescue StandardError => e
122
+ "Error reloading primitive: #{e.message}"
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def universal_primitive?(primitive)
129
+ primitive.class.name&.start_with?("PromptObjects::Universal")
130
+ end
131
+
132
+ def validate_syntax(code)
133
+ eval("proc { #{code} }")
134
+ nil
135
+ rescue SyntaxError => e
136
+ e.message.sub(/^\(eval\):\d+: /, "")
137
+ rescue StandardError
138
+ nil
139
+ end
140
+
141
+ def generate_ruby_class(class_name, prim_name, description, params_schema, code)
142
+ escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
143
+
144
+ <<~RUBY
145
+ # frozen_string_literal: true
146
+ # Auto-generated primitive: #{prim_name}
147
+ # Modified at #{Time.now.iso8601}
148
+
149
+ module PromptObjects
150
+ module Primitives
151
+ class #{class_name} < Primitive
152
+ def name
153
+ "#{prim_name}"
154
+ end
155
+
156
+ def description
157
+ "#{escaped_desc}"
158
+ end
159
+
160
+ def parameters
161
+ #{params_schema.inspect}
162
+ end
163
+
164
+ def receive(message, context:)
165
+ #{indent_code(code, 10)}
166
+ end
167
+ end
168
+ end
169
+ end
170
+ RUBY
171
+ end
172
+
173
+ def indent_code(code, spaces)
174
+ code.lines.map.with_index do |line, i|
175
+ i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
176
+ end.join("\n")
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Universal
5
+ # Universal capability to request a primitive from the human.
6
+ # Similar to ask_human but specifically for requesting new tools.
7
+ # The human can approve, modify, or reject the request.
8
+ class RequestPrimitive < Primitive
9
+ def name
10
+ "request_primitive"
11
+ end
12
+
13
+ def description
14
+ "Request a new primitive from the human. Use this when you need a tool that doesn't exist. The human can approve and create it, or reject the request."
15
+ end
16
+
17
+ def parameters
18
+ {
19
+ type: "object",
20
+ properties: {
21
+ name: {
22
+ type: "string",
23
+ description: "Suggested name for the primitive (lowercase, underscores)"
24
+ },
25
+ description: {
26
+ type: "string",
27
+ description: "What this primitive should do"
28
+ },
29
+ reason: {
30
+ type: "string",
31
+ description: "Why you need this primitive"
32
+ },
33
+ suggested_code: {
34
+ type: "string",
35
+ description: "Optional: Your suggested Ruby implementation for the receive method"
36
+ },
37
+ parameters_schema: {
38
+ type: "object",
39
+ description: "Optional: Suggested JSON Schema for parameters"
40
+ }
41
+ },
42
+ required: ["name", "description", "reason"]
43
+ }
44
+ end
45
+
46
+ def receive(message, context:)
47
+ prim_name = message[:name] || message["name"]
48
+ description = message[:description] || message["description"]
49
+ reason = message[:reason] || message["reason"]
50
+ suggested_code = message[:suggested_code] || message["suggested_code"]
51
+ params_schema = message[:parameters_schema] || message["parameters_schema"]
52
+
53
+ # Validate name format
54
+ unless prim_name && prim_name.match?(/\A[a-z][a-z0-9_]*\z/)
55
+ return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
56
+ end
57
+
58
+ # Check if already exists
59
+ if context.env.registry.exists?(prim_name)
60
+ return "Error: A capability named '#{prim_name}' already exists."
61
+ end
62
+
63
+ # In TUI mode, use the human queue
64
+ if context.tui_mode && context.human_queue
65
+ return receive_tui(prim_name, description, reason, suggested_code, params_schema, context)
66
+ end
67
+
68
+ # REPL mode - use stdin directly
69
+ receive_repl(prim_name, description, reason, suggested_code, params_schema, context)
70
+ end
71
+
72
+ private
73
+
74
+ def receive_tui(prim_name, description, reason, suggested_code, params_schema, context)
75
+ # Build the question with all details
76
+ question = build_request_question(prim_name, description, reason, suggested_code)
77
+
78
+ # Queue the request
79
+ request = context.human_queue.enqueue(
80
+ capability: context.current_capability,
81
+ question: question,
82
+ options: ["approve", "reject"]
83
+ )
84
+
85
+ # Log to message bus
86
+ context.bus.publish(
87
+ from: context.current_capability,
88
+ to: "human",
89
+ message: "[primitive request] #{prim_name}: #{description}"
90
+ )
91
+
92
+ # Wait for response
93
+ response = request.wait_for_response
94
+
95
+ # Handle the response
96
+ handle_response(response, prim_name, description, suggested_code, params_schema, context)
97
+ end
98
+
99
+ def receive_repl(prim_name, description, reason, suggested_code, params_schema, context)
100
+ puts
101
+ puts "┌─ Primitive Request ─────────────────────────────────────────┐"
102
+ puts "│"
103
+ puts "│ From: #{context.calling_po || context.current_capability}"
104
+ puts "│ Requested: #{prim_name}"
105
+ puts "│"
106
+ puts "│ Description: #{description}"
107
+ puts "│"
108
+ puts "│ Reason: #{reason}"
109
+ puts "│"
110
+
111
+ if suggested_code
112
+ puts "│ Suggested Code:"
113
+ suggested_code.lines.each { |line| puts "│ #{line}" }
114
+ puts "│"
115
+ end
116
+
117
+ puts "├─────────────────────────────────────────────────────────────┤"
118
+ puts "│ [a] Approve (use suggested code)"
119
+ puts "│ [e] Edit (provide different code)"
120
+ puts "│ [r] Reject"
121
+ puts "└─────────────────────────────────────────────────────────────┘"
122
+ print "Your choice: "
123
+
124
+ choice = $stdin.gets&.chomp&.downcase
125
+
126
+ case choice
127
+ when "a", "approve"
128
+ if suggested_code
129
+ create_and_register(prim_name, description, suggested_code, params_schema, context)
130
+ else
131
+ puts "No suggested code provided. Please enter the code (end with 'END' on its own line):"
132
+ code = read_multiline_input
133
+ create_and_register(prim_name, description, code, params_schema, context)
134
+ end
135
+ when "e", "edit"
136
+ puts "Enter the Ruby code for the receive method (end with 'END' on its own line):"
137
+ code = read_multiline_input
138
+ create_and_register(prim_name, description, code, params_schema, context)
139
+ when "r", "reject"
140
+ "Request rejected by human."
141
+ else
142
+ "Request deferred."
143
+ end
144
+ end
145
+
146
+ def build_request_question(prim_name, description, reason, suggested_code)
147
+ lines = []
148
+ lines << "**Primitive Request: #{prim_name}**"
149
+ lines << ""
150
+ lines << "**Description:** #{description}"
151
+ lines << ""
152
+ lines << "**Reason:** #{reason}"
153
+
154
+ if suggested_code
155
+ lines << ""
156
+ lines << "**Suggested Code:**"
157
+ lines << "```ruby"
158
+ lines << suggested_code
159
+ lines << "```"
160
+ end
161
+
162
+ lines.join("\n")
163
+ end
164
+
165
+ def handle_response(response, prim_name, description, suggested_code, params_schema, context)
166
+ case response.to_s.downcase
167
+ when "approve", "approved", "yes", "y"
168
+ if suggested_code
169
+ create_and_register(prim_name, description, suggested_code, params_schema, context)
170
+ else
171
+ "Request approved but no code provided. Human should create the primitive manually."
172
+ end
173
+ when "reject", "rejected", "no", "n"
174
+ "Request rejected by human."
175
+ else
176
+ # Treat anything else as custom code provided by human
177
+ if response && !response.strip.empty?
178
+ create_and_register(prim_name, description, response, params_schema, context)
179
+ else
180
+ "Request deferred."
181
+ end
182
+ end
183
+ end
184
+
185
+ def create_and_register(prim_name, description, code, params_schema, context)
186
+ params_schema ||= { type: "object", properties: {}, required: [] }
187
+
188
+ # Validate syntax
189
+ begin
190
+ eval("proc { #{code} }")
191
+ rescue SyntaxError => e
192
+ return "Error: Invalid Ruby syntax - #{e.message}"
193
+ end
194
+
195
+ # Generate and write the file
196
+ class_name = prim_name.split("_").map(&:capitalize).join
197
+ ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, code)
198
+
199
+ FileUtils.mkdir_p(context.env.primitives_dir)
200
+ path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
201
+ File.write(path, ruby_content, encoding: "UTF-8")
202
+
203
+ # Load and register
204
+ begin
205
+ load(path)
206
+ klass = PromptObjects::Primitives.const_get(class_name)
207
+ context.env.registry.register(klass.new)
208
+ rescue StandardError => e
209
+ File.delete(path) if File.exist?(path)
210
+ return "Error creating primitive: #{e.message}"
211
+ end
212
+
213
+ # Auto-add to the requesting PO
214
+ saved = false
215
+ if context.calling_po
216
+ caller_po = context.env.registry.get(context.calling_po)
217
+ if caller_po.is_a?(PromptObject)
218
+ caller_po.config["capabilities"] ||= []
219
+ unless caller_po.config["capabilities"].include?(prim_name)
220
+ caller_po.config["capabilities"] << prim_name
221
+ saved = caller_po.save
222
+ end
223
+ end
224
+ end
225
+
226
+ # Log to bus
227
+ context.bus.publish(
228
+ from: "human",
229
+ to: context.current_capability,
230
+ message: "[approved] Created primitive '#{prim_name}'"
231
+ )
232
+
233
+ save_msg = saved ? " and saved to file" : ""
234
+ "Primitive '#{prim_name}' created and added to your capabilities#{save_msg}."
235
+ end
236
+
237
+ def generate_ruby_class(class_name, prim_name, description, params_schema, code)
238
+ escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
239
+
240
+ <<~RUBY
241
+ # frozen_string_literal: true
242
+ # Requested primitive: #{prim_name}
243
+ # Created at #{Time.now.iso8601}
244
+
245
+ module PromptObjects
246
+ module Primitives
247
+ class #{class_name} < Primitive
248
+ def name
249
+ "#{prim_name}"
250
+ end
251
+
252
+ def description
253
+ "#{escaped_desc}"
254
+ end
255
+
256
+ def parameters
257
+ #{params_schema.inspect}
258
+ end
259
+
260
+ def receive(message, context:)
261
+ #{indent_code(code, 10)}
262
+ end
263
+ end
264
+ end
265
+ end
266
+ RUBY
267
+ end
268
+
269
+ def indent_code(code, spaces)
270
+ code.lines.map.with_index do |line, i|
271
+ i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
272
+ end.join("\n")
273
+ end
274
+
275
+ def read_multiline_input
276
+ lines = []
277
+ loop do
278
+ line = $stdin.gets
279
+ break if line.nil? || line.strip == "END"
280
+
281
+ lines << line
282
+ end
283
+ lines.join
284
+ end
285
+ end
286
+ end
287
+ end