factory-ai 1.3.0 → 1.5.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.
- package/CHANGELOG.md +24 -4
- package/Dockerfile.worker +1 -1
- package/README.md +10 -2
- package/ROADMAP.md +0 -1
- package/bin/factory +130 -3
- package/bootstrap/agent-factory-worker.service +3 -3
- package/bootstrap/auto-update.sh +20 -12
- package/bootstrap/deploy-runtime.sh +7 -3
- package/bootstrap/factory-ai-container-firewall.service +14 -0
- package/bootstrap/factory-ai-ollama.service +16 -0
- package/bootstrap/factory-ai-qdrant.service +16 -0
- package/bootstrap/factory-ai-snapshot.service +1 -0
- package/bootstrap/factory-ai-watchdog.service +21 -0
- package/bootstrap/factory-ai-watchdog.timer +11 -0
- package/bootstrap/setup.sh +33 -8
- package/cmd/factory-ui/main.go +455 -0
- package/go.mod +39 -0
- package/go.sum +82 -0
- package/package.json +7 -3
- package/scripts/test.js +7 -0
- package/src/acp-adapter.js +26 -0
- package/src/acp-cli.js +16 -0
- package/src/activity.js +96 -0
- package/src/agent-executor.js +47 -3
- package/src/agent-runner.js +34 -12
- package/src/approval-policy.js +12 -0
- package/src/azure-harness.js +47 -14
- package/src/bedrock-harness.js +45 -14
- package/src/config.js +13 -0
- package/src/container-runner.js +65 -12
- package/src/context-compaction.js +16 -0
- package/src/control-plane.js +77 -16
- package/src/control-service.js +6 -1
- package/src/dashboard.js +25 -6
- package/src/extension-cli.js +11 -0
- package/src/extension-manifest.js +76 -0
- package/src/hooks.js +62 -0
- package/src/instructions.js +45 -0
- package/src/mcp-tools.js +19 -3
- package/src/objective-status.js +16 -0
- package/src/operator-logs.js +24 -0
- package/src/operator.js +34 -10
- package/src/process.js +3 -3
- package/src/release-gate.js +1 -0
- package/src/release-service.js +4 -1
- package/src/release.js +1 -1
- package/src/repo-map.js +128 -0
- package/src/reporter.js +12 -1
- package/src/retriever.js +154 -0
- package/src/routing.js +11 -1
- package/src/scanner-suite.js +4 -3
- package/src/setup-menu.js +3 -1
- package/src/snapshot.js +16 -0
- package/src/task-entry.js +28 -6
- package/src/task-graph.js +0 -5
- package/src/telegram-service.js +10 -4
- package/src/telegram.js +7 -3
- package/src/telemetry.js +173 -0
- package/src/tui.js +31 -10
- package/src/validation.js +41 -9
- package/src/watchdog.js +62 -0
- package/src/worker.js +34 -3
- package/src/workspace-tools.js +13 -3
- package/src/workspace.js +22 -2
- package/templates/AGENTS.md +15 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"regexp"
|
|
12
|
+
"strconv"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
|
|
16
|
+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
|
17
|
+
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
|
18
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
19
|
+
"github.com/charmbracelet/lipgloss"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type activity struct {
|
|
23
|
+
Type string `json:"type"`
|
|
24
|
+
Tool string `json:"tool"`
|
|
25
|
+
Phase string `json:"phase"`
|
|
26
|
+
OccurredAt string `json:"occurredAt"`
|
|
27
|
+
RetryCount int `json:"retryCount"`
|
|
28
|
+
LastError string `json:"lastError"`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type task struct {
|
|
32
|
+
ID string `json:"id"`
|
|
33
|
+
Role string `json:"role"`
|
|
34
|
+
Title string `json:"title"`
|
|
35
|
+
Model string `json:"model"`
|
|
36
|
+
State string `json:"state"`
|
|
37
|
+
Stale bool `json:"stale"`
|
|
38
|
+
Activity *activity `json:"activity"`
|
|
39
|
+
Retries int `json:"retries"`
|
|
40
|
+
LastError string `json:"lastError"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type objective struct {
|
|
44
|
+
ID string `json:"id"`
|
|
45
|
+
Objective string `json:"objective"`
|
|
46
|
+
Repository string `json:"repository"`
|
|
47
|
+
Status string `json:"status"`
|
|
48
|
+
Tasks []task `json:"tasks"`
|
|
49
|
+
PullRequest string `json:"pullRequest"`
|
|
50
|
+
Blocker string `json:"blocker"`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type usage struct {
|
|
54
|
+
Tasks int `json:"tasks"`
|
|
55
|
+
InputTokens int `json:"inputTokens"`
|
|
56
|
+
CachedInputTokens int `json:"cachedInputTokens"`
|
|
57
|
+
OutputTokens int `json:"outputTokens"`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type dashboard struct {
|
|
61
|
+
GeneratedAt string `json:"generatedAt"`
|
|
62
|
+
Queue struct{ Active, DeadLetter int } `json:"queue"`
|
|
63
|
+
Health struct {
|
|
64
|
+
Status string `json:"status"`
|
|
65
|
+
StaleAgents int `json:"staleAgents"`
|
|
66
|
+
} `json:"health"`
|
|
67
|
+
Cost *struct {
|
|
68
|
+
MonthToDate float64 `json:"monthToDate"`
|
|
69
|
+
Currency string `json:"currency"`
|
|
70
|
+
ByService map[string]float64 `json:"byService"`
|
|
71
|
+
} `json:"cost"`
|
|
72
|
+
Summary struct {
|
|
73
|
+
Objectives map[string]int `json:"objectives"`
|
|
74
|
+
} `json:"summary"`
|
|
75
|
+
Objectives []objective `json:"objectives"`
|
|
76
|
+
ModelUsage map[string]usage `json:"modelUsage"`
|
|
77
|
+
Secrets []struct {
|
|
78
|
+
Name string `json:"name"`
|
|
79
|
+
Updated string `json:"updated"`
|
|
80
|
+
} `json:"secrets"`
|
|
81
|
+
Warnings []string `json:"warnings"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type snapshotMsg struct {
|
|
85
|
+
dashboard dashboard
|
|
86
|
+
logs string
|
|
87
|
+
err error
|
|
88
|
+
}
|
|
89
|
+
type tickMsg time.Time
|
|
90
|
+
type resumeMsg struct{ err error }
|
|
91
|
+
|
|
92
|
+
type model struct {
|
|
93
|
+
client *azblob.Client
|
|
94
|
+
factoryName string
|
|
95
|
+
purpose string
|
|
96
|
+
width int
|
|
97
|
+
height int
|
|
98
|
+
tab int
|
|
99
|
+
scroll int
|
|
100
|
+
dashboard dashboard
|
|
101
|
+
logs string
|
|
102
|
+
loading bool
|
|
103
|
+
err error
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var (
|
|
107
|
+
accent = lipgloss.Color("#78DBA9")
|
|
108
|
+
blue = lipgloss.Color("#77A8FF")
|
|
109
|
+
warn = lipgloss.Color("#EFC46B")
|
|
110
|
+
danger = lipgloss.Color("#EF7D7D")
|
|
111
|
+
muted = lipgloss.Color("#7F8B99")
|
|
112
|
+
panel = lipgloss.Color("#15191F")
|
|
113
|
+
border = lipgloss.Color("#303743")
|
|
114
|
+
tabs = []string{"Overview", "Objectives", "Agents", "Secrets", "Capabilities", "Logs", "Settings"}
|
|
115
|
+
ansi = regexp.MustCompile(`\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?)`)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
func clean(value string) string {
|
|
119
|
+
value = ansi.ReplaceAllString(value, "")
|
|
120
|
+
return strings.Map(func(character rune) rune {
|
|
121
|
+
if character == '\n' || character == '\t' || character >= 0x20 && character != 0x7f {
|
|
122
|
+
return character
|
|
123
|
+
}
|
|
124
|
+
return -1
|
|
125
|
+
}, value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func readConfig() map[string]string {
|
|
129
|
+
result := map[string]string{}
|
|
130
|
+
for _, key := range []string{"FACTORY_STORAGE_ACCOUNT", "FACTORY_NAME", "FACTORY_PURPOSE"} {
|
|
131
|
+
if value := os.Getenv(key); value != "" {
|
|
132
|
+
result[key] = value
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
home, _ := os.UserHomeDir()
|
|
136
|
+
data, err := os.ReadFile(filepath.Join(home, ".config", "factory-ai", "config"))
|
|
137
|
+
if err != nil {
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
for _, line := range strings.Split(string(data), "\n") {
|
|
141
|
+
key, value, ok := strings.Cut(line, "=")
|
|
142
|
+
if !ok || result[key] != "" {
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
value = strings.ReplaceAll(value, `\ `, " ")
|
|
146
|
+
if unquoted, err := strconv.Unquote(value); err == nil {
|
|
147
|
+
value = unquoted
|
|
148
|
+
}
|
|
149
|
+
result[key] = value
|
|
150
|
+
}
|
|
151
|
+
return result
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func download(client *azblob.Client, name string) ([]byte, error) {
|
|
155
|
+
response, err := client.DownloadStream(context.Background(), "operator", name, nil)
|
|
156
|
+
if err != nil {
|
|
157
|
+
return nil, err
|
|
158
|
+
}
|
|
159
|
+
defer response.Body.Close()
|
|
160
|
+
return io.ReadAll(response.Body)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func fetchCmd(client *azblob.Client) tea.Cmd {
|
|
164
|
+
return func() tea.Msg {
|
|
165
|
+
data, err := download(client, "dashboard.json")
|
|
166
|
+
if err != nil {
|
|
167
|
+
return snapshotMsg{err: err}
|
|
168
|
+
}
|
|
169
|
+
var board dashboard
|
|
170
|
+
if err := json.Unmarshal(data, &board); err != nil {
|
|
171
|
+
return snapshotMsg{err: err}
|
|
172
|
+
}
|
|
173
|
+
logData, _ := download(client, "logs.txt")
|
|
174
|
+
return snapshotMsg{dashboard: board, logs: string(logData)}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func tickCmd() tea.Cmd {
|
|
179
|
+
return tea.Tick(15*time.Second, func(value time.Time) tea.Msg { return tickMsg(value) })
|
|
180
|
+
}
|
|
181
|
+
func (m model) Init() tea.Cmd { return tea.Batch(fetchCmd(m.client), tickCmd()) }
|
|
182
|
+
|
|
183
|
+
func (m model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
|
184
|
+
switch msg := message.(type) {
|
|
185
|
+
case tea.KeyMsg:
|
|
186
|
+
switch msg.String() {
|
|
187
|
+
case "q", "ctrl+c":
|
|
188
|
+
return m, tea.Quit
|
|
189
|
+
case "left", "h":
|
|
190
|
+
if m.tab > 0 {
|
|
191
|
+
m.tab--
|
|
192
|
+
m.scroll = 0
|
|
193
|
+
}
|
|
194
|
+
case "right", "l", "tab":
|
|
195
|
+
if m.tab < len(tabs)-1 {
|
|
196
|
+
m.tab++
|
|
197
|
+
} else {
|
|
198
|
+
m.tab = 0
|
|
199
|
+
}
|
|
200
|
+
m.scroll = 0
|
|
201
|
+
case "up", "k":
|
|
202
|
+
if m.scroll > 0 {
|
|
203
|
+
m.scroll--
|
|
204
|
+
}
|
|
205
|
+
case "down", "j":
|
|
206
|
+
m.scroll++
|
|
207
|
+
case "pgup":
|
|
208
|
+
m.scroll -= 10
|
|
209
|
+
if m.scroll < 0 {
|
|
210
|
+
m.scroll = 0
|
|
211
|
+
}
|
|
212
|
+
case "pgdown":
|
|
213
|
+
m.scroll += 10
|
|
214
|
+
case "home":
|
|
215
|
+
m.scroll = 0
|
|
216
|
+
case "o", "n", "a", "p", "u", "y", "x":
|
|
217
|
+
return m, tea.ExecProcess(exec.Command("factory-ui"), func(err error) tea.Msg { return resumeMsg{err: err} })
|
|
218
|
+
case "r":
|
|
219
|
+
m.loading = true
|
|
220
|
+
return m, fetchCmd(m.client)
|
|
221
|
+
default:
|
|
222
|
+
if len(msg.String()) == 1 && msg.String()[0] >= '1' && msg.String()[0] <= '7' {
|
|
223
|
+
m.tab = int(msg.String()[0] - '1')
|
|
224
|
+
m.scroll = 0
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
case tea.WindowSizeMsg:
|
|
228
|
+
m.width, m.height = msg.Width, msg.Height
|
|
229
|
+
case snapshotMsg:
|
|
230
|
+
m.loading = false
|
|
231
|
+
m.err = msg.err
|
|
232
|
+
if msg.err == nil {
|
|
233
|
+
m.dashboard, m.logs = msg.dashboard, msg.logs
|
|
234
|
+
}
|
|
235
|
+
case resumeMsg:
|
|
236
|
+
m.err = msg.err
|
|
237
|
+
if msg.err == nil {
|
|
238
|
+
m.loading = true
|
|
239
|
+
return m, fetchCmd(m.client)
|
|
240
|
+
}
|
|
241
|
+
case tickMsg:
|
|
242
|
+
if !m.loading {
|
|
243
|
+
m.loading = true
|
|
244
|
+
return m, tea.Batch(fetchCmd(m.client), tickCmd())
|
|
245
|
+
}
|
|
246
|
+
return m, tickCmd()
|
|
247
|
+
}
|
|
248
|
+
return m, nil
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func statusColor(value string) lipgloss.Color {
|
|
252
|
+
switch value {
|
|
253
|
+
case "complete", "succeeded":
|
|
254
|
+
return accent
|
|
255
|
+
case "failed", "blocked", "stale":
|
|
256
|
+
return danger
|
|
257
|
+
case "running":
|
|
258
|
+
return blue
|
|
259
|
+
default:
|
|
260
|
+
return warn
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
func badge(value string) string {
|
|
264
|
+
return lipgloss.NewStyle().Foreground(statusColor(value)).Bold(true).Render(value)
|
|
265
|
+
}
|
|
266
|
+
func trim(value string, max int) string {
|
|
267
|
+
value = strings.ReplaceAll(clean(value), "\n", " ")
|
|
268
|
+
if len(value) > max {
|
|
269
|
+
return value[:max-1] + "…"
|
|
270
|
+
}
|
|
271
|
+
return value
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func (m model) overview() string {
|
|
275
|
+
cost := "unavailable"
|
|
276
|
+
if m.dashboard.Cost != nil {
|
|
277
|
+
cost = fmt.Sprintf("%s %.2f", m.dashboard.Cost.Currency, m.dashboard.Cost.MonthToDate)
|
|
278
|
+
}
|
|
279
|
+
counts := []string{}
|
|
280
|
+
for key, value := range m.dashboard.Summary.Objectives {
|
|
281
|
+
counts = append(counts, fmt.Sprintf("%s %d", key, value))
|
|
282
|
+
}
|
|
283
|
+
in, cached, out := 0, 0, 0
|
|
284
|
+
for _, value := range m.dashboard.ModelUsage {
|
|
285
|
+
in += value.InputTokens
|
|
286
|
+
cached += value.CachedInputTokens
|
|
287
|
+
out += value.OutputTokens
|
|
288
|
+
}
|
|
289
|
+
value := fmt.Sprintf("%s\n\nHealth %s\nQueue %s\nDead letters %d\nAzure MTD %s\nObjectives %s\nTokens %d in · %d cached · %d out\n\n%s\n\n", lipgloss.NewStyle().Bold(true).Render("SYSTEM"), badge(m.dashboard.Health.Status), lipgloss.NewStyle().Foreground(accent).Render(fmt.Sprint(m.dashboard.Queue.Active)), m.dashboard.Queue.DeadLetter, lipgloss.NewStyle().Foreground(warn).Render(cost), strings.Join(counts, " "), in, cached, out, lipgloss.NewStyle().Bold(true).Render("RECENT OBJECTIVES"))
|
|
290
|
+
for index := len(m.dashboard.Objectives) - 1; index >= 0 && index >= len(m.dashboard.Objectives)-8; index-- {
|
|
291
|
+
item := m.dashboard.Objectives[index]
|
|
292
|
+
value += fmt.Sprintf("%s %s\n %s\n\n", badge(item.Status), trim(item.Objective, 90), lipgloss.NewStyle().Foreground(muted).Render(clean(item.Repository)))
|
|
293
|
+
}
|
|
294
|
+
return value
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func (m model) objectives() string {
|
|
298
|
+
var value strings.Builder
|
|
299
|
+
for index := len(m.dashboard.Objectives) - 1; index >= 0; index-- {
|
|
300
|
+
item := m.dashboard.Objectives[index]
|
|
301
|
+
fmt.Fprintf(&value, "%s %s\n%s\n", badge(item.Status), trim(item.Objective, 100), lipgloss.NewStyle().Foreground(muted).Render(clean(item.ID)))
|
|
302
|
+
for _, task := range item.Tasks {
|
|
303
|
+
state := task.State
|
|
304
|
+
if task.Stale {
|
|
305
|
+
state = "stale"
|
|
306
|
+
}
|
|
307
|
+
detail := ""
|
|
308
|
+
if task.Activity != nil {
|
|
309
|
+
detail = " · " + task.Activity.Type
|
|
310
|
+
if task.Activity.Tool != "" {
|
|
311
|
+
detail += " " + task.Activity.Tool
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
fmt.Fprintf(&value, " %s %-9s %s%s\n", badge(state), clean(task.Role), trim(task.Title, 70), clean(detail))
|
|
315
|
+
}
|
|
316
|
+
if item.PullRequest != "" {
|
|
317
|
+
fmt.Fprintf(&value, " PR %s\n", clean(item.PullRequest))
|
|
318
|
+
}
|
|
319
|
+
value.WriteString("\n")
|
|
320
|
+
}
|
|
321
|
+
return value.String()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func (m model) agents() string {
|
|
325
|
+
var value strings.Builder
|
|
326
|
+
for _, item := range m.dashboard.Objectives {
|
|
327
|
+
for _, task := range item.Tasks {
|
|
328
|
+
if task.State == "succeeded" {
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
state := task.State
|
|
332
|
+
if task.Stale {
|
|
333
|
+
state = "stale"
|
|
334
|
+
}
|
|
335
|
+
phase := "waiting for activity"
|
|
336
|
+
if task.Activity != nil {
|
|
337
|
+
phase = task.Activity.Phase
|
|
338
|
+
if phase == "" {
|
|
339
|
+
phase = task.Activity.Type
|
|
340
|
+
}
|
|
341
|
+
phase += " · " + task.Activity.OccurredAt
|
|
342
|
+
if task.Retries > 0 {
|
|
343
|
+
phase += fmt.Sprintf(" · %d retries", task.Retries)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
fmt.Fprintf(&value, "%s %s %s\n %s\n %s\n\n", badge(state), clean(task.Role), clean(task.Model), trim(task.Title, 90), lipgloss.NewStyle().Foreground(muted).Render(clean(phase)))
|
|
347
|
+
if task.LastError != "" {
|
|
348
|
+
fmt.Fprintf(&value, " %s\n\n", lipgloss.NewStyle().Foreground(danger).Render(clean(task.LastError)))
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if value.Len() == 0 {
|
|
353
|
+
return "No active agents."
|
|
354
|
+
}
|
|
355
|
+
return value.String()
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func (m model) body() string {
|
|
359
|
+
switch m.tab {
|
|
360
|
+
case 0:
|
|
361
|
+
return m.overview()
|
|
362
|
+
case 1:
|
|
363
|
+
return m.objectives()
|
|
364
|
+
case 2:
|
|
365
|
+
return m.agents()
|
|
366
|
+
case 3:
|
|
367
|
+
var b strings.Builder
|
|
368
|
+
b.WriteString("GLOBAL KEY VAULT\nValues are never displayed.\n\n")
|
|
369
|
+
for _, item := range m.dashboard.Secrets {
|
|
370
|
+
fmt.Fprintf(&b, "● %-55s %s\n", clean(item.Name), clean(item.Updated))
|
|
371
|
+
}
|
|
372
|
+
return b.String()
|
|
373
|
+
case 4:
|
|
374
|
+
return "CURATED CAPABILITIES\n\nSkills: /goal, /loop, TDD, debugging, verification, security, release discipline\nMCP: Context7, Playwright, knowledge-graph memory\nScanners: Trivy, Gitleaks, OSV-Scanner, Semgrep"
|
|
375
|
+
case 5:
|
|
376
|
+
if m.logs == "" {
|
|
377
|
+
return "No log snapshot available."
|
|
378
|
+
}
|
|
379
|
+
return clean(m.logs)
|
|
380
|
+
default:
|
|
381
|
+
return fmt.Sprintf("FACTORY SETTINGS\n\nName %s\nPurpose %s\nStorage Azure Blob snapshot\nRefresh 15 seconds\n\nUse `factory configure models` or `factory models set ROLE PROVIDER/MODEL` to change routing.", clean(m.factoryName), clean(m.purpose))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func (m model) View() string {
|
|
386
|
+
width := m.width
|
|
387
|
+
if width < 80 {
|
|
388
|
+
width = 80
|
|
389
|
+
}
|
|
390
|
+
header := lipgloss.NewStyle().Bold(true).Foreground(accent).Render(strings.ToUpper(clean(m.factoryName))) + " " + lipgloss.NewStyle().Foreground(muted).Render(clean(m.purpose))
|
|
391
|
+
tabValues := []string{}
|
|
392
|
+
for index, value := range tabs {
|
|
393
|
+
style := lipgloss.NewStyle().Padding(0, 1).Foreground(muted)
|
|
394
|
+
if index == m.tab {
|
|
395
|
+
style = style.Foreground(lipgloss.Color("#07130D")).Background(accent).Bold(true)
|
|
396
|
+
}
|
|
397
|
+
tabValues = append(tabValues, style.Render(fmt.Sprintf("%d %s", index+1, value)))
|
|
398
|
+
}
|
|
399
|
+
tabLine := lipgloss.JoinHorizontal(lipgloss.Top, tabValues...)
|
|
400
|
+
height := m.height - 6
|
|
401
|
+
if height < 10 {
|
|
402
|
+
height = 10
|
|
403
|
+
}
|
|
404
|
+
lines := strings.Split(m.body(), "\n")
|
|
405
|
+
visible := height - 2
|
|
406
|
+
maxScroll := len(lines) - visible
|
|
407
|
+
if maxScroll < 0 {
|
|
408
|
+
maxScroll = 0
|
|
409
|
+
}
|
|
410
|
+
scroll := m.scroll
|
|
411
|
+
if scroll > maxScroll {
|
|
412
|
+
scroll = maxScroll
|
|
413
|
+
}
|
|
414
|
+
end := scroll + visible
|
|
415
|
+
if end > len(lines) {
|
|
416
|
+
end = len(lines)
|
|
417
|
+
}
|
|
418
|
+
content := lipgloss.NewStyle().Width(width-4).Height(height).Border(lipgloss.RoundedBorder()).BorderForeground(border).Padding(1, 2).Render(strings.Join(lines[scroll:end], "\n"))
|
|
419
|
+
footer := "←/→ tabs · ↑/↓ scroll · o actions · r refresh · q quit"
|
|
420
|
+
if m.loading {
|
|
421
|
+
footer = "Refreshing snapshot…"
|
|
422
|
+
}
|
|
423
|
+
if m.err != nil {
|
|
424
|
+
footer = lipgloss.NewStyle().Foreground(danger).Render(m.err.Error())
|
|
425
|
+
}
|
|
426
|
+
return lipgloss.JoinVertical(lipgloss.Left, header, tabLine, content, lipgloss.NewStyle().Foreground(muted).Render(footer))
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
func main() {
|
|
430
|
+
config := readConfig()
|
|
431
|
+
account := config["FACTORY_STORAGE_ACCOUNT"]
|
|
432
|
+
if account == "" {
|
|
433
|
+
fmt.Fprintln(os.Stderr, "FACTORY_STORAGE_ACCOUNT is missing; run factory setup")
|
|
434
|
+
os.Exit(1)
|
|
435
|
+
}
|
|
436
|
+
credential, err := azidentity.NewAzureCLICredential(nil)
|
|
437
|
+
if err != nil {
|
|
438
|
+
panic(err)
|
|
439
|
+
}
|
|
440
|
+
client, err := azblob.NewClient(fmt.Sprintf("https://%s.blob.core.windows.net/", account), credential, nil)
|
|
441
|
+
if err != nil {
|
|
442
|
+
panic(err)
|
|
443
|
+
}
|
|
444
|
+
application := model{client: client, factoryName: config["FACTORY_NAME"], purpose: config["FACTORY_PURPOSE"], loading: true}
|
|
445
|
+
if application.factoryName == "" {
|
|
446
|
+
application.factoryName = "Factory AI"
|
|
447
|
+
}
|
|
448
|
+
if application.purpose == "" {
|
|
449
|
+
application.purpose = "Ship secure reviewed software continuously"
|
|
450
|
+
}
|
|
451
|
+
if _, err := tea.NewProgram(application, tea.WithAltScreen()).Run(); err != nil {
|
|
452
|
+
fmt.Fprintln(os.Stderr, err)
|
|
453
|
+
os.Exit(1)
|
|
454
|
+
}
|
|
455
|
+
}
|
package/go.mod
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module github.com/itsvedantkumar/factory-ai
|
|
2
|
+
|
|
3
|
+
go 1.25.0
|
|
4
|
+
|
|
5
|
+
require (
|
|
6
|
+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0
|
|
7
|
+
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0
|
|
8
|
+
github.com/charmbracelet/bubbletea v1.3.10
|
|
9
|
+
github.com/charmbracelet/lipgloss v1.1.0
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
require (
|
|
13
|
+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect
|
|
14
|
+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
|
|
15
|
+
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
|
16
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
17
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
18
|
+
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
|
19
|
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
20
|
+
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
21
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
22
|
+
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
|
23
|
+
github.com/google/uuid v1.6.0 // indirect
|
|
24
|
+
github.com/kylelemons/godebug v1.1.0 // indirect
|
|
25
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
26
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
27
|
+
github.com/mattn/go-localereader v0.0.1 // indirect
|
|
28
|
+
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
29
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
|
30
|
+
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
31
|
+
github.com/muesli/termenv v0.16.0 // indirect
|
|
32
|
+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
|
33
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
34
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
35
|
+
golang.org/x/crypto v0.53.0 // indirect
|
|
36
|
+
golang.org/x/net v0.56.0 // indirect
|
|
37
|
+
golang.org/x/sys v0.46.0 // indirect
|
|
38
|
+
golang.org/x/text v0.38.0 // indirect
|
|
39
|
+
)
|
package/go.sum
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
|
2
|
+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
|
3
|
+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
|
4
|
+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
|
5
|
+
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
|
6
|
+
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso=
|
|
7
|
+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
|
8
|
+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
|
9
|
+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
|
10
|
+
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
|
11
|
+
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0 h1:irsmOWwkp0KCTTNS5e2hdFeIvSQClQo2No3IaNmL3Vw=
|
|
12
|
+
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.8.0/go.mod h1:GWcBkQj3MqN7ozHKLaCCAuNLiXoIGv2RtanfAwSjY/Y=
|
|
13
|
+
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
|
14
|
+
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
|
15
|
+
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
|
16
|
+
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
|
17
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
18
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
19
|
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
|
20
|
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
|
21
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
22
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
23
|
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
24
|
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
25
|
+
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
|
26
|
+
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
|
27
|
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
28
|
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
29
|
+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
30
|
+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
31
|
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
32
|
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
33
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
|
34
|
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
|
35
|
+
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
|
36
|
+
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
|
37
|
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
38
|
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
39
|
+
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
|
40
|
+
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
|
41
|
+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
|
42
|
+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
|
43
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
44
|
+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
45
|
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
46
|
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
47
|
+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
|
48
|
+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
|
49
|
+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
50
|
+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
51
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
|
52
|
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
|
53
|
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
54
|
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
55
|
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
56
|
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
57
|
+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
|
58
|
+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
|
59
|
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
60
|
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
61
|
+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
62
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
63
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
64
|
+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
65
|
+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
66
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
67
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
68
|
+
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
|
69
|
+
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
|
70
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
|
71
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
|
72
|
+
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
|
73
|
+
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
|
74
|
+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
75
|
+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
76
|
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
77
|
+
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
|
78
|
+
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
79
|
+
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
|
80
|
+
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
|
81
|
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
82
|
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factory-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Deploy a private autonomous coding-agent factory on Azure: isolated builders, testers, security reviewers, durable orchestration, multi-model routing, memory, cost controls, and gated GitHub pull requests.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"type": "module",
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
31
|
+
"node": ">=22 <23"
|
|
32
32
|
},
|
|
33
33
|
"bin": {
|
|
34
34
|
"factory": "bin/factory",
|
|
@@ -48,14 +48,18 @@
|
|
|
48
48
|
"bootstrap/",
|
|
49
49
|
"capabilities/",
|
|
50
50
|
"config/",
|
|
51
|
+
"cmd/factory-ui/main.go",
|
|
51
52
|
"infra/",
|
|
53
|
+
"go.mod",
|
|
54
|
+
"go.sum",
|
|
52
55
|
"src/",
|
|
56
|
+
"scripts/",
|
|
53
57
|
"templates/",
|
|
54
58
|
"*.md",
|
|
55
59
|
"Dockerfile.worker"
|
|
56
60
|
],
|
|
57
61
|
"scripts": {
|
|
58
|
-
"test": "node
|
|
62
|
+
"test": "node scripts/test.js",
|
|
59
63
|
"lint": "eslint .",
|
|
60
64
|
"check": "node --check src/*.js"
|
|
61
65
|
},
|
package/scripts/test.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const ignoredWrapperFlags = new Set(["--passWithNoTests", "--no-coverage"]);
|
|
5
|
+
const args = process.argv.slice(2).filter((argument) => !ignoredWrapperFlags.has(argument));
|
|
6
|
+
const result = spawnSync(process.execPath, ["--test", ...args], { stdio: "inherit" });
|
|
7
|
+
process.exitCode = result.status ?? 1;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseObjective } from "./validation.js";
|
|
3
|
+
|
|
4
|
+
const acpObjectiveSchema = z.object({
|
|
5
|
+
protocol: z.literal("acp"),
|
|
6
|
+
version: z.literal("1.0"),
|
|
7
|
+
method: z.literal("objective.submit"),
|
|
8
|
+
params: z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
objective: z.string(),
|
|
11
|
+
repository: z.string(),
|
|
12
|
+
baseBranch: z.string().optional(),
|
|
13
|
+
}).strict(),
|
|
14
|
+
}).strict();
|
|
15
|
+
|
|
16
|
+
export function translateAcpObjective(value, { enabled = false } = {}) {
|
|
17
|
+
if (!enabled) throw new Error("ACP adapter is disabled");
|
|
18
|
+
const request = acpObjectiveSchema.parse(value);
|
|
19
|
+
return parseObjective({
|
|
20
|
+
id: request.params.id,
|
|
21
|
+
type: "objective",
|
|
22
|
+
objective: request.params.objective,
|
|
23
|
+
repository: request.params.repository,
|
|
24
|
+
baseBranch: request.params.baseBranch,
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/acp-cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
4
|
+
import { ServiceBusClient } from "@azure/service-bus";
|
|
5
|
+
import { translateAcpObjective } from "./acp-adapter.js";
|
|
6
|
+
import { sendMessage } from "./bus.js";
|
|
7
|
+
|
|
8
|
+
const file = process.argv[2];
|
|
9
|
+
if (!file) throw new Error("ACP request JSON file is required");
|
|
10
|
+
const objective = translateAcpObjective(JSON.parse(await readFile(file, "utf8")), { enabled: process.env.FACTORY_ACP_ENABLED === "true" });
|
|
11
|
+
const namespace = process.env.SERVICE_BUS_NAMESPACE;
|
|
12
|
+
if (!namespace) throw new Error("SERVICE_BUS_NAMESPACE is required");
|
|
13
|
+
const client = new ServiceBusClient(namespace.includes(".") ? namespace : `${namespace}.servicebus.windows.net`, new DefaultAzureCredential());
|
|
14
|
+
const sender = client.createSender(process.env.CONTROL_QUEUE ?? "control-events");
|
|
15
|
+
try { await sendMessage(sender, objective, objective.id); } finally { await sender.close(); await client.close(); }
|
|
16
|
+
process.stdout.write(`${JSON.stringify({ objectiveId: objective.id, status: "queued" })}\n`);
|