Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
047a183
fix: close final workspace review gaps
Duang777 Oct 5, 2026
703e98e
fix: harden workspace lifecycle boundaries
Duang777 Oct 5, 2026
23eeb95
fix: close workspace recovery review gaps
Duang777 Oct 5, 2026
8da2d6d
fix: close follow-up recovery review gaps
Duang777 Oct 5, 2026
8bdcba8
fix: preserve workspace recovery evidence
Duang777 Oct 5, 2026
b72eb58
fix: classify default command argument errors
Duang777 Oct 5, 2026
139554e
fix: preserve pending workspace recovery state
Duang777 Oct 5, 2026
d44535f
fix: preserve flags in help topic validation
Duang777 Oct 5, 2026
5bf4683
fix: harden durable workspace file copies
Duang777 Oct 5, 2026
3f3d11b
fix: reconcile stale preparation registrations
Duang777 Oct 5, 2026
e93b1d8
fix: validate help topics after flag terminator
Duang777 Oct 5, 2026
2203233
fix: rename read-only sidecars on Windows
Duang777 Oct 5, 2026
cc58866
fix: use writable directory for ack alias moves
Duang777 Oct 5, 2026
59cd271
fix: preserve startup resume retries
Duang777 Oct 5, 2026
d73c88f
fix: make recovery completion durable
Duang777 Oct 5, 2026
99dda06
fix: bind workspace preparation to opened roots
Duang777 Oct 5, 2026
27c630d
fix: bind workspace path mutations to opened identities
Duang777 Oct 5, 2026
89f66a0
fix: close workspace recovery race windows
Duang777 Oct 5, 2026
2eabb59
fix: close remaining recovery race windows
Duang777 Oct 6, 2026
a87c002
fix: close final workspace identity races
Duang777 Oct 6, 2026
7a900a2
fix: harden recovery and git bindings
Duang777 Oct 6, 2026
22ed425
fix: close final workspace race windows
Duang777 Oct 6, 2026
6f80b00
fix: preserve stop failure semantics
Duang777 Oct 6, 2026
9280ba4
fix: normalize tracked include aliases
Duang777 Oct 6, 2026
6ad1dec
fix: harden workspace deletion recovery
Duang777 Oct 6, 2026
2b8c3de
fix: pin BSD worktree repair metadata
Duang777 Oct 6, 2026
8959ba0
Merge remote-tracking branch 'origin/main' into agent-1/issue-23
Duang777 Oct 6, 2026
e2577e4
fix: lock Windows rename source names
Duang777 Oct 6, 2026
4e31b4c
fix: show rejected TUI input errors
Duang777 Oct 6, 2026
7415400
fix: reopen Windows records without delete access
Duang777 Oct 6, 2026
e127a45
fix: recover renamed removal reservations
Duang777 Oct 6, 2026
b1a0bc9
fix: recover interrupted Unix record installs
Duang777 Oct 6, 2026
4e11cbe
fix: recover Unix record removal artifacts
Duang777 Oct 6, 2026
9e4b758
fix: recover removal marker artifacts
Duang777 Oct 6, 2026
201788c
fix: remove anonymous Unix deletion artifacts
Duang777 Oct 6, 2026
60d3789
fix: bind BSD worktree registration
Duang777 Oct 6, 2026
389625c
ci: verify workspace platform implementations
Duang777 Oct 6, 2026
77929be
fix: bind marker artifacts to directory tokens
Duang777 Oct 6, 2026
b6ea4ab
fix: bind promoted worktree registration
Duang777 Oct 6, 2026
c6234c5
fix: honor legacy startup stop intent
Duang777 Oct 6, 2026
5e4a973
fix: forward TUI terminal streams
Duang777 Oct 6, 2026
5795272
fix: recover interrupted record writes
Duang777 Oct 6, 2026
cef0eff
fix: recover removal marker discard
Duang777 Oct 6, 2026
61965ef
fix: stop previews for detached sessions
Duang777 Oct 6, 2026
d42da48
fix: classify expired output requests
Duang777 Oct 6, 2026
dd6e8b4
fix: reject unexpected cli arguments
Duang777 Oct 6, 2026
7b2eb8a
fix: bound failed pty cleanup
Duang777 Oct 6, 2026
de97b08
fix: recover isolated worktree pointers
Duang777 Oct 6, 2026
acc4143
fix: fence uncertain process cleanup
Duang777 Oct 6, 2026
a2762cb
fix: clean failed BSD worktree registration
Duang777 Oct 6, 2026
6823764
fix: bind acknowledgement record deletion
Duang777 Oct 6, 2026
e64cbf3
fix: cancel CLI on SIGTERM
Duang777 Oct 6, 2026
8527edd
fix: serialize startup resume completion
Duang777 Oct 6, 2026
2caed3e
fix: fence output on PTY cleanup failure
Duang777 Oct 6, 2026
c388cc3
fix: bind BSD prepared registration stages
Duang777 Oct 6, 2026
9eb2a54
fix: restore workspace platform compilation
Duang777 Oct 6, 2026
edbefa7
fix: bind BSD checkout temporary files
Duang777 Oct 6, 2026
e0a97d8
fix: reject replaced Git directories in includes
Duang777 Oct 6, 2026
5ae65f3
fix: reopen pinned Windows directories
Duang777 Oct 6, 2026
3dd6f24
fix: reopen Windows roots with NT semantics
Duang777 Oct 6, 2026
191cdb7
test: fix workspace repository line endings
Duang777 Oct 6, 2026
43e6bbe
fix: tolerate completed record source cleanup
Duang777 Oct 6, 2026
1231d35
fix: lock Windows include manifests for reading
Duang777 Oct 6, 2026
726a8ef
test: accept locked Windows source roots
Duang777 Oct 7, 2026
36e1910
test: verify locked source identity on Windows
Duang777 Oct 7, 2026
ed23add
test: release preparation leases before restart
Duang777 Oct 7, 2026
cf2e94d
fix: close recovered Git pointer before cleanup
Duang777 Oct 7, 2026
37ad65b
test: use compatible Windows identity handle
Duang777 Oct 7, 2026
5b34014
test: await Windows record deletion
Duang777 Oct 7, 2026
00fa106
test: release preparation before record cleanup
Duang777 Oct 7, 2026
d7ab5f7
test: enable Git long paths in repositories
Duang777 Oct 7, 2026
3b44f0e
test: release lease before bucket replacement
Duang777 Oct 7, 2026
8432ef2
test: release lease before repository bucket swap
Duang777 Oct 7, 2026
65e4fce
test: release lease before source disappearance
Duang777 Oct 7, 2026
855ded4
test: retain workspace before data directory swap
Duang777 Oct 7, 2026
3209b02
test: release preparation before quarantine recovery
Duang777 Oct 7, 2026
4df0f07
test: report quarantine reconciliation failure
Duang777 Oct 7, 2026
674224a
test: seed marker before quarantine recovery
Duang777 Oct 7, 2026
20b03c9
test: release lease before identity downgrade
Duang777 Oct 7, 2026
6483060
test: normalize registered worktree path
Duang777 Oct 7, 2026
b600366
test: release lease before cleared recovery
Duang777 Oct 7, 2026
955b977
test: release lease before absent-path recovery
Duang777 Oct 7, 2026
8a0c5a4
test: release lease before cleared rejection
Duang777 Oct 7, 2026
f255bc3
test: acknowledge preparation before stale removal
Duang777 Oct 7, 2026
9881b42
test: acknowledge preparation before reservation recovery
Duang777 Oct 7, 2026
6ecea3d
test: acknowledge preparation before cleanup flow
Duang777 Oct 7, 2026
be92961
fix: harden Unix record deletion
Duang777 Oct 7, 2026
2dfdf3e
fix: fence PTY close behind exit callbacks
Duang777 Oct 7, 2026
b55494a
fix: persist startup resume cancellation
Duang777 Oct 7, 2026
21c8093
fix: harden include path identity checks
Duang777 Oct 7, 2026
8e90d53
fix: validate branch names literally
Duang777 Oct 7, 2026
a911ebc
fix: migrate historical preparation records
Duang777 Oct 7, 2026
90e3e51
fix: verify BSD working tree bytes with Git
Duang777 Oct 7, 2026
c672a36
fix: bind prepared worktree common directory
Duang777 Oct 7, 2026
73aff72
fix(workspace): recover record deletion namespaces
Duang777 Oct 7, 2026
4ffe1b1
fix(workspace): bind deletion directory removal
Duang777 Oct 7, 2026
809532d
fix(workspace): require immediate Windows record unlink
Duang777 Oct 7, 2026
3584d6d
fix(workspace): bind deletion debris recovery
Duang777 Oct 7, 2026
f54655d
fix(workspace): adopt advanced committed worktrees
Duang777 Oct 7, 2026
a05587d
fix(workspace): recover isolated deletion transactions
Duang777 Oct 7, 2026
f40b983
fix(workspace): fold Windows namespace lock aliases
Duang777 Oct 7, 2026
88c7e47
test(workspace): exercise committed restart adoption
Duang777 Oct 7, 2026
7e99445
Merge remote-tracking branch 'origin/main' into agent-1/issue-23
Duang777 Oct 7, 2026
36a15e2
fix: unblock Windows workspace cleanup
Duang777 Oct 7, 2026
1f60f7e
test: release remaining preparation leases
Duang777 Oct 7, 2026
89d8196
test: close Windows preparation leases
Duang777 Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@ jobs:
name: coverage-${{ matrix.go }}
path: coverage.out

workspace-runtime:
name: Workspace Runtime (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.24.2"
cache: true

- name: Test workspace
run: go test ./internal/workspace -count=1 -timeout=20m

workspace-platform-compile:
name: Workspace Platform Compile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.24.2"
cache: true

- name: Compile workspace platform families
run: scripts/check-workspace-platforms.sh

web:
name: Web
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ BIN := bin
MODULE := github.com/Duang777/drove
VERSION ?= $(shell git describe --tags --always 2>/dev/null || echo dev)

.PHONY: all build build-cli build-daemon test vet lint fmt clean install help
.PHONY: all build build-cli build-daemon test test-workspace-platforms vet lint fmt clean install help

all: build

Expand All @@ -21,6 +21,10 @@ test:
$(GO) test ./... -race -coverprofile=coverage.out
$(GO) tool cover -func=coverage.out | tail -n 1

## test-workspace-platforms: 编译 workspace 的全部平台实现与测试
test-workspace-platforms:
GO=$(GO) scripts/check-workspace-platforms.sh

## vet: 静态检查
vet:
$(GO) vet ./...
Expand Down
1 change: 1 addition & 0 deletions cmd/drove/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ log / timeline / explain / attach / tui / stop / worktree / token rotate / web /

- main 必须保持极薄:解析参数 → 调用 client → 退出码语义化(0 成功 / 1 用户错误 / 2 运行时错误)。
- 未知顶层或嵌套子命令属于用户输入错误,必须返回退出码 1。
- Cobra 的隐藏补全入口 `__complete` 与 `__completeNoDesc` 必须绕过未知命令预检。
- 禁止在 cmd 中复制业务逻辑或直接构造 session/pty/store;一律走 `internal/client`。
- 本地自定义类型必须与 daemon API 的 JSON 契约一致(增删字段需同步 API 层测试)。
- 导出符号:无(main 包不导出)。
95 changes: 79 additions & 16 deletions cmd/drove/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"unicode/utf8"

Expand Down Expand Up @@ -59,7 +60,7 @@ func main() {
// CLI 静默日志,避免污染输出。
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})))

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
ctx, stop := newRootContext()
defer stop()
root := newRootCmd()
root.SetContext(ctx)
Expand All @@ -69,28 +70,87 @@ func main() {
}
}

func newRootContext() (context.Context, context.CancelFunc) {
return signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
}

func executeRoot(root *cobra.Command, args []string) error {
root.InitDefaultHelpCmd()
root.InitDefaultCompletionCmd()
command, remaining, err := root.Find(args)
if err != nil {
if err := validateDefaultHelpArgs(root, args); err != nil {
return err
}
if !isCompletionRequest(args) {
command, remaining, err := root.Find(args)
if err != nil {
return markUsageError(err)
}
afterTerminator := len(remaining) > 0 && remaining[0] == "--"
if len(remaining) > 0 && remaining[0] == "--" {
remaining = remaining[1:]
}
if command.HasSubCommands() &&
!command.Runnable() &&
len(remaining) > 0 &&
(afterTerminator || !strings.HasPrefix(remaining[0], "-")) {
return markUsageError(fmt.Errorf(
"unknown command %q for %q",
remaining[0],
command.CommandPath(),
))
}
}
root.SetArgs(args)
err := root.Execute()
if err != nil && len(args) > 0 && args[0] == "completion" {
return markUsageError(err)
}
if len(remaining) > 0 && remaining[0] == "--" {
remaining = remaining[1:]
return err
}

func validateDefaultHelpArgs(root *cobra.Command, args []string) error {
if len(args) < 2 || args[0] != "help" {
return nil
}
topic := make([]string, 0, len(args)-1)
afterTerminator := false
for _, argument := range args[1:] {
if afterTerminator {
topic = append(topic, argument)
continue
}
if argument == "--" {
afterTerminator = true
continue
}
if argument == "-h" || argument == "--help" {
return nil
}
if strings.HasPrefix(argument, "-") {
continue
}
topic = append(topic, argument)
}
if command.HasSubCommands() &&
!command.Runnable() &&
len(remaining) > 0 &&
!strings.HasPrefix(remaining[0], "-") {
return markUsageError(fmt.Errorf(
"unknown command %q for %q",
remaining[0],
command.CommandPath(),
))
if len(topic) == 0 {
return nil
}
root.SetArgs(args)
return root.Execute()
_, remaining, err := root.Find(topic)
if err == nil && len(remaining) == 0 {
return nil
}
return markUsageError(fmt.Errorf(
"unknown help topic %q",
strings.Join(topic, " "),
))
}

func isCompletionRequest(args []string) bool {
return len(args) > 0 &&
(args[0] == "__complete" || args[0] == "__completeNoDesc")
}

func commandExitCode(err error) int {
Expand Down Expand Up @@ -174,6 +234,7 @@ func newInitCmd() *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "初始化配置与数据目录",
Args: usageArgs(cobra.NoArgs),
RunE: func(_ *cobra.Command, _ []string) error {
cfg := config.Defaults()
path := config.DefaultPath()
Expand Down Expand Up @@ -259,6 +320,7 @@ func newPSCmd() *cobra.Command {
return &cobra.Command{
Use: "ps",
Short: "列出全部 Agent 会话",
Args: usageArgs(cobra.NoArgs),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
c, err := newClient(ctx)
Expand Down Expand Up @@ -1129,6 +1191,7 @@ func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "输出版本信息",
Args: usageArgs(cobra.NoArgs),
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("drove %s (commit=%s, built=%s)\n", version.Version, version.Commit, version.Date)
},
Expand Down
119 changes: 119 additions & 0 deletions cmd/drove/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
"time"
"unicode/utf8"

"github.com/spf13/cobra"

"github.com/Duang777/drove/internal/agent"
"github.com/Duang777/drove/internal/auth"
"github.com/Duang777/drove/internal/cliattach"
Expand All @@ -30,6 +34,29 @@ import (
"github.com/Duang777/drove/internal/workspace"
)

func TestRootContextCancelsOnSIGTERM(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not deliver POSIX SIGTERM")
}
ctx, stop := newRootContext()
defer stop()
process, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("find test process: %v", err)
}
if err := process.Signal(syscall.SIGTERM); err != nil {
t.Fatalf("send SIGTERM: %v", err)
}
select {
case <-ctx.Done():
if !errors.Is(ctx.Err(), context.Canceled) {
t.Fatalf("root context error = %v, want context canceled", ctx.Err())
}
case <-time.After(time.Second):
t.Fatal("SIGTERM did not cancel the root context")
}
}

func TestSessionStartRequestMapsRunMode(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -269,6 +296,8 @@ func TestExecuteRootTreatsUnknownCommandsAsUsageErrors(t *testing.T) {
{"worktree", "unknown"},
{"--", "unknown"},
{"worktree", "--", "unknown"},
{"--", "--bogus"},
{"worktree", "--", "--bogus"},
}
for _, args := range tests {
t.Run(strings.Join(args, "/"), func(t *testing.T) {
Expand All @@ -283,6 +312,57 @@ func TestExecuteRootTreatsUnknownCommandsAsUsageErrors(t *testing.T) {
}
}

func TestZeroArgumentCommandsRejectArguments(t *testing.T) {
for _, command := range []*cobra.Command{
newInitCmd(),
newPSCmd(),
newVersionCmd(),
} {
t.Run(command.Name(), func(t *testing.T) {
if command.Args == nil {
t.Fatal("argument validator is nil")
}
if err := command.Args(command, nil); err != nil {
t.Fatalf("validate no arguments: %v", err)
}
err := command.Args(command, []string{"unexpected"})
if err == nil {
t.Fatal("accepted an unexpected argument")
}
if got := commandExitCode(err); got != exitUsage {
t.Fatalf("exit code = %d, want %d for %v", got, exitUsage, err)
}
})
}
}

func TestExecuteRootTreatsDefaultCommandArgumentErrorsAsUsageErrors(
t *testing.T,
) {
tests := [][]string{
{"help", "definitely-not-a-command"},
{"help", "--", "definitely-not-a-command"},
{"help", "worktree", "--", "definitely-not-a-command"},
{"completion", "bash", "extra"},
}
for _, args := range tests {
t.Run(strings.Join(args, "/"), func(t *testing.T) {
err := executeRoot(newRootCmd(), args)
if err == nil {
t.Fatalf("execute %v succeeded", args)
}
if got := commandExitCode(err); got != exitUsage {
t.Fatalf(
"exit code = %d, want %d for %v",
got,
exitUsage,
err,
)
}
})
}
}

func TestExecuteRootSupportsDefaultCommands(t *testing.T) {
tests := []struct {
name string
Expand All @@ -294,11 +374,50 @@ func TestExecuteRootSupportsDefaultCommands(t *testing.T) {
args: []string{"help", "worktree"},
wantOutput: "Usage:",
},
{
name: "help command flag",
args: []string{"help", "--help"},
wantOutput: "Usage:",
},
{
name: "help target flag",
args: []string{"help", "worktree", "--help"},
wantOutput: "Usage:",
},
{
name: "help flag overrides invalid topic",
args: []string{
"help",
"--help",
"definitely-not-a-command",
},
wantOutput: "Usage:",
},
{
name: "target help flag overrides invalid topic",
args: []string{
"help",
"worktree",
"-h",
"definitely-not-a-command",
},
wantOutput: "Usage:",
},
{
name: "completion",
args: []string{"completion", "bash"},
wantOutput: "__start_drove",
},
{
name: "completion request",
args: []string{"__complete", "workt"},
wantOutput: "worktree",
},
{
name: "completion request without descriptions",
args: []string{"__completeNoDesc", "workt"},
wantOutput: "worktree",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/muesli/cancelreader v0.2.2
github.com/spf13/cobra v1.8.1
golang.org/x/sys v0.41.0
golang.org/x/text v0.29.0
modernc.org/sqlite v1.33.1
)

Expand Down Expand Up @@ -45,7 +46,6 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.3.8 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
Expand Down
Loading
Loading