Update beads and p9 implementation
This commit is contained in:
parent
cbace3407c
commit
425fa53877
17
BEADS.md
17
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 <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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, "/"), "/")
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue