Remove BeadsFS.store field, fix subtask creation to use mounted project stores

This commit is contained in:
Levi Neely 2026-02-28 22:52:14 +01:00
parent 7db743483b
commit 495136c70c
18 changed files with 61 additions and 128 deletions

View File

@ -81,7 +81,6 @@ type MountedProject struct {
// 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
lastQuery *bd.IssueFilter
queryResult []*bd.Issue
@ -92,7 +91,6 @@ type BeadsFS struct {
// NewBeadsFS creates a beads filesystem handler from an existing store.
func NewBeadsFS(store bd.Storage, ctx context.Context) *BeadsFS {
return &BeadsFS{
store: store,
ctx: ctx,
mounts: make(map[string]*MountedProject),
}
@ -223,7 +221,7 @@ func (b *BeadsFS) Read(path string) ([]byte, error) {
if hasMounts {
return b.readReadyAggregate()
}
return b.readReady()
return nil, fmt.Errorf("no mounts")
default:
return nil, fmt.Errorf("invalid path: %s", path)
}
@ -288,7 +286,36 @@ func (b *BeadsFS) Write(path string, data []byte, sessionID string) error {
return fmt.Errorf("write not allowed: %s", path)
}
return b.executeCtl(string(data))
// Global ctl commands (mount/umount/sync)
command, args, err := parseCtlCommand(string(data))
if err != nil {
return err
}
switch command {
case "mount":
if len(args) < 1 {
return fmt.Errorf("usage: mount <cwd> [name]")
}
cwd := args[0]
name := filepath.Base(cwd)
if len(args) >= 2 {
name = args[1]
}
return b.Mount(name, cwd)
case "umount":
if len(args) < 1 {
return fmt.Errorf("usage: umount <name>")
}
return b.Umount(args[0])
case "sync":
if len(args) < 1 {
return fmt.Errorf("usage: sync <name>")
}
return b.Sync(args[0])
default:
return fmt.Errorf("command %s requires a mounted project", command)
}
}
func (b *BeadsFS) writeToMount(m *MountedProject, endpoint string, data []byte, sessionID string) error {
@ -299,9 +326,6 @@ func (b *BeadsFS) writeToMount(m *MountedProject, endpoint string, data []byte,
return fmt.Errorf("unsupported mount write endpoint: %s", endpoint)
}
func (b *BeadsFS) readList() ([]byte, error) {
return b.readListFromStore(b.store)
}
func (b *BeadsFS) readMtab() ([]byte, error) {
b.mountsMu.RLock()
@ -355,9 +379,6 @@ func (b *BeadsFS) readListFromStore(store bd.Storage) ([]byte, error) {
return json.MarshalIndent(result, "", " ")
}
func (b *BeadsFS) readReady() ([]byte, error) {
return b.readReadyFromStore(b.store)
}
func (b *BeadsFS) readReadyFromStore(store bd.Storage) ([]byte, error) {
filter := bd.WorkFilter{}
@ -425,9 +446,6 @@ func (b *BeadsFS) readReadyAggregate() ([]byte, error) {
return json.MarshalIndent(allReady, "", " ")
}
func (b *BeadsFS) readSearch(query string) ([]byte, error) {
return b.readSearchFromStore(b.store, query)
}
func (b *BeadsFS) readSearchFromStore(store bd.Storage, query string) ([]byte, error) {
issues, err := store.SearchIssues(b.ctx, query, bd.IssueFilter{})
@ -440,9 +458,6 @@ func (b *BeadsFS) readSearchFromStore(store bd.Storage, query string) ([]byte, e
return json.MarshalIndent(issues, "", " ")
}
func (b *BeadsFS) readByExternalRef(ref string) ([]byte, error) {
return b.readByExternalRefFromStore(b.store, ref)
}
func (b *BeadsFS) readByExternalRefFromStore(store bd.Storage, ref string) ([]byte, error) {
issue, err := store.GetIssueByExternalRef(b.ctx, ref)
@ -452,25 +467,8 @@ func (b *BeadsFS) readByExternalRefFromStore(store bd.Storage, ref string) ([]by
return json.MarshalIndent(issue, "", " ")
}
func (b *BeadsFS) readConfig(key string) ([]byte, error) {
value, err := b.store.GetConfig(b.ctx, key)
if err != nil {
return nil, err
}
return []byte(value), nil
}
func (b *BeadsFS) readAllConfig() ([]byte, error) {
config, err := b.store.GetAllConfig(b.ctx)
if err != nil {
return nil, err
}
return json.MarshalIndent(config, "", " ")
}
func (b *BeadsFS) readBatch(ids string) ([]byte, error) {
return b.readBatchFromStore(b.store, ids)
}
func (b *BeadsFS) readBatchFromStore(store bd.Storage, ids string) ([]byte, error) {
idList := strings.Split(ids, ",")
@ -484,9 +482,6 @@ func (b *BeadsFS) readBatchFromStore(store bd.Storage, ids string) ([]byte, erro
return json.MarshalIndent(issues, "", " ")
}
func (b *BeadsFS) readByLabel(label string) ([]byte, error) {
return b.readByLabelFromStore(b.store, label)
}
func (b *BeadsFS) readByLabelFromStore(store bd.Storage, label string) ([]byte, error) {
issues, err := store.GetIssuesByLabel(b.ctx, label)
@ -499,9 +494,6 @@ func (b *BeadsFS) readByLabelFromStore(store bd.Storage, label string) ([]byte,
return json.MarshalIndent(issues, "", " ")
}
func (b *BeadsFS) readBlocked() ([]byte, error) {
return b.readBlockedFromStore(b.store)
}
func (b *BeadsFS) readBlockedFromStore(store bd.Storage) ([]byte, error) {
filter := bd.WorkFilter{}
@ -517,9 +509,6 @@ func (b *BeadsFS) readBlockedFromStore(store bd.Storage) ([]byte, error) {
// readPending returns beads in pending_approval or pending_review status,
// i.e. those awaiting a human APPROVAL_RESPONSE or REVIEW_RESPONSE.
func (b *BeadsFS) readPending() ([]byte, error) {
return b.readPendingFromStore(b.store)
}
func (b *BeadsFS) readPendingFromStore(store bd.Storage) ([]byte, error) {
issues, err := store.SearchIssues(b.ctx, "", bd.IssueFilter{})
@ -535,9 +524,6 @@ func (b *BeadsFS) readPendingFromStore(store bd.Storage) ([]byte, error) {
return json.MarshalIndent(pending, "", " ")
}
func (b *BeadsFS) readDependenciesMeta(beadID string) ([]byte, error) {
return b.readDependenciesMetaFromStore(b.store, beadID)
}
func (b *BeadsFS) readDependenciesMetaFromStore(store bd.Storage, beadID string) ([]byte, error) {
deps, err := store.GetDependenciesWithMetadata(b.ctx, beadID)
@ -547,9 +533,6 @@ func (b *BeadsFS) readDependenciesMetaFromStore(store bd.Storage, beadID string)
return json.MarshalIndent(deps, "", " ")
}
func (b *BeadsFS) readDependentsMeta(beadID string) ([]byte, error) {
return b.readDependentsMetaFromStore(b.store, beadID)
}
func (b *BeadsFS) readDependentsMetaFromStore(store bd.Storage, beadID string) ([]byte, error) {
deps, err := store.GetDependentsWithMetadata(b.ctx, beadID)
@ -559,9 +542,6 @@ func (b *BeadsFS) readDependentsMetaFromStore(store bd.Storage, beadID string) (
return json.MarshalIndent(deps, "", " ")
}
func (b *BeadsFS) readChildren(parentID string) ([]byte, error) {
return b.readChildrenFromStore(b.store, parentID)
}
func (b *BeadsFS) readChildrenFromStore(store bd.Storage, parentID string) ([]byte, error) {
filter := bd.IssueFilter{ParentID: &parentID}
@ -575,9 +555,6 @@ func (b *BeadsFS) readChildrenFromStore(store bd.Storage, parentID string) ([]by
return json.MarshalIndent(children, "", " ")
}
func (b *BeadsFS) readStale() ([]byte, error) {
return b.readStaleFromStore(b.store)
}
func (b *BeadsFS) readStaleFromStore(store bd.Storage) ([]byte, error) {
// Get all non-closed issues
@ -599,21 +576,6 @@ func (b *BeadsFS) readStaleFromStore(store bd.Storage) ([]byte, error) {
return json.MarshalIndent(stale, "", " ")
}
func (b *BeadsFS) executeQuery(data []byte) error {
var filter bd.IssueFilter
if err := json.Unmarshal(data, &filter); err != nil {
return fmt.Errorf("invalid JSON filter: %w", err)
}
issues, err := b.store.SearchIssues(b.ctx, "", filter)
if err != nil {
return err
}
b.lastQuery = &filter
b.queryResult = issues
return nil
}
func (b *BeadsFS) readQuery() ([]byte, error) {
if b.queryResult == nil {
@ -622,17 +584,7 @@ func (b *BeadsFS) readQuery() ([]byte, error) {
return json.MarshalIndent(b.queryResult, "", " ")
}
func (b *BeadsFS) readStats() ([]byte, error) {
stats, err := b.store.GetStatistics(b.ctx)
if err != nil {
return nil, err
}
return json.MarshalIndent(stats, "", " ")
}
func (b *BeadsFS) readBeadProperty(beadID, property string) ([]byte, error) {
return b.readBeadPropertyFromStore(b.store, beadID, property)
}
func (b *BeadsFS) readBeadPropertyFromStore(store bd.Storage, beadID, property string) ([]byte, error) {
issue, err := store.GetIssue(b.ctx, beadID)
@ -699,9 +651,6 @@ func (b *BeadsFS) readBeadPropertyFromStore(store bd.Storage, beadID, property s
}
}
func (b *BeadsFS) getBlockers(id string) ([]string, error) {
return b.getBlockersFromStore(b.store, id)
}
func (b *BeadsFS) getBlockersFromStore(store bd.Storage, id string) ([]string, error) {
deps, err := store.GetDependenciesWithMetadata(b.ctx, id)
@ -717,9 +666,6 @@ func (b *BeadsFS) getBlockersFromStore(store bd.Storage, id string) ([]string, e
return blockers, nil
}
func (b *BeadsFS) executeCtl(cmd string) error {
return b.executeCtlOnStore(b.store, cmd)
}
func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error {
command, args, err := parseCtlCommand(cmd)
@ -737,27 +683,6 @@ func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error {
}
return store.SetConfig(b.ctx, "issue_prefix", prefix)
case "mount":
if len(args) < 1 {
return fmt.Errorf("usage: mount <cwd> [name]")
}
cwd := args[0]
name := filepath.Base(cwd)
if len(args) >= 2 {
name = args[1]
}
return b.Mount(name, cwd)
case "umount":
if len(args) < 1 {
return fmt.Errorf("usage: umount <name>")
}
return b.Umount(args[0])
case "sync":
if len(args) < 1 {
return fmt.Errorf("usage: sync <name>")
}
return b.Sync(args[0])
case "new", "create":
if len(args) < 1 {
return fmt.Errorf("usage: new 'title' ['description'] [parent-id] [--no-lint] [capability=low|standard|high]")
@ -799,7 +724,7 @@ func (b *BeadsFS) executeCtlOnStore(store bd.Storage, cmd string) error {
}
if parentID != "" {
childID, err := b.createSubtask(parentID, title, description, actor)
childID, err := b.createSubtaskOnStore(store, parentID, title, description, actor)
if err != nil {
return err
}
@ -1261,9 +1186,10 @@ func isAlphanumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')
}
func (b *BeadsFS) createSubtask(parentID, title, description, actor string) (string, error) {
func (b *BeadsFS) createSubtaskOnStore(store bd.Storage, parentID, title, description, actor string) (string, error) {
// Verify parent exists
parent, err := b.store.GetIssue(b.ctx, parentID)
parent, err := store.GetIssue(b.ctx, parentID)
if err != nil {
return "", fmt.Errorf("failed to get parent: %w", err)
}
@ -1273,7 +1199,7 @@ func (b *BeadsFS) createSubtask(parentID, title, description, actor string) (str
// Find next child number by scanning existing IDs with parent filter
filter := bd.IssueFilter{ParentID: &parentID}
children, err := b.store.SearchIssues(b.ctx, "", filter)
children, err := store.SearchIssues(b.ctx, "", filter)
if err != nil {
return "", fmt.Errorf("failed to search children: %w", err)
}
@ -1301,7 +1227,7 @@ func (b *BeadsFS) createSubtask(parentID, title, description, actor string) (str
Priority: 2,
}
if err := b.store.CreateIssue(b.ctx, issue, actor); err != nil {
if err := store.CreateIssue(b.ctx, issue, actor); err != nil {
return "", err
}
return childID, nil

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "dep $*" | 9p write agent/beads/ctl
echo "dep $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "batch-create $1" | 9p write agent/beads/ctl
echo "batch-create $1" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "claim $*" | 9p write agent/beads/ctl
echo "claim $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "comment $*" | 9p write agent/beads/ctl
echo "comment $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "complete $1" | 9p write agent/beads/ctl
echo "complete $1" | 9p write agent/beads/list

View File

@ -14,4 +14,11 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "new $*" | 9p write agent/beads/ctl
# Find the first mounted project
MOUNT=$(9p ls agent/beads | grep -v '^ctl$' | grep -v '^mtab$' | grep -v '^ready$' | head -1)
if [ -z "$MOUNT" ]; then
echo "Error: No beads projects mounted" >&2
exit 1
fi
echo "new \"$1\" \"${2:-}\" ${3:-} ${4:-} ${5:-}" | 9p write agent/beads/$MOUNT/ctl

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "delete $1" | 9p write agent/beads/ctl
echo "delete $1" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "fail $*" | 9p write agent/beads/ctl
echo "fail $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "label $*" | 9p write agent/beads/ctl
echo "label $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "pending-approval $*" | 9p write agent/beads/ctl
echo "pending-approval $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "pending-review $*" | 9p write agent/beads/ctl
echo "pending-review $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "undep $*" | 9p write agent/beads/ctl
echo "undep $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "resume $*" | 9p write agent/beads/ctl
echo "resume $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "set-capability $*" | 9p write agent/beads/ctl
echo "set-capability $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 1 ]; then
exit 1
fi
echo "unclaim $1" | 9p write agent/beads/ctl
echo "unclaim $1" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 2 ]; then
exit 1
fi
echo "unlabel $*" | 9p write agent/beads/ctl
echo "unlabel $*" | 9p write agent/beads/list

View File

@ -14,4 +14,4 @@ if [ $# -lt 3 ]; then
exit 1
fi
echo "update $*" | 9p write agent/beads/ctl
echo "update $*" | 9p write agent/beads/list