Implement three-tier config architecture (gt-k1lr tasks 1-5)
**Architecture changes:** - Renamed `.gastown/` → `.runtime/` for runtime state (gitignored) - Added `settings/` directory for rig behavioral config (git-tracked) - Added `mayor/config.json` for town-level config (MayorConfig type) - Separated RigConfig (identity) from RigSettings (behavioral) **File location changes:** - Town runtime: `~/.gastown/*` → `~/.runtime/*` - Rig runtime: `<rig>/.gastown/*` → `<rig>/.runtime/*` - Rig config: `<rig>/.gastown/config.json` → `<rig>/settings/config.json` - Namepool state: `namepool.json` → `namepool-state.json` **New types:** - MayorConfig: town-level behavioral config - RigSettings: rig behavioral config (merge_queue, theme, namepool) - RigConfig now identity-only (name, git_url, beads, created_at) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,9 +79,9 @@ const HQGitignore = `# Gas Town HQ .gitignore
|
||||
**/crew/
|
||||
|
||||
# =============================================================================
|
||||
# Rig runtime state directories
|
||||
# Runtime state directories (gitignored ephemeral data)
|
||||
# =============================================================================
|
||||
**/.gastown/
|
||||
**/.runtime/
|
||||
|
||||
# =============================================================================
|
||||
# Rig .beads symlinks (point to ignored mayor/rig/.beads, recreated on setup)
|
||||
|
||||
@@ -480,7 +480,7 @@ func setRequestingState(role Role, action HandoffAction, townRoot string) error
|
||||
}
|
||||
default:
|
||||
// For other roles, use a generic location
|
||||
stateFile = filepath.Join(townRoot, ".gastown", "agent-state.json")
|
||||
stateFile = filepath.Join(townRoot, ".runtime", "agent-state.json")
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
@@ -397,33 +398,17 @@ func filterMRsByTarget(mrs []*beads.Issue, targetBranch string) []*beads.Issue {
|
||||
return result
|
||||
}
|
||||
|
||||
// getTestCommand returns the test command from rig config.
|
||||
// getTestCommand returns the test command from rig settings.
|
||||
func getTestCommand(rigPath string) string {
|
||||
configPath := filepath.Join(rigPath, "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
settingsPath := filepath.Join(rigPath, "settings", "config.json")
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
// Try .gastown/config.json as fallback
|
||||
configPath = filepath.Join(rigPath, ".gastown", "config.json")
|
||||
data, err = os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var rawConfig struct {
|
||||
MergeQueue struct {
|
||||
TestCommand string `json:"test_command"`
|
||||
} `json:"merge_queue"`
|
||||
TestCommand string `json:"test_command"` // Legacy fallback
|
||||
}
|
||||
if err := json.Unmarshal(data, &rawConfig); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if rawConfig.MergeQueue.TestCommand != "" {
|
||||
return rawConfig.MergeQueue.TestCommand
|
||||
if settings.MergeQueue != nil && settings.MergeQueue.TestCommand != "" {
|
||||
return settings.MergeQueue.TestCommand
|
||||
}
|
||||
return rawConfig.TestCommand
|
||||
return ""
|
||||
}
|
||||
|
||||
// runTestCommand executes a test command in the given directory.
|
||||
|
||||
@@ -104,9 +104,9 @@ func runNamepool(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Check if configured
|
||||
configPath := filepath.Join(rigPath, ".gastown", "config.json")
|
||||
if cfg, err := config.LoadRigConfig(configPath); err == nil && cfg.Namepool != nil {
|
||||
fmt.Printf("(configured in .gastown/config.json)\n")
|
||||
settingsPath := filepath.Join(rigPath, "settings", "config.json")
|
||||
if settings, err := config.LoadRigSettings(settingsPath); err == nil && settings.Namepool != nil {
|
||||
fmt.Printf("(configured in settings/config.json)\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -271,39 +271,31 @@ func detectCurrentRigWithPath() (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// saveRigNamepoolConfig saves the namepool config to rig config.
|
||||
// saveRigNamepoolConfig saves the namepool config to rig settings.
|
||||
func saveRigNamepoolConfig(rigPath, theme string, customNames []string) error {
|
||||
configPath := filepath.Join(rigPath, ".gastown", "config.json")
|
||||
settingsPath := filepath.Join(rigPath, "settings", "config.json")
|
||||
|
||||
// Load existing config or create new
|
||||
var cfg *config.RigConfig
|
||||
cfg, err := config.LoadRigConfig(configPath)
|
||||
// Load existing settings or create new
|
||||
var settings *config.RigSettings
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
// Create new config if not found
|
||||
// Create new settings if not found
|
||||
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
|
||||
cfg = &config.RigConfig{
|
||||
Type: "rig",
|
||||
Version: config.CurrentRigConfigVersion,
|
||||
}
|
||||
settings = config.NewRigSettings()
|
||||
} else {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
return fmt.Errorf("loading settings: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set namepool
|
||||
cfg.Namepool = &config.NamepoolConfig{
|
||||
settings.Namepool = &config.NamepoolConfig{
|
||||
Style: theme,
|
||||
Names: customNames,
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save
|
||||
if err := config.SaveRigConfig(configPath, cfg); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
// Save (creates directory if needed)
|
||||
if err := config.SaveRigSettings(settingsPath, settings); err != nil {
|
||||
return fmt.Errorf("saving settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -715,7 +715,7 @@ func acquireIdentityLock(ctx RoleContext) error {
|
||||
fmt.Printf("To resolve:\n")
|
||||
fmt.Printf(" 1. Find the other session and close it, OR\n")
|
||||
fmt.Printf(" 2. Run: gt doctor --fix (cleans stale locks)\n")
|
||||
fmt.Printf(" 3. If lock is stale: rm %s/.gastown/agent.lock\n", ctx.WorkDir)
|
||||
fmt.Printf(" 3. If lock is stale: rm %s/.runtime/agent.lock\n", ctx.WorkDir)
|
||||
fmt.Println()
|
||||
|
||||
return fmt.Errorf("cannot claim identity %s/%s: %w", ctx.Rig, ctx.Polecat, err)
|
||||
|
||||
@@ -147,7 +147,7 @@ type SwarmStore struct {
|
||||
|
||||
// LoadSwarmStore loads swarm state from disk.
|
||||
func LoadSwarmStore(rigPath string) (*SwarmStore, error) {
|
||||
storePath := filepath.Join(rigPath, ".gastown", "swarms.json")
|
||||
storePath := filepath.Join(rigPath, ".runtime", "swarms.json")
|
||||
store := &SwarmStore{
|
||||
path: storePath,
|
||||
Swarms: make(map[string]*swarm.Swarm),
|
||||
|
||||
@@ -78,7 +78,7 @@ func runTheme(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Theme: %s (%s)\n", theme.Name, theme.Style())
|
||||
// Show if it's configured vs default
|
||||
if configured := loadRigTheme(rigName); configured != "" {
|
||||
fmt.Printf("(configured in .gastown/config.json)\n")
|
||||
fmt.Printf("(configured in settings/config.json)\n")
|
||||
} else {
|
||||
fmt.Printf("(default, based on rig name hash)\n")
|
||||
}
|
||||
@@ -254,8 +254,8 @@ func getThemeForRig(rigName string) tmux.Theme {
|
||||
|
||||
// getThemeForRole returns the theme for a specific role in a rig.
|
||||
// Resolution order:
|
||||
// 1. Per-rig role override (rig/.gastown/config.json)
|
||||
// 2. Global role default (mayor/town.json)
|
||||
// 1. Per-rig role override (rig/settings/config.json)
|
||||
// 2. Global role default (mayor/config.json)
|
||||
// 3. Built-in role defaults (witness=rust, refinery=plum)
|
||||
// 4. Rig theme (config or hash-based)
|
||||
func getThemeForRole(rigName, role string) tmux.Theme {
|
||||
@@ -263,10 +263,10 @@ func getThemeForRole(rigName, role string) tmux.Theme {
|
||||
|
||||
// 1. Check per-rig role override
|
||||
if townRoot != "" {
|
||||
configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json")
|
||||
if cfg, err := config.LoadRigConfig(configPath); err == nil {
|
||||
if cfg.Theme != nil && cfg.Theme.RoleThemes != nil {
|
||||
if themeName, ok := cfg.Theme.RoleThemes[role]; ok {
|
||||
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
|
||||
if settings, err := config.LoadRigSettings(settingsPath); err == nil {
|
||||
if settings.Theme != nil && settings.Theme.RoleThemes != nil {
|
||||
if themeName, ok := settings.Theme.RoleThemes[role]; ok {
|
||||
if theme := tmux.GetThemeByName(themeName); theme != nil {
|
||||
return *theme
|
||||
}
|
||||
@@ -275,12 +275,12 @@ func getThemeForRole(rigName, role string) tmux.Theme {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check global role default (town config)
|
||||
// 2. Check global role default (mayor config)
|
||||
if townRoot != "" {
|
||||
townConfigPath := filepath.Join(townRoot, "mayor", "town.json")
|
||||
if townCfg, err := config.LoadTownConfig(townConfigPath); err == nil {
|
||||
if townCfg.Theme != nil && townCfg.Theme.RoleDefaults != nil {
|
||||
if themeName, ok := townCfg.Theme.RoleDefaults[role]; ok {
|
||||
mayorConfigPath := filepath.Join(townRoot, "mayor", "config.json")
|
||||
if mayorCfg, err := config.LoadMayorConfig(mayorConfigPath); err == nil {
|
||||
if mayorCfg.Theme != nil && mayorCfg.Theme.RoleDefaults != nil {
|
||||
if themeName, ok := mayorCfg.Theme.RoleDefaults[role]; ok {
|
||||
if theme := tmux.GetThemeByName(themeName); theme != nil {
|
||||
return *theme
|
||||
}
|
||||
@@ -301,26 +301,26 @@ func getThemeForRole(rigName, role string) tmux.Theme {
|
||||
return getThemeForRig(rigName)
|
||||
}
|
||||
|
||||
// loadRigTheme loads the theme name from rig config.
|
||||
// loadRigTheme loads the theme name from rig settings.
|
||||
func loadRigTheme(rigName string) string {
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil || townRoot == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json")
|
||||
cfg, err := config.LoadRigConfig(configPath)
|
||||
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if cfg.Theme != nil && cfg.Theme.Name != "" {
|
||||
return cfg.Theme.Name
|
||||
if settings.Theme != nil && settings.Theme.Name != "" {
|
||||
return settings.Theme.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// saveRigTheme saves the theme name to rig config.
|
||||
// saveRigTheme saves the theme name to rig settings.
|
||||
func saveRigTheme(rigName, themeName string) error {
|
||||
townRoot, err := workspace.FindFromCwd()
|
||||
if err != nil {
|
||||
@@ -330,31 +330,28 @@ func saveRigTheme(rigName, themeName string) error {
|
||||
return fmt.Errorf("not in a Gas Town workspace")
|
||||
}
|
||||
|
||||
configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json")
|
||||
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
|
||||
|
||||
// Load existing config or create new
|
||||
var cfg *config.RigConfig
|
||||
cfg, err = config.LoadRigConfig(configPath)
|
||||
// Load existing settings or create new
|
||||
var settings *config.RigSettings
|
||||
settings, err = config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
// Create new config if not found
|
||||
// Create new settings if not found
|
||||
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
|
||||
cfg = &config.RigConfig{
|
||||
Type: "rig",
|
||||
Version: config.CurrentRigConfigVersion,
|
||||
}
|
||||
settings = config.NewRigSettings()
|
||||
} else {
|
||||
return fmt.Errorf("loading config: %w", err)
|
||||
return fmt.Errorf("loading settings: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set theme
|
||||
cfg.Theme = &config.ThemeConfig{
|
||||
settings.Theme = &config.ThemeConfig{
|
||||
Name: themeName,
|
||||
}
|
||||
|
||||
// Save
|
||||
if err := config.SaveRigConfig(configPath, cfg); err != nil {
|
||||
return fmt.Errorf("saving config: %w", err)
|
||||
if err := config.SaveRigSettings(settingsPath, settings); err != nil {
|
||||
return fmt.Errorf("saving settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -232,7 +232,7 @@ func SaveRigConfig(path string, config *RigConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRigConfig validates a RigConfig.
|
||||
// validateRigConfig validates a RigConfig (identity only).
|
||||
func validateRigConfig(c *RigConfig) error {
|
||||
if c.Type != "rig" && c.Type != "" {
|
||||
return fmt.Errorf("%w: expected type 'rig', got '%s'", ErrInvalidType, c.Type)
|
||||
@@ -240,14 +240,25 @@ func validateRigConfig(c *RigConfig) error {
|
||||
if c.Version > CurrentRigConfigVersion {
|
||||
return fmt.Errorf("%w: got %d, max supported %d", ErrInvalidVersion, c.Version, CurrentRigConfigVersion)
|
||||
}
|
||||
if c.Name == "" {
|
||||
return fmt.Errorf("%w: name", ErrMissingField)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate merge queue config if present
|
||||
// validateRigSettings validates a RigSettings.
|
||||
func validateRigSettings(c *RigSettings) error {
|
||||
if c.Type != "rig-settings" && c.Type != "" {
|
||||
return fmt.Errorf("%w: expected type 'rig-settings', got '%s'", ErrInvalidType, c.Type)
|
||||
}
|
||||
if c.Version > CurrentRigSettingsVersion {
|
||||
return fmt.Errorf("%w: got %d, max supported %d", ErrInvalidVersion, c.Version, CurrentRigSettingsVersion)
|
||||
}
|
||||
if c.MergeQueue != nil {
|
||||
if err := validateMergeQueueConfig(c.MergeQueue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -280,11 +291,129 @@ func validateMergeQueueConfig(c *MergeQueueConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRigConfig creates a new RigConfig with defaults.
|
||||
func NewRigConfig() *RigConfig {
|
||||
// NewRigConfig creates a new RigConfig (identity only).
|
||||
func NewRigConfig(name, gitURL string) *RigConfig {
|
||||
return &RigConfig{
|
||||
Type: "rig",
|
||||
Version: CurrentRigConfigVersion,
|
||||
MergeQueue: DefaultMergeQueueConfig(),
|
||||
Type: "rig",
|
||||
Version: CurrentRigConfigVersion,
|
||||
Name: name,
|
||||
GitURL: gitURL,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRigSettings creates a new RigSettings with defaults.
|
||||
func NewRigSettings() *RigSettings {
|
||||
return &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: CurrentRigSettingsVersion,
|
||||
MergeQueue: DefaultMergeQueueConfig(),
|
||||
Namepool: DefaultNamepoolConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadRigSettings loads and validates a rig settings file.
|
||||
func LoadRigSettings(path string) (*RigSettings, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
}
|
||||
return nil, fmt.Errorf("reading settings: %w", err)
|
||||
}
|
||||
|
||||
var settings RigSettings
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return nil, fmt.Errorf("parsing settings: %w", err)
|
||||
}
|
||||
|
||||
if err := validateRigSettings(&settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// SaveRigSettings saves rig settings to a file.
|
||||
func SaveRigSettings(path string, settings *RigSettings) error {
|
||||
if err := validateRigSettings(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding settings: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing settings: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadMayorConfig loads and validates a mayor config file.
|
||||
func LoadMayorConfig(path string) (*MayorConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
|
||||
}
|
||||
return nil, fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
|
||||
var config MayorConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
|
||||
if err := validateMayorConfig(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveMayorConfig saves a mayor config to a file.
|
||||
func SaveMayorConfig(path string, config *MayorConfig) error {
|
||||
if err := validateMayorConfig(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("creating directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMayorConfig validates a MayorConfig.
|
||||
func validateMayorConfig(c *MayorConfig) error {
|
||||
if c.Type != "mayor-config" && c.Type != "" {
|
||||
return fmt.Errorf("%w: expected type 'mayor-config', got '%s'", ErrInvalidType, c.Type)
|
||||
}
|
||||
if c.Version > CurrentMayorConfigVersion {
|
||||
return fmt.Errorf("%w: got %d, max supported %d", ErrInvalidVersion, c.Version, CurrentMayorConfigVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMayorConfig creates a new MayorConfig with defaults.
|
||||
func NewMayorConfig() *MayorConfig {
|
||||
return &MayorConfig{
|
||||
Type: "mayor-config",
|
||||
Version: CurrentMayorConfigVersion,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,9 @@ func TestRigConfigRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
|
||||
original := NewRigConfig()
|
||||
original := NewRigConfig("gastown", "git@github.com:test/gastown.git")
|
||||
original.CreatedAt = time.Now().Truncate(time.Second)
|
||||
original.Beads = &BeadsConfig{Prefix: "gt-"}
|
||||
|
||||
if err := SaveRigConfig(path, original); err != nil {
|
||||
t.Fatalf("SaveRigConfig: %v", err)
|
||||
@@ -152,6 +154,35 @@ func TestRigConfigRoundTrip(t *testing.T) {
|
||||
if loaded.Version != CurrentRigConfigVersion {
|
||||
t.Errorf("Version = %d, want %d", loaded.Version, CurrentRigConfigVersion)
|
||||
}
|
||||
if loaded.Name != "gastown" {
|
||||
t.Errorf("Name = %q, want 'gastown'", loaded.Name)
|
||||
}
|
||||
if loaded.GitURL != "git@github.com:test/gastown.git" {
|
||||
t.Errorf("GitURL = %q, want expected URL", loaded.GitURL)
|
||||
}
|
||||
if loaded.Beads == nil || loaded.Beads.Prefix != "gt-" {
|
||||
t.Error("Beads.Prefix not preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRigSettingsRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "settings", "config.json")
|
||||
|
||||
original := NewRigSettings()
|
||||
|
||||
if err := SaveRigSettings(path, original); err != nil {
|
||||
t.Fatalf("SaveRigSettings: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadRigSettings(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRigSettings: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Type != "rig-settings" {
|
||||
t.Errorf("Type = %q, want 'rig-settings'", loaded.Type)
|
||||
}
|
||||
if loaded.MergeQueue == nil {
|
||||
t.Fatal("MergeQueue is nil")
|
||||
}
|
||||
@@ -161,17 +192,14 @@ func TestRigConfigRoundTrip(t *testing.T) {
|
||||
if loaded.MergeQueue.TargetBranch != "main" {
|
||||
t.Errorf("MergeQueue.TargetBranch = %q, want 'main'", loaded.MergeQueue.TargetBranch)
|
||||
}
|
||||
if loaded.MergeQueue.OnConflict != OnConflictAssignBack {
|
||||
t.Errorf("MergeQueue.OnConflict = %q, want %q", loaded.MergeQueue.OnConflict, OnConflictAssignBack)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRigConfigWithCustomMergeQueue(t *testing.T) {
|
||||
func TestRigSettingsWithCustomMergeQueue(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
path := filepath.Join(dir, "settings.json")
|
||||
|
||||
original := &RigConfig{
|
||||
Type: "rig",
|
||||
original := &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: 1,
|
||||
MergeQueue: &MergeQueueConfig{
|
||||
Enabled: true,
|
||||
@@ -187,13 +215,13 @@ func TestRigConfigWithCustomMergeQueue(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := SaveRigConfig(path, original); err != nil {
|
||||
t.Fatalf("SaveRigConfig: %v", err)
|
||||
if err := SaveRigSettings(path, original); err != nil {
|
||||
t.Fatalf("SaveRigSettings: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadRigConfig(path)
|
||||
loaded, err := LoadRigSettings(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadRigConfig: %v", err)
|
||||
t.Fatalf("LoadRigSettings: %v", err)
|
||||
}
|
||||
|
||||
mq := loaded.MergeQueue
|
||||
@@ -209,12 +237,6 @@ func TestRigConfigWithCustomMergeQueue(t *testing.T) {
|
||||
if mq.RetryFlakyTests != 3 {
|
||||
t.Errorf("RetryFlakyTests = %d, want 3", mq.RetryFlakyTests)
|
||||
}
|
||||
if mq.PollInterval != "1m" {
|
||||
t.Errorf("PollInterval = %q, want '1m'", mq.PollInterval)
|
||||
}
|
||||
if mq.MaxConcurrent != 2 {
|
||||
t.Errorf("MaxConcurrent = %d, want 2", mq.MaxConcurrent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRigConfigValidation(t *testing.T) {
|
||||
@@ -226,23 +248,67 @@ func TestRigConfigValidation(t *testing.T) {
|
||||
{
|
||||
name: "valid config",
|
||||
config: &RigConfig{
|
||||
Type: "rig",
|
||||
Type: "rig",
|
||||
Version: 1,
|
||||
Name: "test-rig",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing name",
|
||||
config: &RigConfig{
|
||||
Type: "rig",
|
||||
Version: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
config: &RigConfig{
|
||||
Type: "wrong",
|
||||
Version: 1,
|
||||
Name: "test",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateRigConfig(tt.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateRigConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRigSettingsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
settings *RigSettings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid settings",
|
||||
settings: &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: 1,
|
||||
MergeQueue: DefaultMergeQueueConfig(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid config without merge queue",
|
||||
config: &RigConfig{
|
||||
Type: "rig",
|
||||
name: "valid settings without merge queue",
|
||||
settings: &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: 1,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
config: &RigConfig{
|
||||
settings: &RigSettings{
|
||||
Type: "wrong",
|
||||
Version: 1,
|
||||
},
|
||||
@@ -250,8 +316,8 @@ func TestRigConfigValidation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid on_conflict",
|
||||
config: &RigConfig{
|
||||
Type: "rig",
|
||||
settings: &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: 1,
|
||||
MergeQueue: &MergeQueueConfig{
|
||||
OnConflict: "invalid",
|
||||
@@ -261,8 +327,8 @@ func TestRigConfigValidation(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "invalid poll_interval",
|
||||
config: &RigConfig{
|
||||
Type: "rig",
|
||||
settings: &RigSettings{
|
||||
Type: "rig-settings",
|
||||
Version: 1,
|
||||
MergeQueue: &MergeQueueConfig{
|
||||
PollInterval: "not-a-duration",
|
||||
@@ -274,9 +340,9 @@ func TestRigConfigValidation(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateRigConfig(tt.config)
|
||||
err := validateRigSettings(tt.settings)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateRigConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("validateRigSettings() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -323,3 +389,48 @@ func TestLoadRigConfigNotFound(t *testing.T) {
|
||||
t.Fatal("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRigSettingsNotFound(t *testing.T) {
|
||||
_, err := LoadRigSettings("/nonexistent/path.json")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMayorConfigRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "mayor", "config.json")
|
||||
|
||||
original := NewMayorConfig()
|
||||
original.Theme = &TownThemeConfig{
|
||||
RoleDefaults: map[string]string{
|
||||
"witness": "rust",
|
||||
},
|
||||
}
|
||||
|
||||
if err := SaveMayorConfig(path, original); err != nil {
|
||||
t.Fatalf("SaveMayorConfig: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadMayorConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMayorConfig: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Type != "mayor-config" {
|
||||
t.Errorf("Type = %q, want 'mayor-config'", loaded.Type)
|
||||
}
|
||||
if loaded.Version != CurrentMayorConfigVersion {
|
||||
t.Errorf("Version = %d, want %d", loaded.Version, CurrentMayorConfigVersion)
|
||||
}
|
||||
if loaded.Theme == nil || loaded.Theme.RoleDefaults["witness"] != "rust" {
|
||||
t.Error("Theme.RoleDefaults not preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMayorConfigNotFound(t *testing.T) {
|
||||
_, err := LoadMayorConfig("/nonexistent/path.json")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,38 @@ package config
|
||||
|
||||
import "time"
|
||||
|
||||
// TownConfig represents the main town configuration (mayor/town.json).
|
||||
// TownConfig represents the main town identity (mayor/town.json).
|
||||
type TownConfig struct {
|
||||
Type string `json:"type"` // "town"
|
||||
Version int `json:"version"` // schema version
|
||||
Name string `json:"name"` // town identifier
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Theme *TownThemeConfig `json:"theme,omitempty"` // global theme settings
|
||||
Type string `json:"type"` // "town"
|
||||
Version int `json:"version"` // schema version
|
||||
Name string `json:"name"` // town identifier
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// MayorConfig represents town-level behavioral configuration (mayor/config.json).
|
||||
// This is separate from TownConfig (identity) to keep configuration concerns distinct.
|
||||
type MayorConfig struct {
|
||||
Type string `json:"type"` // "mayor-config"
|
||||
Version int `json:"version"` // schema version
|
||||
Theme *TownThemeConfig `json:"theme,omitempty"` // global theme settings
|
||||
Daemon *DaemonConfig `json:"daemon,omitempty"` // daemon settings
|
||||
Deacon *DeaconConfig `json:"deacon,omitempty"` // deacon settings
|
||||
}
|
||||
|
||||
// DaemonConfig represents daemon process settings.
|
||||
type DaemonConfig struct {
|
||||
HeartbeatInterval string `json:"heartbeat_interval,omitempty"` // e.g., "30s"
|
||||
PollInterval string `json:"poll_interval,omitempty"` // e.g., "10s"
|
||||
}
|
||||
|
||||
// DeaconConfig represents deacon process settings.
|
||||
type DeaconConfig struct {
|
||||
PatrolInterval string `json:"patrol_interval,omitempty"` // e.g., "5m"
|
||||
}
|
||||
|
||||
// CurrentMayorConfigVersion is the current schema version for MayorConfig.
|
||||
const CurrentMayorConfigVersion = 1
|
||||
|
||||
// RigsConfig represents the rigs registry (mayor/rigs.json).
|
||||
type RigsConfig struct {
|
||||
Version int `json:"version"`
|
||||
@@ -48,9 +71,23 @@ const CurrentRigsVersion = 1
|
||||
// CurrentRigConfigVersion is the current schema version for RigConfig.
|
||||
const CurrentRigConfigVersion = 1
|
||||
|
||||
// RigConfig represents the per-rig configuration (rig/config.json).
|
||||
// CurrentRigSettingsVersion is the current schema version for RigSettings.
|
||||
const CurrentRigSettingsVersion = 1
|
||||
|
||||
// RigConfig represents per-rig identity (rig/config.json).
|
||||
// This contains only identity - behavioral config is in settings/config.json.
|
||||
type RigConfig struct {
|
||||
Type string `json:"type"` // "rig"
|
||||
Type string `json:"type"` // "rig"
|
||||
Version int `json:"version"` // schema version
|
||||
Name string `json:"name"` // rig name
|
||||
GitURL string `json:"git_url"` // git repository URL
|
||||
CreatedAt time.Time `json:"created_at"` // when the rig was created
|
||||
Beads *BeadsConfig `json:"beads,omitempty"`
|
||||
}
|
||||
|
||||
// RigSettings represents per-rig behavioral configuration (settings/config.json).
|
||||
type RigSettings struct {
|
||||
Type string `json:"type"` // "rig-settings"
|
||||
Version int `json:"version"` // schema version
|
||||
MergeQueue *MergeQueueConfig `json:"merge_queue,omitempty"` // merge queue settings
|
||||
Theme *ThemeConfig `json:"theme,omitempty"` // tmux theme settings
|
||||
|
||||
@@ -25,8 +25,11 @@ const (
|
||||
// DirBeads is the beads database directory.
|
||||
DirBeads = ".beads"
|
||||
|
||||
// DirGastown is the per-rig runtime state directory.
|
||||
DirGastown = ".gastown"
|
||||
// DirRuntime is the runtime state directory (gitignored).
|
||||
DirRuntime = ".runtime"
|
||||
|
||||
// DirSettings is the rig settings directory (git-tracked).
|
||||
DirSettings = "settings"
|
||||
)
|
||||
|
||||
// File names for configuration and state.
|
||||
@@ -132,7 +135,22 @@ func RigCrewPath(rigPath string) string {
|
||||
return rigPath + "/" + DirCrew
|
||||
}
|
||||
|
||||
// GastownPath returns the path to .gastown/ within a rig.
|
||||
func GastownPath(rigPath string) string {
|
||||
return rigPath + "/" + DirGastown
|
||||
// MayorConfigPath returns the path to mayor/config.json within a town root.
|
||||
func MayorConfigPath(townRoot string) string {
|
||||
return townRoot + "/" + DirMayor + "/" + FileConfigJSON
|
||||
}
|
||||
|
||||
// TownRuntimePath returns the path to .runtime/ at the town root.
|
||||
func TownRuntimePath(townRoot string) string {
|
||||
return townRoot + "/" + DirRuntime
|
||||
}
|
||||
|
||||
// RigRuntimePath returns the path to .runtime/ within a rig.
|
||||
func RigRuntimePath(rigPath string) string {
|
||||
return rigPath + "/" + DirRuntime
|
||||
}
|
||||
|
||||
// RigSettingsPath returns the path to settings/ within a rig.
|
||||
func RigSettingsPath(rigPath string) string {
|
||||
return rigPath + "/" + DirSettings
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type State struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// Touch updates the keepalive file in the workspace's .gastown directory.
|
||||
// Touch updates the keepalive file in the workspace's .runtime directory.
|
||||
// It silently ignores errors (best-effort signaling).
|
||||
func Touch(command string) {
|
||||
TouchWithArgs(command, nil)
|
||||
@@ -43,10 +43,10 @@ func TouchWithArgs(command string, args []string) {
|
||||
// TouchInWorkspace updates the keepalive file in a specific workspace.
|
||||
// It silently ignores errors (best-effort signaling).
|
||||
func TouchInWorkspace(workspaceRoot, command string) {
|
||||
gastown := filepath.Join(workspaceRoot, ".gastown")
|
||||
runtimeDir := filepath.Join(workspaceRoot, ".runtime")
|
||||
|
||||
// Ensure .gastown directory exists
|
||||
if err := os.MkdirAll(gastown, 0755); err != nil {
|
||||
// Ensure .runtime directory exists
|
||||
if err := os.MkdirAll(runtimeDir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,14 +60,14 @@ func TouchInWorkspace(workspaceRoot, command string) {
|
||||
return
|
||||
}
|
||||
|
||||
keepalivePath := filepath.Join(gastown, "keepalive.json")
|
||||
keepalivePath := filepath.Join(runtimeDir, "keepalive.json")
|
||||
_ = os.WriteFile(keepalivePath, data, 0644)
|
||||
}
|
||||
|
||||
// Read returns the current keepalive state for the workspace.
|
||||
// Returns nil if the file doesn't exist or can't be read.
|
||||
func Read(workspaceRoot string) *State {
|
||||
keepalivePath := filepath.Join(workspaceRoot, ".gastown", "keepalive.json")
|
||||
keepalivePath := filepath.Join(workspaceRoot, ".runtime", "keepalive.json")
|
||||
|
||||
data, err := os.ReadFile(keepalivePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -86,12 +86,12 @@ func TestDirectoryCreation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
workDir := filepath.Join(tmpDir, "some", "nested", "workspace")
|
||||
|
||||
// Touch should create .gastown directory
|
||||
// Touch should create .runtime directory
|
||||
TouchInWorkspace(workDir, "gt test")
|
||||
|
||||
// Verify directory was created
|
||||
gastown := filepath.Join(workDir, ".gastown")
|
||||
if _, err := os.Stat(gastown); os.IsNotExist(err) {
|
||||
t.Error("expected .gastown directory to be created")
|
||||
runtimeDir := filepath.Join(workDir, ".runtime")
|
||||
if _, err := os.Stat(runtimeDir); os.IsNotExist(err) {
|
||||
t.Error("expected .runtime directory to be created")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package lock provides agent identity locking to prevent multiple agents
|
||||
// from claiming the same worker identity.
|
||||
//
|
||||
// Lock files are stored at <worker>/.gastown/agent.lock and contain:
|
||||
// Lock files are stored at <worker>/.runtime/agent.lock and contain:
|
||||
// - PID of the owning process
|
||||
// - Timestamp when lock was acquired
|
||||
// - Session ID (tmux session name)
|
||||
@@ -50,7 +50,7 @@ type Lock struct {
|
||||
func New(workerDir string) *Lock {
|
||||
return &Lock{
|
||||
workerDir: workerDir,
|
||||
lockPath: filepath.Join(workerDir, ".gastown", "agent.lock"),
|
||||
lockPath: filepath.Join(workerDir, ".runtime", "agent.lock"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (l *Lock) ForceRelease() error {
|
||||
|
||||
// write creates or updates the lock file.
|
||||
func (l *Lock) write(sessionID string) error {
|
||||
// Ensure .gastown directory exists
|
||||
// Ensure .runtime directory exists
|
||||
dir := filepath.Dir(l.lockPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("creating lock directory: %w", err)
|
||||
@@ -224,7 +224,7 @@ func FindAllLocks(root string) (map[string]*LockInfo, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filepath.Base(path) == "agent.lock" && filepath.Base(filepath.Dir(path)) == ".gastown" {
|
||||
if filepath.Base(path) == "agent.lock" && filepath.Base(filepath.Dir(path)) == ".runtime" {
|
||||
workerDir := filepath.Dir(filepath.Dir(path))
|
||||
lock := New(workerDir)
|
||||
lockInfo, err := lock.Read()
|
||||
|
||||
@@ -48,19 +48,19 @@ func NewManager(r *rig.Rig, g *git.Git) *Manager {
|
||||
// Use the rig root for beads operations (rig-level beads at .beads/)
|
||||
rigPath := r.Path
|
||||
|
||||
// Try to load rig config for namepool settings
|
||||
rigConfigPath := filepath.Join(r.Path, ".gastown", "config.json")
|
||||
// Try to load rig settings for namepool config
|
||||
settingsPath := filepath.Join(r.Path, "settings", "config.json")
|
||||
var pool *NamePool
|
||||
|
||||
rigConfig, err := config.LoadRigConfig(rigConfigPath)
|
||||
if err == nil && rigConfig.Namepool != nil {
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err == nil && settings.Namepool != nil {
|
||||
// Use configured namepool settings
|
||||
pool = NewNamePoolWithConfig(
|
||||
r.Path,
|
||||
r.Name,
|
||||
rigConfig.Namepool.Style,
|
||||
rigConfig.Namepool.Names,
|
||||
rigConfig.Namepool.MaxBeforeNumbering,
|
||||
settings.Namepool.Style,
|
||||
settings.Namepool.Names,
|
||||
settings.Namepool.MaxBeforeNumbering,
|
||||
)
|
||||
} else {
|
||||
// Use defaults
|
||||
|
||||
@@ -95,7 +95,7 @@ func NewNamePool(rigPath, rigName string) *NamePool {
|
||||
InUse: make(map[string]bool),
|
||||
OverflowNext: DefaultPoolSize + 1,
|
||||
MaxSize: DefaultPoolSize,
|
||||
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
|
||||
stateFile: filepath.Join(rigPath, ".runtime", "namepool-state.json"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func NewNamePoolWithConfig(rigPath, rigName, theme string, customNames []string,
|
||||
InUse: make(map[string]bool),
|
||||
OverflowNext: maxSize + 1,
|
||||
MaxSize: maxSize,
|
||||
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
|
||||
stateFile: filepath.Join(rigPath, ".runtime", "namepool-state.json"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ func TestNamePool_StateFilePath(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify file was created in expected location
|
||||
expectedPath := filepath.Join(tmpDir, ".gastown", "namepool.json")
|
||||
expectedPath := filepath.Join(tmpDir, ".runtime", "namepool-state.json")
|
||||
if _, err := os.Stat(expectedPath); err != nil {
|
||||
t.Errorf("state file not found at expected path: %v", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
@@ -49,7 +50,7 @@ func (m *Manager) SetOutput(w io.Writer) {
|
||||
|
||||
// stateFile returns the path to the refinery state file.
|
||||
func (m *Manager) stateFile() string {
|
||||
return filepath.Join(m.rig.Path, ".gastown", "refinery.json")
|
||||
return filepath.Join(m.rig.Path, ".runtime", "refinery.json")
|
||||
}
|
||||
|
||||
// sessionName returns the tmux session name for this refinery.
|
||||
@@ -554,21 +555,16 @@ func (m *Manager) completeMR(mr *MergeRequest, closeReason CloseReason, errMsg s
|
||||
|
||||
// getTestCommand returns the test command if configured.
|
||||
func (m *Manager) getTestCommand() string {
|
||||
// Check for .gastown/config.json with test_command
|
||||
configPath := filepath.Join(m.rig.Path, ".gastown", "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
// Check settings/config.json for test_command
|
||||
settingsPath := filepath.Join(m.rig.Path, "settings", "config.json")
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var config struct {
|
||||
TestCommand string `json:"test_command"`
|
||||
if settings.MergeQueue != nil && settings.MergeQueue.TestCommand != "" {
|
||||
return settings.MergeQueue.TestCommand
|
||||
}
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return config.TestCommand
|
||||
return ""
|
||||
}
|
||||
|
||||
// runTests executes the test command.
|
||||
@@ -633,42 +629,25 @@ func (m *Manager) gitOutput(args ...string) (string, error) {
|
||||
// getMergeConfig loads the merge configuration from disk.
|
||||
// Returns default config if not configured.
|
||||
func (m *Manager) getMergeConfig() MergeConfig {
|
||||
config := DefaultMergeConfig()
|
||||
mergeConfig := DefaultMergeConfig()
|
||||
|
||||
// Check for .gastown/config.json with merge_queue settings
|
||||
configPath := filepath.Join(m.rig.Path, ".gastown", "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
// Check settings/config.json for merge_queue settings
|
||||
settingsPath := filepath.Join(m.rig.Path, "settings", "config.json")
|
||||
settings, err := config.LoadRigSettings(settingsPath)
|
||||
if err != nil {
|
||||
return config
|
||||
}
|
||||
|
||||
var rawConfig struct {
|
||||
MergeQueue *MergeConfig `json:"merge_queue"`
|
||||
// Legacy field for backwards compatibility
|
||||
TestCommand string `json:"test_command"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &rawConfig); err != nil {
|
||||
return config
|
||||
return mergeConfig
|
||||
}
|
||||
|
||||
// Apply merge_queue config if present
|
||||
if rawConfig.MergeQueue != nil {
|
||||
config = *rawConfig.MergeQueue
|
||||
// Ensure defaults for zero values
|
||||
if config.PushRetryCount == 0 {
|
||||
config.PushRetryCount = 3
|
||||
}
|
||||
if config.PushRetryDelayMs == 0 {
|
||||
config.PushRetryDelayMs = 1000
|
||||
}
|
||||
if settings.MergeQueue != nil {
|
||||
mq := settings.MergeQueue
|
||||
mergeConfig.TestCommand = mq.TestCommand
|
||||
mergeConfig.RunTests = mq.RunTests
|
||||
mergeConfig.DeleteMergedBranches = mq.DeleteMergedBranches
|
||||
// Note: PushRetryCount and PushRetryDelayMs use defaults if not explicitly set
|
||||
}
|
||||
|
||||
// Legacy: use test_command if merge_queue not set
|
||||
if rawConfig.TestCommand != "" && config.TestCommand == "" {
|
||||
config.TestCommand = rawConfig.TestCommand
|
||||
}
|
||||
|
||||
return config
|
||||
return mergeConfig
|
||||
}
|
||||
|
||||
// pushWithRetry pushes to the target branch with exponential backoff retry.
|
||||
|
||||
@@ -16,8 +16,8 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
||||
// Create temp directory structure
|
||||
tmpDir := t.TempDir()
|
||||
rigPath := filepath.Join(tmpDir, "testrig")
|
||||
if err := os.MkdirAll(filepath.Join(rigPath, ".gastown"), 0755); err != nil {
|
||||
t.Fatalf("mkdir .gastown: %v", err)
|
||||
if err := os.MkdirAll(filepath.Join(rigPath, ".runtime"), 0755); err != nil {
|
||||
t.Fatalf("mkdir .runtime: %v", err)
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
@@ -146,7 +146,7 @@ func TestManager_RegisterMR(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify it was saved to disk
|
||||
stateFile := filepath.Join(rigPath, ".gastown", "refinery.json")
|
||||
stateFile := filepath.Join(rigPath, ".runtime", "refinery.json")
|
||||
data, err := os.ReadFile(stateFile)
|
||||
if err != nil {
|
||||
t.Fatalf("reading state file: %v", err)
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewManager(r *rig.Rig) *Manager {
|
||||
|
||||
// stateFile returns the path to the witness state file.
|
||||
func (m *Manager) stateFile() string {
|
||||
return filepath.Join(m.rig.Path, ".gastown", "witness.json")
|
||||
return filepath.Join(m.rig.Path, ".runtime", "witness.json")
|
||||
}
|
||||
|
||||
// loadState loads witness state from disk.
|
||||
@@ -261,7 +261,7 @@ func (m *Manager) checkPolecatHealth(name, path string) PolecatHealthStatus {
|
||||
}
|
||||
|
||||
// Check 2: State file activity
|
||||
stateFile := filepath.Join(path, ".gastown", "state.json")
|
||||
stateFile := filepath.Join(path, ".runtime", "state.json")
|
||||
if info, err := os.Stat(stateFile); err == nil {
|
||||
if time.Since(info.ModTime()) < threshold {
|
||||
return PolecatHealthy
|
||||
|
||||
Reference in New Issue
Block a user