@cyperx/clawforge 1.2.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. package/tui/styles.go +69 -0
package/tui/model.go ADDED
@@ -0,0 +1,166 @@
1
+ package main
2
+
3
+ import (
4
+ "time"
5
+
6
+ tea "charm.land/bubbletea/v2"
7
+ )
8
+
9
+ // RefreshTickMsg triggers periodic data reload.
10
+ type RefreshTickMsg time.Time
11
+
12
+ const refreshInterval = 2 * time.Second
13
+
14
+ // Model is the top-level Bubble Tea model for the ClawForge TUI dashboard.
15
+ type Model struct {
16
+ agents []Agent
17
+ selected int
18
+ filter string
19
+ filterMode bool
20
+ viewMode string // all | running | finished
21
+ showHelp bool
22
+ animating bool
23
+ width int
24
+ height int
25
+ steerMode bool
26
+ showPreview bool
27
+ steerInput string
28
+ confirmStop bool
29
+ frame int
30
+ noAnim bool
31
+ }
32
+
33
+ // filteredAgents returns the agents matching the current filter.
34
+ func (m Model) filteredAgents() []Agent {
35
+ agents := m.agents
36
+ switch m.viewMode {
37
+ case "running":
38
+ var tmp []Agent
39
+ for _, a := range agents {
40
+ if a.Status == "running" || a.Status == "spawned" {
41
+ tmp = append(tmp, a)
42
+ }
43
+ }
44
+ agents = tmp
45
+ case "finished":
46
+ var tmp []Agent
47
+ for _, a := range agents {
48
+ if a.Status == "done" || a.Status == "archived" || a.Status == "failed" || a.Status == "cancelled" || a.Status == "timeout" {
49
+ tmp = append(tmp, a)
50
+ }
51
+ }
52
+ agents = tmp
53
+ }
54
+ return FilterAgents(agents, m.filter)
55
+ }
56
+
57
+ // NewModel creates a new Model. If noAnim is true, the startup animation is skipped.
58
+ func NewModel(noAnim bool) Model {
59
+ return Model{
60
+ animating: !noAnim,
61
+ noAnim: noAnim,
62
+ viewMode: "all",
63
+ }
64
+ }
65
+
66
+ // Init starts the animation tick (or loads agents directly) and kicks off refresh.
67
+ func (m Model) Init() tea.Cmd {
68
+ if m.animating {
69
+ return animationTick()
70
+ }
71
+ // No animation: load agents immediately and start refresh cycle.
72
+ m.agents = LoadAgents()
73
+ return refreshTick()
74
+ }
75
+
76
+ // refreshTick returns a command that sends a RefreshTickMsg after refreshInterval.
77
+ func refreshTick() tea.Cmd {
78
+ return tea.Tick(refreshInterval, func(t time.Time) tea.Msg {
79
+ return RefreshTickMsg(t)
80
+ })
81
+ }
82
+
83
+ // Update handles all incoming messages and dispatches to the appropriate handler.
84
+ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
85
+ switch msg := msg.(type) {
86
+ case tea.WindowSizeMsg:
87
+ m.width = msg.Width
88
+ m.height = msg.Height
89
+ return m, nil
90
+
91
+ case AnimationTickMsg:
92
+ if m.animating {
93
+ m.frame++
94
+ if m.frame >= len(forgeFrames) {
95
+ m.animating = false
96
+ m.agents = LoadAgents()
97
+ return m, refreshTick()
98
+ }
99
+ return m, animationTick()
100
+ }
101
+ return m, nil
102
+
103
+ case AnimationDoneMsg:
104
+ // Animation was skipped via keypress — load agents and start refresh.
105
+ m.agents = LoadAgents()
106
+ return m, refreshTick()
107
+
108
+ case RefreshTickMsg:
109
+ if !m.animating {
110
+ m.agents = LoadAgents()
111
+ // Clamp selection after refresh.
112
+ filtered := m.filteredAgents()
113
+ if m.selected >= len(filtered) && len(filtered) > 0 {
114
+ m.selected = len(filtered) - 1
115
+ }
116
+ }
117
+ return m, refreshTick()
118
+
119
+ case steerDoneMsg:
120
+ // Steer command finished — refresh data.
121
+ m.agents = LoadAgents()
122
+ return m, nil
123
+
124
+ case stopDoneMsg:
125
+ // Stop command finished — refresh data.
126
+ m.agents = LoadAgents()
127
+ return m, nil
128
+
129
+ case attachDoneMsg:
130
+ // Returned from tmux attach — refresh data.
131
+ m.agents = LoadAgents()
132
+ return m, nil
133
+
134
+ case nudgeDoneMsg:
135
+ // Nudge command finished — refresh data.
136
+ m.agents = LoadAgents()
137
+ return m, nil
138
+
139
+ case tea.KeyPressMsg:
140
+ if m.animating {
141
+ // Skip animation on any key press.
142
+ m.animating = false
143
+ m.frame = 0
144
+ return m, func() tea.Msg { return AnimationDoneMsg{} }
145
+ }
146
+ var cmd tea.Cmd
147
+ m, cmd = handleKeyPress(m, msg)
148
+ return m, cmd
149
+ }
150
+
151
+ return m, nil
152
+ }
153
+
154
+ // View renders either the animation or the dashboard based on current state.
155
+ func (m Model) View() tea.View {
156
+ var content string
157
+ if m.animating {
158
+ content = renderAnimation(m.frame, m.width, m.height)
159
+ } else {
160
+ content = renderDashboard(m)
161
+ }
162
+
163
+ v := tea.NewView(content)
164
+ v.AltScreen = true
165
+ return v
166
+ }
package/tui/steer.go ADDED
@@ -0,0 +1,69 @@
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "os/exec"
6
+
7
+ tea "charm.land/bubbletea/v2"
8
+ "charm.land/lipgloss/v2"
9
+ )
10
+
11
+ // steerDoneMsg is sent when the steer command finishes.
12
+ type steerDoneMsg struct{ err error }
13
+
14
+ // handleSteerKey processes key input while in steer mode.
15
+ func handleSteerKey(m Model, key string) (Model, tea.Cmd) {
16
+ switch key {
17
+ case "esc":
18
+ m.steerMode = false
19
+ m.steerInput = ""
20
+ return m, nil
21
+ case "enter":
22
+ if m.steerInput != "" {
23
+ agents := m.filteredAgents()
24
+ if len(agents) > 0 {
25
+ agent := agents[m.selected]
26
+ msg := m.steerInput
27
+ m.steerMode = false
28
+ m.steerInput = ""
29
+ cmd := exec.Command("clawforge", "steer", agent.ID, msg)
30
+ return m, tea.ExecProcess(cmd, func(err error) tea.Msg {
31
+ return steerDoneMsg{err}
32
+ })
33
+ }
34
+ }
35
+ m.steerMode = false
36
+ m.steerInput = ""
37
+ return m, nil
38
+ case "backspace":
39
+ if len(m.steerInput) > 0 {
40
+ m.steerInput = m.steerInput[:len(m.steerInput)-1]
41
+ }
42
+ return m, nil
43
+ default:
44
+ // Only append printable single characters.
45
+ if len(key) == 1 {
46
+ m.steerInput += key
47
+ } else if key == "space" {
48
+ m.steerInput += " "
49
+ }
50
+ return m, nil
51
+ }
52
+ }
53
+
54
+ // RenderSteerInput renders the steer prompt line at the bottom of the screen.
55
+ func RenderSteerInput(m Model) string {
56
+ agents := m.filteredAgents()
57
+ id := "?"
58
+ if len(agents) > 0 && m.selected < len(agents) {
59
+ id = agents[m.selected].ID
60
+ }
61
+
62
+ prompt := fmt.Sprintf("⚒️ Steer agent #%s: %s", id, m.steerInput)
63
+ cursor := "█"
64
+
65
+ return lipgloss.NewStyle().
66
+ Foreground(lipgloss.Color("#FF8C00")).
67
+ Bold(true).
68
+ Render(prompt + cursor)
69
+ }
package/tui/styles.go ADDED
@@ -0,0 +1,69 @@
1
+ package main
2
+
3
+ import (
4
+ "charm.land/lipgloss/v2"
5
+ )
6
+
7
+ // Forge theme colors.
8
+ var (
9
+ colorPrimary = lipgloss.Color("#FF8C00") // dark orange / amber
10
+ colorSecondary = lipgloss.Color("#FFA500") // orange
11
+ colorAccent = lipgloss.Color("#FF6600") // red-orange
12
+ colorMuted = lipgloss.Color("#CC5500") // brown-orange
13
+ )
14
+
15
+ // Header style: bold + amber.
16
+ var headerStyle = lipgloss.NewStyle().
17
+ Bold(true).
18
+ Foreground(colorPrimary)
19
+
20
+ // Table header style: bold + secondary.
21
+ var tableHeaderStyle = lipgloss.NewStyle().
22
+ Bold(true).
23
+ Foreground(colorSecondary)
24
+
25
+ // Separator style: muted.
26
+ var separatorStyle = lipgloss.NewStyle().
27
+ Foreground(colorMuted)
28
+
29
+ // Selected row: reverse + amber foreground.
30
+ var selectedRowStyle = lipgloss.NewStyle().
31
+ Reverse(true).
32
+ Foreground(colorPrimary)
33
+
34
+ // Status bar: dim + muted.
35
+ var statusBarStyle = lipgloss.NewStyle().
36
+ Faint(true).
37
+ Foreground(colorMuted)
38
+
39
+ // Borders: rounded, amber.
40
+ var borderStyle = lipgloss.NewStyle().
41
+ Border(lipgloss.RoundedBorder()).
42
+ BorderForeground(colorPrimary)
43
+
44
+ // Mode badge style.
45
+ var modeBadgeStyle = lipgloss.NewStyle().
46
+ Bold(true).
47
+ Padding(0, 1).
48
+ Foreground(lipgloss.Color("#000000")).
49
+ Background(colorSecondary)
50
+
51
+ // Status indicator styles.
52
+ var (
53
+ statusRunningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
54
+ statusIdleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFF00"))
55
+ statusFailedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
56
+ statusDoneStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#808080"))
57
+ )
58
+
59
+ // Help overlay style.
60
+ var helpOverlayStyle = lipgloss.NewStyle().
61
+ Border(lipgloss.RoundedBorder()).
62
+ BorderForeground(colorPrimary).
63
+ Padding(1, 2).
64
+ Foreground(colorSecondary)
65
+
66
+ // Animation frame style: centered, amber.
67
+ var animFrameStyle = lipgloss.NewStyle().
68
+ Foreground(colorAccent).
69
+ Bold(true)