Compare commits
9 Commits
v0.5.0
...
johno/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e3eb094c5 | ||
|
|
6be7fdd76c | ||
|
|
f0014bb21a | ||
| c9f844b477 | |||
| 2a639ff999 | |||
| eed8941126 | |||
| 9f9b2376ea | |||
| 7fd073810d | |||
|
|
b158ff27c2 |
@@ -8,6 +8,11 @@
|
|||||||
# polecat/* - Polecat working branches (Refinery merges these)
|
# polecat/* - Polecat working branches (Refinery merges these)
|
||||||
|
|
||||||
while read local_ref local_sha remote_ref remote_sha; do
|
while read local_ref local_sha remote_ref remote_sha; do
|
||||||
|
# Skip tags - they're allowed for releases
|
||||||
|
if [[ "$remote_ref" == refs/tags/* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
branch="${remote_ref#refs/heads/}"
|
branch="${remote_ref#refs/heads/}"
|
||||||
|
|
||||||
case "$branch" in
|
case "$branch" in
|
||||||
|
|||||||
51
.github/workflows/block-internal-prs.yml
vendored
51
.github/workflows/block-internal-prs.yml
vendored
@@ -1,51 +0,0 @@
|
|||||||
name: Block Internal PRs
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
block-internal-prs:
|
|
||||||
name: Block Internal PRs
|
|
||||||
# Only run if PR is from the same repo (not a fork)
|
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Close PR and comment
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const prNumber = context.issue.number;
|
|
||||||
const branch = context.payload.pull_request.head.ref;
|
|
||||||
|
|
||||||
const body = [
|
|
||||||
'**Internal PRs are not allowed.**',
|
|
||||||
'',
|
|
||||||
'Gas Town agents push directly to main. PRs are for external contributors only.',
|
|
||||||
'',
|
|
||||||
'To land your changes:',
|
|
||||||
'```bash',
|
|
||||||
'git checkout main',
|
|
||||||
'git merge ' + branch,
|
|
||||||
'git push origin main',
|
|
||||||
'git push origin --delete ' + branch,
|
|
||||||
'```',
|
|
||||||
'',
|
|
||||||
'See CLAUDE.md: "Crew workers push directly to main. No feature branches. NEVER create PRs."'
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
state: 'closed'
|
|
||||||
});
|
|
||||||
|
|
||||||
core.setFailed('Internal PR blocked. Push directly to main instead.');
|
|
||||||
@@ -182,6 +182,22 @@ Examples:
|
|||||||
RunE: runDogDispatch,
|
RunE: runDogDispatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dogDoneCmd = &cobra.Command{
|
||||||
|
Use: "done [name]",
|
||||||
|
Short: "Mark a dog as idle (work complete)",
|
||||||
|
Long: `Mark a dog as idle after completing its work.
|
||||||
|
|
||||||
|
Dogs call this command after finishing plugin execution to reset their state
|
||||||
|
to idle, allowing them to receive new work dispatches.
|
||||||
|
|
||||||
|
If no name is provided, attempts to detect the current dog from BD_ACTOR.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt dog done alpha # Explicit dog name
|
||||||
|
gt dog done # Auto-detect from BD_ACTOR (e.g., "deacon/dogs/alpha")`,
|
||||||
|
RunE: runDogDone,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// List flags
|
// List flags
|
||||||
dogListCmd.Flags().BoolVar(&dogListJSON, "json", false, "Output as JSON")
|
dogListCmd.Flags().BoolVar(&dogListJSON, "json", false, "Output as JSON")
|
||||||
@@ -212,6 +228,7 @@ func init() {
|
|||||||
dogCmd.AddCommand(dogCallCmd)
|
dogCmd.AddCommand(dogCallCmd)
|
||||||
dogCmd.AddCommand(dogStatusCmd)
|
dogCmd.AddCommand(dogStatusCmd)
|
||||||
dogCmd.AddCommand(dogDispatchCmd)
|
dogCmd.AddCommand(dogDispatchCmd)
|
||||||
|
dogCmd.AddCommand(dogDoneCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(dogCmd)
|
rootCmd.AddCommand(dogCmd)
|
||||||
}
|
}
|
||||||
@@ -500,6 +517,34 @@ func runDogStatus(cmd *cobra.Command, args []string) error {
|
|||||||
return showPackStatus(mgr)
|
return showPackStatus(mgr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runDogDone(cmd *cobra.Command, args []string) error {
|
||||||
|
mgr, err := getDogManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if len(args) > 0 {
|
||||||
|
name = args[0]
|
||||||
|
} else {
|
||||||
|
// Try to detect from BD_ACTOR (e.g., "deacon/dogs/alpha")
|
||||||
|
actor := os.Getenv("BD_ACTOR")
|
||||||
|
if actor != "" && strings.HasPrefix(actor, "deacon/dogs/") {
|
||||||
|
name = strings.TrimPrefix(actor, "deacon/dogs/")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("no dog name provided and could not detect from BD_ACTOR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.ClearWork(name); err != nil {
|
||||||
|
return fmt.Errorf("marking dog %s as done: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ %s marked as idle (ready for new work)\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func showDogStatus(mgr *dog.Manager, name string) error {
|
func showDogStatus(mgr *dog.Manager, name string) error {
|
||||||
d, err := mgr.Get(name)
|
d, err := mgr.Get(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -791,6 +836,35 @@ func runDogDispatch(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("sending plugin mail to dog: %w", err)
|
return fmt.Errorf("sending plugin mail to dog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn a session for the dog to execute the work.
|
||||||
|
// Without a session, the dog's mail inbox is never checked.
|
||||||
|
// See: https://github.com/steveyegge/gastown/issues/XXX (dog dispatch doesn't execute)
|
||||||
|
t := tmux.NewTmux()
|
||||||
|
townName, err := workspace.GetTownName(townRoot)
|
||||||
|
if err != nil {
|
||||||
|
townName = "gt" // fallback
|
||||||
|
}
|
||||||
|
dogSessionName := fmt.Sprintf("gt-%s-deacon-%s", townName, targetDog.Name)
|
||||||
|
|
||||||
|
// Kill any stale session first
|
||||||
|
if has, _ := t.HasSession(dogSessionName); has {
|
||||||
|
_ = t.KillSessionWithProcesses(dogSessionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build startup command with initial prompt to check mail and execute plugin
|
||||||
|
// Use BuildDogStartupCommand to properly set BD_ACTOR=deacon/dogs/<name> in the startup command
|
||||||
|
initialPrompt := fmt.Sprintf("I am dog %s. Check my mail inbox with 'gt mail inbox' and execute the plugin instructions I received.", targetDog.Name)
|
||||||
|
startCmd := config.BuildDogStartupCommand(targetDog.Name, townRoot, targetDog.Path, initialPrompt)
|
||||||
|
|
||||||
|
// Create session from dog's directory
|
||||||
|
if err := t.NewSessionWithCommand(dogSessionName, targetDog.Path, startCmd); err != nil {
|
||||||
|
if !dogDispatchJSON {
|
||||||
|
fmt.Printf(" Warning: could not spawn dog session: %v\n", err)
|
||||||
|
}
|
||||||
|
// Non-fatal: mail was sent, dog is marked as working, but no session to execute
|
||||||
|
// The deacon or human can manually start the session later
|
||||||
|
}
|
||||||
|
|
||||||
// Success - output result
|
// Success - output result
|
||||||
if dogDispatchJSON {
|
if dogDispatchJSON {
|
||||||
return json.NewEncoder(os.Stdout).Encode(result)
|
return json.NewEncoder(os.Stdout).Encode(result)
|
||||||
|
|||||||
@@ -456,7 +456,7 @@ notifyWitness:
|
|||||||
|
|
||||||
// Notify dispatcher if work was dispatched by another agent
|
// Notify dispatcher if work was dispatched by another agent
|
||||||
if issueID != "" {
|
if issueID != "" {
|
||||||
if dispatcher := getDispatcherFromBead(cwd, issueID); dispatcher != "" && dispatcher != sender {
|
if dispatcher := getDispatcherFromBead(townRoot, cwd, issueID); dispatcher != "" && dispatcher != sender {
|
||||||
dispatcherNotification := &mail.Message{
|
dispatcherNotification := &mail.Message{
|
||||||
To: dispatcher,
|
To: dispatcher,
|
||||||
From: sender,
|
From: sender,
|
||||||
@@ -645,7 +645,7 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
|
|||||||
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
|
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
|
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
|
||||||
}
|
}
|
||||||
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZFC #10: Self-report cleanup status
|
// ZFC #10: Self-report cleanup status
|
||||||
@@ -678,12 +678,19 @@ func getIssueFromAgentHook(bd *beads.Beads, agentBeadID string) string {
|
|||||||
|
|
||||||
// getDispatcherFromBead retrieves the dispatcher agent ID from the bead's attachment fields.
|
// getDispatcherFromBead retrieves the dispatcher agent ID from the bead's attachment fields.
|
||||||
// Returns empty string if no dispatcher is recorded.
|
// Returns empty string if no dispatcher is recorded.
|
||||||
func getDispatcherFromBead(cwd, issueID string) string {
|
//
|
||||||
|
// BUG FIX (sc-g7bl3): Use townRoot and ResolveHookDir for bead lookup instead of
|
||||||
|
// ResolveBeadsDir(cwd). When the polecat's worktree is deleted before gt done finishes,
|
||||||
|
// ResolveBeadsDir(cwd) fails because the redirect file is gone. ResolveHookDir uses
|
||||||
|
// prefix-based routing via routes.jsonl which works regardless of worktree state.
|
||||||
|
func getDispatcherFromBead(townRoot, cwd, issueID string) string {
|
||||||
if issueID == "" {
|
if issueID == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
bd := beads.New(beads.ResolveBeadsDir(cwd))
|
// Use ResolveHookDir for resilient bead lookup - works even if worktree is deleted
|
||||||
|
beadsDir := beads.ResolveHookDir(townRoot, issueID, cwd)
|
||||||
|
bd := beads.New(beadsDir)
|
||||||
issue, err := bd.Show(issueID)
|
issue, err := bd.Show(issueID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ func detectSenderFromRole(role string) string {
|
|||||||
return fmt.Sprintf("%s/refinery", rig)
|
return fmt.Sprintf("%s/refinery", rig)
|
||||||
}
|
}
|
||||||
return detectSenderFromCwd()
|
return detectSenderFromCwd()
|
||||||
|
case "dog":
|
||||||
|
// Dogs use BD_ACTOR directly (set by BuildDogStartupCommand)
|
||||||
|
actor := os.Getenv("BD_ACTOR")
|
||||||
|
if actor != "" {
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
return detectSenderFromCwd()
|
||||||
default:
|
default:
|
||||||
// Unknown role, try cwd detection
|
// Unknown role, try cwd detection
|
||||||
return detectSenderFromCwd()
|
return detectSenderFromCwd()
|
||||||
|
|||||||
@@ -49,36 +49,48 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
|
|||||||
case "mayor":
|
case "mayor":
|
||||||
env["BD_ACTOR"] = "mayor"
|
env["BD_ACTOR"] = "mayor"
|
||||||
env["GIT_AUTHOR_NAME"] = "mayor"
|
env["GIT_AUTHOR_NAME"] = "mayor"
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = "mayor@gastown.local"
|
||||||
|
|
||||||
case "deacon":
|
case "deacon":
|
||||||
env["BD_ACTOR"] = "deacon"
|
env["BD_ACTOR"] = "deacon"
|
||||||
env["GIT_AUTHOR_NAME"] = "deacon"
|
env["GIT_AUTHOR_NAME"] = "deacon"
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = "deacon@gastown.local"
|
||||||
|
|
||||||
case "boot":
|
case "boot":
|
||||||
env["BD_ACTOR"] = "deacon-boot"
|
env["BD_ACTOR"] = "deacon-boot"
|
||||||
env["GIT_AUTHOR_NAME"] = "boot"
|
env["GIT_AUTHOR_NAME"] = "boot"
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = "boot@gastown.local"
|
||||||
|
|
||||||
|
case "dog":
|
||||||
|
env["BD_ACTOR"] = fmt.Sprintf("deacon/dogs/%s", cfg.AgentName)
|
||||||
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("dog-%s", cfg.AgentName)
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("dog-%s@gastown.local", cfg.AgentName)
|
||||||
|
|
||||||
case "witness":
|
case "witness":
|
||||||
env["GT_RIG"] = cfg.Rig
|
env["GT_RIG"] = cfg.Rig
|
||||||
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
env["BD_ACTOR"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
||||||
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/witness", cfg.Rig)
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-witness@gastown.local", cfg.Rig)
|
||||||
|
|
||||||
case "refinery":
|
case "refinery":
|
||||||
env["GT_RIG"] = cfg.Rig
|
env["GT_RIG"] = cfg.Rig
|
||||||
env["BD_ACTOR"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
env["BD_ACTOR"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
||||||
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
env["GIT_AUTHOR_NAME"] = fmt.Sprintf("%s/refinery", cfg.Rig)
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-refinery@gastown.local", cfg.Rig)
|
||||||
|
|
||||||
case "polecat":
|
case "polecat":
|
||||||
env["GT_RIG"] = cfg.Rig
|
env["GT_RIG"] = cfg.Rig
|
||||||
env["GT_POLECAT"] = cfg.AgentName
|
env["GT_POLECAT"] = cfg.AgentName
|
||||||
env["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", cfg.Rig, cfg.AgentName)
|
env["BD_ACTOR"] = fmt.Sprintf("%s/polecats/%s", cfg.Rig, cfg.AgentName)
|
||||||
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-polecat-%s@gastown.local", cfg.Rig, cfg.AgentName)
|
||||||
|
|
||||||
case "crew":
|
case "crew":
|
||||||
env["GT_RIG"] = cfg.Rig
|
env["GT_RIG"] = cfg.Rig
|
||||||
env["GT_CREW"] = cfg.AgentName
|
env["GT_CREW"] = cfg.AgentName
|
||||||
env["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", cfg.Rig, cfg.AgentName)
|
env["BD_ACTOR"] = fmt.Sprintf("%s/crew/%s", cfg.Rig, cfg.AgentName)
|
||||||
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
env["GIT_AUTHOR_NAME"] = cfg.AgentName
|
||||||
|
env["GIT_AUTHOR_EMAIL"] = fmt.Sprintf("%s-crew-%s@gastown.local", cfg.Rig, cfg.AgentName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set GT_ROOT if provided
|
// Only set GT_ROOT if provided
|
||||||
@@ -121,7 +133,7 @@ func AgentEnvSimple(role, rig, agentName string) map[string]string {
|
|||||||
|
|
||||||
// ShellQuote returns a shell-safe quoted string.
|
// ShellQuote returns a shell-safe quoted string.
|
||||||
// Values containing special characters are wrapped in single quotes.
|
// Values containing special characters are wrapped in single quotes.
|
||||||
// Single quotes within the value are escaped using the '\'' idiom.
|
// Single quotes within the value are escaped using the '\” idiom.
|
||||||
func ShellQuote(s string) string {
|
func ShellQuote(s string) string {
|
||||||
// Check if quoting is needed (contains shell special chars)
|
// Check if quoting is needed (contains shell special chars)
|
||||||
needsQuoting := false
|
needsQuoting := false
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func TestAgentEnv_Mayor(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_ROLE", "mayor")
|
assertEnv(t, env, "GT_ROLE", "mayor")
|
||||||
assertEnv(t, env, "BD_ACTOR", "mayor")
|
assertEnv(t, env, "BD_ACTOR", "mayor")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "mayor")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "mayor")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "mayor@gastown.local")
|
||||||
assertEnv(t, env, "GT_ROOT", "/town")
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
assertNotSet(t, env, "GT_RIG")
|
assertNotSet(t, env, "GT_RIG")
|
||||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
@@ -31,6 +32,7 @@ func TestAgentEnv_Witness(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_RIG", "myrig")
|
assertEnv(t, env, "GT_RIG", "myrig")
|
||||||
assertEnv(t, env, "BD_ACTOR", "myrig/witness")
|
assertEnv(t, env, "BD_ACTOR", "myrig/witness")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/witness")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/witness")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-witness@gastown.local")
|
||||||
assertEnv(t, env, "GT_ROOT", "/town")
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ func TestAgentEnv_Polecat(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_POLECAT", "Toast")
|
assertEnv(t, env, "GT_POLECAT", "Toast")
|
||||||
assertEnv(t, env, "BD_ACTOR", "myrig/polecats/Toast")
|
assertEnv(t, env, "BD_ACTOR", "myrig/polecats/Toast")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "Toast")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "Toast")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-polecat-Toast@gastown.local")
|
||||||
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/Toast")
|
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/Toast")
|
||||||
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
||||||
}
|
}
|
||||||
@@ -68,6 +71,7 @@ func TestAgentEnv_Crew(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_CREW", "emma")
|
assertEnv(t, env, "GT_CREW", "emma")
|
||||||
assertEnv(t, env, "BD_ACTOR", "myrig/crew/emma")
|
assertEnv(t, env, "BD_ACTOR", "myrig/crew/emma")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "emma")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "emma")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-crew-emma@gastown.local")
|
||||||
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/emma")
|
assertEnv(t, env, "BEADS_AGENT_NAME", "myrig/emma")
|
||||||
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,7 @@ func TestAgentEnv_Refinery(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_RIG", "myrig")
|
assertEnv(t, env, "GT_RIG", "myrig")
|
||||||
assertEnv(t, env, "BD_ACTOR", "myrig/refinery")
|
assertEnv(t, env, "BD_ACTOR", "myrig/refinery")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/refinery")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "myrig/refinery")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "myrig-refinery@gastown.local")
|
||||||
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
assertEnv(t, env, "BEADS_NO_DAEMON", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +103,7 @@ func TestAgentEnv_Deacon(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_ROLE", "deacon")
|
assertEnv(t, env, "GT_ROLE", "deacon")
|
||||||
assertEnv(t, env, "BD_ACTOR", "deacon")
|
assertEnv(t, env, "BD_ACTOR", "deacon")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "deacon")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "deacon")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "deacon@gastown.local")
|
||||||
assertEnv(t, env, "GT_ROOT", "/town")
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
assertNotSet(t, env, "GT_RIG")
|
assertNotSet(t, env, "GT_RIG")
|
||||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
@@ -113,6 +119,24 @@ func TestAgentEnv_Boot(t *testing.T) {
|
|||||||
assertEnv(t, env, "GT_ROLE", "boot")
|
assertEnv(t, env, "GT_ROLE", "boot")
|
||||||
assertEnv(t, env, "BD_ACTOR", "deacon-boot")
|
assertEnv(t, env, "BD_ACTOR", "deacon-boot")
|
||||||
assertEnv(t, env, "GIT_AUTHOR_NAME", "boot")
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "boot")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "boot@gastown.local")
|
||||||
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
|
assertNotSet(t, env, "GT_RIG")
|
||||||
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgentEnv_Dog(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
env := AgentEnv(AgentEnvConfig{
|
||||||
|
Role: "dog",
|
||||||
|
AgentName: "alpha",
|
||||||
|
TownRoot: "/town",
|
||||||
|
})
|
||||||
|
|
||||||
|
assertEnv(t, env, "GT_ROLE", "dog")
|
||||||
|
assertEnv(t, env, "BD_ACTOR", "deacon/dogs/alpha")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_NAME", "dog-alpha")
|
||||||
|
assertEnv(t, env, "GIT_AUTHOR_EMAIL", "dog-alpha@gastown.local")
|
||||||
assertEnv(t, env, "GT_ROOT", "/town")
|
assertEnv(t, env, "GT_ROOT", "/town")
|
||||||
assertNotSet(t, env, "GT_RIG")
|
assertNotSet(t, env, "GT_RIG")
|
||||||
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
assertNotSet(t, env, "BEADS_NO_DAEMON")
|
||||||
|
|||||||
@@ -1457,6 +1457,17 @@ func BuildPolecatStartupCommandWithAgentOverride(rigName, polecatName, rigPath,
|
|||||||
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
return BuildStartupCommandWithAgentOverride(envVars, rigPath, prompt, agentOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildDogStartupCommand builds the startup command for a deacon dog.
|
||||||
|
// Sets GT_ROLE, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||||
|
func BuildDogStartupCommand(dogName, townRoot, dogPath, prompt string) string {
|
||||||
|
envVars := AgentEnv(AgentEnvConfig{
|
||||||
|
Role: "dog",
|
||||||
|
AgentName: dogName,
|
||||||
|
TownRoot: townRoot,
|
||||||
|
})
|
||||||
|
return BuildStartupCommand(envVars, dogPath, prompt)
|
||||||
|
}
|
||||||
|
|
||||||
// BuildCrewStartupCommand builds the startup command for a crew member.
|
// BuildCrewStartupCommand builds the startup command for a crew member.
|
||||||
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
// Sets GT_ROLE, GT_RIG, GT_CREW, BD_ACTOR, GIT_AUTHOR_NAME, and GT_ROOT.
|
||||||
func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
|
func BuildCrewStartupCommand(rigName, crewName, rigPath, prompt string) string {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const (
|
|||||||
ShutdownNotifyDelay = 500 * time.Millisecond
|
ShutdownNotifyDelay = 500 * time.Millisecond
|
||||||
|
|
||||||
// ClaudeStartTimeout is how long to wait for Claude to start in a session.
|
// ClaudeStartTimeout is how long to wait for Claude to start in a session.
|
||||||
// Increased to 60s because Claude can take 30s+ on slower machines.
|
// Increased to 120s because Claude can take 60s+ on slower machines or under load.
|
||||||
ClaudeStartTimeout = 60 * time.Second
|
ClaudeStartTimeout = 120 * time.Second
|
||||||
|
|
||||||
// ShellReadyTimeout is how long to wait for shell prompt after command.
|
// ShellReadyTimeout is how long to wait for shell prompt after command.
|
||||||
ShellReadyTimeout = 5 * time.Second
|
ShellReadyTimeout = 5 * time.Second
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ drive shaft - if you stall, the whole town stalls.
|
|||||||
**Your startup behavior:**
|
**Your startup behavior:**
|
||||||
1. Check hook (`gt hook`)
|
1. Check hook (`gt hook`)
|
||||||
2. If work is hooked → EXECUTE (no announcement beyond one line, no waiting)
|
2. If work is hooked → EXECUTE (no announcement beyond one line, no waiting)
|
||||||
3. If hook empty → Check mail, then wait for user instructions
|
3. If hook empty → Check escalations (`gt escalate list`)
|
||||||
|
4. Handle any pending escalations (these are urgent items from other agents)
|
||||||
|
5. Check mail, then wait for user instructions
|
||||||
|
|
||||||
**Note:** "Hooked" means work assigned to you. This triggers autonomous mode even
|
**Note:** "Hooked" means work assigned to you. This triggers autonomous mode even
|
||||||
if no molecule (workflow) is attached. Don't confuse with "pinned" which is for
|
if no molecule (workflow) is attached. Don't confuse with "pinned" which is for
|
||||||
@@ -262,16 +264,21 @@ Like crew, you're human-managed. But the hook protocol still applies:
|
|||||||
gt hook # Shows hooked work (if any)
|
gt hook # Shows hooked work (if any)
|
||||||
|
|
||||||
# Step 2: Work hooked? → RUN IT
|
# Step 2: Work hooked? → RUN IT
|
||||||
# Hook empty? → Check mail for attached work
|
|
||||||
|
# Step 3: Hook empty? → Check escalations (mayor-specific)
|
||||||
|
gt escalate list # Shows pending escalations from other agents
|
||||||
|
# Handle any pending escalations - these are urgent items requiring your attention
|
||||||
|
|
||||||
|
# Step 4: Check mail for attached work
|
||||||
gt mail inbox
|
gt mail inbox
|
||||||
# If mail contains attached work, hook it:
|
# If mail contains attached work, hook it:
|
||||||
gt mol attach-from-mail <mail-id>
|
gt mol attach-from-mail <mail-id>
|
||||||
|
|
||||||
# Step 3: Still nothing? Wait for user instructions
|
# Step 5: Still nothing? Wait for user instructions
|
||||||
# You're the Mayor - the human directs your work
|
# You're the Mayor - the human directs your work
|
||||||
```
|
```
|
||||||
|
|
||||||
**Work hooked → Run it. Hook empty → Check mail. Nothing anywhere → Wait for user.**
|
**Work hooked → Run it. Hook empty → Check escalations → Check mail. Nothing anywhere → Wait for user.**
|
||||||
|
|
||||||
Your hooked work persists across sessions. Handoff mail (🤝 HANDOFF subject) provides context notes.
|
Your hooked work persists across sessions. Handoff mail (🤝 HANDOFF subject) provides context notes.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user