Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
23042d2
Freeze the dispatcher's interfaces: driver, tasks and attempts, hooks
jorgemanrubia Sep 17, 2026
58ed8a9
Run the connector: tests, the run command, and the worker seam
jorgemanrubia Sep 17, 2026
13d0010
Terminate the leader by pid too; pin --setting-sources in the args test
jorgemanrubia Sep 17, 2026
1fecaf8
Launch on #736's createTask; one live task per event is retired_at's
jorgemanrubia Sep 17, 2026
4904303
Bound the wait on a worker's pipes, so a stray descendant cannot hang…
jorgemanrubia Sep 17, 2026
68fdb10
Fail, not hang, when a per-task workspace session never starts
jorgemanrubia Sep 17, 2026
71a773e
Answer the first review: starvation, stop reasons, recovery, containment
jorgemanrubia Sep 17, 2026
7521b7e
End an attempt through #736's supersedeTask, which returns unexposed …
jorgemanrubia Sep 17, 2026
10fdc10
Answer the second review: scope, authorization, and what a stop means
jorgemanrubia Sep 17, 2026
c7ec041
Preallocate the stranded query's arguments
jorgemanrubia Sep 17, 2026
0b77076
Answer the third review: groups, locations, slots, truncation, the skill
jorgemanrubia Sep 17, 2026
2c44b06
Name the one-owner rule and hold everything to it
jorgemanrubia Sep 17, 2026
547d150
One release point, and nothing may reach around it
jorgemanrubia Sep 17, 2026
06bf1c1
Write the driver contract down, and make the code keep it
jorgemanrubia Sep 17, 2026
125c118
On #736's 67aac1d: settlement cannot meet a moved handed record; desc…
jorgemanrubia Sep 17, 2026
462cac6
The task token's carriage: a one-use socket and the worker-mcp bridge
jorgemanrubia Sep 17, 2026
6eb4666
Withdraw through #736's withdrawExposure, after the supersession it r…
jorgemanrubia Sep 17, 2026
bcaa010
A worker's MCP server may be its descendant in a group of its own: Co…
jorgemanrubia Sep 17, 2026
4a8e52f
The prompt's worst case fits the budget: a URL over 120 characters is…
jorgemanrubia Sep 17, 2026
d58536a
A process group whose members are all zombies is gone
jorgemanrubia Sep 17, 2026
60196d4
drivertest: a secret scan never opens a SQLite database or its journals
jorgemanrubia Sep 17, 2026
5b59bca
Tests start their helper processes with a context
jorgemanrubia Sep 17, 2026
82ee3ec
The redaction rule: one function every text leaving a worker passes t…
jorgemanrubia Sep 17, 2026
58587b6
The refusal rule: a refusal is recorded in the ledger as it happens, …
jorgemanrubia Sep 17, 2026
a00b414
Take no descriptor's range on trust at the syscall boundary
jorgemanrubia Sep 17, 2026
df6ff26
Copilot: a stub that matches its Unix twin, a turn that keeps its ref…
jorgemanrubia Sep 17, 2026
57bfbf3
The token's window is the worker's MCP server's, and starts when the …
jorgemanrubia Sep 17, 2026
9963e3d
The release point ends the MCP server the agent started outside the w…
jorgemanrubia Sep 17, 2026
efeb109
A restart ends the MCP server that took the token, and a clean finish…
jorgemanrubia Sep 17, 2026
8483da8
A token socket always has a path a unix socket can carry
jorgemanrubia Sep 17, 2026
af7e411
The moved token socket is the connector's own: swept, checked, named …
jorgemanrubia Sep 17, 2026
7e49e63
A restarted MCP server takes the token again, and four paths that ans…
jorgemanrubia Sep 17, 2026
d17eb08
The socket arms again only when the server holding the token is gone,…
jorgemanrubia Sep 17, 2026
e061398
A spent handoff budget is said out loud
jorgemanrubia Sep 17, 2026
59017dc
Write down what counts as one refusal, and why the handoff budget is …
jorgemanrubia Sep 17, 2026
0fc79b0
Say what became of every handoff, and count a nameless refusal every …
jorgemanrubia Sep 17, 2026
87479f5
Add the Codex spawn driver: codex exec --json under the AgentDriver
jorgemanrubia Sep 17, 2026
0eeaaf3
Register codex as a worker: setup.Workers and spawn.New
jorgemanrubia Sep 17, 2026
99526f0
Give each task its own git worktree, and keep the ones holding work
jorgemanrubia Sep 17, 2026
40671a2
Add basecamp connect worktrees list and prune; wire --worktrees into …
jorgemanrubia Sep 17, 2026
e5a5cf4
Satisfy the linter
jorgemanrubia Sep 17, 2026
14a2e45
Account for connect worktrees in smoke coverage
jorgemanrubia Sep 17, 2026
1fe98cb
Test the worktree state edges
jorgemanrubia Sep 17, 2026
0c0920d
Prove a second prompt is refused as such
jorgemanrubia Sep 17, 2026
bbecfee
Blank every configured content filter when the connector runs git
jorgemanrubia Sep 17, 2026
992c134
A forced prune keeps a detached HEAD's unheld commit on a branch of i…
jorgemanrubia Sep 17, 2026
695b995
Codex: a failed turn waits for the policy check; a cancel before the …
jorgemanrubia Sep 17, 2026
178a5e4
Keep a worktree holding ignored files or index entries that hide edits
jorgemanrubia Sep 17, 2026
189a68f
Back off a failing worktree; keep settling worktrees after they are s…
jorgemanrubia Sep 17, 2026
1b06e84
Satisfy the linter
jorgemanrubia Sep 17, 2026
0f0f13f
Codex: a canceled turn reports a policy check that already failed
jorgemanrubia Sep 17, 2026
c644b0d
Name the ignored-file window the removal leaves
jorgemanrubia Sep 17, 2026
16603bc
Judge a worktree by its disk and every commit it reaches; blank filte…
jorgemanrubia Sep 17, 2026
8c83610
Codex: verify the filesystem policy the sandbox is built from
jorgemanrubia Sep 17, 2026
4b82914
Run with worktrees now that they exist: drop the refusal
jorgemanrubia Sep 17, 2026
02f48c9
Own the task branch before making it, and anchor every unheld HEAD
jorgemanrubia Sep 17, 2026
d31eff2
One place decides a task branch is ours to delete
jorgemanrubia Sep 17, 2026
780ae69
Close a Codex session without waiting on an escaped child, and keep a…
jorgemanrubia Sep 17, 2026
61e7512
Close the updates channel last, read stderr after the worker exits, a…
jorgemanrubia Sep 17, 2026
85fed18
Prove the last refusal and the unreadable path
jorgemanrubia Sep 17, 2026
14a3667
Say what a prune refuses, and clean up after a worktree that never ap…
jorgemanrubia Sep 17, 2026
912cbad
Refuse a prompt that arrives after the worker's output ended
jorgemanrubia Sep 17, 2026
2d0f147
Never let a worker that stops reading hold cancel or close
jorgemanrubia Sep 17, 2026
5e88a43
Prove Close alone survives a worker that stopped reading
jorgemanrubia Sep 17, 2026
47d6cbc
Prove Close alone survives a worker that stopped reading
jorgemanrubia Sep 17, 2026
5a3967b
Read a worktree's record as git writes it, and say a refused force wh…
jorgemanrubia Sep 17, 2026
78a92ea
A route that cannot take a worktree waits out of the dispatch window
jorgemanrubia Sep 17, 2026
650e35b
Never let git look inside a submodule, and never force one away
jorgemanrubia Sep 17, 2026
6d66e0e
Leave git's record of a missing worktree to git; report an unrecorded…
jorgemanrubia Sep 17, 2026
aaf5fad
Name the worktrees starvation test apart from the dispatcher's
jorgemanrubia Sep 17, 2026
5371964
Hold the codex driver to the credential rule's places; the strict no-…
jorgemanrubia Sep 17, 2026
a94bdbd
Keep a missing worktree whose record still holds work, abandon a stuc…
jorgemanrubia Sep 17, 2026
535384c
One worktree, one removal: the rule written once, and one function th…
jorgemanrubia Sep 17, 2026
f928465
Lock git's record while a worktree is frozen, and harden the rule's e…
jorgemanrubia Sep 17, 2026
c2de7ae
Codex: a cancel with no prompt ends the worker now; a failed turn's s…
jorgemanrubia Sep 17, 2026
590f3af
Codex: take the task token over the connector's socket; no env file, …
jorgemanrubia Sep 17, 2026
efaf918
Say what the codex tests now check
jorgemanrubia Sep 17, 2026
cbaee71
Prompt honors its context while its write blocks; a missing worktree …
jorgemanrubia Sep 17, 2026
799d2ac
Never remove git data of a repository inside a worktree; judge a work…
jorgemanrubia Sep 17, 2026
00707aa
Route the codex driver's errors, updates and stderr through the share…
jorgemanrubia Sep 17, 2026
40273e2
Route the codex driver's errors, updates and stderr through the share…
jorgemanrubia Sep 17, 2026
f74db69
Record every Codex refusal through the shared recorder, cancels included
jorgemanrubia Sep 17, 2026
51b3c96
Satisfy the linter
jorgemanrubia Sep 17, 2026
e95b5f0
Verify every holder in the transaction that ends a removal
jorgemanrubia Sep 17, 2026
9944de2
The connector never removes a worktree
jorgemanrubia Sep 17, 2026
e73ffbd
Say what a kept worktree takes up, and nothing of one that is gone
jorgemanrubia Sep 17, 2026
43ebf3c
Judge a missing worktree's record by everything it still reaches
jorgemanrubia Sep 17, 2026
27adb41
The connector deletes no ref of its own accord either
jorgemanrubia Sep 17, 2026
ae890be
Record every refusal Codex only logs, not just its last line
jorgemanrubia Sep 17, 2026
1414473
Leave a worktree someone else deleted exactly as it is
jorgemanrubia Sep 17, 2026
36a2a1c
Read a pseudo-ref as what it is, and say what an orphan's force does
jorgemanrubia Sep 17, 2026
17d5971
Close the last things reviews found in what is left of the judgment
jorgemanrubia Sep 17, 2026
97534fa
Let every canceled turn read the worker's last word, and say what is …
jorgemanrubia Sep 17, 2026
27e5702
Report the refusals of a session stopped for its policy, and the comm…
jorgemanrubia Sep 17, 2026
19ac0ed
Merge connect-dispatcher: 736 landed as one commit, and the worktrees…
jorgemanrubia Sep 18, 2026
23c0391
Judge a worktree by the commits its refs reach
jorgemanrubia Sep 18, 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
72 changes: 72 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,9 @@ CMD basecamp config untrust
CMD basecamp connect
CMD basecamp connect setup
CMD basecamp connect show
CMD basecamp connect worktrees
CMD basecamp connect worktrees list
CMD basecamp connect worktrees prune
CMD basecamp docs
CMD basecamp docs archive
CMD basecamp docs doc
Expand Down Expand Up @@ -5425,6 +5428,72 @@ FLAG basecamp connect show --stats type=bool
FLAG basecamp connect show --styled type=bool
FLAG basecamp connect show --todolist type=string
FLAG basecamp connect show --verbose type=count
FLAG basecamp connect worktrees --account type=string
FLAG basecamp connect worktrees --agent type=bool
FLAG basecamp connect worktrees --cache-dir type=string
FLAG basecamp connect worktrees --count type=bool
FLAG basecamp connect worktrees --help type=bool
FLAG basecamp connect worktrees --hints type=bool
FLAG basecamp connect worktrees --ids-only type=bool
FLAG basecamp connect worktrees --in type=string
FLAG basecamp connect worktrees --jq type=string
FLAG basecamp connect worktrees --json type=bool
FLAG basecamp connect worktrees --markdown type=bool
FLAG basecamp connect worktrees --md type=bool
FLAG basecamp connect worktrees --no-hints type=bool
FLAG basecamp connect worktrees --no-stats type=bool
FLAG basecamp connect worktrees --profile type=string
FLAG basecamp connect worktrees --project type=string
FLAG basecamp connect worktrees --quiet type=bool
FLAG basecamp connect worktrees --stats type=bool
FLAG basecamp connect worktrees --styled type=bool
FLAG basecamp connect worktrees --todolist type=string
FLAG basecamp connect worktrees --verbose type=count
FLAG basecamp connect worktrees list --account type=string
FLAG basecamp connect worktrees list --agent type=bool
FLAG basecamp connect worktrees list --cache-dir type=string
FLAG basecamp connect worktrees list --count type=bool
FLAG basecamp connect worktrees list --help type=bool
FLAG basecamp connect worktrees list --hints type=bool
FLAG basecamp connect worktrees list --ids-only type=bool
FLAG basecamp connect worktrees list --in type=string
FLAG basecamp connect worktrees list --jq type=string
FLAG basecamp connect worktrees list --json type=bool
FLAG basecamp connect worktrees list --markdown type=bool
FLAG basecamp connect worktrees list --md type=bool
FLAG basecamp connect worktrees list --no-hints type=bool
FLAG basecamp connect worktrees list --no-stats type=bool
FLAG basecamp connect worktrees list --profile type=string
FLAG basecamp connect worktrees list --project type=string
FLAG basecamp connect worktrees list --quiet type=bool
FLAG basecamp connect worktrees list --shadow type=bool
FLAG basecamp connect worktrees list --stats type=bool
FLAG basecamp connect worktrees list --styled type=bool
FLAG basecamp connect worktrees list --todolist type=string
FLAG basecamp connect worktrees list --verbose type=count
FLAG basecamp connect worktrees prune --account type=string
FLAG basecamp connect worktrees prune --agent type=bool
FLAG basecamp connect worktrees prune --cache-dir type=string
FLAG basecamp connect worktrees prune --count type=bool
FLAG basecamp connect worktrees prune --force type=stringArray
FLAG basecamp connect worktrees prune --help type=bool
FLAG basecamp connect worktrees prune --hints type=bool
FLAG basecamp connect worktrees prune --ids-only type=bool
FLAG basecamp connect worktrees prune --in type=string
FLAG basecamp connect worktrees prune --jq type=string
FLAG basecamp connect worktrees prune --json type=bool
FLAG basecamp connect worktrees prune --markdown type=bool
FLAG basecamp connect worktrees prune --md type=bool
FLAG basecamp connect worktrees prune --no-hints type=bool
FLAG basecamp connect worktrees prune --no-stats type=bool
FLAG basecamp connect worktrees prune --profile type=string
FLAG basecamp connect worktrees prune --project type=string
FLAG basecamp connect worktrees prune --quiet type=bool
FLAG basecamp connect worktrees prune --shadow type=bool
FLAG basecamp connect worktrees prune --stats type=bool
FLAG basecamp connect worktrees prune --styled type=bool
FLAG basecamp connect worktrees prune --todolist type=string
FLAG basecamp connect worktrees prune --verbose type=count
FLAG basecamp docs --account type=string
FLAG basecamp docs --agent type=bool
FLAG basecamp docs --cache-dir type=string
Expand Down Expand Up @@ -18606,6 +18675,9 @@ SUB basecamp config untrust
SUB basecamp connect
SUB basecamp connect setup
SUB basecamp connect show
SUB basecamp connect worktrees
SUB basecamp connect worktrees list
SUB basecamp connect worktrees prune
SUB basecamp docs
SUB basecamp docs archive
SUB basecamp docs doc
Expand Down
8 changes: 8 additions & 0 deletions e2e/smoke/smoke_lifecycle.bats
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ load smoke_helper
mark_out_of_scope "Reads the connector policy a connected profile's setup wrote — covered by Go tests in internal/commands"
}

@test "connect worktrees list is out of scope" {
mark_out_of_scope "Reads a local connector's ledger — covered by Go tests in internal/commands and internal/connector"
}

@test "connect worktrees prune is out of scope" {
mark_out_of_scope "Removes local git worktrees a connector kept — covered by Go tests in internal/commands and internal/connector"
}

@test "auth refresh is out of scope" {
mark_out_of_scope "Requires OAuth credentials"
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func CommandCategories() []CommandCategory {
{Name: "bonfire", Category: "additional", Description: "Multi-chat orchestration", Actions: []string{"split", "layout"}, Experimental: true, DevOnly: true},
{Name: "api", Category: "additional", Description: "Raw API access"},
{Name: "mcp", Category: "additional", Description: "Serve Basecamp to MCP clients over stdio"},
{Name: "connect", Category: "additional", Description: "Set up a local agent connector for a Basecamp agent", Actions: []string{"setup", "show"}},
{Name: "connect", Category: "additional", Description: "Set up a local agent connector for a Basecamp agent", Actions: []string{"setup", "show", "worktrees"}},
{Name: "help", Category: "additional", Description: "Show help"},
{Name: "version", Category: "additional", Description: "Show version"},
},
Expand Down
3 changes: 2 additions & 1 deletion internal/commands/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ isolated state directory and dispatches nothing. macOS and Linux only.`,
cmd.AddCommand(newConnectSetupCmd())
cmd.AddCommand(newConnectWorkerMCPCmd())
cmd.AddCommand(newConnectShowCmd())
cmd.AddCommand(newConnectWorktreesCmd())
return cmd
}

Expand Down Expand Up @@ -180,7 +181,7 @@ func connectShowDisplay(path string, f setup.File, markdown bool) map[string]any
"agent": agent,
"operator": fmt.Sprintf("person %d", f.Trust.OperatorID),
"trust": trust,
"workers": fmt.Sprintf("%s, concurrency %d, deadline %s, worktrees %s", f.Driver, f.Concurrency, time.Duration(f.Deadline), worktrees),
"workers": fmt.Sprintf("%s %s, concurrency %d, deadline %s, worktrees %s", f.Driver, f.WorkerName(), f.Concurrency, time.Duration(f.Deadline), worktrees),
"projects": strconv.Itoa(len(f.Projects)) + " routed",
}
for id, r := range f.Projects {
Expand Down
44 changes: 35 additions & 9 deletions internal/commands/connect_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,29 @@ func connectStateDir(file setup.File, shadow bool) (string, error) {
if err != nil {
return "", err
}
group := "connect"
group, dir := connectStateParts(file, shadow)
return ensurePrivateChain(stateHome, "basecamp", group, dir)
}

// connectStateDirPath is the same directory, named and not created: what
// reads a connector's state resolves.
func connectStateDirPath(file setup.File, shadow bool) (string, error) {
stateHome, err := connectStateHome()
if err != nil {
return "", err
}
group, dir := connectStateParts(file, shadow)
return filepath.Join(stateHome, "basecamp", group, dir), nil
}

func connectStateParts(file setup.File, shadow bool) (group, dir string) {
group = "connect"
if shadow {
// An isolated ledger, lock and checkpoint: a shadow never shares a
// position or a record with the connector it watches beside.
group = "connect-shadow"
}
return ensurePrivateChain(stateHome, "basecamp", group, connector.StateDirName(file.AccountID, file.Agent.PersonID))
return group, connector.StateDirName(file.AccountID, file.Agent.PersonID)
}

// connectSessionsDir is where a session's short-lived files go — the MCP
Expand Down Expand Up @@ -156,11 +172,6 @@ func runConnect(cmd *cobra.Command, f *connectRunFlags) error {
case err != nil:
return output.ErrUsage("connect.json cannot be used: " + err.Error())
}
if file.Worktrees && !f.shadow {
// Refused rather than ignored: workers would share the route's
// checkout while connect.json says each task gets its own.
return output.ErrUsage("connect.json asks for worktrees, which this basecamp does not support yet; run setup with --worktrees=false")
}
driverName := file.Driver
if f.driver != "" {
driverName = f.driver
Expand Down Expand Up @@ -274,12 +285,27 @@ func runConnect(cmd *cobra.Command, f *connectRunFlags) error {
if err != nil {
return output.ErrUsage(err.Error())
}
dispatcher, err = connector.NewDispatcher(connectDispatcherOptions(connectDispatch{
// Built with worktrees off too, so the ones made while they were on
// are still settled and recovered.
worktreesRoot, err := ensurePrivateChain(stateDir, connectWorktreesDir)
if err != nil {
return err
}
workspaces, err := connector.NewWorktrees(connector.WorktreesOptions{
Ledger: ledger, Root: worktreesRoot, Logger: logger, Off: !file.Worktrees,
Redaction: driver.Redaction{Dirs: []string{stateDir}},
})
if err != nil {
return err
}
options := connectDispatcherOptions(connectDispatch{
File: file, Buckets: buckets, Ledger: ledger, Driver: worker, Routes: routes.Current,
Profile: name, Executable: exe, StateDir: stateDir, SessionsDir: sessions,
Replies: connector.SDKReplies{Client: accountClient, AgentID: agentID},
Lines: lines, Logger: logger,
}))
})
options.Workspaces = workspaces
dispatcher, err = connector.NewDispatcher(options)
if err != nil {
return err
}
Expand Down
Loading
Loading