This commit is contained in:
Ben Kraus
2026-01-02 09:32:51 -07:00
committed by Cameron Palmer
parent 38adfa4d8b
commit 98e154b18e
6 changed files with 105 additions and 8 deletions

View File

@@ -19,6 +19,7 @@ Complete setup guide for Gas Town multi-agent orchestrator.
| **tmux** | 3.0+ | `tmux -V` | See below | | **tmux** | 3.0+ | `tmux -V` | See below |
| **Claude Code** (default) | latest | `claude --version` | See [claude.ai/claude-code](https://claude.ai/claude-code) | | **Claude Code** (default) | latest | `claude --version` | See [claude.ai/claude-code](https://claude.ai/claude-code) |
| **Codex CLI** (optional) | latest | `codex --version` | See [developers.openai.com/codex/cli](https://developers.openai.com/codex/cli) | | **Codex CLI** (optional) | latest | `codex --version` | See [developers.openai.com/codex/cli](https://developers.openai.com/codex/cli) |
| **OpenCode CLI** (optional) | latest | `opencode --version` | See [opencode.ai](https://opencode.ai) |
## Installing Prerequisites ## Installing Prerequisites

View File

@@ -7,6 +7,7 @@ import (
"github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/constants"
"github.com/steveyegge/gastown/internal/crew" "github.com/steveyegge/gastown/internal/crew"
"github.com/steveyegge/gastown/internal/runtime"
"github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace" "github.com/steveyegge/gastown/internal/workspace"
@@ -88,6 +89,7 @@ func runCrewAt(cmd *cobra.Command, args []string) error {
} }
runtimeConfig := config.LoadRuntimeConfig(r.Path) runtimeConfig := config.LoadRuntimeConfig(r.Path)
_ = runtime.EnsureSettingsForRole(worker.ClonePath, "crew", runtimeConfig)
// Check if session exists // Check if session exists
t := tmux.NewTmux() t := tmux.NewTmux()

View File

@@ -269,7 +269,7 @@ type RuntimeSessionConfig struct {
// RuntimeHooksConfig configures runtime hook installation. // RuntimeHooksConfig configures runtime hook installation.
type RuntimeHooksConfig struct { type RuntimeHooksConfig struct {
// Provider controls which hook templates to install: "claude" or "none". // Provider controls which hook templates to install: "claude", "opencode", or "none".
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
// Dir is the settings directory (e.g., ".claude"). // Dir is the settings directory (e.g., ".claude").
@@ -435,6 +435,8 @@ func defaultRuntimeCommand(provider string) string {
switch provider { switch provider {
case "codex": case "codex":
return "codex" return "codex"
case "opencode":
return "opencode"
case "generic": case "generic":
return "" return ""
default: default:
@@ -455,6 +457,8 @@ func defaultPromptMode(provider string) string {
switch provider { switch provider {
case "codex": case "codex":
return "none" return "none"
case "opencode":
return "none"
default: default:
return "arg" return "arg"
} }
@@ -475,24 +479,36 @@ func defaultConfigDirEnv(provider string) string {
} }
func defaultHooksProvider(provider string) string { func defaultHooksProvider(provider string) string {
if provider == "claude" { switch provider {
case "claude":
return "claude" return "claude"
case "opencode":
return "opencode"
default:
return "none"
} }
return "none"
} }
func defaultHooksDir(provider string) string { func defaultHooksDir(provider string) string {
if provider == "claude" { switch provider {
case "claude":
return ".claude" return ".claude"
case "opencode":
return ".opencode/plugin"
default:
return ""
} }
return ""
} }
func defaultHooksFile(provider string) string { func defaultHooksFile(provider string) string {
if provider == "claude" { switch provider {
case "claude":
return "settings.json" return "settings.json"
case "opencode":
return "gastown.js"
default:
return ""
} }
return ""
} }
func defaultProcessNames(provider, command string) []string { func defaultProcessNames(provider, command string) []string {
@@ -526,6 +542,9 @@ func defaultInstructionsFile(provider string) string {
if provider == "codex" { if provider == "codex" {
return "AGENTS.md" return "AGENTS.md"
} }
if provider == "opencode" {
return "AGENTS.md"
}
return "CLAUDE.md" return "CLAUDE.md"
} }

View File

@@ -0,0 +1,40 @@
// Package opencode provides OpenCode plugin management.
package opencode
import (
"embed"
"fmt"
"os"
"path/filepath"
)
//go:embed plugin/gastown.js
var pluginFS embed.FS
// EnsurePluginAt ensures the Gas Town OpenCode plugin exists.
// If the file already exists, it's left unchanged.
func EnsurePluginAt(workDir, pluginDir, pluginFile string) error {
if pluginDir == "" || pluginFile == "" {
return nil
}
pluginPath := filepath.Join(workDir, pluginDir, pluginFile)
if _, err := os.Stat(pluginPath); err == nil {
return nil
}
if err := os.MkdirAll(filepath.Dir(pluginPath), 0755); err != nil {
return fmt.Errorf("creating plugin directory: %w", err)
}
content, err := pluginFS.ReadFile("plugin/gastown.js")
if err != nil {
return fmt.Errorf("reading plugin template: %w", err)
}
if err := os.WriteFile(pluginPath, content, 0644); err != nil {
return fmt.Errorf("writing plugin: %w", err)
}
return nil
}

View File

@@ -0,0 +1,32 @@
// Gas Town OpenCode plugin: hooks SessionStart/Compaction via events.
export const GasTown = async ({ $, directory }) => {
const role = (process.env.GT_ROLE || "").toLowerCase();
const autonomousRoles = new Set(["polecat", "witness", "refinery", "deacon"]);
let didInit = false;
const run = async (cmd) => {
try {
await $`/bin/sh -lc ${cmd}`.cwd(directory);
} catch (err) {
console.error(`[gastown] ${cmd} failed`, err?.message || err);
}
};
const onSessionCreated = async () => {
if (didInit) return;
didInit = true;
await run("gt prime");
if (autonomousRoles.has(role)) {
await run("gt mail check --inject");
}
await run("gt nudge deacon session-started");
};
return {
event: async ({ event }) => {
if (event?.type === "session.created") {
await onSessionCreated();
}
},
};
};

View File

@@ -8,6 +8,7 @@ import (
"github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/claude"
"github.com/steveyegge/gastown/internal/config" "github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/opencode"
"github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/tmux"
) )
@@ -24,6 +25,8 @@ func EnsureSettingsForRole(workDir, role string, rc *config.RuntimeConfig) error
switch rc.Hooks.Provider { switch rc.Hooks.Provider {
case "claude": case "claude":
return claude.EnsureSettingsForRoleAt(workDir, role, rc.Hooks.Dir, rc.Hooks.SettingsFile) return claude.EnsureSettingsForRoleAt(workDir, role, rc.Hooks.Dir, rc.Hooks.SettingsFile)
case "opencode":
return opencode.EnsurePluginAt(workDir, rc.Hooks.Dir, rc.Hooks.SettingsFile)
default: default:
return nil return nil
} }
@@ -56,7 +59,7 @@ func StartupFallbackCommands(role string, rc *config.RuntimeConfig) []string {
if rc == nil { if rc == nil {
rc = config.DefaultRuntimeConfig() rc = config.DefaultRuntimeConfig()
} }
if rc.Hooks != nil && rc.Hooks.Provider == "claude" { if rc.Hooks != nil && rc.Hooks.Provider != "" && rc.Hooks.Provider != "none" {
return nil return nil
} }