From 63e00a6b9675a469bf28580b479184fec3969785 Mon Sep 17 00:00:00 2001 From: Michael Laccetti Date: Thu, 15 Oct 2026 16:38:53 -0400 Subject: [PATCH 1/3] feat: make harness linking harness-wide --- PROMPT-build.md | 4 +- cmd/muster/e2e_link_test.go | 13 +++-- docs/linking.md | 60 +++++++++------------ docs/projections/codex.md | 4 +- docs/projections/cursor.md | 10 +--- docs/upgrades.md | 2 +- internal/cli/link.go | 34 +++++------- internal/cli/link_integration_test.go | 12 +++-- internal/link/antigravity.go | 17 ------ internal/link/claude.go | 19 ------- internal/link/codex.go | 23 -------- internal/link/cursor.go | 26 +++------ internal/link/harness.go | 30 ----------- internal/link/link.go | 68 ++++++------------------ internal/link/link_test.go | 70 ++++++++++++------------- internal/link/manifest.go | 1 - internal/link/project.go | 42 --------------- internal/link/verify.go | 28 +--------- internal/paths/paths.go | 2 +- kit/link/cursor/muster-orchestrator.mdc | 8 --- kit/link/cursor/muster-worker.mdc | 8 --- 21 files changed, 118 insertions(+), 363 deletions(-) delete mode 100644 internal/link/project.go delete mode 100644 kit/link/cursor/muster-orchestrator.mdc delete mode 100644 kit/link/cursor/muster-worker.mdc diff --git a/PROMPT-build.md b/PROMPT-build.md index 48b2463..d2c1476 100644 --- a/PROMPT-build.md +++ b/PROMPT-build.md @@ -89,9 +89,9 @@ See [docs/architecture.md](docs/architecture.md) and [docs/linking.md](docs/link ## Linking -- Auto-detect harnesses on `PATH`; flags `--harness`, `--all`, `--global`, `--project`, `--unlink`. +- Auto-detect harnesses on `PATH`; flags `--harness`, `--all`, `--unlink`. Linking always targets harness-wide user configuration and never writes in the current repository. - Record state in `$MUSTER_HOME/link-manifest.json`. -- Codex: native `AGENTS.md` instructions + custom worker agent; Claude/Agy: settings JSON agent selector; Cursor: always-applied project rule (global User Rules remain a Settings-only step). +- Codex: native global `AGENTS.md` instructions + custom worker agent; Claude/Agy: settings JSON agent selector; Cursor: global User Rules require manual setup in Cursor Settings because there is no supported CLI or file interface. - Optional Antigravity shim: `agy-muster` under `$MUSTER_HOME/bin`. Legacy managed markdown blocks (`internal/link/block.go`) remain for upsert helpers/tests; primary delivery is symlinks + kit/link prompts. diff --git a/cmd/muster/e2e_link_test.go b/cmd/muster/e2e_link_test.go index 0f78f05..0ed86e8 100644 --- a/cmd/muster/e2e_link_test.go +++ b/cmd/muster/e2e_link_test.go @@ -11,7 +11,7 @@ import ( "github.com/mlaccetti/muster/internal/testutil" ) -func TestE2E_linkProjectNoRealHarness(t *testing.T) { +func TestE2E_linkHarnessWideNoRealHarness(t *testing.T) { home := t.TempDir() project := t.TempDir() bin := t.TempDir() @@ -31,16 +31,21 @@ func TestE2E_linkProjectNoRealHarness(t *testing.T) { t.Fatalf("install: %s", out.String()) } if cli.RunForTest([]string{ - "link", "--harness", "claude", "--project-dir", project, "--global=false", + "link", "--harness", "claude", }, &out, &out) != 0 { t.Fatalf("link: %s", out.String()) } - raw, err := os.ReadFile(filepath.Join(project, ".claude", "settings.json")) + raw, err := os.ReadFile(filepath.Join(home, ".claude", "settings.json")) if err != nil { t.Fatal(err) } if !strings.Contains(string(raw), "muster-orchestrator") { - t.Fatal(".claude/settings.json not linked") + t.Fatal("harness-wide .claude/settings.json not linked") + } + for _, path := range []string{"AGENTS.md", ".muster", ".claude", ".codex", ".cursor", ".agents"} { + if _, err := os.Lstat(filepath.Join(project, path)); !os.IsNotExist(err) { + t.Fatalf("link wrote project path %s (err=%v)", path, err) + } } if cli.RunForTest([]string{"doctor"}, &out, &out) != 0 { t.Fatalf("doctor: %s", out.String()) diff --git a/docs/linking.md b/docs/linking.md index 737c73b..8081511 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -1,49 +1,40 @@ # Linking harnesses -`muster install` lays down the versioned **kit** (prose policies and `link/` harness entrypoints). **`muster link`** symlinks those entrypoints into each harness and sets the **default orchestrator** selector. Assignments and handoffs are ordinary harness conversation; Muster requires no fixed message format. +`muster install` lays down the versioned **kit** (policy and harness entrypoints). **`muster link`** connects those entrypoints to harness-wide user configuration and selects Muster as the default orchestrator where the harness supports that setting. Link and unlink never write into the current repository. ## Behavior -| Mode | Default | What it does | -|------|---------|----------------| -| Auto-detect | yes (`muster link`) | Links only harnesses whose CLIs are on `PATH` (`claude`, `codex`, `cursor-agent`, `agy`) | -| `--harness` | optional | Link specific IDs even if not on `PATH` | -| `--all` | off | Link all four supported harnesses | -| `--global` | on | User-level harness config | -| `--project` | on | Repo-level harness config + `.muster/` pointers | -| `--project-dir` | cwd | Repo root to link | -| `--unlink` | off | Remove symlinks, config patches, and shims | - -State is recorded in `$MUSTER_HOME/link-manifest.json`. `muster doctor` reports link status and runs **`link.VerifyReports`** on each linked harness. - -## Per harness (global) - -| ID | Native activation | Linked definition | -|----|----------------------|---------------------------------------------| -| `claude` | `~/.claude/settings.json` → `"agent": "muster-orchestrator"` | `~/.claude/agents/muster-orchestrator.md`, `muster-worker.md` | -| `codex` | `~/.codex/AGENTS.md` (every Codex run) | `~/.codex/agents/muster-worker.toml` | -| `cursor` | Project-scoped `.cursor/rules/muster-orchestrator.mdc` with `alwaysApply: true`; global User Rules must be set in Cursor Settings | `.cursor/rules/muster-orchestrator.mdc` | -| `antigravity` (`agy`) | `~/.gemini/antigravity-cli/settings.json` → `"agent": "muster-orchestrator"` | `~/.gemini/config/agents/muster-orchestrator/agent.md`, `muster-worker/agent.md` | +| Command or option | Behavior | +|-------------------|----------| +| `muster link` | Auto-detect installed harness CLIs and link their harness-wide configuration | +| `--harness ` | Link a specific harness, whether or not its CLI is on `PATH` | +| `--all` | Attempt all supported harnesses | +| `--unlink` | Remove Muster's harness-wide configuration | -Codex also receives the orchestrator instructions in the linked project's root `AGENTS.md`. Its native worker agent uses `gpt-6-luna` with `medium` reasoning effort. Cursor CLI automatically loads the project rule; global Cursor User Rules must be configured in Cursor Settings. +State for successfully linked harnesses is recorded in `$MUSTER_HOME/link-manifest.json`. `muster doctor` checks each recorded link. -Project linking adds `.claude/`, `.codex/`, `.cursor/rules/`, or `.agents/agents/` equivalents under the repo. +## Harness support -## Optional shims +| ID | Harness-wide activation | Linked definition | +|----|------------------------|-------------------| +| `claude` | `~/.claude/settings.json` → `"agent": "muster-orchestrator"` | `~/.claude/agents/muster-orchestrator.md` | +| `codex` | `~/.codex/AGENTS.md` (loaded for every Codex run) | `~/.codex/agents/muster-worker.toml` | +| `cursor` | Add Muster policy to User Rules in Cursor Settings | Manual setup; the documented interface is Settings, with no supported CLI or file interface for configuring global User Rules | +| `antigravity` (`agy`) | `~/.gemini/antigravity-cli/settings.json` → `"agent": "muster-orchestrator"` | `~/.gemini/config/agents/muster-orchestrator/agent.md`, `muster-worker/agent.md` | -After link, `$MUSTER_HOME/bin/` contains: +Codex's worker agent uses `gpt-6-luna` with `medium` reasoning effort. Muster reports Cursor as skipped because it cannot configure Cursor's global User Rules through a supported interface. To use Muster in Cursor, copy the contents of `muster kit path` + `/policy/orchestrator.md` into Cursor Settings → Rules → User Rules. Cursor's worker and event behavior depends on the capabilities exposed by the active session. -- **`agy-muster`**: `agy --agent muster-orchestrator "$@"` +## Optional shims -Add `$MUSTER_HOME/bin` to `PATH` if you want those names instead of the stock CLIs. +After link, `$MUSTER_HOME/bin/` contains `agy-muster`, which runs `agy --agent muster-orchestrator "$@"`. Add that directory to `PATH` to use the shim. ## Validation ```bash muster install muster link --all -muster doctor # link.VerifyReports on manifest harnesses -muster doctor --probe # harness binaries on PATH +muster doctor # validate recorded harness-wide links +muster doctor --probe # probe harness binaries on PATH ``` **Claude** @@ -58,15 +49,12 @@ readlink ~/.claude/agents/muster-orchestrator.md ```bash rg 'MUSTER_LINK_START|default orchestrator' ~/.codex/AGENTS.md readlink ~/.codex/agents/muster-worker.toml -codex debug prompt-input "Summarize the active Muster instructions" # inspect loaded prompt inputs +codex debug prompt-input "Summarize the active Muster instructions" ``` -**cursor-agent** +**Cursor** -```bash -test -f .cursor/rules/muster-orchestrator.mdc -cursor-agent -p --output-format text "Summarize the active Muster project rule" -``` +Confirm Muster's orchestrator policy appears under Cursor Settings → Rules → User Rules. **agy** @@ -80,4 +68,4 @@ Re-run **`muster link`** after `muster install` upgrades the kit so symlinks tar ## Testing without real harnesses -CI uses `muster link --harness claude --project --global=false`. Package tests cover Codex global and project `AGENTS.md` instruction wiring, native worker agent files, Cursor project rules, and Antigravity global linking with temporary homes. +Package and CLI tests use temporary homes to validate harness-wide configuration, worker definitions, preservation of existing settings, unlink behavior, and the Cursor Settings limitation. Tests never write into the caller's project directory. diff --git a/docs/projections/codex.md b/docs/projections/codex.md index 364f9dd..a1cb23a 100644 --- a/docs/projections/codex.md +++ b/docs/projections/codex.md @@ -1,8 +1,8 @@ # Codex projection -Prefer **`muster link`**: it adds the orchestrator policy to native global and project `AGENTS.md` instructions and installs a native worker agent. +Use **`muster link`** to add the orchestrator policy to Codex's native global `AGENTS.md` instructions and install a native worker agent. -Codex has no separate root-agent selector in this setup: the active Codex session is the orchestrator because native instruction discovery loads the managed global and project guidance. The `muster-worker` custom agent is available for delegation. +Codex has no separate root-agent selector in this setup: the active Codex session is the orchestrator because native instruction discovery loads the managed global guidance. The `muster-worker` custom agent is available for delegation. ```bash rg 'MUSTER_LINK_START|default orchestrator' ~/.codex/AGENTS.md diff --git a/docs/projections/cursor.md b/docs/projections/cursor.md index 447387c..c9e05db 100644 --- a/docs/projections/cursor.md +++ b/docs/projections/cursor.md @@ -1,11 +1,5 @@ # Cursor Agent projection -Prefer **`muster link --project`**: it installs `.cursor/rules/muster-orchestrator.mdc` with `alwaysApply: true`. Cursor CLI loads project rules. Global User Rules are managed in Cursor Settings, so Muster cannot set them through a supported file path. - -```bash -test -f .cursor/rules/muster-orchestrator.mdc -cat .muster/orchestrator.path -``` - -Binary resolved as `cursor-agent`. The linked always-applied project rule makes the current session follow the Muster orchestrator policy. Cursor CLI documentation currently describes a single CLI agent and does not document a native subagent dispatch or lifecycle callback interface. The rule therefore tells the agent to use only worker and event tools actually exposed by the session; Cursor Background Agents are a separate capability. +Muster cannot configure Cursor's global User Rules through a supported CLI or file interface. Add the contents of Muster's `policy/orchestrator.md` to Cursor Settings → Rules → User Rules to make Muster the default orchestrator for Cursor. +Cursor Agent resolves as `cursor-agent`. Worker delegation and event-driven waiting depend on capabilities exposed by the active Cursor session; the projection policy asks the orchestrator to use only capabilities actually available. diff --git a/docs/upgrades.md b/docs/upgrades.md index e125870..acd2e8f 100644 --- a/docs/upgrades.md +++ b/docs/upgrades.md @@ -12,7 +12,7 @@ muster link # refresh harness snippets if you use link The CLI checks GitHub **at most once per day** for a newer release and prints a hint to stderr: ```text -muster v0.2.0 is available (you have v0.1.0). Run: brew update && brew upgrade muster +muster v0.3.0 is available (you have v0.2.0). Run: brew update && brew upgrade muster ``` Skip the check in CI with `MUSTER_SKIP_UPDATE_CHECK=1`. `dev` builds never notify. diff --git a/internal/cli/link.go b/internal/cli/link.go index f19ab62..604fd32 100644 --- a/internal/cli/link.go +++ b/internal/cli/link.go @@ -9,36 +9,30 @@ import ( func newLinkCmd() *cobra.Command { var ( - projectDir string - harnesses []string - all bool - global bool - project bool - dryRun bool - unlink bool + harnesses []string + all bool + dryRun bool + unlink bool ) cmd := &cobra.Command{ Use: "link", Short: "Wire muster orchestrator and worker policies into harness configs", Long: `Link installs managed snippets that point every harness at the same kit policies. -By default, muster auto-detects harnesses whose CLIs are on PATH and links both global -(user) and project (current repo) entrypoints. Use --harness to pin specific IDs, or ---all to link every supported harness even when not installed.`, +By default, muster auto-detects harnesses whose CLIs are on PATH and links their +harness-wide (user) configuration. Use --harness to pin specific IDs, or --all to +link every supported harness even when not installed.`, Example: ` muster install muster link - muster link --harness claude --harness codex --project-dir . + muster link --harness claude --harness codex muster link --all --dry-run muster link --unlink`, RunE: func(cmd *cobra.Command, args []string) error { res, err := link.Run(link.Options{ - ProjectDir: projectDir, - Harnesses: harnesses, - All: all, - Global: global, - Project: project, - DryRun: dryRun, - Unlink: unlink, + Harnesses: harnesses, + All: all, + DryRun: dryRun, + Unlink: unlink, }) if err != nil { return err @@ -48,7 +42,6 @@ By default, muster auto-detects harnesses whose CLIs are on PATH and links both return nil } writef(cmd.OutOrStdout(), "kit: %s\n", res.KitDir) - writef(cmd.OutOrStdout(), "project: %s\n", res.Project) for _, h := range res.Harnesses { writef(cmd.OutOrStdout(), "linked %s:\n", h.ID) for _, t := range h.Targets { @@ -64,11 +57,8 @@ By default, muster auto-detects harnesses whose CLIs are on PATH and links both return nil }, } - cmd.Flags().StringVar(&projectDir, "project-dir", "", "Project directory (default: cwd)") cmd.Flags().StringArrayVar(&harnesses, "harness", nil, "Harness id (repeatable); default: auto-detect on PATH") cmd.Flags().BoolVar(&all, "all", false, "Link all supported harnesses (do not require CLI on PATH)") - cmd.Flags().BoolVar(&global, "global", true, "Link user-level harness config") - cmd.Flags().BoolVar(&project, "project", true, "Link project-level config (.muster, .claude/, .cursor/rules, etc.)") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Print actions without writing files") cmd.Flags().BoolVar(&unlink, "unlink", false, "Remove managed muster snippets") return cmd diff --git a/internal/cli/link_integration_test.go b/internal/cli/link_integration_test.go index a488643..fdcd8eb 100644 --- a/internal/cli/link_integration_test.go +++ b/internal/cli/link_integration_test.go @@ -10,25 +10,27 @@ import ( "github.com/mlaccetti/muster/internal/kit" ) -func TestCLI_linkProjectClaude(t *testing.T) { +func TestCLI_linkClaudeHarnessWide(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("MUSTER_HOME", filepath.Join(home, "muster")) - project := t.TempDir() if _, err := kit.Install(); err != nil { t.Fatal(err) } code := cli.RunForTest([]string{ - "link", "--harness", "claude", "--project-dir", project, "--global=false", + "link", "--harness", "claude", }, nil, nil) if code != 0 { t.Fatalf("link exit %d", code) } - raw, err := os.ReadFile(filepath.Join(project, ".claude", "settings.json")) + raw, err := os.ReadFile(filepath.Join(home, ".claude", "settings.json")) if err != nil { t.Fatal(err) } if !strings.Contains(string(raw), "muster-orchestrator") { - t.Fatalf("project settings not linked: %s", raw) + t.Fatalf("harness-wide settings not linked: %s", raw) + } + if _, err := os.Stat(filepath.Join(home, ".codex")); !os.IsNotExist(err) { + t.Fatalf("explicit harness selection linked an unrelated harness, err=%v", err) } } diff --git a/internal/link/antigravity.go b/internal/link/antigravity.go index 88a4299..bfe571a 100644 --- a/internal/link/antigravity.go +++ b/internal/link/antigravity.go @@ -78,20 +78,3 @@ func unlinkAntigravityGlobal(dryRun bool) error { } return restoreAgyDefaultAgent(filepath.Join(settingsDir, "settings.json"), filepath.Join(settingsDir, ".muster-link.json"), dryRun) } - -func linkAntigravityProject(projectDir, kitDir string, dryRun bool) ([]string, error) { - dir := filepath.Join(projectDir, ".agents", "agents", orchestratorAgentName) - target := filepath.Join(dir, "agent.md") - if err := linkKitFileDryRun(target, dryRun, kitDir, "agy", orchestratorAgentName, "agent.md"); err != nil { - return nil, err - } - return []string{target}, nil -} - -func unlinkAntigravityProject(projectDir string, dryRun bool) error { - if !dryRun { - _ = removeLink(filepath.Join(projectDir, ".agents", "agents", orchestratorAgentName, "agent.md")) - _ = os.RemoveAll(filepath.Join(projectDir, ".agents", "agents", orchestratorAgentName)) - } - return nil -} diff --git a/internal/link/claude.go b/internal/link/claude.go index 444a178..c2882a9 100644 --- a/internal/link/claude.go +++ b/internal/link/claude.go @@ -13,10 +13,6 @@ func claudeGlobalDir() (string, error) { return filepath.Join(home, ".claude"), nil } -func claudeProjectDir(projectDir string) string { - return filepath.Join(projectDir, ".claude") -} - func linkClaudeGlobal(kitDir string, dryRun bool) ([]string, error) { dir, err := claudeGlobalDir() if err != nil { @@ -49,18 +45,3 @@ func unlinkClaudeGlobal(dryRun bool) error { } return nil } - -func linkClaudeProject(projectDir, kitDir string, dryRun bool) ([]string, error) { - return linkClaudeAgentWiring(claudeProjectDir(projectDir), kitDir, dryRun) -} - -func unlinkClaudeProject(projectDir string, dryRun bool) error { - target := filepath.Join(projectDir, "CLAUDE.md") - if err := patchFile(target, removeManagedBlock, dryRun); err != nil { - return err - } - if !dryRun { - _ = os.Remove(filepath.Join(projectDir, ".muster", "claude-worker.md")) - } - return unlinkClaudeAgentWiring(claudeProjectDir(projectDir), dryRun) -} diff --git a/internal/link/codex.go b/internal/link/codex.go index 231cd1b..aeb5932 100644 --- a/internal/link/codex.go +++ b/internal/link/codex.go @@ -72,26 +72,3 @@ func unlinkCodexGlobal(dryRun bool) error { } return nil } - -func linkCodexProject(projectDir, kitDir string, dryRun bool) ([]string, error) { - if !dryRun { - _ = removeLink(filepath.Join(projectDir, ".codex", "muster-orchestrator.config.toml")) - } - workerAgent := filepath.Join(projectDir, ".codex", "agents", workerAgentName+".toml") - if err := linkKitFileDryRun(workerAgent, dryRun, kitDir, "codex", workerAgentName+".config.toml"); err != nil { - return nil, err - } - agentsPath := filepath.Join(projectDir, "AGENTS.md") - if err := patchFile(agentsPath, func(s string) string { return upsertManagedBlock(s, orchestratorBlock(kitDir)) }, dryRun); err != nil { - return nil, err - } - return []string{workerAgent, agentsPath}, nil -} - -func unlinkCodexProject(projectDir string, dryRun bool) error { - if !dryRun { - _ = removeLink(filepath.Join(projectDir, ".codex", "agents", workerAgentName+".toml")) - _ = removeLink(filepath.Join(projectDir, ".codex", "muster-orchestrator.config.toml")) - } - return patchFile(filepath.Join(projectDir, "AGENTS.md"), removeManagedBlock, dryRun) -} diff --git a/internal/link/cursor.go b/internal/link/cursor.go index c1416bd..db4ae09 100644 --- a/internal/link/cursor.go +++ b/internal/link/cursor.go @@ -1,10 +1,14 @@ package link import ( + "errors" + "fmt" "os" "path/filepath" ) +var errManualHarnessSetup = errors.New("requires manual harness setup") + func cursorRulesDir() (string, error) { home, err := os.UserHomeDir() if err != nil { @@ -14,9 +18,9 @@ func cursorRulesDir() (string, error) { } func linkCursorGlobal(_ string, _ bool) ([]string, error) { - // Cursor User Rules are configured in Cursor Settings, not in ~/.cursor/rules. - // Global rule installation is not supported by the filesystem linker. - return nil, nil + // Cursor's documented global User Rules are edited in Cursor Settings, so + // filesystem linking cannot configure this harness-wide setting. + return nil, fmt.Errorf("global User Rules must be added in Cursor Settings; no supported CLI or file interface is documented: %w", errManualHarnessSetup) } func unlinkCursorGlobal(dryRun bool) error { @@ -30,19 +34,3 @@ func unlinkCursorGlobal(dryRun bool) error { } return nil } - -func linkCursorProject(projectDir, kitDir string, dryRun bool) ([]string, error) { - dir := filepath.Join(projectDir, ".cursor", "rules") - target := filepath.Join(dir, "muster-orchestrator.mdc") - if err := linkKitFileDryRun(target, dryRun, kitDir, "cursor", "muster-orchestrator.mdc"); err != nil { - return nil, err - } - return []string{target}, nil -} - -func unlinkCursorProject(projectDir string, dryRun bool) error { - if !dryRun { - _ = removeLink(filepath.Join(projectDir, ".cursor", "rules", "muster-orchestrator.mdc")) - } - return nil -} diff --git a/internal/link/harness.go b/internal/link/harness.go index b5749f1..534960b 100644 --- a/internal/link/harness.go +++ b/internal/link/harness.go @@ -25,21 +25,6 @@ func (h harnessWiring) linkGlobal(kitDir string, dryRun bool) ([]string, error) } } -func (h harnessWiring) linkProject(projectDir, kitDir string, dryRun bool) ([]string, error) { - switch h.id { - case "claude": - return linkClaudeProject(projectDir, kitDir, dryRun) - case "codex": - return linkCodexProject(projectDir, kitDir, dryRun) - case "cursor": - return linkCursorProject(projectDir, kitDir, dryRun) - case "antigravity": - return linkAntigravityProject(projectDir, kitDir, dryRun) - default: - return nil, fmt.Errorf("unknown harness %q", h.id) - } -} - func (h harnessWiring) unlinkGlobal(dryRun bool) error { switch h.id { case "claude": @@ -55,21 +40,6 @@ func (h harnessWiring) unlinkGlobal(dryRun bool) error { } } -func (h harnessWiring) unlinkProject(projectDir string, dryRun bool) error { - switch h.id { - case "claude": - return unlinkClaudeProject(projectDir, dryRun) - case "codex": - return unlinkCodexProject(projectDir, dryRun) - case "cursor": - return unlinkCursorProject(projectDir, dryRun) - case "antigravity": - return unlinkAntigravityProject(projectDir, dryRun) - default: - return fmt.Errorf("unknown harness %q", h.id) - } -} - func patchFile(path string, mutate func(string) string, dryRun bool) error { if dryRun { return nil diff --git a/internal/link/link.go b/internal/link/link.go index b337a59..75c5abc 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -1,6 +1,7 @@ package link import ( + "errors" "fmt" "os" @@ -13,18 +14,14 @@ func adapterAllIDs() []string { } type Options struct { - ProjectDir string - Harnesses []string // empty = auto-detect on PATH (unless All) - All bool // link all known harness IDs without PATH detect - Global bool - Project bool - DryRun bool - Unlink bool + Harnesses []string // empty = auto-detect on PATH (unless All) + All bool // link all known harness IDs without PATH detect + DryRun bool + Unlink bool } type Result struct { KitDir string - Project string Harnesses []HarnessLink Skipped []string } @@ -40,14 +37,6 @@ func Run(opts Options) (*Result, error) { } } - projectDir := opts.ProjectDir - if projectDir == "" { - projectDir, err = os.Getwd() - if err != nil { - return nil, err - } - } - var ids []string var err2 error if opts.All { @@ -62,49 +51,30 @@ func Run(opts Options) (*Result, error) { return nil, fmt.Errorf("no harness CLIs found on PATH; install a CLI, pass --harness , or --all") } - res := &Result{KitDir: kitDir, Project: projectDir} + res := &Result{KitDir: kitDir} autoDetect := !opts.All && len(opts.Harnesses) == 0 for _, id := range ids { h := harnessWiring{id: id} if opts.Unlink { - if opts.Global { - if err := h.unlinkGlobal(opts.DryRun); err != nil { - return nil, fmt.Errorf("%s global unlink: %w", id, err) - } - } - if opts.Project { - if err := h.unlinkProject(projectDir, opts.DryRun); err != nil { - return nil, fmt.Errorf("%s project unlink: %w", id, err) - } + if err := h.unlinkGlobal(opts.DryRun); err != nil { + return nil, fmt.Errorf("%s unlink: %w", id, err) } continue } var targets []string - if opts.Global { - t, err := h.linkGlobal(kitDir, opts.DryRun) - if err != nil { - if autoDetect { - res.Skipped = append(res.Skipped, id+": "+err.Error()) - continue - } - return nil, fmt.Errorf("%s global link: %w", id, err) - } - targets = append(targets, t...) - } - if opts.Project { - t, err := h.linkProject(projectDir, kitDir, opts.DryRun) - if err != nil { - return nil, fmt.Errorf("%s project link: %w", id, err) + t, err := h.linkGlobal(kitDir, opts.DryRun) + if err != nil { + if autoDetect || errors.Is(err, errManualHarnessSetup) { + res.Skipped = append(res.Skipped, id+": "+err.Error()) + continue } - targets = append(targets, t...) + return nil, fmt.Errorf("%s harness link: %w", id, err) } + targets = append(targets, t...) res.Harnesses = append(res.Harnesses, HarnessLink{ID: id, Targets: targets}) } if opts.Unlink { - if opts.Project && !opts.DryRun { - _ = unlinkProject(projectDir, false) - } _ = removeLinkShims(opts.DryRun) if !opts.DryRun { _ = os.Remove(manifestPathSafe()) @@ -112,17 +82,11 @@ func Run(opts Options) (*Result, error) { return res, nil } - if opts.Project { - if _, err := linkProject(projectDir, kitDir, opts.DryRun); err != nil { - return nil, err - } - } - if err := installLinkShims(opts.DryRun); err != nil { return nil, err } if !opts.DryRun { - m := &Manifest{KitDir: kitDir, Project: projectDir, Harnesses: res.Harnesses} + m := &Manifest{KitDir: kitDir, Harnesses: res.Harnesses} if err := SaveManifest(m); err != nil { return nil, err } diff --git a/internal/link/link_test.go b/internal/link/link_test.go index f4316be..7ab2ea6 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -10,11 +10,10 @@ import ( "github.com/mlaccetti/muster/internal/kit" ) -func TestLinkProjectClaudeOnly(t *testing.T) { +func TestLinkClaudeHarnessWide(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("MUSTER_HOME", filepath.Join(home, "muster")) - project := t.TempDir() kitDir, err := kit.Install() if err != nil { @@ -22,10 +21,7 @@ func TestLinkProjectClaudeOnly(t *testing.T) { } res, err := Run(Options{ - ProjectDir: project, - Harnesses: []string{"claude"}, - Global: false, - Project: true, + Harnesses: []string{"claude"}, }) if err != nil { t.Fatal(err) @@ -33,7 +29,7 @@ func TestLinkProjectClaudeOnly(t *testing.T) { if len(res.Harnesses) != 1 || res.Harnesses[0].ID != "claude" { t.Fatalf("res %+v", res) } - settingsPath := filepath.Join(project, ".claude", "settings.json") + settingsPath := filepath.Join(home, ".claude", "settings.json") raw, err := os.ReadFile(settingsPath) if err != nil { t.Fatal(err) @@ -45,7 +41,7 @@ func TestLinkProjectClaudeOnly(t *testing.T) { if settings["agent"] != orchestratorAgentName { t.Fatalf("settings agent: %+v", settings) } - orchAgent := filepath.Join(project, ".claude", "agents", orchestratorAgentName+".md") + orchAgent := filepath.Join(home, ".claude", "agents", orchestratorAgentName+".md") target, err := filepath.EvalSymlinks(orchAgent) if err != nil { t.Fatal(err) @@ -54,13 +50,13 @@ func TestLinkProjectClaudeOnly(t *testing.T) { if target != want { t.Fatalf("agent link %s != %s", target, want) } - if _, err := os.Stat(filepath.Join(project, ".muster", "orchestrator.path")); err != nil { - t.Fatal(err) - } m, err := LoadManifest() if err != nil || len(m.Harnesses) != 1 { t.Fatalf("manifest %+v err %v", m, err) } + if m.Harnesses[0].ID != "claude" || len(m.Harnesses[0].Targets) == 0 { + t.Fatalf("manifest does not describe harness-wide targets: %+v", m.Harnesses[0]) + } } func TestLinkGlobalCodexCursorAgy(t *testing.T) { @@ -73,10 +69,8 @@ func TestLinkGlobalCodexCursorAgy(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = Run(Options{ + res, err := Run(Options{ Harnesses: []string{"codex", "cursor", "antigravity"}, - Global: true, - Project: false, }) if err != nil { t.Fatal(err) @@ -101,6 +95,16 @@ func TestLinkGlobalCodexCursorAgy(t *testing.T) { if _, err := os.Stat(filepath.Join(home, ".cursor", "rules", "muster-orchestrator.mdc")); !os.IsNotExist(err) { t.Fatalf("Cursor global rules are not a supported filesystem target; got err %v", err) } + if len(res.Skipped) != 1 || !strings.Contains(res.Skipped[0], "Cursor Settings") { + t.Fatalf("Cursor setup limitation should be reported: %+v", res.Skipped) + } + manifest, err := LoadManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Harnesses) != 2 { + t.Fatalf("manifest should include only configured harnesses: %+v", manifest.Harnesses) + } agyAgent := filepath.Join(home, ".gemini", "config", "agents", orchestratorAgentName, "agent.md") if _, err := os.Stat(agyAgent); err != nil { t.Fatal(err) @@ -111,7 +115,7 @@ func TestLinkGlobalCodexCursorAgy(t *testing.T) { } } -func TestLinkProjectCodexCursorDefaultInstructions(t *testing.T) { +func TestLinkNeverWritesProjectFiles(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("CODEX_HOME", filepath.Join(home, ".codex")) @@ -120,54 +124,46 @@ func TestLinkProjectCodexCursorDefaultInstructions(t *testing.T) { if err := os.WriteFile(filepath.Join(project, "AGENTS.md"), []byte("# Existing project guidance\n"), 0o644); err != nil { t.Fatal(err) } - kitDir, err := kit.Install() - if err != nil { + if _, err := kit.Install(); err != nil { t.Fatal(err) } - if _, err := Run(Options{ProjectDir: project, Harnesses: []string{"codex", "cursor"}, Global: false, Project: true}); err != nil { + if _, err := Run(Options{Harnesses: []string{"codex", "cursor"}}); err != nil { t.Fatal(err) } - projectInstructions, err := os.ReadFile(filepath.Join(project, "AGENTS.md")) - if err != nil || !strings.Contains(string(projectInstructions), markerStart) || !strings.Contains(string(projectInstructions), "Existing project guidance") { - t.Fatalf("project instructions did not preserve and add managed block: %v %s", err, projectInstructions) - } - workerAgent := filepath.Join(project, ".codex", "agents", workerAgentName+".toml") - if target, err := filepath.EvalSymlinks(workerAgent); err != nil || target != filepath.Join(kitDir, "link", "codex", workerAgentName+".config.toml") { - t.Fatalf("project worker agent: %v %s", err, target) + guidance, err := os.ReadFile(filepath.Join(project, "AGENTS.md")) + if err != nil || string(guidance) != "# Existing project guidance\n" { + t.Fatalf("project guidance changed: %v %q", err, guidance) } - cursorRule := filepath.Join(project, ".cursor", "rules", "muster-orchestrator.mdc") - if target, err := filepath.EvalSymlinks(cursorRule); err != nil || target != filepath.Join(kitDir, "link", "cursor", "muster-orchestrator.mdc") { - t.Fatalf("project Cursor rule: %v %s", err, target) + for _, path := range []string{".codex", ".cursor", ".claude", ".agents", ".muster"} { + if _, err := os.Lstat(filepath.Join(project, path)); !os.IsNotExist(err) { + t.Fatalf("link wrote project path %s (err=%v)", path, err) + } } manifest, err := LoadManifest() if err != nil { t.Fatal(err) } if issues := VerifyReports(manifest); len(issues) != 0 { - t.Fatalf("linked Codex and Cursor project wiring failed verification: %v", issues) + t.Fatalf("linked Codex harness wiring failed verification: %v", issues) } } func TestLinkAllWithoutGlobalPaths(t *testing.T) { home := t.TempDir() t.Setenv("MUSTER_HOME", filepath.Join(home, "muster")) - project := t.TempDir() _, err := kit.Install() if err != nil { t.Fatal(err) } res, err := Run(Options{ - ProjectDir: project, - All: true, - Global: false, - Project: true, - DryRun: true, + All: true, + DryRun: true, }) if err != nil { t.Fatal(err) } - if len(res.Harnesses) != 4 { - t.Fatalf("expected 4 harnesses, got %d", len(res.Harnesses)) + if len(res.Harnesses) != 3 || len(res.Skipped) != 1 { + t.Fatalf("expected three configured harnesses and Cursor Settings guidance, got %+v", res) } } diff --git a/internal/link/manifest.go b/internal/link/manifest.go index d3f3390..9392d7e 100644 --- a/internal/link/manifest.go +++ b/internal/link/manifest.go @@ -15,7 +15,6 @@ type Manifest struct { Version int `json:"version"` UpdatedAt string `json:"updated_at"` KitDir string `json:"kit_dir"` - Project string `json:"project,omitempty"` Harnesses []HarnessLink `json:"harnesses"` } diff --git a/internal/link/project.go b/internal/link/project.go deleted file mode 100644 index aacd855..0000000 --- a/internal/link/project.go +++ /dev/null @@ -1,42 +0,0 @@ -package link - -import ( - "os" - "path/filepath" -) - -func linkProject(projectDir, kitDir string, dryRun bool) (string, error) { - dir := filepath.Join(projectDir, ".muster") - if dryRun { - return dir, nil - } - if err := os.MkdirAll(dir, 0o755); err != nil { - return "", err - } - readme := `# Muster project wiring - -Orchestrator policy: ` + kitDir + `/policy/orchestrator.md -Worker policy: ` + kitDir + `/policy/worker.md - -Harness-specific entrypoints are linked by ` + "`muster link`" + ` (see $MUSTER_HOME/link-manifest.json). -` - if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte(readme), 0o644); err != nil { - return "", err - } - // Stable relative pointers for tools that read from the repo. - if err := os.WriteFile(filepath.Join(dir, "orchestrator.path"), []byte(kitDir+"/policy/orchestrator.md\n"), 0o644); err != nil { - return "", err - } - if err := os.WriteFile(filepath.Join(dir, "worker.path"), []byte(kitDir+"/policy/worker.md\n"), 0o644); err != nil { - return "", err - } - return dir, nil -} - -func unlinkProject(projectDir string, dryRun bool) error { - dir := filepath.Join(projectDir, ".muster") - if dryRun { - return nil - } - return os.RemoveAll(dir) -} diff --git a/internal/link/verify.go b/internal/link/verify.go index 93252ed..f0103e2 100644 --- a/internal/link/verify.go +++ b/internal/link/verify.go @@ -81,35 +81,11 @@ func verifyCodex(kitDir string, targets []string) []string { issues = append(issues, globalInstructions+": muster orchestrator instructions missing") } } - for _, target := range targets { - if target != globalAgent && strings.HasSuffix(target, filepath.Join(".codex", "agents", workerAgentName+".toml")) { - projectDir := filepath.Dir(filepath.Dir(filepath.Dir(target))) - projectAgent := filepath.Join(projectDir, ".codex", "agents", workerAgentName+".toml") - if msg := verifySymlinkToKit(projectAgent, kitDir, "codex", workerAgentName+".config.toml"); msg != "" { - issues = append(issues, msg) - } - projectInstructions, err := os.ReadFile(filepath.Join(projectDir, "AGENTS.md")) - if err != nil || !strings.Contains(string(projectInstructions), markerStart) { - issues = append(issues, filepath.Join(projectDir, "AGENTS.md")+": muster orchestrator instructions missing") - } - } - } return issues } -func verifyCursor(kitDir string, targets []string) []string { - for _, target := range targets { - if strings.HasSuffix(target, filepath.Join(".cursor", "rules", "muster-orchestrator.mdc")) { - if msg := verifySymlinkToKit(target, kitDir, "cursor", "muster-orchestrator.mdc"); msg != "" { - return []string{msg} - } - return nil - } - } - if len(targets) == 0 { - return []string{"cursor: global User Rules must be configured in Cursor Settings; no project rule is linked"} - } - return []string{"cursor: project-scoped Muster rule missing"} +func verifyCursor(_ string, _ []string) []string { + return []string{"cursor: global User Rules must be configured in Cursor Settings; Muster cannot configure them through a supported CLI or file interface"} } func hasTarget(targets []string, want string) bool { diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 8c19d75..608fa7e 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -5,7 +5,7 @@ import ( "path/filepath" ) -const KitVersion = "v0.2.2" +const KitVersion = "v0.3.0" func Home() (string, error) { if v := os.Getenv("MUSTER_HOME"); v != "" { diff --git a/kit/link/cursor/muster-orchestrator.mdc b/kit/link/cursor/muster-orchestrator.mdc deleted file mode 100644 index e6fafcc..0000000 --- a/kit/link/cursor/muster-orchestrator.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: Default Muster orchestrator. Delegate bounded work in parallel, integrate, and verify. -alwaysApply: true ---- - -# Muster orchestrator - -Read and follow the orchestrator policy path recorded in `.muster/orchestrator.path`. Keep coordination small and do small tasks directly when delegation adds overhead. Use only worker, parallelism, worktree, and lifecycle tools actually exposed by this Cursor Agent session; do not claim a handoff or wait for an event the harness cannot produce. Integrate and verify any work returned by native agents. diff --git a/kit/link/cursor/muster-worker.mdc b/kit/link/cursor/muster-worker.mdc deleted file mode 100644 index 4d0a7bf..0000000 --- a/kit/link/cursor/muster-worker.mdc +++ /dev/null @@ -1,8 +0,0 @@ ---- -description: Muster worker for one bounded task; return a concise conversational handoff. -alwaysApply: false ---- - -# Muster worker - -Do only the assigned bounded task. Preserve unrelated work, surface unclear or conflicting scope before expanding it, and do not sub-delegate or take on adjacent tasks. Do not commit, push, queue or merge pull requests, or publish releases unless explicitly assigned. Report relevant checks with exact commands and observed results, then return a concise conversational handoff. No fixed response format is required. From f0e5a698e2dccb8d536c1b09e0099584733b6aa0 Mon Sep 17 00:00:00 2001 From: Michael Laccetti Date: Thu, 15 Oct 2026 16:39:29 -0400 Subject: [PATCH 2/3] docs: remove build prompt document --- PROMPT-build.md | 141 ------------------------------------------------ 1 file changed, 141 deletions(-) delete mode 100644 PROMPT-build.md diff --git a/PROMPT-build.md b/PROMPT-build.md deleted file mode 100644 index d2c1476..0000000 --- a/PROMPT-build.md +++ /dev/null @@ -1,141 +0,0 @@ -# Agent prompt: build **muster** (standalone policy + link) - -Use this document as the **single source of truth** for **muster**: a public Go CLI and installable **kit** (orchestrator + worker policy, harness link entrypoints) that gives the same orchestration behavior across coding agents **without** tying sessions to one vendor. - -Operators need only `muster install` and `muster link`. - -**Upstream:** `github.com/mlaccetti/muster` (public). -**Homebrew:** `github.com/mlaccetti/homebrew-tap` (formula `muster`). -**License:** MIT. - ---- - -## Name and positioning - -**Project name:** `muster` -**CLI binary:** `muster` - -**Tagline (README):** *Native orchestration, shared policy, prose handoffs.* - ---- - -## Mission - -Deliver in one installable unit: - -1. **Kit (data):** Versioned `kit/policy/orchestrator.md`, `kit/policy/worker.md`, and `kit/link/*` harness entrypoints (embedded, versioned). -2. **CLI (code):** `muster install`, `muster link`, `muster doctor`, `muster kit path`. -3. **Docs:** Architecture, linking, upgrades, and short per-harness projections under `docs/projections/`. - -**Supported harness IDs** (closed set): - -| ID | Real binary | -|----|-------------| -| `claude` | `claude` | -| `codex` | `codex` | -| `cursor` | `cursor-agent` | -| `antigravity` | `agy` | - -**Default install location:** - -| Variable | Default | -|----------|---------| -| `MUSTER_HOME` | `$XDG_CONFIG_HOME/muster` or `~/.config/muster` | -| Kit files | `$MUSTER_HOME/kit//` with `current` symlink | - ---- - -## Non-goals (this repo) - -- No `muster worker`, `muster dispatch`, or `muster pass` subcommands. -- No harness subprocess dispatch, tier pools, brief rendering, or pass runners in this codebase. -- No required structured format for orchestrator-to-worker assignments or worker handoffs. -- No calls to real `claude` / `codex` / `cursor-agent` / `agy` in CI or default tests. -- No `time.Sleep` in production code; gate with `scripts/check-no-sleep.sh`. - ---- - -## Architecture - -```text -┌────────────────────────────────────────────┐ -│ muster CLI: install, link, doctor, kit │ -├────────────────────────────────────────────┤ -│ Kit: policy + kit/link harness entrypoints │ -└────────────────────────────────────────────┘ -``` - -Orchestrator policy: plan units, delegate to **worker** subagents in the harness, verify evidence. - -The orchestrator keeps coordination small, delegates bounded work, parallelizes independent tasks, and uses native lifecycle events or callbacks to wait. Use `gh-stacking` for dependent pull requests. After required checks are green, add the exact `@mergify queue` comment and verify Mergify, merge, and post-merge CI events. Do not poll or sleep. - -Worker assignments and handoffs are ordinary harness conversation. Muster must not define a required schema, status enum, or serialized task/result artifact. - -See [docs/architecture.md](docs/architecture.md) and [docs/linking.md](docs/linking.md). - ---- - -## CLI surface - -| Command | Purpose | -|---------|---------| -| `muster install` | Lay down embedded kit under `MUSTER_HOME` | -| `muster link` | Symlink `kit/link/*`, patch harness defaults, optional shims | -| `muster unlink` | Remove link artifacts | -| `muster doctor` | Kit path, link manifest, `link.VerifyReports`, optional `--probe` | -| `muster kit path` | Print active kit directory | - ---- - -## Linking - -- Auto-detect harnesses on `PATH`; flags `--harness`, `--all`, `--unlink`. Linking always targets harness-wide user configuration and never writes in the current repository. -- Record state in `$MUSTER_HOME/link-manifest.json`. -- Codex: native global `AGENTS.md` instructions + custom worker agent; Claude/Agy: settings JSON agent selector; Cursor: global User Rules require manual setup in Cursor Settings because there is no supported CLI or file interface. -- Optional Antigravity shim: `agy-muster` under `$MUSTER_HOME/bin`. - -Legacy managed markdown blocks (`internal/link/block.go`) remain for upsert helpers/tests; primary delivery is symlinks + kit/link prompts. - ---- - -## Testing - -- Unit and integration tests with temp `MUSTER_HOME` and temp `HOME` / `CODEX_HOME`. -- `cmd/muster/e2e_link_test.go` subprocess smoke where needed. -- `just check`: fmt, lint, check-no-sleep, `go test ./... -race`. -- No network in CI after module download. - ---- - -## CI and release - -- `.github/workflows/ci.yml`: `just check` on Ubuntu. -- Tag `v*` → GoReleaser, Homebrew formula in `mlaccetti/homebrew-tap`. - ---- - -## README requirements - -1. Mission and install (`brew`, `go install`, `muster install`, `muster link`). -2. Projections section. -3. Environment table, `just check`, MIT license. - ---- - -## Acceptance criteria - -- [ ] `just check` green without real harness binaries. -- [ ] `muster install` + `muster link` + `muster doctor` work in tests. -- [ ] Orchestrator link prompts delegate to workers; kit policy matches docs. -- [ ] Policies require parallel independent work, event-driven waits, `gh-stacking`, and the exact Mergify queue comment without imposing a handoff schema. - ---- - -## Agent invocation - -```text -Implement or maintain **muster** per PROMPT-build.md. - -Module: github.com/mlaccetti/muster. CLI: Cobra (+ Fang/Carapace as in repo). -Hard constraints: install/link kit only; no worker/dispatch/pass subcommands; no real harness binaries in CI. -``` From d14b2e48f0cfaa9b1767ad1afe211dae05b80dc7 Mon Sep 17 00:00:00 2001 From: Michael Laccetti Date: Thu, 15 Oct 2026 17:42:08 -0400 Subject: [PATCH 3/3] docs: allow nonblocking worker check watchers --- kit/policy/orchestrator.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kit/policy/orchestrator.md b/kit/policy/orchestrator.md index 4d52db8..ff2e1f2 100644 --- a/kit/policy/orchestrator.md +++ b/kit/policy/orchestrator.md @@ -13,7 +13,7 @@ You are the default orchestrator for this harness session. Use its native worker - For substantial work, split the goal into bounded tasks with distinct outcomes and clear file or component ownership. State dependencies, relevant context, completion expectations, verification, and out-of-scope work in ordinary prose. Adapt the detail to the task; there is no fixed task format. - Start independent tasks concurrently. Serialize dependent tasks and tasks whose write ownership overlaps. Use native isolated worktrees for parallel changes. Use `gh-stacking` when dependent pull requests need a reviewable stack. - Workers execute only their assignment and do not sub-delegate. If an assignment is unclear or cannot be completed within its scope, they should ask or report the issue. -- Wait for worker completion through native lifecycle events or callbacks. Do not poll, sleep, or invent progress. If the harness lacks a needed event source, state the limitation and use an available callback or subscription before claiming the wait is complete. +- Wait for worker completion through native lifecycle events or callbacks. Do not poll, sleep, or block the orchestrator's main session while waiting. A dedicated worker may run a watcher such as `gh run watch` when that watcher does not block the orchestrator; keep making independent progress while it waits, then receive and verify its result through the worker's completion event or callback. Do not invent progress. If the harness lacks a needed event source, state the limitation and use an available callback or subscription before claiming the wait is complete. ## Integrate and verify @@ -25,7 +25,7 @@ You are the default orchestrator for this harness session. Use its native worker - On a pull request, wait until required checks and reviews are green before adding the exact GitHub comment `@mergify queue`. - After commenting, wait for the Mergify event or callback and verify Mergify's response and the actual merge. Then wait for post-merge CI on the target branch and verify it is green. -- Never infer queue success, merge completion, or post-merge CI from the comment. Use event-driven waits or callbacks; do not poll or sleep. +- Never infer queue success, merge completion, or post-merge CI from the comment. Prefer event-driven waits or callbacks. A dedicated worker may watch GitHub checks or merge state with a command such as `gh run watch` while the orchestrator continues independent work; do not have the orchestrator block on that watcher. - Do not publish, release, or perform another irreversible action without the authorization required by the user or repository policy. ## Completion report