Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
639b9ae
Serve a connector worker its dispatch over MCP: basecamp_connect
jorgemanrubia Sep 17, 2026
3cee0bf
One live task per event; the three actions agree on what is dispatchable
jorgemanrubia Sep 17, 2026
8883096
Make a task only of instructions a worker can pull
jorgemanrubia Sep 17, 2026
2e6e9e7
Open an existing ledger without any path that can create it
jorgemanrubia Sep 17, 2026
4f5f6d7
Supersede returns unexposed work; finished work is never handed out anew
jorgemanrubia Sep 17, 2026
b1a06c4
One task per conversation, one mention parser, one state-root check
jorgemanrubia Sep 17, 2026
a29c397
A removed mention leaves a space, so nothing joins across it
jorgemanrubia Sep 17, 2026
5d8864c
Answer from the row, refuse with fields, name the domain
jorgemanrubia Sep 17, 2026
ff6fc5d
Take links as a list of strings from any caller
jorgemanrubia Sep 17, 2026
3f6c7bf
Build the instruction with the ledger free
jorgemanrubia Sep 17, 2026
67aac1d
Write the dispatch lifecycle down, enforce it in one place, try every…
jorgemanrubia Sep 17, 2026
d91e361
Withdraw a failed spawn's exposure; bound the token read; refuse an e…
jorgemanrubia Sep 17, 2026
32fe533
Withdraw only what the ledger can see no worker had
jorgemanrubia Sep 17, 2026
b146b57
A record is dispatched exactly while a live task carries it, in the d…
jorgemanrubia Sep 17, 2026
95fe772
Make the lifecycle table say what the code does; use the checked dire…
jorgemanrubia Sep 17, 2026
9dbed2a
One privacy check per ledger file; the task's own columns are the dat…
jorgemanrubia Sep 17, 2026
7e21802
Know the ledger file by what it is, close it once, refuse before writing
jorgemanrubia Sep 17, 2026
c11e554
Claim the ledger file by identity, release it after the database closes
jorgemanrubia Sep 17, 2026
fca7015
A pull is what hands work to a worker; a live task keeps its conversa…
jorgemanrubia Sep 17, 2026
9a1042f
Read the token descriptor the way the flag parser will, or not at all
jorgemanrubia Sep 17, 2026
6796808
Tie retirement and pulls to the task, and read only this command's de…
jorgemanrubia Sep 17, 2026
a089855
Let cobra say which invocation this is, and the database tie the rest…
jorgemanrubia Sep 17, 2026
efc7c4a
Hold a later open to the owner the check passed, and read for one inv…
jorgemanrubia Sep 17, 2026
670c82e
Read the arguments without touching the command that runs them
jorgemanrubia Sep 17, 2026
a74d10b
Keep the token descriptor from children instead of racing to read it
jorgemanrubia Sep 17, 2026
0e493b0
Vet the whole path on a later open, without opening the ledger
jorgemanrubia Sep 17, 2026
4496992
Say why the descriptor number converts safely
jorgemanrubia Sep 17, 2026
1bac99f
Seal inherited descriptors at startup instead of scanning argv
jorgemanrubia Sep 17, 2026
65972ca
Freeze the dispatcher's interfaces: driver, tasks and attempts, hooks
jorgemanrubia Sep 17, 2026
6fc3d9e
Run the connector: tests, the run command, and the worker seam
jorgemanrubia Sep 17, 2026
3d0d689
Terminate the leader by pid too; pin --setting-sources in the args test
jorgemanrubia Sep 17, 2026
84d8ba6
Launch on #736's createTask; one live task per event is retired_at's
jorgemanrubia Sep 17, 2026
cd90f34
Bound the wait on a worker's pipes, so a stray descendant cannot hang…
jorgemanrubia Sep 17, 2026
116c628
Fail, not hang, when a per-task workspace session never starts
jorgemanrubia Sep 17, 2026
95e2bea
Answer the first review: starvation, stop reasons, recovery, containment
jorgemanrubia Sep 17, 2026
ad7eaef
End an attempt through #736's supersedeTask, which returns unexposed …
jorgemanrubia Sep 17, 2026
75b8b99
Answer the second review: scope, authorization, and what a stop means
jorgemanrubia Sep 17, 2026
1b0c808
Preallocate the stranded query's arguments
jorgemanrubia Sep 17, 2026
ce9d89f
Answer the third review: groups, locations, slots, truncation, the skill
jorgemanrubia Sep 17, 2026
096a3d8
Name the one-owner rule and hold everything to it
jorgemanrubia Sep 17, 2026
f7e1632
One release point, and nothing may reach around it
jorgemanrubia Sep 17, 2026
c5ecb73
Write the driver contract down, and make the code keep it
jorgemanrubia Sep 17, 2026
9c82798
On #736's 67aac1d: settlement cannot meet a moved handed record; desc…
jorgemanrubia Sep 17, 2026
f8c8c22
The task token's carriage: a one-use socket and the worker-mcp bridge
jorgemanrubia Sep 17, 2026
3b1a566
Withdraw through #736's withdrawExposure, after the supersession it r…
jorgemanrubia Sep 17, 2026
2a1ef38
A worker's MCP server may be its descendant in a group of its own: Co…
jorgemanrubia Sep 17, 2026
1efa646
The prompt's worst case fits the budget: a URL over 120 characters is…
jorgemanrubia Sep 17, 2026
f59c304
A process group whose members are all zombies is gone
jorgemanrubia Sep 17, 2026
0ae03a4
drivertest: a secret scan never opens a SQLite database or its journals
jorgemanrubia Sep 17, 2026
3cd0869
Tests start their helper processes with a context
jorgemanrubia Sep 17, 2026
cacfe8c
The redaction rule: one function every text leaving a worker passes t…
jorgemanrubia Sep 17, 2026
d6bc577
The refusal rule: a refusal is recorded in the ledger as it happens, …
jorgemanrubia Sep 17, 2026
7cdae08
Take no descriptor's range on trust at the syscall boundary
jorgemanrubia Sep 17, 2026
91507de
Copilot: a stub that matches its Unix twin, a turn that keeps its ref…
jorgemanrubia Sep 17, 2026
1252f15
The token's window is the worker's MCP server's, and starts when the …
jorgemanrubia Sep 17, 2026
d1f2054
The release point ends the MCP server the agent started outside the w…
jorgemanrubia Sep 17, 2026
d89aa88
A restart ends the MCP server that took the token, and a clean finish…
jorgemanrubia Sep 17, 2026
6bb9d95
A token socket always has a path a unix socket can carry
jorgemanrubia Sep 17, 2026
08dc6a9
The moved token socket is the connector's own: swept, checked, named …
jorgemanrubia Sep 17, 2026
cbcb460
A restarted MCP server takes the token again, and four paths that ans…
jorgemanrubia Sep 17, 2026
d2b1bf5
The socket arms again only when the server holding the token is gone,…
jorgemanrubia Sep 17, 2026
76069af
A spent handoff budget is said out loud
jorgemanrubia Sep 17, 2026
21e4175
Write down what counts as one refusal, and why the handoff budget is …
jorgemanrubia Sep 17, 2026
2f98e27
Say what became of every handoff, and count a nameless refusal every …
jorgemanrubia Sep 17, 2026
caab8f1
A worker acknowledges and completes what it pulled
jorgemanrubia Sep 17, 2026
031b77c
Merge main: 736 landed, and its migration 6 is not this branch's
jorgemanrubia Sep 18, 2026
2116ae1
Drop the token reader main renamed away
jorgemanrubia Sep 18, 2026
d70fd6c
Identify a recorded worker by the kernel's own start time, exactly
jorgemanrubia Sep 18, 2026
195b13d
Hold an attempt whose task token went to a process nobody can account…
jorgemanrubia Sep 18, 2026
b180f48
Follow a dangling symlink to where it points before deciding it is in…
jorgemanrubia Sep 18, 2026
70a5465
Finish recovery on a context a shutdown does not cancel, and stop ado…
jorgemanrubia Sep 18, 2026
1e7dddd
Run the connector on Linux only
jorgemanrubia Sep 18, 2026
720827e
Refuse a negative --since, and give a shadow run its own feed lineage
jorgemanrubia Sep 18, 2026
f212ba4
Say which worker the driver runs in connect show
jorgemanrubia Sep 18, 2026
4164395
Tell adoption about a shutdown with a channel rather than a second co…
jorgemanrubia Sep 18, 2026
2eeb937
Hold an attempt whose handoff was still deciding when the socket was …
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
4 changes: 4 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -5348,6 +5348,7 @@ FLAG basecamp connect --account type=string
FLAG basecamp connect --agent type=bool
FLAG basecamp connect --cache-dir type=string
FLAG basecamp connect --count type=bool
FLAG basecamp connect --driver type=string
FLAG basecamp connect --help type=bool
FLAG basecamp connect --hints type=bool
FLAG basecamp connect --ids-only type=bool
Expand All @@ -5361,6 +5362,8 @@ FLAG basecamp connect --no-stats type=bool
FLAG basecamp connect --profile type=string
FLAG basecamp connect --project type=string
FLAG basecamp connect --quiet type=bool
FLAG basecamp connect --shadow type=bool
FLAG basecamp connect --since type=int64
FLAG basecamp connect --stats type=bool
FLAG basecamp connect --styled type=bool
FLAG basecamp connect --todolist type=string
Expand Down Expand Up @@ -5399,6 +5402,7 @@ FLAG basecamp connect setup --todolist type=string
FLAG basecamp connect setup --trust type=string
FLAG basecamp connect setup --verbose type=count
FLAG basecamp connect setup --watch-completions type=stringArray
FLAG basecamp connect setup --worker type=string
FLAG basecamp connect setup --worktrees type=bool
FLAG basecamp connect show --account type=string
FLAG basecamp connect show --agent type=bool
Expand Down
6 changes: 6 additions & 0 deletions STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ recording's change history and predates the account-wide event feed that
rather than becoming a group: turning it into one would break every existing
`basecamp events <id>` invocation to gain nothing.

`connect` is the other exception. The spec names the connector's run as the bare
`basecamp connect -P <agent>`, a long-running foreground command in the grain of
`basecamp mcp`, with `setup` beside it as the one-off that prepares it. Making the
run a `connect run` subcommand would put a verb under a command that is already
the verb.

`scripts/check-bare-groups.sh` enforces this with an allowlist; a command added
there belongs in this section too, with the reason it is an exception.

Expand Down
39 changes: 36 additions & 3 deletions internal/commands/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"time"
Expand All @@ -29,19 +30,41 @@ import (

// NewConnectCmd is the local agent connector's command group.
func NewConnectCmd() *cobra.Command {
var run connectRunFlags
cmd := &cobra.Command{
Use: "connect",
Short: "Set up a local agent connector for a Basecamp agent",
Short: "Run a local agent connector for a Basecamp agent",
Long: `Run a local agent connector: it listens to the account event feed as a
Basecamp agent, admits what a trusted person asks of that agent, and hands
the work to a local coding agent that replies in Basecamp as the agent.

Connect the agent to a profile first (basecamp auth agent connect -P <profile>),
then run setup on that profile: it records who may drive the agent, maps
projects to the directories their work runs in, and checks the connector is
ready. Show prints what setup recorded.`,
ready. Show prints what setup recorded. Then run the connector on it:

basecamp connect -P <profile> [--project <id>]... [--shadow]

It runs in the foreground until interrupted. Stdout is a wire of one JSON
object per line (events seen, verdicts, dispatches; never content), and logs
go to stderr. SIGINT and SIGTERM cancel live workers with stop reason
shutdown, settle them, and exit 130 and 143. --shadow admits and logs in an
isolated state directory and dispatches nothing. Linux only.`,
Example: ` basecamp connect setup -P agent --operator-profile me --route 12345=/src/app
basecamp connect -P agent
basecamp connect -P agent --project 12345 --shadow`,
Args: cobra.NoArgs,
Annotations: map[string]string{
"agent_notes": "Long-running; stdout is NDJSON pointer lines, logs on stderr. Not for interactive use.",
"stdout_wire": "connect",
},
RunE: func(cmd *cobra.Command, _ []string) error {
return runConnect(cmd, &run)
},
}
addConnectRunFlags(cmd, &run)
cmd.AddCommand(newConnectSetupCmd())
cmd.AddCommand(newConnectWorkerMCPCmd())
cmd.AddCommand(newConnectShowCmd())
return cmd
}
Expand Down Expand Up @@ -157,7 +180,11 @@ 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),
// The worker as well as the driver: the file records which coding
// agent the driver runs, and a file written before that field
// existed still means the default, which is what a person reading
// show needs to see.
"workers": fmt.Sprintf("%s running %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 Expand Up @@ -233,6 +260,7 @@ type connectSetupFlags struct {
unwatch []string
unroute []string
driver string
worker string
parallel int
deadline time.Duration
worktrees bool
Expand Down Expand Up @@ -315,6 +343,7 @@ Examples:
fl.StringArrayVar(&f.watch, "watch-completions", nil, "Admit every trusted completion in a routed project (repeatable)")
fl.StringArrayVar(&f.unwatch, "no-watch-completions", nil, "Stop watching a project's completions (repeatable)")
fl.StringVar(&f.driver, "driver", "", "How workers are run: spawn or acp (default spawn)")
fl.StringVar(&f.worker, "worker", "", fmt.Sprintf("The coding agent workers run: %s (default %s)", strings.Join(setup.Workers, ", "), setup.DefaultWorker))
fl.IntVar(&f.parallel, "concurrency", 0, fmt.Sprintf("Workers at once (default %d)", setup.DefaultConcurrency))
fl.DurationVar(&f.deadline, "deadline", 0, fmt.Sprintf("Deadline per task (default %s)", setup.DefaultDeadline))
fl.BoolVar(&f.worktrees, "worktrees", false, "Give each task its own git worktree")
Expand Down Expand Up @@ -749,6 +778,10 @@ func (f *connectSetupFlags) changes(cmd *cobra.Command) (setup.Changes, error) {
default:
return ch, output.ErrUsage(fmt.Sprintf("Invalid --driver %q: use spawn or acp", f.driver))
}
if f.worker != "" && !slices.Contains(setup.Workers, f.worker) {
return ch, output.ErrUsage(fmt.Sprintf("Invalid --worker %q: use %s", f.worker, strings.Join(setup.Workers, ", ")))
}
ch.Worker = f.worker
// A typed zero is out of range, not a request for the default: the flags
// are read as typed, not as their zero values.
if cmd.Flags().Changed("concurrency") {
Expand Down
Loading
Loading