fix(forge): issues by activity order + tags newest-first
0355245c153c6949448f1870d0d923c70e49e9e5
humdrum <me@humdrum.me> · 2026-06-18 09:17
parent af4dbb0e
fix(forge): issues by activity order + tags newest-first - issues groups sorted in progress → paused → backlog → done → other (by StatusKind, independent of repo config order) - refs tags sorted semver-descending (newest version on top) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4 files changed
- → custard-Claude-skill-plugin.md +28 −0
@@ -0,0 +1,28 @@
+---
+id: TASK-008
+title: custard Claude skill + plugin
+status: "\U0001F7E6 Backlog"
+assignee: []
+created_date: '2026-06-18 16:11'
+labels:
+ - feature
+dependencies: []
+priority: medium
+ordinal: 8000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Teach Claude the custard/soft-serve workflow so it can drive repos for the user and for anyone installing custard. Core deliverable is a SKILL.md (when/how to use check/preview/promote/release, soft-serve conventions, .custard.yaml schema, tags-as-versions, gotchas, safety rules). Wrap it as an installable Claude Code plugin (skill + slash commands) for distribution. MCP deferred until custard has a JSON API (separate task).
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [ ] #1 SKILL.md: soft-serve conventions (soft: remotes, ssh soft admin, /git clone) + custard rhythm (push → check → preview → promote) + .custard.yaml schema + tags-as-versions + release
+- [ ] #2 Documents gotchas: preview builds HEAD, requires pushed, excludes uncommitted; promote/release are prod/publish
+- [ ] #3 Safety: check/preview safe to run; promote/release require explicit user confirmation (prod/publish)
+- [ ] #4 Generic/adoptable — reads .custard.yaml/config, no hardcoded domain (personal specifics stay in user's CLAUDE.md)
+- [ ] #5 Packaged as a Claude Code plugin (skill + optional /preview /promote /ship slash commands); installable; hosted in custard repo or its own soft repo
+- [ ] #6 MCP deferred to a follow-up (needs custard JSON API first)
+<!-- AC:END -->
internal/gitread/gitread.go +12 −1
@@ -259,7 +259,18 @@ return nil
})
}
sort.Slice(out.Branches, func(i, j int) bool { return out.Branches[i].Name < out.Branches[j].Name })
- sort.Slice(out.Tags, func(i, j int) bool { return out.Tags[i].Name < out.Tags[j].Name })
+ // Tags newest-first: semver descending, non-semver after (reverse-lexical).
+ sort.Slice(out.Tags, func(i, j int) bool {
+ vi, oki := parseSemver(out.Tags[i].Name)
+ vj, okj := parseSemver(out.Tags[j].Name)
+ if oki && okj {
+ return semverLess(vj, vi)
+ }
+ if oki != okj {
+ return oki
+ }
+ return out.Tags[i].Name > out.Tags[j].Name
+ })
return out, nil
}
internal/server/server.go +1 −2
@@ -261,10 +261,9 @@ if err != nil {
s.notFound(w, r)
return
}
- cfg := backlog.ReadConfig(s.store, name, ref)
page := templates.IssuesPage{
Meta: s.meta(r, name, ref, "issues"),
- Groups: templates.GroupByStatus(tasks, cfg.Statuses),
+ Groups: templates.GroupByStatus(tasks),
Total: len(tasks),
}
s.render(w, r, templates.Issues(page))
web/templates/view.go +40 −25
@@ -5,6 +5,7 @@
import (
"fmt"
"html/template"
+ "sort"
"strings"
"time"
@@ -16,13 +17,13 @@ )
// Meta is the shared chrome data every page needs.
type Meta struct {
- Title string
- Repo string // current repo name, "" on the index
- Ref string // current ref, when relevant
- HasIssues bool // show the issues tab only when backlog tasks exist
- HasReadme bool // show the readme tab only when a README exists
- Theme string // active data-theme, resolved from cookie (default flexoki)
- Tab string // active repo tab: code | readme | log | refs | issues
+ Title string
+ Repo string // current repo name, "" on the index
+ Ref string // current ref, when relevant
+ HasIssues bool // show the issues tab only when backlog tasks exist
+ HasReadme bool // show the readme tab only when a README exists
+ Theme string // active data-theme, resolved from cookie (default flexoki)
+ Tab string // active repo tab: code | readme | log | refs | issues
CloneURL string // read-only HTTP clone URL, shown in the footer bar
License *license.License // detected repo license, shown as a badge
DeployState string // production | preview | unverified | "" (no badge)
@@ -156,32 +157,46 @@ Task backlog.Task
Body template.HTML
}
-// GroupByStatus buckets tasks following the repo's configured status order,
-// appending any statuses not in that order at the end. Empty buckets are
-// dropped. An empty order falls back to the Backlog.md defaults.
-func GroupByStatus(tasks []backlog.Task, order []string) []IssueGroup {
- if len(order) == 0 {
- order = backlog.DefaultStatuses
- }
- known := make(map[string]bool, len(order))
- for _, s := range order {
- known[s] = true
- }
+// GroupByStatus buckets tasks and orders the groups by activity: in progress,
+// paused, backlog, done, then anything unrecognized — regardless of how a repo
+// words its statuses (matched via StatusKind).
+func GroupByStatus(tasks []backlog.Task) []IssueGroup {
byStatus := map[string][]backlog.Task{}
- var extra []string
+ var statuses []string
for _, t := range tasks {
- if _, seen := byStatus[t.Status]; !seen && !known[t.Status] {
- extra = append(extra, t.Status)
+ if _, seen := byStatus[t.Status]; !seen {
+ statuses = append(statuses, t.Status)
}
byStatus[t.Status] = append(byStatus[t.Status], t)
}
- var groups []IssueGroup
- for _, s := range append(append([]string{}, order...), extra...) {
- if ts := byStatus[s]; len(ts) > 0 {
- groups = append(groups, IssueGroup{Status: s, Tasks: ts})
+ sort.SliceStable(statuses, func(i, j int) bool {
+ ri, rj := statusRank(statuses[i]), statusRank(statuses[j])
+ if ri != rj {
+ return ri < rj
}
+ return statuses[i] < statuses[j]
+ })
+ groups := make([]IssueGroup, 0, len(statuses))
+ for _, s := range statuses {
+ groups = append(groups, IssueGroup{Status: s, Tasks: byStatus[s]})
}
return groups
+}
+
+// statusRank orders status buckets: in progress → paused → backlog → done → other.
+func statusRank(status string) int {
+ switch StatusKind(status) {
+ case "in-progress":
+ return 0
+ case "paused":
+ return 1
+ case "backlog":
+ return 2
+ case "done":
+ return 3
+ default:
+ return 4
+ }
}
// LabelColor maps a label to a theme color-family name (phase-3 tokens key off