zillacore 0.0.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +126 -0
  6. data/README.md +1166 -0
  7. data/Rakefile +12 -0
  8. data/bin/zillacore +1521 -0
  9. data/certs/stowzilla.pem +26 -0
  10. data/docs/waybar-config.md +96 -0
  11. data/lib/user_registry.rb +159 -0
  12. data/lib/zillacore/agents.rb +203 -0
  13. data/lib/zillacore/brain.rb +197 -0
  14. data/lib/zillacore/card_index.rb +389 -0
  15. data/lib/zillacore/config.rb +263 -0
  16. data/lib/zillacore/cron.rb +629 -0
  17. data/lib/zillacore/deployments.rb +258 -0
  18. data/lib/zillacore/handlers/discord.rb +1643 -0
  19. data/lib/zillacore/handlers/fizzy.rb +1249 -0
  20. data/lib/zillacore/handlers/github.rb +598 -0
  21. data/lib/zillacore/handlers/zoho.rb +487 -0
  22. data/lib/zillacore/helpers.rb +760 -0
  23. data/lib/zillacore/planning.rb +237 -0
  24. data/lib/zillacore/prompts.rb +620 -0
  25. data/lib/zillacore/sessions.rb +282 -0
  26. data/lib/zillacore/skills.rb +276 -0
  27. data/lib/zillacore/users.rb +76 -0
  28. data/lib/zillacore/version.rb +6 -0
  29. data/lib/zillacore/zoho_mail_api.rb +109 -0
  30. data/lib/zillacore.rb +10 -0
  31. data/monitor/daemon.rb +99 -0
  32. data/monitor/deploy-env-macos.rb +131 -0
  33. data/monitor/menubar.rb +295 -0
  34. data/monitor/open-action.sh +15 -0
  35. data/monitor/setup-menubar.rb +78 -0
  36. data/monitor/setup-waybar-deploy-envs.rb +121 -0
  37. data/monitor/setup-waybar-deployments.rb +96 -0
  38. data/monitor/setup-waybar-module.rb +113 -0
  39. data/monitor/setup-xbar-plugin.rb +35 -0
  40. data/monitor/view-logs-macos.rb +210 -0
  41. data/monitor/view-logs-rofi.rb +194 -0
  42. data/monitor/view-logs.rb +119 -0
  43. data/monitor/waybar-config-updater.rb +56 -0
  44. data/monitor/waybar-deploy-env.rb +206 -0
  45. data/monitor/waybar-deployments.rb +239 -0
  46. data/monitor/waybar.rb +146 -0
  47. data/monitor/xbar.3s.rb +179 -0
  48. data/receiver.rb +956 -0
  49. data/templates/agents.json.example +10 -0
  50. data/templates/discord.json.example +17 -0
  51. data/templates/fizzy.json.example +24 -0
  52. data/templates/github.json.example +4 -0
  53. data/templates/testflight.json.example +8 -0
  54. data/templates/users.json.example +121 -0
  55. data/templates/zoho.json.example +27 -0
  56. data/views/dashboard.erb +437 -0
  57. data/zillacore.gemspec +30 -0
  58. data.tar.gz.sig +2 -0
  59. metadata +235 -0
  60. metadata.gz.sig +0 -0
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Planning mode: interactive requirement gathering and task breakdown.
4
+ #
5
+ # Triggered by [plan] tag in Discord or 'plan' tag in Fizzy cards.
6
+ # Agent asks clarifying questions, logs Q&A to memory, generates a plan
7
+ # markdown file, and creates Fizzy steps for each task.
8
+
9
+ PLANS_DIR = File.join(ZILLACORE_DIR, "plans")
10
+ FileUtils.mkdir_p(PLANS_DIR)
11
+
12
+ # Detect if a message/card should trigger planning mode.
13
+ # Returns: { mode: :planning, card_id: '...', card_number: 123 } or nil
14
+ def detect_planning_mode(text:, tags: [], card_internal_id: nil, card_number: nil)
15
+ # Discord: [plan] anywhere in message
16
+ # Fizzy: 'plan' tag on card
17
+ has_plan_tag = text.match?(/\[plan\]/i) || tags.any? { |t| (t.is_a?(Hash) ? t["name"] : t).to_s.downcase == "plan" }
18
+ return nil unless has_plan_tag
19
+
20
+ {
21
+ mode: :planning,
22
+ card_id: card_internal_id || "discord-#{Time.now.to_i}",
23
+ card_number: card_number
24
+ }
25
+ end
26
+
27
+ # Check if planning is complete for a given card.
28
+ # Planning is complete when:
29
+ # 1. A plan file exists at PLANS_DIR/card-{id}-plan.md
30
+ # 2. Memory file indicates planning_complete: true
31
+ def planning_complete?(card_id, agent_name)
32
+ memory_file = File.join(memory_dir_for(agent_name), "card-#{card_id}.md")
33
+ return false unless File.exist?(memory_file)
34
+
35
+ memory_content = File.read(memory_file)
36
+ memory_content.match?(/planning_complete:\s*true/i)
37
+ end
38
+
39
+ # Generate plan markdown from memory Q&A and create Fizzy steps.
40
+ # This is called when the agent determines it has enough information.
41
+ def finalize_plan(card_id:, card_number:, agent_name:, project_key:, repo_path:)
42
+ memory_file = File.join(memory_dir_for(agent_name), "card-#{card_id}.md")
43
+ unless File.exist?(memory_file)
44
+ LOG.error "[Planning] Cannot finalize plan — no memory file found for card #{card_id}"
45
+ return { success: false, error: "No memory file found" }
46
+ end
47
+
48
+ memory_content = File.read(memory_file)
49
+
50
+ # Extract Q&A from memory (agents should format it consistently)
51
+ # Expected format in memory:
52
+ # ## Planning Q&A
53
+ # Q: What's the goal?
54
+ # A: Build a feature that does X
55
+ # Q: Should it support Y?
56
+ # A: Yes, and also Z
57
+
58
+ plan_file = File.join(PLANS_DIR, "card-#{card_id}-plan.md")
59
+
60
+ # The agent should have already written the plan to the file during its session.
61
+ # This function just validates it exists and creates Fizzy steps.
62
+ unless File.exist?(plan_file)
63
+ LOG.error "[Planning] Plan file not found at #{plan_file}"
64
+ return { success: false, error: "Plan file not generated by agent" }
65
+ end
66
+
67
+ plan_content = File.read(plan_file)
68
+
69
+ # Parse tasks from plan markdown.
70
+ # Expected format:
71
+ # ## Task Breakdown
72
+ # ### Task 1: Title
73
+ # ### Task 2: Title
74
+ tasks = []
75
+ plan_content.scan(/^###\s+Task\s+\d+:\s+(.+)$/i) do |match|
76
+ tasks << match[0].strip
77
+ end
78
+
79
+ if tasks.empty?
80
+ LOG.warn "[Planning] No tasks found in plan file #{plan_file}"
81
+ return { success: false, error: "No tasks found in plan" }
82
+ end
83
+
84
+ # Create Fizzy steps for each task
85
+ if card_number
86
+ LOG.info "[Planning] Creating #{tasks.size} Fizzy steps for card ##{card_number}"
87
+ tasks.each do |task_title|
88
+ run_cmd("fizzy", "step", "create", "--card", card_number.to_s, "--content", task_title,
89
+ chdir: repo_path, env: fizzy_env_for(agent_name))
90
+ LOG.info "[Planning] Created step: #{task_title}"
91
+ rescue StandardError => e
92
+ LOG.error "[Planning] Failed to create step '#{task_title}': #{e.message}"
93
+ end
94
+ end
95
+
96
+ # Mark planning as complete in memory
97
+ updated_memory = memory_content.sub(/planning_complete:\s*false/i, "planning_complete: true")
98
+ updated_memory += "\n\nplanning_complete: true\n" unless updated_memory.include?("planning_complete: true")
99
+ File.write(memory_file, updated_memory)
100
+
101
+ LOG.info "[Planning] Plan finalized for card #{card_id}: #{plan_file}"
102
+ { success: true, plan_file: plan_file, tasks: tasks }
103
+ end
104
+
105
+ # Planning mode prompt — prepended to the core prompt.
106
+ PROMPT_PLANNING_MODE = <<~PROMPT
107
+ ## Planning Mode (ACTIVE)
108
+
109
+ You are in **planning mode**. Your job is to gather requirements and break down the work into actionable tasks.
110
+
111
+ ### Your Role
112
+ - Ask clarifying questions to understand the problem, constraints, and desired outcome
113
+ - Continue asking until you have a clear picture (don't rush to a plan)
114
+ - Understand user intent naturally — "go ahead", "that's enough", "proceed" all mean the same thing
115
+ - When you have enough information OR the user signals they're ready, generate the plan
116
+
117
+ ### Question Guidelines
118
+ - Ask specific, focused questions (not generic "anything else?")
119
+ - Build on previous answers — reference what you've learned
120
+ - Prioritize questions that would significantly change the approach
121
+ - If you're 90% confident, proceed. If you're 60% confident, ask.
122
+
123
+ ### When to Stop Asking
124
+ The user will signal they're ready in natural language:
125
+ - "go ahead", "proceed", "that's enough", "looks good", "yeah do it"
126
+ - "I think you have enough", "start working", "make the plan"
127
+
128
+ You should also stop if:
129
+ - You've asked 5+ questions and have a clear understanding
130
+ - The user is getting impatient or frustrated
131
+ - The remaining unknowns are minor details you can decide yourself
132
+
133
+ ### Generating the Plan
134
+ When ready, create a plan file at `{{PLAN_FILE}}` with this structure:
135
+
136
+ ```markdown
137
+ # Feature: [Title]
138
+
139
+ ## Problem Statement
140
+ [What we're solving and why]
141
+
142
+ ## Requirements
143
+ - Requirement 1
144
+ - Requirement 2
145
+ - Requirement 3
146
+
147
+ ## Approach
148
+ [High-level strategy and key decisions]
149
+
150
+ ## Task Breakdown
151
+ ### Task 1: [Clear, actionable title]
152
+ - **Objective**: [What this task accomplishes]
153
+ - **Approach**: [How to implement it]
154
+ - **Demo**: [What "done" looks like]
155
+
156
+ ### Task 2: [Clear, actionable title]
157
+ - **Objective**: [What this task accomplishes]
158
+ - **Approach**: [How to implement it]
159
+ - **Demo**: [What "done" looks like]
160
+
161
+ [Continue for all tasks...]
162
+ ```
163
+
164
+ ### Memory Management
165
+ Log every question and answer to your memory file in this format:
166
+
167
+ ```
168
+ ## Planning Q&A
169
+ Q: [Your question]
170
+ A: [User's answer]
171
+
172
+ Q: [Next question]
173
+ A: [User's answer]
174
+ ```
175
+
176
+ Also track:
177
+ - `planning_complete: false` (update to `true` when plan is generated)
178
+ - Key decisions and constraints discovered
179
+ - Any blockers or unknowns that remain
180
+
181
+ ### After Planning
182
+ Once you've written the plan file:
183
+ 1. Update memory with `planning_complete: true`
184
+ 2. Post a comment summarizing the plan and linking to the file
185
+ 3. The system will automatically create Fizzy steps from your task breakdown
186
+
187
+ ### Important
188
+ - You are READ-ONLY during planning — no code changes, no commits
189
+ - Focus on understanding the problem, not solving it yet
190
+ - The plan should be detailed enough that any agent could execute it
191
+ - Task titles should be clear and actionable (they become Fizzy step names)
192
+
193
+ PROMPT
194
+
195
+ # Render planning mode prompt with appropriate channel rules.
196
+ def render_planning_prompt(situation_template, vars = {}, brain_context: "", card_context: "", agent_name: AI_AGENT_NAME, channel: :fizzy,
197
+ board_key: nil)
198
+ result = ""
199
+ result += "#{brain_context}\n" unless brain_context.empty?
200
+ result += card_context unless card_context.empty?
201
+ result += PROMPT_CORE
202
+
203
+ # Add planning mode instructions BEFORE channel rules
204
+ plan_file = File.join(PLANS_DIR, "card-#{vars["CARD_ID"]}-plan.md")
205
+ planning_vars = vars.merge("PLAN_FILE" => plan_file)
206
+ result += PROMPT_PLANNING_MODE.gsub("{{PLAN_FILE}}", plan_file)
207
+
208
+ result += CHANNEL_PROMPTS.fetch(channel, PROMPT_FIZZY_CHANNEL)
209
+ result += situation_template
210
+ result += PROMPT_REFLECTION
211
+
212
+ planning_vars["KNOWLEDGE_DIR"] ||= KNOWLEDGE_DIR
213
+ planning_vars["MEMORY_DIR"] ||= memory_dir_for(agent_name)
214
+ planning_vars["PERSONA_DIR"] ||= persona_dir_for(agent_name)
215
+ planning_vars["PERSONA_COLLECTION"] ||= persona_collection_for(agent_name)
216
+ planning_vars["AGENT_NAME"] ||= agent_name
217
+
218
+ # Populate column IDs from board config, falling back to defaults
219
+ DEFAULT_COLUMN_IDS.each do |col_name, default_id|
220
+ var_name = "#{col_name.upcase}_COLUMN_ID"
221
+ planning_vars[var_name] ||= (board_key && board_column_id(board_key, col_name)) || default_id
222
+ end
223
+
224
+ # Touch memory file if CARD_ID is present — ensures file exists before agent tries to read it
225
+ if vars["CARD_ID"]
226
+ memory_file = File.join(planning_vars["MEMORY_DIR"], "card-#{vars["CARD_ID"]}.md")
227
+ FileUtils.mkdir_p(planning_vars["MEMORY_DIR"])
228
+ FileUtils.touch(memory_file)
229
+ end
230
+
231
+ roster = agent_roster
232
+ roster_lines = roster.map { |_key, display| " - @#{display}" }.join("\n")
233
+ planning_vars["AGENT_ROSTER"] ||= roster_lines
234
+
235
+ planning_vars.each { |key, val| result.gsub!("{{#{key}}}", val.to_s) }
236
+ result
237
+ end