Compare commits
7 Commits
v0.4.0
...
87e2a6a634
| Author | SHA1 | Date | |
|---|---|---|---|
| 87e2a6a634 | |||
| e9d987d19a | |||
| ee1bc35f3b | |||
| e9a262bca8 | |||
|
|
b8eb936219 | ||
|
|
dcf7b81011 | ||
|
|
37f465bde5 |
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.');
|
|
||||||
@@ -258,6 +258,11 @@ func runSlingFormula(args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip nudge during tests to prevent agent self-interruption
|
||||||
|
if os.Getenv("GT_TEST_NO_NUDGE") != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var prompt string
|
var prompt string
|
||||||
if slingArgs != "" {
|
if slingArgs != "" {
|
||||||
prompt = fmt.Sprintf("Formula %s slung. Args: %s. Run `gt hook` to see your hook, then execute using these args.", formulaName, slingArgs)
|
prompt = fmt.Sprintf("Formula %s slung. Args: %s. Run `gt hook` to see your hook, then execute using these args.", formulaName, slingArgs)
|
||||||
|
|||||||
35
internal/cmd/tap.go
Normal file
35
internal/cmd/tap.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tapCmd = &cobra.Command{
|
||||||
|
Use: "tap",
|
||||||
|
Short: "Claude Code hook handlers",
|
||||||
|
Long: `Hook handlers for Claude Code PreToolUse and PostToolUse events.
|
||||||
|
|
||||||
|
These commands are called by Claude Code hooks to implement policies,
|
||||||
|
auditing, and input transformation. They tap into the tool execution
|
||||||
|
flow to guard, audit, inject, or check.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
guard - Block forbidden operations (PreToolUse, exit 2)
|
||||||
|
audit - Log/record tool executions (PostToolUse) [planned]
|
||||||
|
inject - Modify tool inputs (PreToolUse, updatedInput) [planned]
|
||||||
|
check - Validate after execution (PostToolUse) [planned]
|
||||||
|
|
||||||
|
Hook configuration in .claude/settings.json:
|
||||||
|
{
|
||||||
|
"PreToolUse": [{
|
||||||
|
"matcher": "Bash(gh pr create*)",
|
||||||
|
"hooks": [{"command": "gt tap guard pr-workflow"}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
See ~/gt/docs/HOOKS.md for full documentation.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(tapCmd)
|
||||||
|
}
|
||||||
116
internal/cmd/tap_guard.go
Normal file
116
internal/cmd/tap_guard.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tapGuardCmd = &cobra.Command{
|
||||||
|
Use: "guard",
|
||||||
|
Short: "Block forbidden operations (PreToolUse hook)",
|
||||||
|
Long: `Block forbidden operations via Claude Code PreToolUse hooks.
|
||||||
|
|
||||||
|
Guard commands exit with code 2 to BLOCK tool execution when a policy
|
||||||
|
is violated. They're called before the tool runs, preventing the
|
||||||
|
forbidden operation entirely.
|
||||||
|
|
||||||
|
Available guards:
|
||||||
|
pr-workflow - Block PR creation and feature branches
|
||||||
|
|
||||||
|
Example hook configuration:
|
||||||
|
{
|
||||||
|
"PreToolUse": [{
|
||||||
|
"matcher": "Bash(gh pr create*)",
|
||||||
|
"hooks": [{"command": "gt tap guard pr-workflow"}]
|
||||||
|
}]
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var tapGuardPRWorkflowCmd = &cobra.Command{
|
||||||
|
Use: "pr-workflow",
|
||||||
|
Short: "Block PR creation and feature branches",
|
||||||
|
Long: `Block PR workflow operations in Gas Town.
|
||||||
|
|
||||||
|
Gas Town workers push directly to main. PRs add friction that breaks
|
||||||
|
the autonomous execution model (GUPP principle).
|
||||||
|
|
||||||
|
This guard blocks:
|
||||||
|
- gh pr create
|
||||||
|
- git checkout -b (feature branches)
|
||||||
|
- git switch -c (feature branches)
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 - Operation allowed (not in Gas Town agent context)
|
||||||
|
2 - Operation BLOCKED (in agent context)
|
||||||
|
|
||||||
|
The guard only blocks when running as a Gas Town agent (crew, polecat,
|
||||||
|
witness, etc.). Humans running outside Gas Town can still use PRs.`,
|
||||||
|
RunE: runTapGuardPRWorkflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tapCmd.AddCommand(tapGuardCmd)
|
||||||
|
tapGuardCmd.AddCommand(tapGuardPRWorkflowCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTapGuardPRWorkflow(cmd *cobra.Command, args []string) error {
|
||||||
|
// Check if we're in a Gas Town agent context
|
||||||
|
if !isGasTownAgentContext() {
|
||||||
|
// Not in a Gas Town managed context - allow the operation
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in a Gas Town context - block PR operations
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "╔══════════════════════════════════════════════════════════════════╗")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ ❌ PR WORKFLOW BLOCKED ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "╠══════════════════════════════════════════════════════════════════╣")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ Gas Town workers push directly to main. PRs are forbidden. ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ Instead of: gh pr create / git checkout -b / git switch -c ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ Do this: git add . && git commit && git push origin main ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ Why? PRs add friction that breaks autonomous execution. ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "║ See: ~/gt/docs/PRIMING.md (GUPP principle) ║")
|
||||||
|
fmt.Fprintln(os.Stderr, "╚══════════════════════════════════════════════════════════════════╝")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
os.Exit(2) // Exit 2 = BLOCK in Claude Code hooks
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGasTownAgentContext returns true if we're running as a Gas Town managed agent.
|
||||||
|
func isGasTownAgentContext() bool {
|
||||||
|
// Check environment variables set by Gas Town session management
|
||||||
|
envVars := []string{
|
||||||
|
"GT_POLECAT",
|
||||||
|
"GT_CREW",
|
||||||
|
"GT_WITNESS",
|
||||||
|
"GT_REFINERY",
|
||||||
|
"GT_MAYOR",
|
||||||
|
"GT_DEACON",
|
||||||
|
}
|
||||||
|
for _, env := range envVars {
|
||||||
|
if os.Getenv(env) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if we're in a crew or polecat worktree by path
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
agentPaths := []string{"/crew/", "/polecats/"}
|
||||||
|
for _, path := range agentPaths {
|
||||||
|
if strings.Contains(cwd, path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -49,36 +49,43 @@ 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 "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
|
||||||
|
|||||||
@@ -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,7 @@ 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")
|
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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -241,16 +243,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