docs: update CHANGELOG for v0.2.4 release
Add 0.2.4 changelog entry covering: - Priming subsystem (PRIME.md, post-handoff detection, doctor checks) - gt prime --dry-run, --state, --explain flags - ZFC improvements (query tmux directly, remove PID detection) - Cross-level hook visibility fixes - Rig-level default formulas Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
47
CHANGELOG.md
47
CHANGELOG.md
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.4] - 2026-01-10
|
||||||
|
|
||||||
|
Priming subsystem overhaul and Zero File Coordination (ZFC) improvements.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
#### Priming Subsystem
|
||||||
|
- **PRIME.md provisioning** - Auto-provision PRIME.md at rig level so all workers inherit Gas Town context (GUPP, hooks, propulsion) (#hq-5z76w)
|
||||||
|
- **Post-handoff detection** - `gt prime` detects handoff marker and outputs "HANDOFF COMPLETE" warning to prevent handoff loop bug (#hq-ukjrr)
|
||||||
|
- **Priming health checks** - `gt doctor` validates priming subsystem: SessionStart hook, gt prime command, PRIME.md presence, CLAUDE.md size (#hq-5scnt)
|
||||||
|
- **`gt prime --dry-run`** - Preview priming without side effects
|
||||||
|
- **`gt prime --state`** - Output session state (normal, post-handoff, crash-recovery, autonomous)
|
||||||
|
- **`gt prime --explain`** - Add [EXPLAIN] tags for debugging priming decisions
|
||||||
|
|
||||||
|
#### Formula & Configuration
|
||||||
|
- **Rig-level default formulas** - Configure default formula at rig level (#297)
|
||||||
|
- **Witness --agent/--env overrides** - Override agent and environment variables for witness (#293, #294)
|
||||||
|
|
||||||
|
#### Developer Experience
|
||||||
|
- **UX system import** - Comprehensive UX system from beads (#311)
|
||||||
|
- **Explicit handoff instructions** - Clearer nudge message for handoff recipients
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
#### Zero File Coordination (ZFC)
|
||||||
|
- **Query tmux directly** - Remove marker TTL, query tmux for agent state
|
||||||
|
- **Remove PID-based detection** - Agent liveness from tmux, not PIDs
|
||||||
|
- **Agent-controlled thresholds** - Stuck detection moved to agent config
|
||||||
|
- **Remove pending.json tracking** - Eliminated anti-pattern
|
||||||
|
- **Derive state from files** - ZFC state from filesystem, not memory cache
|
||||||
|
- **Remove Go-side computation** - No stderr parsing violations
|
||||||
|
|
||||||
|
#### Hooks & Beads
|
||||||
|
- **Cross-level hook visibility** - Hooked beads visible to mayor/deacon (#aeb4c0d)
|
||||||
|
- **Warn on closed hooked bead** - Alert when hooked bead already closed (#2f50a59)
|
||||||
|
- **Correct agent bead ID format** - Fix bd create flags for agent beads (#c4fcdd8)
|
||||||
|
|
||||||
|
#### Formula
|
||||||
|
- **rigPath fallback** - Set rigPath when falling back to gastown default (#afb944f)
|
||||||
|
|
||||||
|
#### Doctor
|
||||||
|
- **Full AgentEnv for env-vars check** - Use complete environment for validation (#ce231a3)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Refactored beads/mail modules** - Split large files into focused modules for maintainability
|
||||||
|
|
||||||
## [0.2.3] - 2026-01-09
|
## [0.2.3] - 2026-01-09
|
||||||
|
|
||||||
Worker safety release - prevents accidental termination of active agents.
|
Worker safety release - prevents accidental termination of active agents.
|
||||||
|
|||||||
@@ -61,21 +61,6 @@ Role detection:
|
|||||||
|
|
||||||
This command is typically used in shell prompts or agent initialization.
|
This command is typically used in shell prompts or agent initialization.
|
||||||
|
|
||||||
FLAGS:
|
|
||||||
|
|
||||||
STATE MODE (--state):
|
|
||||||
Output detected role information as JSON and exit early.
|
|
||||||
Useful for scripting and programmatic role detection.
|
|
||||||
This flag is standalone - cannot be combined with other flags.
|
|
||||||
|
|
||||||
DRY-RUN MODE (--dry-run):
|
|
||||||
Show what context would be output without side effects.
|
|
||||||
Skips session ID persistence, lock acquisition, and event emission.
|
|
||||||
|
|
||||||
EXPLAIN MODE (--explain):
|
|
||||||
Provide verbose explanations for role detection decisions.
|
|
||||||
Shows why certain context is being included.
|
|
||||||
|
|
||||||
HOOK MODE (--hook):
|
HOOK MODE (--hook):
|
||||||
When called as an LLM runtime hook, use --hook to enable session ID handling.
|
When called as an LLM runtime hook, use --hook to enable session ID handling.
|
||||||
This reads session metadata from stdin and persists it for the session.
|
This reads session metadata from stdin and persists it for the session.
|
||||||
@@ -86,11 +71,7 @@ HOOK MODE (--hook):
|
|||||||
Claude Code sends JSON on stdin:
|
Claude Code sends JSON on stdin:
|
||||||
{"session_id": "uuid", "transcript_path": "/path", "source": "startup|resume"}
|
{"session_id": "uuid", "transcript_path": "/path", "source": "startup|resume"}
|
||||||
|
|
||||||
Other agents can set GT_SESSION_ID environment variable instead.
|
Other agents can set GT_SESSION_ID environment variable instead.`,
|
||||||
|
|
||||||
FLAG COMBINATIONS:
|
|
||||||
--state is mutually exclusive with all other flags.
|
|
||||||
--dry-run, --explain, and --hook can be combined.`,
|
|
||||||
RunE: runPrime,
|
RunE: runPrime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,11 +102,6 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("finding workspace: %w", err)
|
return fmt.Errorf("finding workspace: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mutually exclusive flags
|
|
||||||
if primeState && (primeHookMode || primeDryRun || primeExplain) {
|
|
||||||
return fmt.Errorf("--state cannot be combined with other flags (--hook, --dry-run, --explain)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// "Discover, Don't Track" principle:
|
// "Discover, Don't Track" principle:
|
||||||
// - If we're in a workspace, proceed - the workspace's existence IS the enable signal
|
// - If we're in a workspace, proceed - the workspace's existence IS the enable signal
|
||||||
// - If we're NOT in a workspace, check the global enabled state
|
// - If we're NOT in a workspace, check the global enabled state
|
||||||
@@ -147,12 +123,10 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
if cwd != townRoot {
|
if cwd != townRoot {
|
||||||
persistSessionID(cwd, sessionID)
|
persistSessionID(cwd, sessionID)
|
||||||
}
|
}
|
||||||
// Set environment for this process (affects event emission below)
|
|
||||||
_ = os.Setenv("GT_SESSION_ID", sessionID)
|
|
||||||
_ = os.Setenv("CLAUDE_SESSION_ID", sessionID) // Legacy compatibility
|
|
||||||
} else if primeExplain {
|
|
||||||
fmt.Println("[dry-run] Would persist session ID:", sessionID)
|
|
||||||
}
|
}
|
||||||
|
// Set environment for this process (affects event emission below)
|
||||||
|
_ = os.Setenv("GT_SESSION_ID", sessionID)
|
||||||
|
_ = os.Setenv("CLAUDE_SESSION_ID", sessionID) // Legacy compatibility
|
||||||
// Output session beacon
|
// Output session beacon
|
||||||
explain(true, "Session beacon: hook mode enabled, session ID from stdin")
|
explain(true, "Session beacon: hook mode enabled, session ID from stdin")
|
||||||
fmt.Printf("[session:%s]\n", sessionID)
|
fmt.Printf("[session:%s]\n", sessionID)
|
||||||
@@ -175,24 +149,6 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("detecting role: %w", err)
|
return fmt.Errorf("detecting role: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --state mode: output JSON and exit early
|
|
||||||
if primeState {
|
|
||||||
return outputStateJSON(roleInfo, cwd, townRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --explain mode: show role detection reasoning
|
|
||||||
if primeExplain {
|
|
||||||
fmt.Printf("[explain] Role detection source: %s\n", roleInfo.Source)
|
|
||||||
fmt.Printf("[explain] Detected role: %s\n", roleInfo.Role)
|
|
||||||
if roleInfo.Rig != "" {
|
|
||||||
fmt.Printf("[explain] Rig: %s\n", roleInfo.Rig)
|
|
||||||
}
|
|
||||||
if roleInfo.Polecat != "" {
|
|
||||||
fmt.Printf("[explain] Polecat/Crew: %s\n", roleInfo.Polecat)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn prominently if there's a role/cwd mismatch
|
// Warn prominently if there's a role/cwd mismatch
|
||||||
if roleInfo.Mismatch {
|
if roleInfo.Mismatch {
|
||||||
fmt.Printf("\n%s\n", style.Bold.Render("⚠️ ROLE/LOCATION MISMATCH"))
|
fmt.Printf("\n%s\n", style.Bold.Render("⚠️ ROLE/LOCATION MISMATCH"))
|
||||||
@@ -228,18 +184,12 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
if err := acquireIdentityLock(ctx); err != nil {
|
if err := acquireIdentityLock(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if primeExplain {
|
|
||||||
fmt.Println("[dry-run] Would acquire identity lock for:", getAgentIdentity(ctx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure beads redirect exists for worktree-based roles
|
// Ensure beads redirect exists for worktree-based roles
|
||||||
// Skip if there's a role/location mismatch to avoid creating bad redirects
|
// Skip if there's a role/location mismatch to avoid creating bad redirects
|
||||||
if !roleInfo.Mismatch {
|
if !roleInfo.Mismatch && !primeDryRun {
|
||||||
if !primeDryRun {
|
ensureBeadsRedirect(ctx)
|
||||||
ensureBeadsRedirect(ctx)
|
|
||||||
} else if primeExplain {
|
|
||||||
fmt.Println("[dry-run] Would ensure beads redirect")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: reportAgentState("running") removed (gt-zecmc)
|
// NOTE: reportAgentState("running") removed (gt-zecmc)
|
||||||
@@ -249,8 +199,6 @@ func runPrime(cmd *cobra.Command, args []string) error {
|
|||||||
// Emit session_start event for seance discovery
|
// Emit session_start event for seance discovery
|
||||||
if !primeDryRun {
|
if !primeDryRun {
|
||||||
emitSessionEvent(ctx)
|
emitSessionEvent(ctx)
|
||||||
} else if primeExplain {
|
|
||||||
fmt.Println("[dry-run] Would emit session_start event")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output session metadata for seance discovery
|
// Output session metadata for seance discovery
|
||||||
@@ -387,47 +335,6 @@ func detectRole(cwd, townRoot string) RoleInfo {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrimeState represents the JSON output for --state mode.
|
|
||||||
type PrimeState struct {
|
|
||||||
Role Role `json:"role"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
Rig string `json:"rig,omitempty"`
|
|
||||||
Polecat string `json:"polecat,omitempty"`
|
|
||||||
TownRoot string `json:"town_root"`
|
|
||||||
WorkDir string `json:"work_dir"`
|
|
||||||
Mismatch bool `json:"mismatch,omitempty"`
|
|
||||||
CwdRole Role `json:"cwd_role,omitempty"`
|
|
||||||
Identity string `json:"identity,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputStateJSON outputs role state as JSON and returns (for --state flag).
|
|
||||||
func outputStateJSON(roleInfo RoleInfo, cwd, townRoot string) error {
|
|
||||||
state := PrimeState{
|
|
||||||
Role: roleInfo.Role,
|
|
||||||
Source: roleInfo.Source,
|
|
||||||
Rig: roleInfo.Rig,
|
|
||||||
Polecat: roleInfo.Polecat,
|
|
||||||
TownRoot: townRoot,
|
|
||||||
WorkDir: cwd,
|
|
||||||
Mismatch: roleInfo.Mismatch,
|
|
||||||
CwdRole: roleInfo.CwdRole,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute identity string
|
|
||||||
ctx := RoleContext{
|
|
||||||
Role: roleInfo.Role,
|
|
||||||
Rig: roleInfo.Rig,
|
|
||||||
Polecat: roleInfo.Polecat,
|
|
||||||
TownRoot: townRoot,
|
|
||||||
WorkDir: cwd,
|
|
||||||
}
|
|
||||||
state.Identity = getAgentIdentity(ctx)
|
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func outputPrimeContext(ctx RoleContext) error {
|
func outputPrimeContext(ctx RoleContext) error {
|
||||||
// Try to use templates first
|
// Try to use templates first
|
||||||
tmpl, err := templates.New()
|
tmpl, err := templates.New()
|
||||||
@@ -1161,50 +1068,45 @@ func outputRefineryPatrolContext(ctx RoleContext) {
|
|||||||
outputPatrolContext(cfg)
|
outputPatrolContext(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findHookedBead returns the bead currently on an agent's hook, if any.
|
// checkSlungWork checks for hooked work on the agent's hook.
|
||||||
// It checks both "hooked" status and "in_progress" status (for interrupted sessions
|
// If found, displays AUTONOMOUS WORK MODE and tells the agent to execute immediately.
|
||||||
// where work was claimed but the session was interrupted before completion).
|
// Returns true if hooked work was found (caller should skip normal startup directive).
|
||||||
// Returns nil if no hooked bead is found.
|
func checkSlungWork(ctx RoleContext) bool {
|
||||||
func findHookedBead(ctx RoleContext) *beads.Issue {
|
// Determine agent identity
|
||||||
agentID := getAgentIdentity(ctx)
|
agentID := getAgentIdentity(ctx)
|
||||||
if agentID == "" {
|
if agentID == "" {
|
||||||
return nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for hooked beads (work on the agent's hook)
|
||||||
b := beads.New(ctx.WorkDir)
|
b := beads.New(ctx.WorkDir)
|
||||||
|
|
||||||
// Check for hooked beads
|
|
||||||
hookedBeads, err := b.List(beads.ListOptions{
|
hookedBeads, err := b.List(beads.ListOptions{
|
||||||
Status: beads.StatusHooked,
|
Status: beads.StatusHooked,
|
||||||
Assignee: agentID,
|
Assignee: agentID,
|
||||||
Priority: -1,
|
Priority: -1,
|
||||||
})
|
})
|
||||||
if err == nil && len(hookedBeads) > 0 {
|
if err != nil {
|
||||||
return hookedBeads[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check in_progress beads (for interrupted sessions)
|
|
||||||
inProgressBeads, err := b.List(beads.ListOptions{
|
|
||||||
Status: "in_progress",
|
|
||||||
Assignee: agentID,
|
|
||||||
Priority: -1,
|
|
||||||
})
|
|
||||||
if err == nil && len(inProgressBeads) > 0 {
|
|
||||||
return inProgressBeads[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSlungWork checks for hooked work on the agent's hook.
|
|
||||||
// If found, displays AUTONOMOUS WORK MODE and tells the agent to execute immediately.
|
|
||||||
// Returns true if hooked work was found (caller should skip normal startup directive).
|
|
||||||
func checkSlungWork(ctx RoleContext) bool {
|
|
||||||
hookedBead := findHookedBead(ctx)
|
|
||||||
if hookedBead == nil {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no hooked beads found, also check in_progress beads assigned to this agent.
|
||||||
|
// This handles the case where work was claimed (status changed to in_progress)
|
||||||
|
// but the session was interrupted before completion. The hook should persist.
|
||||||
|
if len(hookedBeads) == 0 {
|
||||||
|
inProgressBeads, err := b.List(beads.ListOptions{
|
||||||
|
Status: "in_progress",
|
||||||
|
Assignee: agentID,
|
||||||
|
Priority: -1,
|
||||||
|
})
|
||||||
|
if err != nil || len(inProgressBeads) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hookedBeads = inProgressBeads
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the first hooked bead (agents typically have one)
|
||||||
|
hookedBead := hookedBeads[0]
|
||||||
|
|
||||||
// Build the role announcement string
|
// Build the role announcement string
|
||||||
roleAnnounce := buildRoleAnnouncement(ctx)
|
roleAnnounce := buildRoleAnnouncement(ctx)
|
||||||
|
|
||||||
@@ -1871,10 +1773,30 @@ func detectSessionState(ctx RoleContext) SessionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for hooked work (autonomous state)
|
// Check for hooked work (autonomous state)
|
||||||
if hookedBead := findHookedBead(ctx); hookedBead != nil {
|
agentID := getAgentIdentity(ctx)
|
||||||
state.State = "autonomous"
|
if agentID != "" {
|
||||||
state.HookedBead = hookedBead.ID
|
b := beads.New(ctx.WorkDir)
|
||||||
return state
|
hookedBeads, err := b.List(beads.ListOptions{
|
||||||
|
Status: beads.StatusHooked,
|
||||||
|
Assignee: agentID,
|
||||||
|
Priority: -1,
|
||||||
|
})
|
||||||
|
if err == nil && len(hookedBeads) > 0 {
|
||||||
|
state.State = "autonomous"
|
||||||
|
state.HookedBead = hookedBeads[0].ID
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
// Also check in_progress beads
|
||||||
|
inProgressBeads, err := b.List(beads.ListOptions{
|
||||||
|
Status: "in_progress",
|
||||||
|
Assignee: agentID,
|
||||||
|
Priority: -1,
|
||||||
|
})
|
||||||
|
if err == nil && len(inProgressBeads) > 0 {
|
||||||
|
state.State = "autonomous"
|
||||||
|
state.HookedBead = inProgressBeads[0].ID
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|||||||
@@ -149,6 +149,20 @@ func (c *PrimingCheck) checkAgentPriming(townRoot, agentDir, agentType string) [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check AGENTS.md is minimal (bootstrap pointer, not full context)
|
||||||
|
agentsMdPath := filepath.Join(agentPath, "AGENTS.md")
|
||||||
|
if fileExists(agentsMdPath) {
|
||||||
|
lines := c.countLines(agentsMdPath)
|
||||||
|
if lines > 20 {
|
||||||
|
issues = append(issues, primingIssue{
|
||||||
|
location: agentDir,
|
||||||
|
issueType: "large_agents_md",
|
||||||
|
description: fmt.Sprintf("AGENTS.md has %d lines (should be <20 for bootstrap pointer)", lines),
|
||||||
|
fixable: false, // Full context should come from gt prime templates
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user