v0.2.0 42a5079

main.go RawHistoryFolder

1// stubchild is a minimal subagent-worker stand-in used by the runner
2// end-to-end test. It speaks the daemon protocol the real zut
3// binary will implement next:
4//
5//   - parses --subagent-worker <path>, --session <path>, --cwd <path>,
6//     and an optional positional task,
7//   - opens a unix-socket listener at the inbox path so the
8//     supervisor's Inbox can dial through,
9//   - emits well-formed JSONL events on stdout that the supervisor
10//     mirrors into the durable event log,
11//   - reads one line per supervisor message and echoes it back as
12//     a "user_message" event followed by a fake "assistant_message"
13//     so the runner sees the dialogue happen.
14//
15// The runner test compiles this binary into a tempdir, points
16// subagent's execRunner at it via Command, and asserts the events
17// flow through correctly without needing the full zut model
18// machinery.
19package main
20
21import (
22	"bufio"
23	"encoding/json"
24	"flag"
25	"fmt"
26	"net"
27	"os"
28	"sync"
29	"time"
30
31	"github.com/bnema/zut/packages/agent/subagents"
32)
33
34func main() {
35	var (
36		inboxPath   string
37		sessionPath string
38		cwd         string
39	)
40	flag.StringVar(&inboxPath, "subagent-worker", "", "inbox socket path")
41	flag.StringVar(&sessionPath, "session", "", "session file path")
42	flag.StringVar(&cwd, "cwd", "", "working directory")
43	flag.Parse()
44
45	if inboxPath == "" {
46		fmt.Fprintln(os.Stderr, "stubchild: --subagent-worker required")
47		os.Exit(2)
48	}
49
50	emit := newEmitter()
51
52	// Open the inbox listener BEFORE doing any work so the supervisor
53	// can dial through as soon as it sees the agent_ready event. If we
54	// processed the initial task first, the parent's send call
55	// would race the stub's net.Listen and trip ErrNotReady.
56	ln, err := net.Listen("unix", inboxPath)
57	if err != nil {
58		emit("error", map[string]any{"message": err.Error()})
59		os.Exit(1)
60	}
61	defer os.Remove(inboxPath)
62
63	emit("agent_ready", map[string]any{
64		"inbox":   inboxPath,
65		"session": sessionPath,
66		"cwd":     cwd,
67	})
68
69	// Initial task lives in the positional. Process it as the first
70	// user turn so the supervisor's "initial task" path is exercised.
71	if task := flag.Arg(0); task != "" {
72		runTurn(emit, task, 0)
73	}
74
75	turn := 1
76	for {
77		c, err := ln.Accept()
78		if err != nil {
79			return
80		}
81		br := bufio.NewReader(c)
82		for {
83			line, err := br.ReadString('\n')
84			if line != "" {
85				msg := trimNL(line)
86				command, parseErr := subagents.ParseCommand(msg)
87				if parseErr == nil {
88					switch command.Type {
89					case subagents.CommandAgentShutdown:
90						emit("agent_stopped", map[string]any{"reason": "shutdown"})
91						_ = c.Close()
92						return
93					case subagents.CommandTurnCancel:
94						emit("turn_end", map[string]any{"stop": "cancelled"})
95					case subagents.CommandTurnStart:
96						var payload subagents.TurnStartPayload
97						if command.DecodePayload(&payload) == nil {
98							runTurn(emit, payload.Prompt, turn)
99							turn++
100						}
101					}
102				}
103			}
104
105			if err != nil {
106				_ = c.Close()
107				break
108			}
109		}
110	}
111}
112
113// runTurn fakes one model round-trip: turn_start, an echoed
114// user_message, an assistant_message that echoes back, and a
115// turn_end. Enough event variety that applyEventToSink has
116// something to interpret.
117func runTurn(emit emitter, text string, step int) {
118	emit("turn_start", map[string]any{"step": step})
119	emit("user_message", map[string]any{
120		"content": []any{
121			map[string]any{"type": "text", "text": text},
122		},
123	})
124	emit("assistant_message", map[string]any{
125		"content": []any{
126			map[string]any{"type": "text", "text": "echo: " + text},
127		},
128	})
129	emit("turn_end", map[string]any{"stop": "end"})
130}
131
132type emitter = func(string, map[string]any)
133
134func newEmitter() emitter {
135	var mu sync.Mutex
136	enc := json.NewEncoder(os.Stdout)
137	versioned := os.Getenv("ZUT_STUB_PROTOCOL") == "1"
138	return func(typ string, data map[string]any) {
139		mu.Lock()
140		defer mu.Unlock()
141		if data == nil {
142			data = map[string]any{}
143		}
144		if versioned {
145			envelope := subagents.NewEventEnvelope(typ, "stub-agent", "", data)
146			_ = enc.Encode(envelope)
147			return
148		}
149		data["type"] = typ
150		data["time"] = time.Now().Format(time.RFC3339Nano)
151		_ = enc.Encode(data)
152	}
153}
154
155func trimNL(s string) string {
156	for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
157		s = s[:len(s)-1]
158	}
159	return s
160}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close