Update beads and p9 implementation

This commit is contained in:
Levi Neely 2026-02-27 21:16:03 +01:00
parent cbace3407c
commit 425fa53877
7 changed files with 168 additions and 6 deletions

View File

@ -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 <bead-id>` | Claim bead (sets assignee + in_progress) |
| `complete` | `complete <bead-id>` | Mark bead as completed |
| `close` | `close <bead-id>` | Alias for complete |
| `fail` | `fail <bead-id> 'reason'` | Mark bead as failed |
| `dep` | `dep <child-id> <parent-id>` | Add dependency (parent blocks child) |
| `add-dep` | `add-dep <child-id> <parent-id>` | Alias for dep |
| `undep` | `undep <child-id> <parent-id>` | Remove dependency |
| `rm-dep` | `rm-dep <child-id> <parent-id>` | Alias for undep |
| `update` | `update <bead-id> <field> 'value'` | Update bead field |
| `delete` | `delete <bead-id>` | Delete bead |
| `rm` | `rm <bead-id>` | Alias for delete |
| `comment` | `comment <bead-id> 'text'` | Add comment to bead |
| `label` | `label <bead-id> 'label'` | Add label to bead |
| `unlabel` | `unlabel <bead-id> 'label'` | Remove label from bead |
| `set-capability` | `set-capability <bead-id> low\|standard\|high` | Set capability level (replaces existing) |
| `pending-approval` | `pending-approval <bead-id> [assignee]` | Set status=pending_approval and assignee (default: user) |
| `pending-review` | `pending-review <bead-id> [assignee]` | Set status=pending_review and assignee (default: user) |
| `resume` | `resume <bead-id> [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 <json-array>` | 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

View File

@ -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')
}

View File

@ -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, "/"), "/")

View File

@ -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])
}
}
})
}
}

View File

@ -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

View File

@ -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

View File

@ -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"`
}