Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8fb373
senior-dev works in a private worktree copy; effort defaults to high
ZeroPoint95 Sep 28, 2026
cfcee97
senior-dev's copy never loses work; fixes from the adversarial review
ZeroPoint95 Sep 28, 2026
ab853d9
senior-dev's copy starts from the person's folder, uncommitted work i…
ZeroPoint95 Sep 29, 2026
6f36e88
senior-dev's own check matters less; dependencies installed into the …
ZeroPoint95 Sep 29, 2026
044df6d
senior-dev's task page names the models its run was launched on
ZeroPoint95 Sep 29, 2026
7cb80cc
senior-dev's open brief is the whole brief, a document the page scrolls
ZeroPoint95 Sep 29, 2026
a8f8a44
The shell run's test reads the branch that starts from the person's c…
ZeroPoint95 Sep 29, 2026
d81cf35
Change entry for #1674
ZeroPoint95 Sep 29, 2026
b191069
session: senior-dev's first commit takes only what git status names
AbirAbbas Sep 29, 2026
8956d0e
session: a linked node_modules is never carried into senior-dev's cop…
AbirAbbas Sep 29, 2026
2b4d86c
session: a swept copy stays on disk when its saved candidate cannot b…
AbirAbbas Sep 29, 2026
3382437
session: a swept copy puts no branch on senior-dev's starting tree
AbirAbbas Sep 29, 2026
02465d7
manual: ▸ brief does not show the line naming senior-dev's copy
AbirAbbas Sep 29, 2026
46cc528
session: a file staged and deleted since costs senior-dev's copy none…
AbirAbbas Sep 29, 2026
96b9c08
Fix senior-dev landing guidance and worktree review nits
ZeroPoint95 Sep 29, 2026
36785a0
Copy senior-dev local inputs without committing untracked files
ZeroPoint95 Sep 29, 2026
fc7feb0
An untracked file senior-dev changed is its work and goes on its branch
ZeroPoint95 Sep 29, 2026
920008a
session: the ending's stash takes only the untracked files it names
AbirAbbas Sep 29, 2026
546fd41
session, seniordev: commits of named paths take those paths literally
AbirAbbas Sep 29, 2026
bc2bd56
session: the ending names a changed untracked file whose name git quotes
AbirAbbas Sep 29, 2026
97f0852
session: the ending's stash reads a name that starts with a colon lit…
AbirAbbas Sep 29, 2026
580e84b
manual: what to do with the two stashes after bringing senior-dev's b…
AbirAbbas Sep 29, 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
105 changes: 94 additions & 11 deletions cmd/codeaf/carried.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/delegate"
"github.com/Agent-Field/codeaf/internal/delegate/builtin"
"github.com/Agent-Field/codeaf/internal/gitidentity"
"github.com/Agent-Field/codeaf/internal/home"
lanes "github.com/Agent-Field/codeaf/internal/lane"
"github.com/Agent-Field/codeaf/internal/modelsource"
Expand Down Expand Up @@ -263,6 +264,10 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
if err != nil {
return err
}
// AND THE SEAT IS WHAT THE PROGRAM WORKS ON, as a conversation's crew is
// ([carriedSeatedLine]): a shell run and a chat run of one program on
// one profile route on the same model.
inv.Line = carriedSeatedLine(inv, road.seat)
}
// A PERSON TYPED THIS AND IS WATCHING ITS LINES, which is the fact the
// lane layer reads for the calls that ride no context of the door's own
Expand All @@ -271,11 +276,11 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
record := carriedRecordDir(inv.Program.Name)
view := newCarriedView(carriedStdout, inv, record)
// THE FOLDER IS READIED BEFORE ANYTHING STARTS, the one way a conversation's
// run readies it (internal/session's programfolder.go): the folder itself,
// on a branch of its own in a repository, and a refusal — changes that are
// not committed, another program's run in it — before a cent is spent. A
// plain folder is no longer the program's first-line failure: codeaf says
// so on the program's line.
// run readies it (internal/session's programfolder.go): a copy of its own on
// a branch of its own in a repository, the folder itself otherwise, and a
// refusal — another program's run in a plain folder — before a cent is
// spent. A plain folder is no longer the program's first-line failure:
// codeaf says so on the program's line.
folder, err := carriedFolder(inv, record)
if err != nil {
fmt.Fprintln(carriedStderr, "error:", err)
Expand Down Expand Up @@ -382,10 +387,12 @@ func runCarriedHost(ctx context.Context, inv *delegate.Invocation) error {
Args: carriedInFolder(carriedChildLine(inv), inv, folder),
// NO PROVIDER KEY IS INHERITED BY THE PROGRAM (delegate.ChildEnv): the engine
// gets the loopback token it needs, and model commands lose that token.
Env: append(delegate.ChildEnv(api.API()), "SENIOR_DEV_EXPECTED_BRANCH="+folder.Branch, "SENIOR_DEV_IGNORED_AT_START="+folder.IgnoredFile()),
Env: append(delegate.ChildEnv(api.API()), "SENIOR_DEV_EXPECTED_BRANCH="+folder.Branch, "SENIOR_DEV_IGNORED_AT_START="+folder.IgnoredFile(),
gitidentity.InputsEnv+"="+folder.InputsFile()),
Dir: here,
StderrPath: filepath.Join(record, carriedStderrName),
Grace: grace,
Hold: folder.Hold(),
}, view)
// THE INSTANT THE PROCESS WAS GONE, not the instant its stdout drained, for
// both the last line and the record's end: a helper the program left holding
Expand Down Expand Up @@ -431,11 +438,12 @@ func carriedFolder(inv *delegate.Invocation, record string) (*session.ProgramFol
}

// carriedInFolder puts on a shell run's child line what codeaf decided about
// its folder, after --json and before the person's own words: the folder
// itself when it is not the one the line names (a folder inside a repository
// is worked in at the repository's root, and the person's own --dir is taken
// off so it cannot win), and the program's own flags for a folder worked in
// without git ([delegate.Delegate.PlainFolder]).
// its folder, after --json and before the person's own words: the folder it
// works in when it is not the one the line names (a repository is worked in
// in a copy of its own, and the person's own --dir is taken off so it cannot
// win), the program's own flags for a folder worked in without git
// ([delegate.Delegate.PlainFolder]), and, ahead of the brief, the line that
// says where a copy is ([session.ProgramFolder.BriefNote]).
func carriedInFolder(child []string, inv *delegate.Invocation, folder *session.ProgramFolder) []string {
if folder == nil {
return child
Expand Down Expand Up @@ -463,6 +471,10 @@ func carriedInFolder(child []string, inv *delegate.Invocation, folder *session.P
if folder.Plain() {
head = append(head, inv.Program.PlainFolder...)
}
if note := folder.BriefNote(); note != "" && len(inv.Args) > 0 && len(rest) >= len(inv.Args) {
flags, words := rest[:len(rest)-len(inv.Args)], rest[len(rest)-len(inv.Args):]
rest = append(append(append([]string(nil), flags...), note+"\n\n"), words...)
}
return append(head, rest...)
}

Expand Down Expand Up @@ -515,6 +527,35 @@ func carriedChildLine(inv *delegate.Invocation) []string {
return append(head, line...)
}

// carriedSeatedLine puts the profile's work seat on a shell run's line as the
// program's working seat, in the program's own crew flags
// ([delegate.Delegate.CrewFlags]) — for senior-dev the pool it routes on, and
// the seat's own rung (`model:high`) as its effort — exactly as a
// conversation hands its crew over. They go first, so a flag the person typed
// after them still wins. A program with no crew flags, or no seat, keeps the
// line as typed.
//
// A SHELL RUN USED TO ROUTE ON SENIOR-DEV'S OWN LIST while the same program
// started from the chat worked on the person's crew, so one person on one
// profile got a different model depending on the door; the seat was only a
// fallback for calls nothing here could serve.
func carriedSeatedLine(inv *delegate.Invocation, seat string) []string {
model, rung := roles.SplitEffort(seat)
if inv.Program.CrewFlags == nil || strings.TrimSpace(model) == "" {
return inv.Line
}
flags := inv.Program.CrewFlags(delegate.Crew{Hands: strings.TrimSpace(model), Effort: rung})
at := 0
if len(inv.Line) > 0 {
if _, named := inv.Program.Command(inv.Line[0]); named {
at = 1
}
}
line := append([]string(nil), inv.Line[:at]...)
line = append(line, flags...)
return append(line, inv.Line[at:]...)
}

// carriedResolvedHigh replaces only the model flag's value on the person's
// line. Every other program flag and every word of the brief stays as typed.
func carriedResolvedHigh(line []string, model string) []string {
Expand Down Expand Up @@ -672,6 +713,23 @@ func (v *carriedView) begin() {
return
}
v.say("%s · working in %s · %s", v.inv.Program.Name, v.where(), v.inv.Ceilings.Summary())
// A COPY IS CUT FROM A COMMIT, so what the person had not committed is not
// in it, and a person at a shell is told so before the run spends a cent on
// work that needed it — as a conversation's receipt tells them.
if left := v.leftBehind(); left != "" {
v.say("%s", left)
}
}

// leftBehind is the line about the changes the person's checkout had not
// committed when the program's copy was cut ([session.ProgramFolder.LeftBehindWords]).
func (v *carriedView) leftBehind() string {
v.mu.Lock()
defer v.mu.Unlock()
if v.folder == nil {
return ""
}
return v.folder.LeftBehindWords()
}

// inFolder keeps the folder the run was readied in, for the line that says
Expand All @@ -693,6 +751,9 @@ func (v *carriedView) where() string {
if v.folder.Plain() {
return v.folder.Dir
}
if v.folder.Copied() {
return v.folder.Repo + ", in a copy of its own on its own branch " + v.folder.Branch
}
return v.folder.Dir + ", on its own branch " + v.folder.Branch
}

Expand Down Expand Up @@ -728,6 +789,16 @@ func (v *carriedView) opened(at time.Time) {
})
}

// heard writes the models the program says it runs on onto its record, when a
// stage names them ([delegate.StageRecord.Models]).
func (v *carriedView) heard(record delegate.StageRecord) {
v.mu.Lock()
defer v.mu.Unlock()
if v.program.Heard(record) {
v.write()
}
}

// closed writes the instant the program's process was gone.
func (v *carriedView) closed(at time.Time) {
v.remember(func(record *delegate.ProgramRecord) { record.EndedAt = at })
Expand All @@ -739,6 +810,12 @@ func (v *carriedView) remember(change func(record *delegate.ProgramRecord)) {
v.mu.Lock()
defer v.mu.Unlock()
change(&v.program)
v.write()
}

// write writes the program record whole, naming the program when nothing has
// yet. Its caller holds the lock.
func (v *carriedView) write() {
if v.program.Name == "" {
v.program.Name = v.inv.Program.Name
}
Expand All @@ -758,6 +835,7 @@ func (v *carriedView) kept(action delegate.Action) {

func (v *carriedView) Stage(record delegate.StageRecord) {
v.kept(delegate.StageAction(time.Now(), record))
v.heard(record)
if v.records != nil {
_ = v.records.Stage(record)
return
Expand Down Expand Up @@ -960,7 +1038,12 @@ func carriedEnding(name string, terminal delegate.Terminal) string {
case delegate.StatusBudget:
said = name + " stopped at its ceiling"
case delegate.StatusFail:
// A change it handed in is finished, whatever its own check of it
// said, the way a conversation's run reads it ([delegate.Terminal.HandedIn]).
said = name + " did not finish"
if terminal.HandedIn() {
said = name + " finished"
}
default:
said = name + " crashed"
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/codeaf/carried_child_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func fakeCarriedProgram() delegate.Delegate {
linger := fs.Duration("linger", 0, "leave a helper holding stdout this long after the program exits")
return func(ctx context.Context, host delegate.Host, args []string) error {
host.Hello([]string{"implement", "verify"})
host.Stage(delegate.StageRecord{Stage: "implement", Status: "running"})
host.Stage(delegate.StageRecord{Stage: "implement", Status: "running", Data: json.RawMessage(`{"models":["vendor/model-a"],"effort":"high"}`)})
for call := 1; call <= *calls && ctx.Err() == nil; call++ {
reply, err := askCarried(ctx, host.Models(), fmt.Sprintf("question %d: %s", call, strings.Join(args, " ")))
if err != nil {
Expand Down
82 changes: 49 additions & 33 deletions cmd/codeaf/carried_folder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ package main
// A SHELL RUN WORKS IN ITS FOLDER THE WAY A CONVERSATION'S RUN DOES
// (internal/session's programfolder.go): a plain folder is worked in as it is
// with the program told so on its line — where it used to end at once with
// "workspace is not a git repository" — a repository gets a branch of its own
// that is left checked out with the work committed on it, and a checkout with
// changes that are not committed is refused before anything is spent.
// "workspace is not a git repository" — and a repository gets a copy of its
// own on a branch of its own, which starts from the person's changes that are
// not committed and is kept with the work committed on it, checked out nowhere,
// while the person's checkout never moves.

import (
"bytes"
Expand Down Expand Up @@ -148,9 +149,10 @@ func TestAShellRunInAPlainFolderIsToldSoAndDoesNotFail(t *testing.T) {
}
}

// A SHELL RUN IN A REPOSITORY WORKS ON A BRANCH OF ITS OWN, and the person's
// branch never moves: the program's work is committed on its branch, which is
// left checked out, and the last lines say how to go back.
// A SHELL RUN IN A REPOSITORY WORKS IN A COPY OF ITS OWN ON A BRANCH OF ITS
// OWN, and the person's checkout never moves: the program's work is committed
// on its branch, the copy is removed, and the last lines say how to bring it
// in.
func TestAShellRunInARepositoryWorksOnABranchOfItsOwn(t *testing.T) {
_, printed, _ := hostWithFolderChild(t)
repo := shellRepo(t)
Expand All @@ -160,11 +162,14 @@ func TestAShellRunInARepositoryWorksOnABranchOfItsOwn(t *testing.T) {
t.Fatalf("the shell run left with %d (%v):\n%s", code, err, printed)
}
out := printed.String()
branch := shellGit(t, repo, "branch", "--show-current")
if !strings.HasPrefix(branch, "task/make-a-file-") {
t.Fatalf("the checkout is on %q, want the run's own branch left checked out", branch)
branch := shellGit(t, repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/task/")
if !strings.HasPrefix(branch, "task/make-a-file-") || shellGit(t, repo, "branch", "--show-current") != "main" {
t.Fatalf("the run's branch is %q and the checkout on %q, want its own branch and the person's checkout left alone", branch, shellGit(t, repo, "branch", "--show-current"))
}
if !strings.Contains(out, fakeFolder+" · working in "+repo+", on its own branch "+branch) || !strings.Contains(out, " folder · git") {
if worktrees := shellGit(t, repo, "worktree", "list", "--porcelain"); strings.Count(worktrees, "worktree ") != 1 {
t.Fatalf("the run's copy was not removed:\n%s", worktrees)
}
if !strings.Contains(out, fakeFolder+" · working in "+repo+", in a copy of its own on its own branch "+branch) || !strings.Contains(out, " folder · git") {
t.Fatalf("the run did not say it works on its own branch with git:\n%s", out)
}
if tip := shellGit(t, repo, "rev-parse", "main"); tip != base {
Expand All @@ -179,45 +184,56 @@ func TestAShellRunInARepositoryWorksOnABranchOfItsOwn(t *testing.T) {
if status := shellGit(t, repo, "status", "--porcelain"); status != "" {
t.Fatalf("the run left changes that are not committed:\n%s", status)
}
if !strings.Contains(out, " its work is on the branch "+branch+" in "+repo+", 1 file, and that branch is checked out there; your branch main is as it was") {
if !strings.Contains(out, " its work is on the branch "+branch+" in "+repo+", 1 file; your checkout was not touched, and `git -C ") {
t.Fatalf("the last lines do not say where the work is:\n%s", out)
}
}

// A SHELL RUN ON A CHECKOUT WITH CHANGES THAT ARE NOT COMMITTED IS REFUSED
// before anything is started or spent, with the paths named.
func TestAShellRunIsRefusedACheckoutWithChangesThatAreNotCommitted(t *testing.T) {
calling, printed, said := hostWithFolderChild(t)
// A SHELL RUN BESIDE CHANGES THAT ARE NOT COMMITTED STARTS FROM THEM, and
// leaves them where they are: they are the first commit on its branch, its
// work comes after them, and the person's checkout is not touched.
func TestAShellRunStartsBesideChangesThatAreNotCommitted(t *testing.T) {
_, printed, _ := hostWithFolderChild(t)
repo := shellRepo(t)
if err := os.WriteFile(filepath.Join(repo, "draft.md"), []byte("mine\n"), 0o644); err != nil {
t.Fatal(err)
}
shellGit(t, repo, "add", "draft.md")
if err := os.WriteFile(filepath.Join(repo, "credentials.json"), []byte("local input\n"), 0o600); err != nil {
t.Fatal(err)
}
err := runCarried(fakeFolderProgram(), []string{"--dir", repo, "make a file"})
if code := exitCodeOf(err); code != int(exitCannotRun) {
t.Fatalf("left with %d, want the rung for a run that could not start", code)
if code := exitCodeOf(err); code != 0 {
t.Fatalf("the shell run left with %d (%v):\n%s", code, err, printed)
}
branch := shellGit(t, repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/task/")
if files := shellGit(t, repo, "ls-tree", "--name-only", branch); files != "draft.md\nmade.txt" {
t.Fatalf("the run's branch holds %q, want the person's draft and its work", files)
}
if want := "error: " + repo + " has changes that are not committed (draft.md); commit or stash them, then ask again"; !strings.Contains(said.String(), want) {
t.Fatalf("the refusal = %q, want %q", said.String(), want)
if first := shellGit(t, repo, "log", "--format=%s", "--reverse", "main.."+branch); !strings.HasPrefix(first, "Your uncommitted changes when ") {
t.Fatalf("the branch does not open on the person's changes:\n%s", first)
}
if len(calling.seen()) != 0 || printed.String() != "" {
t.Fatalf("a refused run did something: %d calls, printed %q", len(calling.seen()), printed.String())
if status := shellGit(t, repo, "status", "--porcelain"); status != "A draft.md\n?? credentials.json" || shellGit(t, repo, "branch", "--show-current") != "main" {
t.Fatalf("the person's checkout was touched: %q", status)
}
if branch := shellGit(t, repo, "branch", "--show-current"); branch != "main" {
t.Fatalf("a refused checkout was switched to %q", branch)
if !strings.Contains(printed.String(), "any it changes are committed as its work, and the rest stay off its branch") {
t.Fatalf("the shell receipt did not distinguish the untracked input:\n%s", printed)
}
}

// A SHELL RUN'S CHILD IS TOLD WHAT CODEAF DECIDED ABOUT ITS FOLDER: the
// program's own flags for a folder without git, and the repository's root in
// place of the person's --dir when that named a folder inside it.
// program's own flags for a folder without git, its copy in place of the
// person's --dir in a repository, and — ahead of the brief — where that copy
// is.
func TestAShellRunsChildLineCarriesItsFolder(t *testing.T) {
program := fakeFolderProgram()
inv, err := delegate.Parse(program, []string{"--dir", "/r/repo/sub", "fix", "it"}, &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
child := carriedInFolder(carriedChildLine(inv), inv, &session.ProgramFolder{Dir: "/r/repo", Branch: "task/fix-it-abc123"})
if got, want := strings.Join(child, " "), fakeFolder+" --json --dir /r/repo fix it"; got != want {
copied := &session.ProgramFolder{Dir: "/tmp/copy", Repo: "/r/repo", Branch: "task/fix-it-abc123"}
child := carriedInFolder(carriedChildLine(inv), inv, copied)
if got, want := strings.Join(child, " "), fakeFolder+" --json --dir /tmp/copy "+copied.BriefNote()+"\n\n fix it"; got != want {
t.Fatalf("the child line = %q, want %q", got, want)
}
plain, err := delegate.Parse(program, []string{"--dir", "/r/plain", "fix", "it"}, &bytes.Buffer{})
Expand Down Expand Up @@ -254,24 +270,24 @@ func newestRecordOf(t *testing.T, name string) string {
// wait is up to seventy seconds, a second ctrl-c during it leaves at once,
// and the folder used to be finished only after it: the repository was left
// on the program's branch with its work uncommitted and nothing said. By the
// time the model API starts closing, the work is committed and the run's
// record says when its program ended.
// time the model API starts closing, the work is committed on its branch and
// the copy is gone, so the branch is released before the wait.
func TestAShellRunFinishesItsFolderBeforeWaitingForPrices(t *testing.T) {
_, printed, _ := hostWithFolderChild(t)
repo := shellRepo(t)
var atClose struct{ status, files string }
previous := carriedAPIClose
carriedAPIClose = func(api *modelapi.Server) error {
atClose.status = shellGit(t, repo, "status", "--porcelain")
atClose.files = shellGit(t, repo, "ls-tree", "--name-only", "HEAD")
atClose.status = shellGit(t, repo, "worktree", "list", "--porcelain")
atClose.files = shellGit(t, repo, "ls-tree", "--name-only", shellGit(t, repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/task/"))
return previous(api)
}
t.Cleanup(func() { carriedAPIClose = previous })
err := runCarried(fakeFolderProgram(), []string{"--dir", repo, "make a file"})
if code := exitCodeOf(err); code != 0 {
t.Fatalf("the shell run left with %d (%v):\n%s", code, err, printed)
}
if atClose.status != "" || atClose.files != "made.txt" {
t.Fatalf("when the API began to close the folder held %q uncommitted and %q committed, want its work committed", atClose.status, atClose.files)
if strings.Count(atClose.status, "worktree ") != 1 || atClose.files != "made.txt" {
t.Fatalf("when the API began to close, the worktrees were %q and the branch held %q, want the copy gone and the work committed", atClose.status, atClose.files)
}
}
9 changes: 8 additions & 1 deletion cmd/codeaf/carried_fresh_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ func testSeniorDevExplicitShellModel(t *testing.T, tc shellModelCase) {
if code := exitCodeOf(err); code != 0 || calls.Load() == 0 {
t.Fatalf("fresh explicit shell run exited %d after %d chat calls: %s", code, calls.Load(), output.String())
}
if content, err := os.ReadFile(workspace + "/feature.txt"); err != nil || string(content) != "implemented by stub\n" {
read := func() (string, error) {
content, err := os.ReadFile(workspace + "/feature.txt")
return string(content), err
}
if !tc.plain {
read = func() (string, error) { return onTheTaskBranch(workspace, "feature.txt") }
}
if content, err := read(); err != nil || content != "implemented by stub\n" {
t.Fatalf("the shell did not make the feature: %q, %v", content, err)
}
if tc.plain {
Expand Down
Loading
Loading