beads: natural sort for hierarchical IDs

Sort bd-1.2 before bd-1.10 instead of lexicographic order.
This commit is contained in:
Levi Neely 2026-03-20 14:28:19 +01:00
parent d70162ad13
commit daf50d9783
6 changed files with 60 additions and 10 deletions

View File

@ -5,6 +5,7 @@ filesystem:
- "/etc/alternatives"
- "/etc/ld.so.cache"
- "/etc/passwd"
- "{HOME}/.local/share/anvillm/mail"
- "/dev/urandom"
rox:
- "/usr"

View File

@ -5,6 +5,7 @@ filesystem:
- "{XDG_CONFIG_HOME}/git"
- "{HOME}/.ssh"
- "{HOME}/.netrc"
- "{HOME}/.local/share/anvillm/mail"
- "/etc/gitconfig"
- "/etc/gitattributes"
- "/etc/ssh/ssh_config"

View File

@ -28,6 +28,11 @@ const windowName = "/AnviLLM/"
// Terminal to use for tmux attach (configurable via environment or flag)
var terminalCommand = getTerminalCommand()
// shellEscape escapes a string for use inside single quotes in shell commands.
func shellEscape(s string) string {
return strings.ReplaceAll(s, "'", "'\\''")
}
func getTerminalCommand() string {
if term := os.Getenv("ANVILLM_TERMINAL"); term != "" {
return term
@ -2506,7 +2511,7 @@ func createBeadFromMarkdown(content, parentID string, mount string) error {
}
// Build command: new 'title' 'description' [parent-id] [blockers=...]
cmd := fmt.Sprintf("new '%s' '%s'", title, description)
cmd := fmt.Sprintf("new '%s' '%s'", shellEscape(title), shellEscape(description))
if parentID != "" {
cmd = fmt.Sprintf("%s %s", cmd, parentID)
}
@ -2897,11 +2902,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, title)
cmd := fmt.Sprintf("update %s title '%s'", beadID, shellEscape(title))
writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
}
if description != "" {
cmd := fmt.Sprintf("update %s description '%s'", beadID, description)
cmd := fmt.Sprintf("update %s description '%s'", beadID, shellEscape(description))
writeFile(filepath.Join("beads", mount, "ctl"), []byte(cmd))
}

View File

@ -308,7 +308,8 @@ func executeCode(code, language string, timeout int, sandboxName string, trusted
return "", fmt.Errorf("unsupported language: %s (supported: bash)", language)
}
// Limit output size (10MB)
// Limit output size (10MB raw, 8KB returned)
const maxToolOutputSize = 8000
var outputBuf bytes.Buffer
lw := &limitedWriter{w: &outputBuf, limit: 10 * 1024 * 1024}
cmd.Stdout = lw
@ -350,7 +351,11 @@ func executeCode(code, language string, timeout int, sandboxName string, trusted
}
logExecution(execLog)
return string(output), nil
result := string(output)
if len(result) > maxToolOutputSize {
result = result[:maxToolOutputSize] + "\n... (output truncated)"
}
return result, nil
}
type limitedWriter struct {

View File

@ -472,7 +472,7 @@ func (b *BeadsFS) readListFromStore(store bd.Storage, limit int) ([]byte, error)
}
// Sort by ID for hierarchical view (bd-a3f8, bd-a3f8.1, bd-a3f8.1.1, etc.)
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
return naturalLess(issues[i].ID, issues[j].ID)
})
// Enrich with blockers, labels, and capability level.
@ -510,6 +510,48 @@ func (b *BeadsFS) readListFromStore(store bd.Storage, limit int) ([]byte, error)
return json.MarshalIndent(items, "", " ")
}
// naturalLess compares two strings with natural number ordering.
// "jira-100.2" < "jira-100.10" (unlike lexicographic where .10 < .2)
func naturalLess(a, b string) bool {
for {
if a == "" {
return b != ""
}
if b == "" {
return false
}
// Find leading non-digit prefix
ai, bi := 0, 0
for ai < len(a) && (a[ai] < '0' || a[ai] > '9') {
ai++
}
for bi < len(b) && (b[bi] < '0' || b[bi] > '9') {
bi++
}
if a[:ai] != b[:bi] {
return a[:ai] < b[:bi]
}
a, b = a[ai:], b[bi:]
if a == "" || b == "" {
continue
}
// Extract numeric part
ai, bi = 0, 0
for ai < len(a) && a[ai] >= '0' && a[ai] <= '9' {
ai++
}
for bi < len(b) && b[bi] >= '0' && b[bi] <= '9' {
bi++
}
na, _ := strconv.Atoi(a[:ai])
nb, _ := strconv.Atoi(b[:bi])
if na != nb {
return na < nb
}
a, b = a[ai:], b[bi:]
}
}
func (b *BeadsFS) readReadyFromStore(store bd.Storage, limit int) ([]byte, error) {
filter := bd.WorkFilter{Limit: limit}

4
mkfile
View File

@ -6,16 +6,12 @@ build:V:
go build -o $INSTALL_PATH/anvilsrv ./cmd/anvilsrv
go build -o $INSTALL_PATH/Assist ./cmd/Assist
go build -o $INSTALL_PATH/anvilmcp ./cmd/anvilmcp
cp scripts/Alog $INSTALL_PATH/Alog
chmod 0755 $INSTALL_PATH/Alog
cp scripts/anvillm-notify $INSTALL_PATH/anvillm-notify
chmod 0755 $INSTALL_PATH/anvillm-notify
cp scripts/anvillm-skills $INSTALL_PATH/anvillm-skills
chmod 0755 $INSTALL_PATH/anvillm-skills
cp scripts/anvillm-hook $INSTALL_PATH/anvillm-hook
chmod 0755 $INSTALL_PATH/anvillm-hook
cp scripts/9p-read-inbox $INSTALL_PATH/9p-read-inbox
chmod 0755 $INSTALL_PATH/9p-read-inbox
cp scripts/anvilspawn $INSTALL_PATH/anvilspawn
chmod 0755 $INSTALL_PATH/anvilspawn
cp scripts/anvillm-supervisor $INSTALL_PATH/anvillm-supervisor