Skip to content

Commit 516ebea

Browse files
authored
[Experimental] Auto-terminate agent when queue action is written (#14)
1 parent b2edfa0 commit 516ebea

File tree

4 files changed

+72
-24
lines changed

4 files changed

+72
-24
lines changed

.claude/commands/go-reviewer.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,18 @@ List each issue with:
5454
- **What's wrong** and **why it matters** - be specific
5555
- **Suggested fix** if you have one
5656

57+
Before finalizing your issues list, re-read each `suggestion` and `concern` and ask yourself: "Would a pragmatic senior engineer consider this overengineering for the current stage and scope of the project?" If the answer is yes - if the suggestion adds complexity, abstraction, or defensive code that isn't justified by a concrete present-day problem - drop it. Only keep suggestions where the cost of NOT doing it is real and near-term.
58+
5759
### Unification opportunities
5860
If you found duplicated logic or missed reuse opportunities, list them here.
5961

62+
### Merge confidence
63+
Rate 1-5 with a one-line justification:
64+
65+
- **5 - Ship it** - No issues found, or only trivial suggestions. Merge without hesitation.
66+
- **4 - Looks good** - Minor suggestions that are nice-to-have but not worth blocking on. Merge, optionally address in a follow-up.
67+
- **3 - Probably fine** - Has concerns that deserve a second look. Author should review the feedback and make a judgement call - could go either way.
68+
- **2 - Needs work** - Has issues that should be fixed before merging. Nothing catastrophic, but the code isn't ready as-is.
69+
- **1 - Do not merge** - Has bugs, regressions, or fundamental design problems that will cause real damage if shipped.
70+
6071
If the code looks good, say so. Don't manufacture issues to seem thorough.

cmd/root.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,8 @@ func resolveNextAction(root string, next *mob.QueuedAction) (workdir, agent stri
769769
if m == nil {
770770
return "", "", false, fmt.Errorf("mob '%s' not found", name)
771771
}
772-
fmt.Fprintf(os.Stderr, "\n \033[33m!\033[0m This will permanently delete mob '%s'. Uncommitted/unpushed changes will be lost.\n", m.Name)
772+
fmt.Fprintln(os.Stderr)
773+
fmt.Fprintf(os.Stderr, " \033[33m!\033[0m This will permanently delete mob '%s'. Uncommitted/unpushed changes will be lost.\n", m.Name)
773774
fmt.Fprint(os.Stderr, " Continue? [y/N]: ")
774775
var input string
775776
fmt.Scanln(&input)
@@ -923,7 +924,7 @@ func runAgent(root, agent, workdir string, resume bool) error {
923924
}
924925

925926
if resume {
926-
err := spawnAgent(binPath, resumeArgs, workdir)
927+
err := spawnAgent(root, binPath, resumeArgs, workdir)
927928
if err == nil {
928929
return nil
929930
}
@@ -936,7 +937,7 @@ func runAgent(root, agent, workdir string, resume bool) error {
936937
mobStatus("No previous session found, starting new session")
937938
}
938939

939-
return spawnAgent(binPath, newArgs, workdir)
940+
return spawnAgent(root, binPath, newArgs, workdir)
940941
}
941942

942943
func cmdInjectArgs(args []string) error {
@@ -996,18 +997,23 @@ func agentArgs(agent, repoRoot string) (binPath string, resumeArgs, newArgs []st
996997
return
997998
}
998999

999-
func spawnAgent(binPath string, args []string, workdir string) error {
1000+
func spawnAgent(root, binPath string, args []string, workdir string) error {
10001001
cmd := exec.Command(binPath, args...)
10011002
cmd.Dir = workdir
10021003
cmd.Stdin = os.Stdin
10031004
cmd.Stdout = os.Stdout
10041005
cmd.Stderr = os.Stderr
10051006
cmd.Env = append(os.Environ(), "CODEMOB_MOB="+filepath.Base(workdir))
10061007

1007-
// Forward signals to child, clean up goroutine on exit
1008+
if err := cmd.Start(); err != nil {
1009+
return err
1010+
}
1011+
1012+
done := make(chan struct{})
1013+
1014+
// Forward signals to child
10081015
sigCh := make(chan os.Signal, 1)
10091016
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
1010-
done := make(chan struct{})
10111017
go func() {
10121018
select {
10131019
case sig := <-sigCh:
@@ -1018,7 +1024,30 @@ func spawnAgent(binPath string, args []string, workdir string) error {
10181024
}
10191025
}()
10201026

1021-
err := cmd.Run()
1027+
// Watch for queue.json - auto-terminate agent when a queued action appears
1028+
if root != "" && filepath.IsAbs(root) {
1029+
queuePath := mob.QueueFilePath(root)
1030+
go func() {
1031+
ticker := time.NewTicker(500 * time.Millisecond)
1032+
defer ticker.Stop()
1033+
for {
1034+
select {
1035+
case <-ticker.C:
1036+
if _, err := os.Stat(queuePath); err == nil {
1037+
mobStatus("Stopping agent...")
1038+
if cmd.Process != nil {
1039+
cmd.Process.Signal(syscall.SIGTERM)
1040+
}
1041+
return
1042+
}
1043+
case <-done:
1044+
return
1045+
}
1046+
}
1047+
}()
1048+
}
1049+
1050+
err := cmd.Wait()
10221051
signal.Stop(sigCh)
10231052
close(done)
10241053
return err

internal/mob/init.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ const triggerGuard = "IMPORTANT: Only invoke this command when the user explicit
2121
"\"mob\" or \"codemob\". Generic requests like \"list\", \"create\", " +
2222
"\"remove\", or \"switch\" without mentioning mob/codemob should NOT trigger this.\n\n"
2323

24+
const confirmationGuardLaunch = `IMPORTANT: Before running the codemob queue command, you MUST get explicit confirmation from the user. Tell them: "This will end our current conversation. codemob will automatically close this session and launch the new one. Are you sure?"
25+
26+
Only run the queue command after the user confirms. If they decline, cancel the operation.
27+
`
28+
29+
const confirmationGuardExit = `IMPORTANT: Before running the codemob queue command, you MUST get explicit confirmation from the user. Tell them: "This will end our current conversation and close this session. Are you sure?"
30+
31+
Only run the queue command after the user confirms. If they decline, cancel the operation.
32+
`
33+
2434
var slashCommandDefs = map[string]commandDef{
2535
"list": {
2636
Description: "List all codemob workspaces and their status",
@@ -29,7 +39,7 @@ var slashCommandDefs = map[string]commandDef{
2939
},
3040
"new": {
3141
Description: "Create a new codemob workspace",
32-
Body: triggerGuard + `Ask the user if they want to provide a name or have one auto-generated.
42+
Body: triggerGuard + confirmationGuardLaunch + `Ask the user if they want to provide a name or have one auto-generated.
3343
3444
If they provide a name, validate it against these rules before running the command:
3545
- Only letters (a-z, A-Z), numbers, and hyphens allowed (no spaces or special characters)
@@ -44,32 +54,26 @@ If the name is valid, run: ` + "`codemob queue new <name>`" + ` (replace ` + "`<
4454
If they want auto-generated, run: ` + "`codemob queue new`" + ` (no name argument — codemob generates one).
4555
4656
Do NOT generate a name yourself — codemob handles name generation.
47-
48-
Then tell the user: "New mob queued. Exit this session (Ctrl+C) and codemob will automatically create and launch the new mob."
4957
`,
5058
},
5159
"switch": {
5260
Description: "Switch to a different codemob workspace",
53-
Body: triggerGuard + `Run ` + "`codemob list-others`" + ` using the Bash tool.
61+
Body: triggerGuard + confirmationGuardLaunch + `Run ` + "`codemob list-others`" + ` using the Bash tool.
5462
5563
If the output says "No mobs", tell the user there are no other mobs to switch to and suggest using /mob-new or /codemob-new to create one.
5664
5765
Otherwise, display the results and ask the user which mob they want to switch to.
5866
5967
Once they pick one, run ` + "`codemob queue switch <name>`" + ` using the Bash tool (replace ` + "`<name>`" + ` with the chosen mob name).
60-
61-
Then tell the user: "Switch queued. Exit this session (Ctrl+C) and codemob will automatically launch the new mob."
6268
`,
6369
},
6470
"change-agent": {
6571
Description: "Switch the current mob to a different AI agent",
66-
Body: triggerGuard + `codemob supports claude and codex out of the box.
72+
Body: triggerGuard + confirmationGuardLaunch + `codemob supports claude and codex out of the box.
6773
6874
Determine the current agent by checking which tool you are (claude or codex). Offer the OTHER agent — do not suggest the one already running.
6975
70-
Once the user confirms, run ` + "`codemob queue change-agent <agent>`" + ` using the Bash tool (replace ` + "`<agent>`" + ` with the chosen agent name).
71-
72-
Then tell the user: "Agent switch queued. Exit this session (Ctrl+C) and codemob will relaunch with the new agent."
76+
Once the user confirms which agent they want, run ` + "`codemob queue change-agent <agent>`" + ` using the Bash tool (replace ` + "`<agent>`" + ` with the chosen agent name).
7377
`,
7478
},
7579
"remove": {
@@ -78,22 +82,23 @@ Then tell the user: "Agent switch queued. Exit this session (Ctrl+C) and codemob
7882
7983
Ask the user which mob they want to remove.
8084
81-
If they choose a DIFFERENT mob (not the one marked with ◀), run ` + "`codemob remove <name>`" + ` directly.
85+
If they choose a DIFFERENT mob (not the one marked with ◀), run ` + "`codemob remove <name>`" + ` directly. No session confirmation needed since the current session stays alive.
86+
87+
If they choose the CURRENT mob (marked with ◀):
8288
83-
If they choose the CURRENT mob (marked with ◀), run this exact command:
89+
` + confirmationGuardExit + `
90+
Run this exact command:
8491
8592
` + "```" + `
8693
codemob queue remove "$CODEMOB_MOB"
8794
` + "```" + `
8895
8996
$CODEMOB_MOB is already set in your environment. There is no need to echo it - the command above will resolve it automatically.
90-
91-
Then tell the user: "Removal queued. Exit this session (Ctrl+C) and codemob will remove the mob."
9297
`,
9398
},
9499
"drop": {
95100
Description: "Remove the current codemob workspace and exit",
96-
Body: triggerGuard + `Run this exact command using the Bash tool:
101+
Body: triggerGuard + confirmationGuardExit + `Run this exact command using the Bash tool:
97102
98103
` + "```" + `
99104
codemob queue remove "$CODEMOB_MOB"
@@ -102,8 +107,6 @@ codemob queue remove "$CODEMOB_MOB"
102107
$CODEMOB_MOB is already set in your environment. There is no need to echo it - the command above will resolve it automatically.
103108
104109
If the command fails, tell the user: "This command can only be used from within a codemob workspace." and stop.
105-
106-
Otherwise, tell the user: "Mob queued for removal. Exit this session (Ctrl+C) and codemob will remove it."
107110
`,
108111
},
109112
}

internal/mob/next.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ func ReadQueuedAction(repoRoot string) (*QueuedAction, error) {
5252
return &action, nil
5353
}
5454

55+
// QueueFilePath returns the absolute path to the queue file.
56+
func QueueFilePath(repoRoot string) string {
57+
return filepath.Join(repoRoot, queueFile)
58+
}
59+
5560
// ClearQueue removes the queued action file.
5661
func ClearQueue(repoRoot string) {
5762
os.Remove(filepath.Join(repoRoot, queueFile))

0 commit comments

Comments
 (0)