Extract beads to standalone 9beads service
- Remove embedded BeadsFS from anvilsrv - Remove beads.go and related tests from p9 package - Update Assist to connect to separate 'beads' 9P service - Update mcptools scripts: anvillm/beads/* -> beads/* Beads now served by 9beads (separate repo/process).
This commit is contained in:
parent
daf50d9783
commit
8e50b84707
|
|
@ -52,7 +52,8 @@ type SessionInfo struct {
|
|||
}
|
||||
|
||||
var (
|
||||
fs *client.Fsys
|
||||
fs *client.Fsys
|
||||
beadsFs *client.Fsys
|
||||
// Track window IDs for prompt windows (client-side state)
|
||||
promptWindows = make(map[string]int) // session ID -> window ID
|
||||
promptWindowsMu sync.RWMutex // protects promptWindows map
|
||||
|
|
@ -97,6 +98,12 @@ func main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to 9beads (optional - beads features disabled if not running)
|
||||
beadsFs, err = connectToBeads()
|
||||
if err != nil {
|
||||
logging.Logger().Info("9beads not running, beads features disabled")
|
||||
}
|
||||
if fs != nil {
|
||||
defer fs.Close()
|
||||
}
|
||||
|
|
@ -360,10 +367,22 @@ func connectToServer() (*client.Fsys, error) {
|
|||
return client.MountService("anvillm")
|
||||
}
|
||||
|
||||
func connectToBeads() (*client.Fsys, error) {
|
||||
ns := client.Namespace()
|
||||
if ns == "" {
|
||||
return nil, fmt.Errorf("no namespace")
|
||||
}
|
||||
return client.MountService("beads")
|
||||
}
|
||||
|
||||
func isConnected() bool {
|
||||
return fs != nil
|
||||
}
|
||||
|
||||
func isBeadsConnected() bool {
|
||||
return beadsFs != nil
|
||||
}
|
||||
|
||||
func createSession(backend, cwd string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv (use Daemon command to start server)")
|
||||
|
|
@ -425,7 +444,7 @@ func getMountForSession(sessionID string) string {
|
|||
if err != nil || sess.Cwd == "" {
|
||||
return ""
|
||||
}
|
||||
data, err := readFile("beads/mtab")
|
||||
data, err := readBeadsFile("mtab")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
|
@ -622,6 +641,44 @@ func writeFile(path string, data []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func readBeadsFile(path string) ([]byte, error) {
|
||||
if !isBeadsConnected() {
|
||||
return nil, fmt.Errorf("not connected to 9beads")
|
||||
}
|
||||
fid, err := beadsFs.Open(path, plan9.OREAD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fid.Close()
|
||||
|
||||
var buf []byte
|
||||
tmp := make([]byte, 8192)
|
||||
for {
|
||||
n, err := fid.Read(tmp)
|
||||
if n > 0 {
|
||||
buf = append(buf, tmp[:n]...)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func writeBeadsFile(path string, data []byte) error {
|
||||
if !isBeadsConnected() {
|
||||
return fmt.Errorf("not connected to 9beads")
|
||||
}
|
||||
fid, err := beadsFs.Open(path, plan9.OWRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fid.Close()
|
||||
|
||||
_, err = fid.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func refreshList(w *acme.Win) {
|
||||
var buf strings.Builder
|
||||
buf.WriteString("Backends: [Kiro] [Claude] [Ollama]\n\n")
|
||||
|
|
@ -2245,7 +2302,7 @@ func handleTasksWindow(w *acme.Win) {
|
|||
fmt.Fprintf(os.Stderr, "Error unmounting: %v\n", err)
|
||||
} else {
|
||||
if windowMount == name {
|
||||
data, _ := readFile("beads/mtab")
|
||||
data, _ := readBeadsFile("mtab")
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
|
|
@ -2271,7 +2328,7 @@ func handleTasksWindow(w *acme.Win) {
|
|||
case "Select":
|
||||
name := strings.TrimSpace(arg)
|
||||
if name == "" {
|
||||
data, err := readFile("beads/mtab")
|
||||
data, err := readBeadsFile("mtab")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -2279,7 +2336,7 @@ func handleTasksWindow(w *acme.Win) {
|
|||
fmt.Fprintf(os.Stderr, "Usage: select mount name, then Select\n")
|
||||
}
|
||||
} else {
|
||||
data, err := readFile("beads/mtab")
|
||||
data, err := readBeadsFile("mtab")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading mtab: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -2382,21 +2439,21 @@ func listBeadsWithFilter(filter string, mount string) ([]Bead, error) {
|
|||
switch filter {
|
||||
case "deferred":
|
||||
if mount == "" {
|
||||
endpoint = "beads/deferred"
|
||||
endpoint = "deferred"
|
||||
} else {
|
||||
endpoint = filepath.Join("beads", mount, "deferred")
|
||||
endpoint = filepath.Join(mount, "deferred")
|
||||
}
|
||||
case "ready":
|
||||
if mount == "" {
|
||||
endpoint = "beads/ready"
|
||||
endpoint = "ready"
|
||||
} else {
|
||||
endpoint = filepath.Join("beads", mount, "ready")
|
||||
endpoint = filepath.Join(mount, "ready")
|
||||
}
|
||||
default:
|
||||
endpoint = filepath.Join("beads", mount, "list")
|
||||
endpoint = filepath.Join(mount, "list")
|
||||
}
|
||||
|
||||
data, err := readFile(endpoint)
|
||||
data, err := readBeadsFile(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -2519,7 +2576,7 @@ func createBeadFromMarkdown(content, parentID string, mount string) error {
|
|||
cmd = fmt.Sprintf("%s blockers=%s", cmd, blockers)
|
||||
}
|
||||
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
|
||||
func openViewBeadWindow(beadID string, mount string) error {
|
||||
|
|
@ -2689,7 +2746,7 @@ func handleCommentWindow(w *acme.Win, beadID string, mount string) {
|
|||
continue
|
||||
}
|
||||
cmd := fmt.Sprintf("comment %s '%s'", beadID, text)
|
||||
if err := writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd)); err != nil {
|
||||
if err := writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd)); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding comment: %v\n", err)
|
||||
} else {
|
||||
w.Addr(",")
|
||||
|
|
@ -2724,7 +2781,7 @@ func handleCommentsWindow(w *acme.Win, beadID string, mount string) {
|
|||
}
|
||||
|
||||
func refreshCommentsWindow(w *acme.Win, beadID string, mount string) {
|
||||
data, err := readFile(filepath.Join("beads", mount, beadID, "comments"))
|
||||
data, err := readBeadsFile(filepath.Join( mount, beadID, "comments"))
|
||||
if err != nil {
|
||||
w.Addr(",")
|
||||
w.Write("data", []byte(fmt.Sprintf("Error reading comments: %v\n", err)))
|
||||
|
|
@ -2766,7 +2823,7 @@ func getBead(beadID string, mount string) (*Bead, error) {
|
|||
return nil, fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
|
||||
data, err := readFile(filepath.Join("beads", mount, beadID, "json"))
|
||||
data, err := readBeadsFile(filepath.Join( mount, beadID, "json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -2786,7 +2843,7 @@ func addBlocksDep(blockerID, blockedID string, mount string) error {
|
|||
// dep <child> <parent> means parent blocks child
|
||||
// So "blockerID blocks blockedID" means blockedID depends on blockerID
|
||||
cmd := fmt.Sprintf("dep %s %s", blockedID, blockerID)
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
|
||||
func removeBlocksDep(beadID, blockerID string, mount string) error {
|
||||
|
|
@ -2794,7 +2851,7 @@ func removeBlocksDep(beadID, blockerID string, mount string) error {
|
|||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
cmd := fmt.Sprintf("undep %s %s", beadID, blockerID)
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
|
||||
func deleteBead(beadID string, mount string) error {
|
||||
|
|
@ -2802,49 +2859,49 @@ func deleteBead(beadID string, mount string) error {
|
|||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
cmd := fmt.Sprintf("delete %s", beadID)
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
|
||||
func claimBeadAsUser(beadID string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("claim %s user", beadID)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("claim %s user", beadID)))
|
||||
}
|
||||
|
||||
func unclaimBead(beadID string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("unclaim %s", beadID)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("unclaim %s", beadID)))
|
||||
}
|
||||
|
||||
func completeBead(beadID string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("complete %s", beadID)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("complete %s", beadID)))
|
||||
}
|
||||
|
||||
func failBead(beadID, reason string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("fail %s %s", beadID, reason)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("fail %s %s", beadID, reason)))
|
||||
}
|
||||
|
||||
func openBead(beadID string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("open %s", beadID)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("open %s", beadID)))
|
||||
}
|
||||
|
||||
func deferBead(beadID string, mount string) error {
|
||||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("defer %s", beadID)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("defer %s", beadID)))
|
||||
}
|
||||
|
||||
func updateBead(beadID, content string, origBlockers []string, mount string) error {
|
||||
|
|
@ -2903,11 +2960,11 @@ func updateBead(beadID, content string, origBlockers []string, mount string) err
|
|||
// Update title and description
|
||||
if title != "" {
|
||||
cmd := fmt.Sprintf("update %s title '%s'", beadID, shellEscape(title))
|
||||
writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
if description != "" {
|
||||
cmd := fmt.Sprintf("update %s description '%s'", beadID, shellEscape(description))
|
||||
writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
|
||||
writeBeadsFile(filepath.Join( mount, "ctl"), []byte(cmd))
|
||||
}
|
||||
|
||||
// Update blockers
|
||||
|
|
@ -2933,16 +2990,16 @@ func updateBead(beadID, content string, origBlockers []string, mount string) err
|
|||
|
||||
// Toggle requires_approval label
|
||||
if requiresApproval == 1 {
|
||||
writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("label %s requires_approval", beadID)))
|
||||
writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("label %s requires_approval", beadID)))
|
||||
} else if requiresApproval == 0 {
|
||||
writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("unlabel %s requires_approval", beadID)))
|
||||
writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("unlabel %s requires_approval", beadID)))
|
||||
}
|
||||
|
||||
// Toggle requires_review label
|
||||
if requiresReview == 1 {
|
||||
writeFile(filepath.Join("beads", "", "ctl"), []byte(fmt.Sprintf("label %s requires_review", beadID)))
|
||||
writeBeadsFile(filepath.Join( "", "ctl"), []byte(fmt.Sprintf("label %s requires_review", beadID)))
|
||||
} else if requiresReview == 0 {
|
||||
writeFile(filepath.Join("beads", "", "ctl"), []byte(fmt.Sprintf("unlabel %s requires_review", beadID)))
|
||||
writeBeadsFile(filepath.Join( "", "ctl"), []byte(fmt.Sprintf("unlabel %s requires_review", beadID)))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -2952,7 +3009,7 @@ func initBeads(prefix string, mount string) error {
|
|||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile(filepath.Join("beads", mount, "ctl"), []byte(fmt.Sprintf("init %s", prefix)))
|
||||
return writeBeadsFile(filepath.Join( mount, "ctl"), []byte(fmt.Sprintf("init %s", prefix)))
|
||||
}
|
||||
|
||||
// mountProject mounts cwd via mount_beads.sh mcptool and returns the generated mount name.
|
||||
|
|
@ -2978,5 +3035,5 @@ func umountProject(name string) error {
|
|||
if !isConnected() {
|
||||
return fmt.Errorf("not connected to anvilsrv")
|
||||
}
|
||||
return writeFile("beads/ctl", []byte(fmt.Sprintf("umount %s", name)))
|
||||
return writeBeadsFile("ctl", []byte(fmt.Sprintf("umount %s", name)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,17 +211,15 @@ func start(daemonize bool) {
|
|||
}
|
||||
}()
|
||||
|
||||
// Start 9P server with empty beads filesystem (mounts created on-demand)
|
||||
beadsFS := p9.NewBeadsFS(nil, context.Background())
|
||||
srv, err := p9.NewServer(mgr, beadsFS)
|
||||
// Start 9P server (beads served separately by 9beads)
|
||||
srv, err := p9.NewServer(mgr)
|
||||
if err != nil {
|
||||
logging.Logger().Fatal("failed to start 9P server", zap.Error(err))
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
// Wire up event bus to session manager and beads filesystem
|
||||
// Wire up event bus to session manager
|
||||
mgr.SetEventBus(srv.Events())
|
||||
beadsFS.SetEventBus(srv.Events())
|
||||
|
||||
// Start maildir writer for message persistence
|
||||
mailDir := filepath.Join(os.Getenv("HOME"), ".local", "share", "anvillm", "mail")
|
||||
|
|
|
|||
1612
internal/p9/beads.go
1612
internal/p9/beads.go
File diff suppressed because it is too large
Load Diff
|
|
@ -1,336 +0,0 @@
|
|||
package p9
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// goodDesc is a well-formed description that should pass all lint rules.
|
||||
// It contains: file path, backtick identifier, Acme address, acceptance
|
||||
// criterion, how signal, imperative start, and a cross-reference.
|
||||
const goodDesc = "Add `lintDescription()` check in `internal/p9/beads.go:615,663` following the pattern of existing checks; the function must return a non-empty warnings slice when the description violates any rule. See bd-9y4 for full rule list."
|
||||
|
||||
func warnCount(desc, substr string) bool {
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestGoodDescPassesAll verifies that a high-quality description triggers no warnings.
|
||||
func TestGoodDescPassesAll(t *testing.T) {
|
||||
warnings := lintDescription(goodDesc)
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("expected no warnings for good description, got: %v", warnings)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 1: File path ---
|
||||
|
||||
func TestMissingFilePath(t *testing.T) {
|
||||
desc := "Add a helper function following the existing pattern; it must return an error when the input is empty."
|
||||
if !warnCount(desc, "missing file path") {
|
||||
t.Error("expected 'missing file path' warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilePathPresent(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following the existing pattern; it must return an error when input is empty. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "missing file path") {
|
||||
t.Errorf("unexpected 'missing file path' warning for desc with .go extension")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 2: Function name or location ---
|
||||
|
||||
func TestMissingFuncOrLine(t *testing.T) {
|
||||
desc := "Update internal/p9/beads.go following the existing pattern; the file must compile without errors after the change. See bd-abc for context."
|
||||
if !warnCount(desc, "missing function name") {
|
||||
t.Error("expected 'missing function name or location' warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncNamePresent(t *testing.T) {
|
||||
desc := "Fix `lintDescription()` in `internal/p9/beads.go` following the existing pattern; it must return warnings for vague descriptions. See bd-9y4."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "missing function name") {
|
||||
t.Errorf("unexpected location warning when func name present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcmeLineAddrRecognised(t *testing.T) {
|
||||
desc := "Fix `foo()` in `cmd/server.go:385` following the existing error-handling pattern; it must return a wrapped error. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "missing function name") {
|
||||
t.Errorf("unexpected location warning when Acme line address present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcmeRegexAddrRecognised(t *testing.T) {
|
||||
desc := "Update `internal/p9/beads.go:/lintDescription/` following the existing pattern; it must emit a warning for vague language. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "missing function name") {
|
||||
t.Errorf("unexpected location warning when Acme /regex/ address present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 3: Minimum length ---
|
||||
|
||||
func TestTooShort(t *testing.T) {
|
||||
if !warnCount("Fix bug.", "too short") {
|
||||
t.Error("expected 'too short' warning for very short description")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 4: Acceptance criterion ---
|
||||
|
||||
func TestMissingAcceptance(t *testing.T) {
|
||||
desc := "Add `helper()` to `internal/util.go:42` following the existing pattern; the function handles empty input gracefully with a cross-reference to bd-abc."
|
||||
if !warnCount(desc, "acceptance criterion") {
|
||||
t.Error("expected 'acceptance criterion' warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptancePresent(t *testing.T) {
|
||||
desc := "Add `helper()` to `internal/util.go:42` following the existing pattern; it must return an error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "acceptance criterion") {
|
||||
t.Errorf("unexpected acceptance warning when 'must' keyword present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 5: Acme address format ---
|
||||
|
||||
func TestHyphenRangeWarning(t *testing.T) {
|
||||
desc := "Fix `lintDescription()` in `internal/p9/beads.go:611-663` following the existing pattern; it must return warnings correctly. See bd-abc for context."
|
||||
if !warnCount(desc, "invalid Acme address") {
|
||||
t.Error("expected 'invalid Acme address' warning for hyphen range")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommaRangeNoWarning(t *testing.T) {
|
||||
desc := "Fix `lintDescription()` in `internal/p9/beads.go:611,663` following the existing pattern; it must return warnings correctly. See bd-abc for context."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "invalid Acme address") {
|
||||
t.Errorf("unexpected Acme address warning for correct comma range")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 6: Imperative verb start ---
|
||||
|
||||
func TestNonImperativeStart(t *testing.T) {
|
||||
desc := "Need to add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
if !warnCount(desc, "imperative verb") {
|
||||
t.Error("expected imperative verb warning for 'Need to' start")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImperativeStart(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "imperative verb") {
|
||||
t.Errorf("unexpected imperative verb warning for description starting with 'Add'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 7: Vague language ---
|
||||
|
||||
func TestVagueLanguage(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must somehow return an error etc. See bd-abc."
|
||||
if !warnCount(desc, "vague language") {
|
||||
t.Error("expected vague language warning for 'somehow'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoVagueLanguage(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must return a wrapped error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "vague language") {
|
||||
t.Errorf("unexpected vague language warning for precise description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 8: "How" signal ---
|
||||
|
||||
func TestMissingHowSignal(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42`; it must return an error on empty input. Cross-reference: bd-abc."
|
||||
if !warnCount(desc, "how' signal") {
|
||||
t.Error("expected 'how' signal warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHowSignalPresent(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following the existing error-handling pattern; it must return an error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "how' signal") {
|
||||
t.Errorf("unexpected 'how' warning when 'following' keyword present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 9: No first-person ---
|
||||
|
||||
func TestFirstPersonWarning(t *testing.T) {
|
||||
desc := "I need to add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
if !warnCount(desc, "first-person") {
|
||||
t.Error("expected first-person warning for 'I need'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoFirstPerson(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "first-person") {
|
||||
t.Errorf("unexpected first-person warning for imperative description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 10: Forbidden vague phrases ---
|
||||
|
||||
func TestForbiddenPhrase(t *testing.T) {
|
||||
desc := "Fix this in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
if !warnCount(desc, "forbidden vague phrase") {
|
||||
t.Error("expected forbidden phrase warning for 'fix this'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoForbiddenPhrase(t *testing.T) {
|
||||
desc := "Fix `lintDescription()` in `internal/p9/beads.go:615` following the existing pattern; it must return warnings for vague descriptions. See bd-9y4."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "forbidden vague phrase") {
|
||||
t.Errorf("unexpected forbidden phrase warning for well-formed description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 11: Inline code (backticks) ---
|
||||
|
||||
func TestMissingBacktick(t *testing.T) {
|
||||
desc := "Add helper() in internal/util.go:42 following existing pattern; it must return error on empty input. See bd-abc."
|
||||
if !warnCount(desc, "inline code") {
|
||||
t.Error("expected inline code warning when file path present but no backticks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBacktickPresent(t *testing.T) {
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input. See bd-abc."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "inline code") {
|
||||
t.Errorf("unexpected inline code warning when backtick identifier present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rule 12: Cross-reference on long descriptions ---
|
||||
|
||||
func TestLongDescMissingCrossRef(t *testing.T) {
|
||||
// >150 chars, has file path and backtick, but no bd-XXX or URL
|
||||
desc := "Add `lintDescription()` check in `internal/p9/beads.go:615` following the pattern of existing checks; the function must return a non-empty warnings slice when the description violates any rule and emit all applicable warnings."
|
||||
if !warnCount(desc, "cross-reference") {
|
||||
t.Error("expected cross-reference warning for long description without bd- ref")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongDescWithCrossRef(t *testing.T) {
|
||||
desc := "Add `lintDescription()` check in `internal/p9/beads.go:615` following the pattern of existing checks; the function must return a non-empty warnings slice when the description violates any rule and emit all applicable warnings. See bd-9y4."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "cross-reference") {
|
||||
t.Errorf("unexpected cross-reference warning when bd- ref present")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortDescNoCrossRefWarning(t *testing.T) {
|
||||
// <=150 chars should not trigger the cross-reference rule
|
||||
desc := "Add `helper()` in `internal/util.go:42` following existing pattern; it must return error on empty input."
|
||||
for _, w := range lintDescription(desc) {
|
||||
if strings.Contains(w, "cross-reference") {
|
||||
t.Errorf("unexpected cross-reference warning for short description")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper function unit tests ---
|
||||
|
||||
func TestContainsLineRef(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"beads.go:385", true},
|
||||
{"see L385 for details", true},
|
||||
{"char pos #4096", true},
|
||||
{"no address here", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := containsLineRef(tc.s); got != tc.want {
|
||||
t.Errorf("containsLineRef(%q) = %v, want %v", tc.s, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsAcmeRegexAddr(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"beads.go:/lintDescription/", true},
|
||||
{"see /funcName/ in the file", true},
|
||||
{"no slashes", false},
|
||||
{"a/b/c directory path", false}, // single-char tokens, below 4-char minimum
|
||||
{"internal/p9/beads.go", false}, // short path components (/p9/ = 2 chars)
|
||||
{"internal/pkg/beads.go", false}, // /pkg/ = 3 chars, below minimum
|
||||
{"see /handleNewCommand/ here", true}, // 16 chars >= 4
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := containsAcmeRegexAddr(tc.s); got != tc.want {
|
||||
t.Errorf("containsAcmeRegexAddr(%q) = %v, want %v", tc.s, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsHyphenRange(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"beads.go:123-125", true},
|
||||
{"beads.go:123,125", false},
|
||||
{"beads.go:123", false},
|
||||
{"version 1-2", false}, // no colon before
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := containsHyphenRange(tc.s); got != tc.want {
|
||||
t.Errorf("containsHyphenRange(%q) = %v, want %v", tc.s, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsBdRef(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"see bd-abc for context", true},
|
||||
{"bd-9y4 and bd-iti", true},
|
||||
{"bd- no alphanumeric", false},
|
||||
{"no reference here", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := containsBdRef(tc.s); got != tc.want {
|
||||
t.Errorf("containsBdRef(%q) = %v, want %v", tc.s, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
package p9
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
bd "github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
func TestMountUnmount(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Unsetenv("HOME")
|
||||
|
||||
store, err := bd.OpenFromConfig(context.Background(), filepath.Join(tmpDir, "main"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
projectDir := filepath.Join(tmpDir, "project1")
|
||||
os.MkdirAll(projectDir, 0755)
|
||||
|
||||
err = beadsFS.Mount("proj1", projectDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Mount failed: %v", err)
|
||||
}
|
||||
|
||||
mounts := beadsFS.ListMounts()
|
||||
if _, ok := mounts["proj1"]; !ok {
|
||||
t.Fatal("Mount not listed")
|
||||
}
|
||||
|
||||
err = beadsFS.Umount("proj1")
|
||||
if err != nil {
|
||||
t.Fatalf("Umount failed: %v", err)
|
||||
}
|
||||
|
||||
mounts = beadsFS.ListMounts()
|
||||
if _, ok := mounts["proj1"]; ok {
|
||||
t.Fatal("Mount still listed after umount")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountReadWrite(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Unsetenv("HOME")
|
||||
|
||||
store, err := bd.OpenFromConfig(context.Background(), filepath.Join(tmpDir, "main"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
projectDir := filepath.Join(tmpDir, "project1")
|
||||
os.MkdirAll(projectDir, 0755)
|
||||
|
||||
err = beadsFS.Mount("proj1", projectDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer beadsFS.Umount("proj1")
|
||||
|
||||
err = beadsFS.Write("proj1/ctl", []byte("init bd"), "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = beadsFS.Write("proj1/ctl", []byte("new 'Test Task' 'Description'"), "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, err := beadsFS.Read("proj1/list")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var beads []map[string]interface{}
|
||||
json.Unmarshal(list, &beads)
|
||||
if len(beads) != 1 {
|
||||
t.Fatalf("Expected 1 bead, got %d", len(beads))
|
||||
}
|
||||
|
||||
beadID := beads[0]["id"].(string)
|
||||
err = beadsFS.Write("proj1/ctl", []byte("label "+beadID+" claimable"), "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ready, err := beadsFS.Read("proj1/ready")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var readyBeads []map[string]interface{}
|
||||
json.Unmarshal(ready, &readyBeads)
|
||||
if len(readyBeads) != 1 {
|
||||
t.Fatalf("Expected 1 ready bead, got %d", len(readyBeads))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyAggregate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.Setenv("HOME", tmpDir)
|
||||
defer os.Unsetenv("HOME")
|
||||
|
||||
store, err := bd.OpenFromConfig(context.Background(), filepath.Join(tmpDir, "main"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
proj1 := filepath.Join(tmpDir, "project1")
|
||||
proj2 := filepath.Join(tmpDir, "project2")
|
||||
os.MkdirAll(proj1, 0755)
|
||||
os.MkdirAll(proj2, 0755)
|
||||
|
||||
beadsFS.Mount("proj1", proj1)
|
||||
beadsFS.Mount("proj2", proj2)
|
||||
defer beadsFS.Umount("proj1")
|
||||
defer beadsFS.Umount("proj2")
|
||||
|
||||
beadsFS.Write("proj1/ctl", []byte("init bd"), "test")
|
||||
beadsFS.Write("proj2/ctl", []byte("init bd"), "test")
|
||||
|
||||
beadsFS.Write("proj1/ctl", []byte("new 'Task 1' 'Desc 1'"), "test")
|
||||
beadsFS.Write("proj2/ctl", []byte("new 'Task 2' 'Desc 2'"), "test")
|
||||
|
||||
ready, err := beadsFS.Read("ready")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var tasks []map[string]interface{}
|
||||
json.Unmarshal(ready, &tasks)
|
||||
|
||||
proj1Count := 0
|
||||
proj2Count := 0
|
||||
for _, task := range tasks {
|
||||
mount, _ := task["mount"].(string)
|
||||
if mount == "proj1" {
|
||||
proj1Count++
|
||||
} else if mount == "proj2" {
|
||||
proj2Count++
|
||||
}
|
||||
}
|
||||
|
||||
if proj1Count != 1 || proj2Count != 1 {
|
||||
t.Fatalf("Expected 1 task from each mount, got proj1=%d proj2=%d", proj1Count, proj2Count)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
package p9
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
bd "github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
func TestBeadsFS_Mount(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
store, err := bd.OpenFromConfig(context.Background(), tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
testCwd := filepath.Join(tmpDir, "project")
|
||||
os.MkdirAll(filepath.Join(testCwd, ".beads"), 0755)
|
||||
|
||||
// Test mount
|
||||
err = beadsFS.Mount("test-project", testCwd)
|
||||
if err != nil {
|
||||
t.Fatalf("Mount failed: %v", err)
|
||||
}
|
||||
|
||||
// Test duplicate mount
|
||||
err = beadsFS.Mount("test-project", testCwd)
|
||||
if err == nil {
|
||||
t.Fatal("Should have failed on duplicate mount")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsFS_Umount(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
store, err := bd.OpenFromConfig(context.Background(), tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
testCwd := filepath.Join(tmpDir, "project")
|
||||
os.MkdirAll(filepath.Join(testCwd, ".beads"), 0755)
|
||||
|
||||
// Mount first
|
||||
err = beadsFS.Mount("test-project", testCwd)
|
||||
if err != nil {
|
||||
t.Fatalf("Mount failed: %v", err)
|
||||
}
|
||||
|
||||
// Test umount
|
||||
err = beadsFS.Umount("test-project")
|
||||
if err != nil {
|
||||
t.Fatalf("Umount failed: %v", err)
|
||||
}
|
||||
|
||||
// Test umount non-existent
|
||||
err = beadsFS.Umount("nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("Should have failed on non-existent mount")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeadsFS_MountRouting(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
store, err := bd.OpenFromConfig(context.Background(), tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
testCwd := filepath.Join(tmpDir, "project")
|
||||
os.MkdirAll(filepath.Join(testCwd, ".beads"), 0755)
|
||||
|
||||
// Mount
|
||||
err = beadsFS.Mount("test-project", testCwd)
|
||||
if err != nil {
|
||||
t.Fatalf("Mount failed: %v", err)
|
||||
}
|
||||
defer beadsFS.Umount("test-project")
|
||||
|
||||
// Test cwd endpoint
|
||||
data, err := beadsFS.Read("anvillm/beads/test-project/cwd")
|
||||
if err != nil {
|
||||
t.Fatalf("Read cwd failed: %v", err)
|
||||
}
|
||||
if string(data) != testCwd {
|
||||
t.Fatalf("Expected cwd %s, got %s", testCwd, string(data))
|
||||
}
|
||||
|
||||
// Test non-existent mount
|
||||
_, err = beadsFS.Read("anvillm/beads/nonexistent/cwd")
|
||||
if err == nil {
|
||||
t.Fatal("Should have failed on non-existent mount")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
package p9
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
bd "github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
func TestLabelClaimable(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
store, err := bd.OpenFromConfig(context.Background(), tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
beadsFS := NewBeadsFS(store, context.Background())
|
||||
|
||||
// Initialize
|
||||
err = beadsFS.Write("ctl", []byte("init bd"), "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a bead
|
||||
err = beadsFS.Write("ctl", []byte("new 'Test Task' 'Test description'"), "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// List beads to get ID
|
||||
list, err := beadsFS.Read("list")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var beads []map[string]interface{}
|
||||
json.Unmarshal(list, &beads)
|
||||
if len(beads) == 0 {
|
||||
t.Fatal("no beads created")
|
||||
}
|
||||
|
||||
beadID := beads[0]["id"].(string)
|
||||
t.Logf("Created bead: %s", beadID)
|
||||
|
||||
// Label it as claimable
|
||||
cmd := fmt.Sprintf("label %s claimable", beadID)
|
||||
t.Logf("Running command: %s", cmd)
|
||||
err = beadsFS.Write("ctl", []byte(cmd), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("label failed: %v", err)
|
||||
}
|
||||
t.Logf("Label command succeeded")
|
||||
|
||||
// Read it back
|
||||
list, err = beadsFS.Read("list")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
json.Unmarshal(list, &beads)
|
||||
t.Logf("Full bead data: %+v", beads[0])
|
||||
labels, ok := beads[0]["labels"].([]interface{})
|
||||
if !ok {
|
||||
labels = []interface{}{}
|
||||
}
|
||||
t.Logf("Labels: %v", labels)
|
||||
|
||||
hasClaimable := false
|
||||
for _, l := range labels {
|
||||
if l.(string) == "claimable" {
|
||||
hasClaimable = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasClaimable {
|
||||
t.Fatal("claimable label not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"anvillm/internal/mailbox"
|
||||
"anvillm/internal/session"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -77,12 +76,6 @@ const (
|
|||
qidUserCompleted // user/completed
|
||||
qidUserCtl // user/ctl
|
||||
qidUserMail // user/mail
|
||||
qidBeads // beads directory
|
||||
qidBeadsCtl // beads/ctl
|
||||
qidBeadsList // beads/list
|
||||
qidBeadsMtab // beads/mtab
|
||||
qidBeadsReady // beads/ready
|
||||
qidBeadsDeferred // beads/deferred
|
||||
qidTools // tools directory
|
||||
qidSkills // skills directory
|
||||
qidRoles // roles directory
|
||||
|
|
@ -92,7 +85,6 @@ const (
|
|||
qidOutboxBase = 0x30000000 // session/{id}/outbox
|
||||
qidCompletedBase = 0x40000000 // session/{id}/completed
|
||||
qidMessageBase = 0x50000000 // message files
|
||||
qidBeadsBase = 0x60000000 // beads/{id}/file
|
||||
qidToolsBase = 0x70000000 // tools/{tool}
|
||||
qidSkillsBase = 0x80000000 // skills/{skill}
|
||||
qidRolesBase = 0x90000000 // roles/{role}
|
||||
|
|
@ -127,7 +119,6 @@ type Server struct {
|
|||
listener net.Listener
|
||||
socketPath string
|
||||
events *eventbus.Bus
|
||||
beads *BeadsFS
|
||||
tools *ToolsFS
|
||||
skills *SkillsFS
|
||||
roles *RolesFS
|
||||
|
|
@ -156,7 +147,7 @@ type fid struct {
|
|||
}
|
||||
|
||||
// NewServer creates and starts the 9P server.
|
||||
func NewServer(mgr *session.Manager, beadsFS *BeadsFS) (*Server, error) {
|
||||
func NewServer(mgr *session.Manager) (*Server, error) {
|
||||
ns := client.Namespace()
|
||||
if ns == "" {
|
||||
return nil, fmt.Errorf("no namespace")
|
||||
|
|
@ -188,7 +179,6 @@ func NewServer(mgr *session.Manager, beadsFS *BeadsFS) (*Server, error) {
|
|||
listener: listener,
|
||||
socketPath: sockPath,
|
||||
events: eventbus.New(),
|
||||
beads: beadsFS,
|
||||
tools: toolsFS,
|
||||
skills: NewSkillsFS(),
|
||||
roles: NewRolesFS(),
|
||||
|
|
@ -228,11 +218,6 @@ func (s *Server) Events() *eventbus.Bus {
|
|||
return s.events
|
||||
}
|
||||
|
||||
// Beads returns the BeadsFS instance.
|
||||
func (s *Server) Beads() *BeadsFS {
|
||||
return s.beads
|
||||
}
|
||||
|
||||
func (s *Server) acceptLoop() {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
|
|
@ -366,9 +351,6 @@ func (s *Server) walk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
case "user":
|
||||
qid = plan9.Qid{Type: QTDir, Path: qidUser}
|
||||
newPath = "/user"
|
||||
case "beads":
|
||||
qid = plan9.Qid{Type: QTDir, Path: qidBeads}
|
||||
newPath = "/beads"
|
||||
case "tools":
|
||||
qid = plan9.Qid{Type: QTDir, Path: qidTools}
|
||||
newPath = "/tools"
|
||||
|
|
@ -408,38 +390,6 @@ func (s *Server) walk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
default:
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
} else if path == "/beads" {
|
||||
// Inside beads directory
|
||||
switch name {
|
||||
case "ctl":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsCtl}
|
||||
newPath = "/beads/ctl"
|
||||
case "list":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsList}
|
||||
newPath = "/beads/list"
|
||||
case "mtab":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsMtab}
|
||||
newPath = "/beads/mtab"
|
||||
case "ready":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsReady}
|
||||
newPath = "/beads/ready"
|
||||
case "deferred":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsDeferred}
|
||||
newPath = "/beads/deferred"
|
||||
default:
|
||||
// Mount directory - verify it exists
|
||||
if s.beads != nil {
|
||||
mounts := s.beads.ListMounts()
|
||||
if _, isMount := mounts[name]; isMount {
|
||||
qid = plan9.Qid{Type: QTDir, Path: qidBeadsBase + hashID(name)}
|
||||
newPath = "/beads/" + name
|
||||
} else {
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
} else {
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
}
|
||||
} else if path == "/tools" {
|
||||
// Inside tools directory - list tools flat
|
||||
if s.tools != nil {
|
||||
|
|
@ -497,32 +447,6 @@ func (s *Server) walk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
} else {
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/beads/") && strings.Count(path, "/") == 2 {
|
||||
// Inside a mount directory
|
||||
mountName := strings.TrimPrefix(path, "/beads/")
|
||||
if s.beads != nil {
|
||||
mounts := s.beads.ListMounts()
|
||||
if _, isMount := mounts[mountName]; isMount {
|
||||
// Check if name is a control file or bead ID
|
||||
switch name {
|
||||
case "cwd", "ctl", "list", "ready", "deferred", "stats", "blocked", "stale", "query", "config":
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+name)}
|
||||
newPath = path + "/" + name
|
||||
default:
|
||||
// Bead ID directory
|
||||
qid = plan9.Qid{Type: QTDir, Path: qidBeadsBase + hashID(mountName+name)}
|
||||
newPath = path + "/" + name
|
||||
}
|
||||
} else {
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
} else {
|
||||
return errFcall(fc, "not found")
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/beads/") && strings.Count(path, "/") == 3 {
|
||||
// Inside a mount's bead directory - property files
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+name)}
|
||||
newPath = path + "/" + name
|
||||
} else if strings.HasPrefix(path, "/user/") && strings.Count(path, "/") == 2 {
|
||||
// Inside user mailbox directory - message files
|
||||
qid = plan9.Qid{Type: QTFile, Path: qidMessageBase + hashID("user"+path[6:]+name)}
|
||||
|
|
@ -677,20 +601,6 @@ func (s *Server) dispatchWrite(cs *connState, f *fid, tag uint16) *plan9.Fcall {
|
|||
input := strings.TrimSpace(string(f.writeBuf))
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||||
|
||||
// Handle beads writes
|
||||
if beadsPath, ok := strings.CutPrefix(path, "/beads/"); ok {
|
||||
if s.beads != nil {
|
||||
cs.mu.Lock()
|
||||
sessionID := cs.sessionID
|
||||
cs.mu.Unlock()
|
||||
if err := s.beads.Write(beadsPath, f.writeBuf, sessionID); err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(f.writeBuf))}
|
||||
}
|
||||
return errFcall(fc, "beads not initialized")
|
||||
}
|
||||
|
||||
// /ctl - create new session or recover orphaned sessions
|
||||
if path == "/ctl" {
|
||||
args := strings.Fields(input)
|
||||
|
|
@ -1084,10 +994,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
|||
Qid: plan9.Qid{Type: QTDir, Path: qidUser},
|
||||
Mode: plan9.DMDIR | 0555, Name: "user", Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTDir, Path: qidBeads},
|
||||
Mode: plan9.DMDIR | 0555, Name: "beads", Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTDir, Path: qidTools},
|
||||
Mode: plan9.DMDIR | 0555, Name: "tools", Uid: "q", Gid: "q", Muid: "q",
|
||||
|
|
@ -1139,119 +1045,6 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
|||
})
|
||||
}
|
||||
}
|
||||
} else if path == "/beads" {
|
||||
// Beads directory - only ctl, mtab, ready (aggregate), and mounted projects
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsCtl}, Mode: 0222, Name: "ctl",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsMtab}, Mode: 0444, Name: "mtab",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsReady}, Mode: 0444, Name: "ready",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsDeferred}, Mode: 0444, Name: "deferred",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
// Add mounted projects as directories
|
||||
if s.beads != nil {
|
||||
for name := range s.beads.ListMounts() {
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTDir, Path: qidBeadsBase + uint64(len(name))}, Mode: 0555 | plan9.DMDIR, Name: name,
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/beads/") && strings.Count(path, "/") == 2 {
|
||||
// Inside a mounted project directory - show all beads endpoints
|
||||
mountName := strings.TrimPrefix(path, "/beads/")
|
||||
if s.beads != nil {
|
||||
mounts := s.beads.ListMounts()
|
||||
if _, exists := mounts[mountName]; exists {
|
||||
// Show all standard beads endpoints for this mount
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"cwd")}, Mode: 0444, Name: "cwd",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"ctl")}, Mode: 0222, Name: "ctl",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"list")}, Mode: 0444, Name: "list",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"ready")}, Mode: 0444, Name: "ready",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"deferred")}, Mode: 0444, Name: "deferred",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"stats")}, Mode: 0444, Name: "stats",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"blocked")}, Mode: 0444, Name: "blocked",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"stale")}, Mode: 0444, Name: "stale",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"query")}, Mode: 0644, Name: "query",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(mountName+"config")}, Mode: 0444, Name: "config",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
// Add bead directories
|
||||
data, err := s.beads.Read(mountName + "/list")
|
||||
if err == nil {
|
||||
var issues []struct{ ID string `json:"id"` }
|
||||
if json.Unmarshal(data, &issues) == nil {
|
||||
for _, issue := range issues {
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTDir, Path: qidBeadsBase + hashID(mountName+issue.ID)}, Mode: 0555 | plan9.DMDIR, Name: issue.ID,
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(path, "/beads/") && strings.Count(path, "/") == 3 {
|
||||
// Inside a mount's bead directory - show property files
|
||||
// e.g., /beads/anvillm/anv-77c/json
|
||||
// Do NOT show the bead ID itself as a subdirectory
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+"json")}, Mode: 0444, Name: "json",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+"status")}, Mode: 0444, Name: "status",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+"title")}, Mode: 0444, Name: "title",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+"description")}, Mode: 0444, Name: "description",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
Qid: plan9.Qid{Type: QTFile, Path: qidBeadsBase + hashID(path+"assignee")}, Mode: 0444, Name: "assignee",
|
||||
Uid: "q", Gid: "q", Muid: "q",
|
||||
})
|
||||
} else if path == "/user" {
|
||||
// User directory - only mailbox subdirs
|
||||
dirs = append(dirs, plan9.Dir{
|
||||
|
|
@ -1439,18 +1232,6 @@ func (s *Server) readFile(path string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// Handle beads paths
|
||||
if beadsPath, ok := strings.CutPrefix(path, "/beads/"); ok {
|
||||
if s.beads != nil {
|
||||
data, err := s.beads.Read(beadsPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
if path == "/list" {
|
||||
var lines []string
|
||||
for _, id := range s.mgr.List() {
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$CHILD" ] || [ -z "$PARENT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "dep $CHILD $PARENT" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "dep $CHILD $PARENT" | 9p write beads/$MOUNT/ctl
|
||||
echo "$CHILD depends on $PARENT"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ if [ -z "$MOUNT" ] || [ -z "$JSON" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -35,5 +35,5 @@ if [ -n "$SCOPE" ]; then
|
|||
JSON=$(echo "$JSON" | jq --arg scope "$SCOPE" '[.[] | . + {scope: $scope}]')
|
||||
fi
|
||||
|
||||
echo "batch-create $JSON" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "batch-create $JSON" | 9p write "beads/$MOUNT/ctl"
|
||||
echo "batch created beads"
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ if [ -z "${AGENT_ID:-}" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "claim $BEAD_ID $AGENT_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "claim $BEAD_ID $AGENT_ID" | 9p write beads/$MOUNT/ctl
|
||||
|
||||
assignee=$(9p read anvillm/beads/$MOUNT/$BEAD_ID/json 2>/dev/null | jq -r '.assignee // empty')
|
||||
assignee=$(9p read beads/$MOUNT/$BEAD_ID/json 2>/dev/null | jq -r '.assignee // empty')
|
||||
if [ "$assignee" != "$AGENT_ID" ]; then
|
||||
echo "error: claim failed (bead is assigned to '$assignee')" >&2
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$TEXT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
printf "comment %s '%s'\n" "$BEAD_ID" "$TEXT" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
printf "comment %s '%s'\n" "$BEAD_ID" "$TEXT" | 9p write beads/$MOUNT/ctl
|
||||
echo "commented on $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "complete $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "complete $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "completed $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$KEY" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "config $KEY $VALUE" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "config $KEY $VALUE" | 9p write beads/$MOUNT/ctl
|
||||
echo "config $KEY = $VALUE"
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ if [ -z "$MOUNT" ] || [ -z "$TITLE" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -44,5 +44,5 @@ CMD="new '$TITLE' '$DESC'"
|
|||
[ -n "$CAP" ] && CMD="$CMD capability=$CAP"
|
||||
[ -n "$SCOPE" ] && CMD="$CMD scope=$SCOPE"
|
||||
|
||||
echo "$CMD" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "$CMD" | 9p write beads/$MOUNT/ctl
|
||||
echo "created (deferred): $TITLE"
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
fi
|
||||
|
||||
if [ -n "$UNTIL" ]; then
|
||||
echo "defer $BEAD_ID until $UNTIL" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "defer $BEAD_ID until $UNTIL" | 9p write beads/$MOUNT/ctl
|
||||
echo "deferred $BEAD_ID until $UNTIL"
|
||||
else
|
||||
echo "defer $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "defer $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "deferred $BEAD_ID"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "delete $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "delete $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "deleted $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$REASON" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
printf "fail %s '%s'\n" "$BEAD_ID" "$REASON" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
printf "fail %s '%s'\n" "$BEAD_ID" "$REASON" | 9p write beads/$MOUNT/ctl
|
||||
echo "failed $BEAD_ID: $REASON"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if [ -z "$MOUNT" ] || [ -z "$REPO" ] || [ -z "$ISSUE" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -34,7 +34,7 @@ SCOPE_ARG=""
|
|||
[ -n "$SCOPE" ] && SCOPE_ARG="scope=$SCOPE"
|
||||
|
||||
# Check if already imported
|
||||
if 9p read "anvillm/beads/$MOUNT/list" | jq -e ".[] | select(.title | contains(\"#$ISSUE\"))" >/dev/null 2>&1; then
|
||||
if 9p read "beads/$MOUNT/list" | jq -e ".[] | select(.title | contains(\"#$ISSUE\"))" >/dev/null 2>&1; then
|
||||
echo "Issue #$ISSUE already imported" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -64,19 +64,19 @@ fi
|
|||
bead_title="#$number: $title"
|
||||
|
||||
# Create parent bead
|
||||
echo "new \"$bead_title\" \"$body\" '' $SCOPE_ARG" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "new \"$bead_title\" \"$body\" '' $SCOPE_ARG" | 9p write "beads/$MOUNT/ctl"
|
||||
|
||||
# Get created bead ID
|
||||
bead_id=$(9p read "anvillm/beads/$MOUNT/list" | jq -r ".[] | select(.title | contains(\"#$number\")) | .id" | head -1)
|
||||
bead_id=$(9p read "beads/$MOUNT/list" | jq -r ".[] | select(.title | contains(\"#$number\")) | .id" | head -1)
|
||||
|
||||
# Update issue type if not task
|
||||
if [[ "$issue_type" != "task" ]]; then
|
||||
echo "update $bead_id issue_type $issue_type" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "update $bead_id issue_type $issue_type" | 9p write "beads/$MOUNT/ctl"
|
||||
fi
|
||||
|
||||
# Update status if closed
|
||||
if [[ "$status" == "closed" ]]; then
|
||||
echo "update $bead_id status closed" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "update $bead_id status closed" | 9p write "beads/$MOUNT/ctl"
|
||||
fi
|
||||
|
||||
# Parse task list from body
|
||||
|
|
@ -84,7 +84,7 @@ if [[ -n "$body" ]]; then
|
|||
# Extract unchecked tasks: - [ ] Task name
|
||||
echo "$body" | grep -E '^\s*-\s+\[\s+\]' | sed -E 's/^\s*-\s+\[\s+\]\s*//' | while IFS= read -r task; do
|
||||
if [[ -n "$task" ]]; then
|
||||
echo "new \"$task\" \"\" $bead_id $SCOPE_ARG" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "new \"$task\" \"\" $bead_id $SCOPE_ARG" | 9p write "beads/$MOUNT/ctl"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "init $PREFIX" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "init $PREFIX" | 9p write "beads/$MOUNT/ctl"
|
||||
echo "initialized $MOUNT (prefix: $PREFIX)"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ if [ -z "$MOUNT" ] || [ -z "$TICKET" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -32,7 +32,7 @@ SCOPE_ARG=""
|
|||
[ -n "$SCOPE" ] && SCOPE_ARG="scope=$SCOPE"
|
||||
|
||||
# Check if already imported
|
||||
if 9p read "anvillm/beads/$MOUNT/list" | jq -e ".[] | select(.title | contains(\"$TICKET\"))" >/dev/null 2>&1; then
|
||||
if 9p read "beads/$MOUNT/list" | jq -e ".[] | select(.title | contains(\"$TICKET\"))" >/dev/null 2>&1; then
|
||||
echo "Ticket $TICKET already imported" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -69,14 +69,14 @@ create_bead() {
|
|||
|
||||
# Create bead
|
||||
if [[ -n "$parent_bead" ]]; then
|
||||
echo "new \"$title\" \"$description\" $parent_bead $SCOPE_ARG" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "new \"$title\" \"$description\" $parent_bead $SCOPE_ARG" | 9p write "beads/$MOUNT/ctl"
|
||||
else
|
||||
echo "new \"$title\" \"$description\" '' $SCOPE_ARG" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "new \"$title\" \"$description\" '' $SCOPE_ARG" | 9p write "beads/$MOUNT/ctl"
|
||||
fi
|
||||
|
||||
# Get created bead ID
|
||||
local bead_id
|
||||
bead_id=$(9p read "anvillm/beads/$MOUNT/list" | jq -r ".[] | select(.title | contains(\"$key\")) | .id" | head -1)
|
||||
bead_id=$(9p read "beads/$MOUNT/list" | jq -r ".[] | select(.title | contains(\"$key\")) | .id" | head -1)
|
||||
|
||||
# Process children
|
||||
local children
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$LABEL" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "label $BEAD_ID $LABEL" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "label $BEAD_ID $LABEL" | 9p write beads/$MOUNT/ctl
|
||||
echo "labeled $BEAD_ID: $LABEL"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ if [ -z "$MOUNT" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MY_SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -29,4 +29,4 @@ if [ -n "$MOUNT_CWD" ]; then
|
|||
fi
|
||||
|
||||
# Strict match: scope must equal (both empty or both same value)
|
||||
9p read "anvillm/beads/$MOUNT/list" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, priority, title, status, scope}]' || echo "[]"
|
||||
9p read "beads/$MOUNT/list" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, priority, title, status, scope}]' || echo "[]"
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
set -euo pipefail
|
||||
|
||||
|
||||
9p read anvillm/beads/mtab 2>/dev/null || echo "No mounted projects"
|
||||
9p read beads/mtab 2>/dev/null || echo "No mounted projects"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ if [ -z "$MOUNT" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MY_SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -29,4 +29,4 @@ if [ -n "$MOUNT_CWD" ]; then
|
|||
fi
|
||||
|
||||
# Strict match: scope must equal (both empty or both same value)
|
||||
9p read "anvillm/beads/$MOUNT/ready" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, priority, title, status, scope}]' || echo "[]"
|
||||
9p read "beads/$MOUNT/ready" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, priority, title, status, scope}]' || echo "[]"
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ fi
|
|||
|
||||
MOUNT=$(uuidgen | cut -d- -f1)
|
||||
|
||||
echo "mount $CWD $MOUNT" | 9p write anvillm/beads/ctl
|
||||
echo "mount $CWD $MOUNT" | 9p write beads/ctl
|
||||
|
||||
# Verify mount succeeded
|
||||
if ! 9p read anvillm/beads/mtab 2>/dev/null | grep -q "^$MOUNT "; then
|
||||
if ! 9p read beads/mtab 2>/dev/null | grep -q "^$MOUNT "; then
|
||||
echo "error: mount failed for $CWD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "open $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "open $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "opened $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$PROPERTY" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
9p read "anvillm/beads/$MOUNT/$BEAD_ID/$PROPERTY" 2>/dev/null || echo "Property not found: $PROPERTY"
|
||||
9p read "beads/$MOUNT/$BEAD_ID/$PROPERTY" 2>/dev/null || echo "Property not found: $PROPERTY"
|
||||
|
|
|
|||
|
|
@ -21,4 +21,4 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
9p read "anvillm/beads/$MOUNT/$BEAD_ID/comments" 2>/dev/null || echo "[]"
|
||||
9p read "beads/$MOUNT/$BEAD_ID/comments" 2>/dev/null || echo "[]"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD1" ] || [ -z "$BEAD2" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "relate $BEAD1 $BEAD2" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "relate $BEAD1 $BEAD2" | 9p write beads/$MOUNT/ctl
|
||||
echo "related $BEAD1 ↔ $BEAD2"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$CHILD" ] || [ -z "$PARENT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "undep $CHILD $PARENT" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "undep $CHILD $PARENT" | 9p write beads/$MOUNT/ctl
|
||||
echo "$CHILD no longer depends on $PARENT"
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "reopen $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "reopen $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "reopened $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fi
|
|||
|
||||
# Direct ID lookup
|
||||
if [ -n "$BEAD_ID" ]; then
|
||||
9p read "anvillm/beads/$MOUNT/$BEAD_ID/json" 2>/dev/null || echo "null"
|
||||
9p read "beads/$MOUNT/$BEAD_ID/json" 2>/dev/null || echo "null"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ if [ -z "$QUERY" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MY_SCOPE=""
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
|
|
@ -46,4 +46,4 @@ if [ -n "$MOUNT_CWD" ]; then
|
|||
MY_SCOPE="${REL_PATH%%/*}"
|
||||
fi
|
||||
|
||||
9p read "anvillm/beads/$MOUNT/search/$QUERY" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, title, status, scope, match_in: (if .id | test("'"$QUERY"'"; "i") then "id" elif .title | test("'"$QUERY"'"; "i") then "title" else "description" end)}]' || echo "[]"
|
||||
9p read "beads/$MOUNT/search/$QUERY" 2>/dev/null | jq --arg scope "$MY_SCOPE" '[.[] | select((.scope // "") == $scope) | {id, title, status, scope, match_in: (if .id | test("'"$QUERY"'"; "i") then "id" elif .title | test("'"$QUERY"'"; "i") then "title" else "description" end)}]' || echo "[]"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$LEVEL" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "set-capability $BEAD_ID $LEVEL" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "set-capability $BEAD_ID $LEVEL" | 9p write beads/$MOUNT/ctl
|
||||
echo "capability set: $BEAD_ID → $LEVEL"
|
||||
|
|
|
|||
|
|
@ -19,5 +19,5 @@ if [ -z "$MOUNT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "sync" | 9p write "anvillm/beads/$MOUNT/ctl"
|
||||
echo "sync" | 9p write "beads/$MOUNT/ctl"
|
||||
echo "synced $MOUNT"
|
||||
|
|
|
|||
|
|
@ -19,5 +19,5 @@ if [ -z "$MOUNT" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "umount $MOUNT" | 9p write anvillm/beads/ctl
|
||||
echo "umount $MOUNT" | 9p write beads/ctl
|
||||
echo "unmounted $MOUNT"
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "unclaim $BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "unclaim $BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
echo "unclaimed $BEAD_ID"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD_ID" ] || [ -z "$LABEL" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "unlabel $BEAD_ID $LABEL" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "unlabel $BEAD_ID $LABEL" | 9p write beads/$MOUNT/ctl
|
||||
echo "unlabeled $BEAD_ID: $LABEL"
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ if [ -z "$MOUNT" ] || [ -z "$BEAD1" ] || [ -z "$BEAD2" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "unrelate $BEAD1 $BEAD2" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
echo "unrelate $BEAD1 $BEAD2" | 9p write beads/$MOUNT/ctl
|
||||
echo "unrelated $BEAD1 ↔ $BEAD2"
|
||||
|
|
|
|||
|
|
@ -27,19 +27,19 @@ fi
|
|||
|
||||
if [ "$FIELD" = "parent" ]; then
|
||||
# Reparent: create new bead under parent with same data, then delete original
|
||||
JSON=$(9p read "anvillm/beads/$MOUNT/$BEAD_ID/json")
|
||||
JSON=$(9p read "beads/$MOUNT/$BEAD_ID/json")
|
||||
TITLE=$(echo "$JSON" | jq -r '.title')
|
||||
DESC=$(echo "$JSON" | jq -r '.description // ""')
|
||||
STATUS=$(echo "$JSON" | jq -r '.status')
|
||||
|
||||
# Create under new parent
|
||||
printf "new '%s' '%s' %s\n" "$TITLE" "$DESC" "$VALUE" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
printf "new '%s' '%s' %s\n" "$TITLE" "$DESC" "$VALUE" | 9p write beads/$MOUNT/ctl
|
||||
|
||||
# Delete original
|
||||
printf "delete %s\n" "$BEAD_ID" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
printf "delete %s\n" "$BEAD_ID" | 9p write beads/$MOUNT/ctl
|
||||
|
||||
echo "reparented $BEAD_ID under $VALUE"
|
||||
else
|
||||
printf "update %s %s '%s'\n" "$BEAD_ID" "$FIELD" "$VALUE" | 9p write anvillm/beads/$MOUNT/ctl
|
||||
printf "update %s %s '%s'\n" "$BEAD_ID" "$FIELD" "$VALUE" | 9p write beads/$MOUNT/ctl
|
||||
echo "updated $BEAD_ID.$FIELD: $VALUE"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ if [ -z "$MOUNT" ]; then
|
|||
fi
|
||||
|
||||
# Derive scope from cwd relative to mount's cwd
|
||||
MOUNT_CWD=$(9p read "anvillm/beads/$MOUNT/cwd" 2>/dev/null)
|
||||
MOUNT_CWD=$(9p read "beads/$MOUNT/cwd" 2>/dev/null)
|
||||
if [ -n "$MOUNT_CWD" ]; then
|
||||
REL_PATH="${PWD#"$MOUNT_CWD"}"
|
||||
REL_PATH="${REL_PATH#/}"
|
||||
|
|
|
|||
Reference in New Issue