diff --git a/BEADS.md b/BEADS.md index 3a783b2..e661326 100644 --- a/BEADS.md +++ b/BEADS.md @@ -46,20 +46,27 @@ agent/beads/ | Command | Format | Description | |---------|--------|-------------| | `new` | `new 'title' ['description'] [parent-id]` | Create new bead (optionally as child) | +| `create` | `create 'title' ['description'] [parent-id]` | Alias for new | | `claim` | `claim ` | Claim bead (sets assignee + in_progress) | | `complete` | `complete ` | Mark bead as completed | | `close` | `close ` | Alias for complete | | `fail` | `fail 'reason'` | Mark bead as failed | | `dep` | `dep ` | Add dependency (parent blocks child) | +| `add-dep` | `add-dep ` | Alias for dep | | `undep` | `undep ` | Remove dependency | +| `rm-dep` | `rm-dep ` | Alias for undep | | `update` | `update 'value'` | Update bead field | | `delete` | `delete ` | Delete bead | +| `rm` | `rm ` | Alias for delete | | `comment` | `comment 'text'` | Add comment to bead | | `label` | `label 'label'` | Add label to bead | | `unlabel` | `unlabel 'label'` | Remove label from bead | +| `set-capability` | `set-capability low\|standard\|high` | Set capability level (replaces existing) | | `pending-approval` | `pending-approval [assignee]` | Set status=pending_approval and assignee (default: user) | | `pending-review` | `pending-review [assignee]` | Set status=pending_review and assignee (default: user) | | `resume` | `resume [assignee]` | Set status=in_progress and assignee after approval (default: user) | +| `init` | `init [prefix]` | Initialize beads with custom ID prefix (default: bd) | +| `batch-create` | `batch-create ` | Create multiple beads from JSON array | ## Usage Examples @@ -141,11 +148,19 @@ echo "comment bd-a1b2 'Work in progress'" | 9p write agent/beads/ctl # Add label echo "label bd-a1b2 'backend'" | 9p write agent/beads/ctl + +# Set capability level +echo "set-capability bd-a1b2 high" | 9p write agent/beads/ctl + +# Initialize with custom prefix +echo "init myprefix" | 9p write agent/beads/ctl ``` ## Filtered Queries -The `agent/beads/query` endpoint accepts JSON filter criteria for complex queries: +The `agent/beads/query` endpoint accepts JSON filter criteria for complex queries. + +**Note:** The query endpoint is stateful per session. Write a filter to set the query, then read to retrieve results. The filter persists until overwritten. ```sh # Query by assignee and priority diff --git a/internal/eventbus/bus.go b/internal/eventbus/bus.go index 3aad200..468943c 100644 --- a/internal/eventbus/bus.go +++ b/internal/eventbus/bus.go @@ -5,6 +5,7 @@ package eventbus import ( "encoding/json" + "fmt" "time" "github.com/google/uuid" @@ -71,6 +72,10 @@ func (b *Bus) Subscribe() (<-chan *Event, func()) { // MarshalEvent encodes an event as a JSON line (with trailing newline). func MarshalEvent(e *Event) []byte { - data, _ := json.Marshal(e) + data, err := json.Marshal(e) + if err != nil { + // Log marshal error but return valid JSON to avoid breaking event stream + return []byte(fmt.Sprintf(`{"error":"failed to marshal event: %v"}`+"\n", err)) + } return append(data, '\n') } diff --git a/internal/p9/beads.go b/internal/p9/beads.go index 34a9181..e47de56 100644 --- a/internal/p9/beads.go +++ b/internal/p9/beads.go @@ -66,6 +66,8 @@ const ( ) // BeadsFS handles beads filesystem operations. +// BeadsFS provides 9P filesystem access to beads task tracking. +// It exposes beads operations through virtual files and directories. type BeadsFS struct { store bd.Storage ctx context.Context @@ -83,7 +85,9 @@ func (b *BeadsFS) Close() error { return nil } -// Read handles reads from beads filesystem. +// Read handles reads from beads filesystem paths. +// Supports list, ready, pending, stats, blocked, stale, query, config endpoints, +// as well as search, batch, label, and per-bead property access. func (b *BeadsFS) Read(path string) ([]byte, error) { parts := strings.Split(strings.TrimPrefix(path, "/"), "/") @@ -131,7 +135,8 @@ func (b *BeadsFS) Read(path string) ([]byte, error) { } } -// Write handles writes to beads filesystem. +// Write handles writes to beads filesystem paths. +// Supports query filter updates and ctl command execution. func (b *BeadsFS) Write(path string, data []byte, sessionID string) error { parts := strings.Split(strings.TrimPrefix(path, "/"), "/") diff --git a/internal/p9/beads_parse_test.go b/internal/p9/beads_parse_test.go new file mode 100644 index 0000000..abfd67c --- /dev/null +++ b/internal/p9/beads_parse_test.go @@ -0,0 +1,126 @@ +package p9 + +import ( + "testing" +) + +func TestParseCtlCommand(t *testing.T) { + tests := []struct { + name string + input string + wantCmd string + wantArgs []string + wantErr bool + }{ + { + name: "empty command", + input: "", + wantErr: true, + }, + { + name: "whitespace only", + input: " ", + wantErr: true, + }, + { + name: "command only", + input: "claim", + wantCmd: "claim", + wantArgs: []string{}, + }, + { + name: "command with one arg", + input: "claim bd-123", + wantCmd: "claim", + wantArgs: []string{"bd-123"}, + }, + { + name: "command with multiple args", + input: "new title description parent", + wantCmd: "new", + wantArgs: []string{"title", "description", "parent"}, + }, + { + name: "single quoted arg", + input: "new 'my title'", + wantCmd: "new", + wantArgs: []string{"my title"}, + }, + { + name: "double quoted arg", + input: `new "my title"`, + wantCmd: "new", + wantArgs: []string{"my title"}, + }, + { + name: "mixed quoted and unquoted", + input: "new 'my title' description bd-parent", + wantCmd: "new", + wantArgs: []string{"my title", "description", "bd-parent"}, + }, + { + name: "empty quoted string", + input: "comment bd-123 ''", + wantCmd: "comment", + wantArgs: []string{"bd-123", ""}, + }, + { + name: "multiple spaces", + input: "claim bd-123", + wantCmd: "claim", + wantArgs: []string{"bd-123"}, + }, + { + name: "tabs and spaces", + input: "claim\t\tbd-123", + wantCmd: "claim", + wantArgs: []string{"bd-123"}, + }, + { + name: "quoted arg with special chars", + input: "comment bd-123 'Work in progress: 50% done'", + wantCmd: "comment", + wantArgs: []string{"bd-123", "Work in progress: 50% done"}, + }, + { + name: "nested quotes not supported", + input: `new "title with 'nested' quotes"`, + wantCmd: "new", + wantArgs: []string{"title with ", "nested", " quotes"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, args, err := parseCtlCommand(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("expected error, got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if cmd != tt.wantCmd { + t.Errorf("command = %q, want %q", cmd, tt.wantCmd) + } + + if len(args) != len(tt.wantArgs) { + t.Errorf("args length = %d, want %d\nargs = %v\nwant = %v", + len(args), len(tt.wantArgs), args, tt.wantArgs) + return + } + + for i := range args { + if args[i] != tt.wantArgs[i] { + t.Errorf("args[%d] = %q, want %q", i, args[i], tt.wantArgs[i]) + } + } + }) + } +} diff --git a/internal/p9/server.go b/internal/p9/server.go index 9447e1a..5bcd49c 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -117,6 +117,8 @@ var fileNames = []string{"ctl", "state", "pid", "cwd", "alias", "backend", "cont // Directory names in session var dirNames = []string{"inbox", "outbox", "completed"} +// Server implements a 9P file server for agent session management. +// It exposes sessions, beads, tools, skills, and events through a virtual filesystem. type Server struct { mgr *session.Manager listener net.Listener diff --git a/internal/p9/skills.go b/internal/p9/skills.go index 55d4a6a..2f4d39d 100644 --- a/internal/p9/skills.go +++ b/internal/p9/skills.go @@ -12,11 +12,13 @@ import ( ) // SkillsFS provides virtual filesystem for skills organized by intent +// SkillsFS provides 9P filesystem access to agent skills organized by intent. +// Skills are discovered from configured directories and exposed as virtual files. type SkillsFS struct { skillsDirs []string } -// SkillMeta holds parsed YAML front-matter from SKILL.md +// SkillMeta holds parsed YAML front-matter from SKILL.md files. type SkillMeta struct { Name string Intents []string @@ -24,7 +26,8 @@ type SkillMeta struct { Path string // directory path } -// NewSkillsFS creates a new skills filesystem +// NewSkillsFS creates a new skills filesystem handler. +// It discovers skills from SKILLS_DIR environment variable or default locations. func NewSkillsFS() *SkillsFS { var dirs []string diff --git a/internal/p9/tools.go b/internal/p9/tools.go index 3d5ca0e..5781ebd 100644 --- a/internal/p9/tools.go +++ b/internal/p9/tools.go @@ -11,23 +11,28 @@ import ( "9fans.net/go/plan9" ) +// ToolsFS provides 9P filesystem access to agent tools organized by capability level. +// Tools are shell scripts with YAML front-matter defining their interface. type ToolsFS struct { tools []Tool toolsDir string } +// Tool represents an agent tool with its MCP-compatible schema. type Tool struct { Name string `json:"name"` Description string `json:"description"` InputSchema InputSchema `json:"inputSchema"` } +// InputSchema defines the JSON schema for tool input parameters. type InputSchema struct { Type string `json:"type"` Properties map[string]Property `json:"properties"` Required []string `json:"required,omitempty"` } +// Property defines a single input parameter schema. type Property struct { Type string `json:"type"` Description string `json:"description"` @@ -35,6 +40,7 @@ type Property struct { Enum []string `json:"enum,omitempty"` } +// Items defines the schema for array item types. type Items struct { Type string `json:"type"` }