git:20260520.dafaa1a to git:20260527.63fe75a

317 added, 630 removed. Audit B to A.

---
name: myco:runtime-environment-binary-management
description: |
- Procedures for managing binary dispatch, runtime environment resolution, and machine-scoped coordination in Myco deployments. Covers layered runtime command resolution (~/.myco/runtime.command pins, project overrides, fallback chains), machine-scoped runtime architecture, binary masquerade detection and prevention, update coordination protocols, and Bun compilation deployment patterns. Use when setting up environments, troubleshooting binary dispatch issues, managing machine-scoped coordination, or implementing system updates, even if the user doesn't explicitly ask for runtime environment management.
+ Procedures for managing binary dispatch, runtime environment resolution, and
+ machine-scoped coordination in Myco deployments. Covers layered runtime command
+ resolution (~/.myco/runtime.command pins, project overrides, fallback chains),
+ machine-scoped runtime architecture, binary masquerade detection and prevention,
+ update coordination protocols, Bun compilation deployment patterns, dogfood routing
+ via dev-build detection, and beta channel global replacement strategy. Use when
+ setting up environments, troubleshooting binary dispatch issues, managing machine-scoped
+ coordination, or implementing system updates, even if the user doesn't explicitly
+ ask for runtime environment management.
managed_by: myco
user-invocable: true
allowed-tools: Read, Edit, Write, Bash, Grep, Glob
---
# Runtime Environment and Binary Management
Comprehensive procedures for managing Myco's binary dispatch system, runtime environment resolution, and machine-scoped coordination. These procedures ensure reliable binary execution, prevent environment conflicts, and maintain proper isolation across different deployment contexts in the Grove multi-project daemon architecture.
- **Architectural shift**: Myco now operates on a **global-first, machine-scoped model** where the machine opts into projects rather than projects configuring themselves. This invertsthe traditional project-centric configuration model — global hooks capture everywhere, runtime configuration is machine-scoped by default, and unknown projects operate in quarantine mode until explicitly registered.
+ **Architectural shift**: Myco now operates on a **global-first, machine-scoped model** where the machine opts into projects rather than projects configuring themselves. This inverts the traditional project-centric configuration model — global hooks capture everywhere, runtime configuration is machine-scoped by default, and unknown projects operate in quarantine mode until explicitly registered.
## Prerequisites
- Myco installation with proper symbiont structure
- Understanding of Myco's Grove multi-project daemon architecture (packages/myco/src/daemon/)
- Access to runtime configuration files (~/.myco/runtime.command, project-level configs)
- Familiarity with Bun compilation and single-file binary patterns
- Knowledge of Grove registration and project binding patterns
- Understanding of machine-scoped opt-in model and global capture hooks
- ## Procedure A: Runtime Command Resolution and Fallback Management
-
- Configure and debug the layered runtime command resolution system with Grove multi-project support and machine-scoped coordination.
-
- ### Machine-Scoped Global Runtime Pins
-
- The machine now maintains global runtime configuration that applies to all projects unless explicitly overridden. This supports the global-first model where hooks are installed globally and capture everywhere.
-
- 1. **Check machine-wide runtime pin** (primary configuration source):
- ```bash
- cat ~/.myco/runtime.command
- ```
-
- 2. **Set machine-wide pin** (affects all projects on the machine):
- ```bash
- echo "/path/to/preferred/myco/binary" > ~/.myco/runtime.command
- ```
-
- 3. **Validate pin target** (ensure the pinned binary exists and is executable globally):
- ```bash
- ls -la $(cat ~/.myco/runtime.command)
- # Verify binary works across project boundaries
- $(cat ~/.myco/runtime.command) --version
- ```
-
- ### Global Hook Capture Configuration
-
- In the global-first model, hooks are installed machine-wide and capture activity from all projects. Runtime resolution must account for this global capture architecture.
-
- 1. **Verify global hook installation** (hooks should be machine-wide, not project-specific):
- ```bash
- # Check for global hook configuration
- ls -la ~/.myco/hooks/
- # Verify hooks point to correct runtime binary
- grep -r "runtime.command\|myco" ~/.myco/hooks/
- ```
-
- 2. **Configure global capture environment**:
- ```bash
- # Ensure global hooks use machine-scoped runtime
- myco hooks install --global
- # Verify all symbionts use machine runtime
- myco symbionts list --check-runtime
- ```
-
- ### Project-Level Override Patterns (Legacy/Exception Cases)
-
- While the machine-scoped model is primary, some projects may need specific binary versions for compatibility.
-
- 1. **Check project-level runtime configuration** (should be rare in global-first model):
- ```bash
- # Look for project-specific runtime overrides (legacy pattern)
- find .myco/ -name "runtime.command" -o -name "*.runtime.conf"
- ```
-
- 2. **Implement project override** (only when machine-wide config is insufficient):
- ```bash
- # Project-level pin takes precedence over machine-wide (legacy support)
- echo "/project/specific/myco/binary" > .myco/runtime.command
- # Note: This breaks global-first model; use sparingly
- ```
-
- ### Global-First Fallback Chain Validation
-
- The fallback chain now prioritizes machine-scoped configuration over project-specific settings, supporting the global capture architecture.
-
- 1. **Test global-first resolution order**:
- ```bash
- # Verify resolution order: project (rare) → machine (primary) → PATH (fallback)
- myco doctor --runtime-resolution
- which myco
- echo $PATH | tr ':' '\n' | grep myco
- ```
-
- 2. **Debug resolution failures in global context**:
- ```bash
- # Check each step of the global-first fallback chain
- [ -f .myco/runtime.command ] && echo "Project pin (legacy): $(cat .myco/runtime.command)"
- [ -f ~/.myco/runtime.command ] && echo "Machine pin (primary): $(cat ~/.myco/runtime.command)"
- which myco || echo "No myco in PATH (fallback)"
-
- # Verify global hooks point to correct runtime
- myco doctor --global-hooks
- ```
-
- ## Procedure B: Machine-Scoped Runtime Architecture
-
- Manage machine-scoped service coordination and runtime environment resolution with global capture hooks and project quarantine mode.
-
- ### Grove Global Daemon with Machine-Scoped Runtime
-
- 1. **Check Grove daemon runtime source status** (should reflect machine-wide configuration):
- ```bash
- # Check daemon runtime source via API (Grove global daemon)
- curl -s http://127.0.0.1:20915/api/stats | jq '.runtime'
- # Verify machine-scoped settings are active
- myco doctor --machine-scope
- ```
-
- 2. **Verify machine-scoped configuration with global capture**:
- ```bash
- # Validate machine-level runtime settings for global operation
- ls -la ~/.myco/runtime.command
- myco doctor # Should report runtime source information for all projects
- # Verify global hooks are properly configured
- myco hooks status --all-projects
- ```
-
- ### Global Capture and Project Quarantine Mode
-
- In the global-first model, hooks capture from all projects but unknown projects operate in quarantine mode until explicitly registered.
-
- 1. **Check global capture status**:
- ```bash
- # Verify hooks capture from all active projects
- myco capture status --global
- # List projects in quarantine mode
- myco projects list --quarantined
- ```
-
- 2. **Manage quarantine mode for unknown projects**:
- ```bash
- # Register project to remove from quarantine
- myco projects register /path/to/project
- # Verify project exits quarantine and uses machine runtime
- cd /path/to/project
- myco --version # Should match machine-wide runtime
- myco status # Should show "registered" not "quarantined"
- ```
-
- ### Multi-Project Grove Coordination with Machine-First Model
-
- 1. **Test cross-Grove runtime consistency** (machine runtime applies to all registered projects):
- ```bash
- # Verify that machine runtime applies across all registered projects
- myco groves list # List all registered Groves
- # Test runtime consistency across projects
- for project in $(myco projects list --registered); do
- echo "Testing runtime for: $project"
- cd "$project"
- myco --version
- done
- ```
-
- 2. **Manage machine-scoped updates across all projects**:
- ```bash
- # Update all projects via machine-scoped coordination
- myco update --machine-wide
- # Verify all registered projects use updated runtime
- myco projects verify-runtime --all
- ```
-
- ### Global Hook Registration and Project Opt-In
-
- 1. **Validate global hook coverage**:
- ```bash
- # Check that global hooks cover all registered projects
- myco hooks verify-coverage --all-projects
- # Test hook functionality across project boundaries
- myco hooks test --sample-projects
- ```
-
- 2. **Handle project registration flow**:
- ```bash
- # Register new project (moves from quarantine to active)
- myco projects register /path/to/new/project
- # Verify project inherits machine-scoped runtime
- cd /path/to/new/project
- myco status # Should show global hook coverage
- myco --version # Should match machine runtime
- ```
-
- ## Procedure C: Binary Masquerade Detection and Prevention
-
- Detect and prevent binary dispatch conflicts between published and development versions across the global capture boundary.
-
- ### Machine-Scoped Version Detection
-
- 1. **Verify binary authenticity across global scope**:
- ```bash
- # Check binary version and source for machine-wide operation
- myco --version
- which myco
- file $(which myco) # Check if it's a compiled binary or script
-
- # Verify consistency across all registered projects
- myco projects verify-binary --all
- ```
-
- 2. **Detect masquerade scenarios in global context**:
- ```bash
- # Compare expected vs actual binary paths across projects
- realpath $(which myco)
- # Check for unexpected symlinks or wrappers affecting global hooks
- ls -la $(dirname $(which myco))/myco*
-
- # Verify global hooks use correct binary
- grep -r "myco" ~/.myco/hooks/ | grep -v "runtime.command"
- ```
-
- ### Machine-Wide Re-exec Logic Validation
-
- 1. **Test binary re-execution across global scope**:
- ```bash
- # Verify that runtime.command pins work correctly for all projects
- strace -e execve myco --version 2>&1 | grep execve
- # Test re-exec consistency across registered projects
- myco projects test-reexec --sample
- ```
-
- 2. **Validate update mechanisms with global hooks**:
- ```bash
- # Check that updates don't break re-exec logic for any project
- myco doctor # Should report consistent binary paths
- myco hooks verify-reexec # Ensure hooks use updated binary
- ```
-
- ## Procedure D: Update Coordination and Environment Management
-
- Coordinate binary updates and environment transitions without disrupting active workflows across the global capture architecture.
-
- ### Machine-Wide Upgrade Path Testing
-
- 1. **Test upgrade in isolated machine context**:
- ```bash
- # Create temporary environment for testing machine-wide changes
- cp ~/.myco/runtime.command ~/.myco/runtime.command.backup
- echo "/path/to/new/binary" > ~/.myco/runtime.command
- myco --version # Verify new binary works
-
- # Test across all registered projects
- myco projects verify-runtime --all
- ```
-
- 2. **Rollback on failure affecting any project**:
- ```bash
- # Restore previous configuration if upgrade fails for any project
- mv ~/.myco/runtime.command.backup ~/.myco/runtime.command
- # Verify rollback worked across all projects
- myco projects verify-runtime --all
- ```
-
- ### NPM Package Upgrade Handling with Global Hooks
-
- 1. **Handle Grove daemon binary version mismatches with global impact**:
- ```bash
- # After npm install -g @goondocks/myco@latest
- myco doctor # Check for version mismatches affecting all projects
- # Restart Grove global daemon if versions don't match
- myco daemon stop
- myco daemon start
-
- # Verify global hooks use updated binary
- myco hooks verify-binary --all
- ```
-
- 2. **Validate machine-wide daemon restart after package updates**:
- ```bash
- # Ensure Grove daemon serves JSON, not HTML after updates
- curl -s http://127.0.0.1:20915/api/stats
- # Should return JSON, not HTML error page
- # Verify all registered projects are accessible with global hooks
- myco projects list --verify-capture
- ```
-
- ### Daemon Upgrade Failure Recovery with Global Scope
+ ## Procedure C: Safe Sandbox Environment for Global Smoke Runs
- 1. **Detect daemon event loop wedge affecting all projects**:
- ```bash
- # Check if daemon port is bound but not responding to any project
- netstat -tuln | grep 20915
- curl -s --connect-timeout 3 http://127.0.0.1:20915/api/stats || echo "Daemon wedged"
-
- # Check for stale daemon processes affecting global capture
- ps aux | grep myco | grep -v grep
- # Verify global hook functionality
- myco hooks test --quick
- ```
+ ### The non-negotiable rule
- 2. **Force daemon restart with SIGKILL** (affects all projects with global hooks):
- ```bash
- # Get daemon PID holding port 20915
- DAEMON_PID=$(lsof -ti:20915)
-
- # Preserve daemon.json before killing process
- cp ~/.myco/daemon.json ~/.myco/daemon.json.backup 2>/dev/null || true
-
- # Force kill wedged process (affects all global hook functionality)
- kill -9 $DAEMON_PID
-
- # Verify port is released and global hooks can reconnect
- sleep 2
- netstat -tuln | grep 20915 || echo "Port released"
- ```
+ When a smoke run may touch **global symbiont config** (`~/.claude/settings.json`, `~/.cursor/hooks.json`, etc.), never sandbox only `MYCO_HOME`. That was the historical escape hatch that wrote temp launchers like `/tmp/myco-*-smoke-*/home/launcher.cjs` into the developer's real global agent config.
- 3. **Handle restart false positives and version-skew detection with global impact**:
- ```bash
- # Start daemon and check for version-skew warnings affecting all projects
- myco daemon start
-
- # Verify daemon serves correct version for all projects
- BINARY_VERSION=$(myco --version)
- DAEMON_VERSION=$(curl -s http://127.0.0.1:20915/api/stats | jq -r '.version' 2>/dev/null)
-
- if [ "$BINARY_VERSION" != "$DAEMON_VERSION" ]; then
- echo "Version skew detected: binary=$BINARY_VERSION daemon=$DAEMON_VERSION"
- echo "This affects all projects with global hooks"
- echo "Restarting daemon to sync versions..."
- myco daemon stop
- myco daemon start
-
- # Verify global hooks reconnected successfully
- myco hooks verify-connection --all
- fi
- ```
+ The four env vars must move together:
- 4. **Restore machine-wide daemon configuration after forced restart**:
- ```bash
- # Restore daemon.json if it was corrupted during forced kill
- if [ ! -s ~/.myco/daemon.json ] && [ -f ~/.myco/daemon.json.backup ]; then
- cp ~/.myco/daemon.json.backup ~/.myco/daemon.json
- echo "Restored daemon.json from backup"
- fi
-
- # Validate daemon configuration integrity for all projects
- myco doctor # Should show no configuration errors
- myco projects verify-connection --all # Test all global hook connections
- ```
+ - `MYCO_SANDBOX_ROOT`
+ - `HOME`
+ - `MYCO_HOME`
+ - `MYCO_LAUNCH_AGENTS_DIR`
- ### Binary Replacement Procedures with Global Hook Updates
+ ### Preferred helper
- 1. **Atomic binary replacement affecting all projects**:
- ```bash
- # Replace binary atomically to avoid partial updates affecting global hooks
- mv /new/myco/binary /usr/local/bin/myco.new
- mv /usr/local/bin/myco.new /usr/local/bin/myco
-
- # Update global hooks to use new binary
- myco hooks update-binary-refs --all
- ```
+ Use the committed helper instead of hand-rolling exports:
- 2. **Validate replacement across global scope**:
- ```bash
- # Ensure new binary works across all projects
- myco doctor
- ps aux | grep myco # Check running Grove daemon still works
- myco projects list --verify-all # Verify all projects remain accessible
-
- # Test global hook functionality with new binary
- myco hooks test --comprehensive
- ```
+ ```bash
+ eval "$(scripts/dev/smoke-sandbox-env.sh subagent-smoke)"
+ ```
- ## Procedure E: Bun Compilation and Deployment
+ This emits a fresh temp sandbox root and exports:
- Handle Bun-specific compilation constraints, asset bundling strategies, virtual filesystem handling, build artifact packaging, multi-target compilation patterns, and binary entry point dispatch for Grove multi-project architecture with machine-scoped global hooks.
+ - `MYCO_SANDBOX_ROOT=<tmp>`
+ - `HOME=<tmp>/home`
+ - `MYCO_HOME=<tmp>/home/.myco`
+ - `MYCO_LAUNCH_AGENTS_DIR=<tmp>/launchagents`
- ### Template and Asset Bundling Strategies for Global Deployment
+ The helper exists because manual smoke runs on `global-symbiont-install` used temp homes such as `myco-subagent-smoke`, `myco-final-smoke`, and `myco-wave2-smoke`, but left `HOME` pointing at the real user home. Manifest `globalHooksTarget` expansion then escaped into real files under `~/.claude/`, `~/.cursor/`, `~/.codex/`, `~/.copilot/`, and `~/.codeium/windsurf/`.
- Myco uses **filesystem-first + bundled-string fallback** pattern for package assets via generated template modules. Static assets like installer templates must be accessible both during development (filesystem reads) and in compiled binaries (bundled strings) across the global capture architecture.
+ ### Why `MYCO_SANDBOX_ROOT` matters
- #### Two-Generator Template System for Machine-Scoped Deployment
+ `packages/myco/src/grove/paths.ts` now enforces a sandbox sentinel: when `MYCO_SANDBOX_ROOT` is set, `expandHome('~/...')` throws unless `HOME` resolves inside that sandbox. The helper codifies the safe shape so ad hoc smoke commands don't have to remember the contract.
- Myco uses a two-generator system handling different asset types, optimized for machine-wide deployment:
+ ### Practical pattern
```bash
- # Generate all template modules at build time for global deployment
- cd packages/myco
- npm run codegen
-
- # This runs two generators:
- # 1. scripts/gen-hook-config.ts → hook-config.generated.ts & manifests.generated.ts
- # 2. scripts/gen-templates.mjs → templates.generated.ts
+ eval "$(scripts/dev/smoke-sandbox-env.sh qa-smoke)"
+ MYCO_SERVICE_VARIANT=dev \
+ packages/myco-darwin-arm64/bin/myco doctor
```
- Each generator walks its respective source directory and embeds files for global deployment:
+ If the smoke run also needs a temp repo or worktree fixture, create it UNDER `"$MYCO_SANDBOX_ROOT"` or another temp path — but keep `HOME`, `MYCO_HOME`, and `MYCO_LAUNCH_AGENTS_DIR` anchored to the helper's exported root.
- ```javascript
- // In scripts/gen-templates.mjs (installer templates for global hooks)
- const files = walk(TEMPLATES_DIR).sort();
- const entries = files.map((abs) => {
- const rel = path.relative(TEMPLATES_DIR, abs).split(path.sep).join('/');
- const body = fs.readFileSync(abs, 'utf-8');
- return [rel, body];
- });
- ```
+ ### Never do this
- ```typescript
- // In scripts/gen-hook-config.ts (global hook config and manifests)
- // Generates both hook-config.generated.ts and manifests.generated.ts
- // from src/hooks/ and src/symbionts/manifests/ respectively
+ ```bash
+ # WRONG — leaks globalHooksTarget writes into the real home
+ MYCO_HOME=/tmp/myco-something packages/myco-darwin-arm64/bin/myco update
```
- #### Implement the fallback pattern for global scope
+ That shape is exactly what created the stale escaped global hook entries the one-shot scrub now repairs.
- The fallback pattern is implemented consistently across different asset loaders, supporting both project-specific and machine-wide deployment:
+ ## Procedure D: Dogfood Routing via Dev-Build Self-Detection
- ```typescript
- // In src/symbionts/installer.ts (templates for global hook installation)
- private readTemplateFile(relPath: string): string | null {
- // Try filesystem first (development and testing)
- const candidates = [
- path.join(this.packageRoot, TEMPLATES_SUBDIR, relPath),
- path.join(this.packageRoot, 'dist', TEMPLATES_SUBDIR, relPath),
- ];
- for (const filePath of candidates) {
- try { return fs.readFileSync(filePath, 'utf-8'); } catch { /* try next */ }
- }
+ ### Development Binary Self-Detection Chain
- // Fall back to bundled strings (compiled binary for global deployment)
- if (this.suppressBundledTemplates) return null;
- const key = relPath.split(path.sep).join('/');
- const bundled = BUNDLED_TEMPLATES[key];
- return bundled !== undefined ? bundled : null;
- }
+ **Problem:** When Myco is in production globally, contributors developing Myco itself need to route hook invocations to their local dev daemon rather than the production system daemon.
- // Similar pattern in manifest loader for global hooks
- private readManifestFile(relPath: string): ManifestData | null {
- // Filesystem first, then BUNDLED_MANIFESTS fallback for global deployment
- const bundled = BUNDLED_MANIFESTS[key];
- return bundled !== undefined ? bundled : null;
- }
- ```
+ **Solution:** The global launcher uses a **dev-build detection chain** to determine whether the running binary is a development build or production, then routes to the appropriate daemon instance.
- **Critical gotcha for global deployment**: Each asset loader can return `null` when the filesystem path fails and no bundled fallback exists. Always check for null across all asset types, especially when installing global hooks:
+ ### The Dogfooding Route Chain
```typescript
- // BAD: Silent failure affecting global hook installation
- const template = installer.readTemplateFile('hook-guard.cjs');
- const manifest = loader.readManifestFile('claude-code.json');
+ // In ~/.myco/launcher.c (global hook bootstrap)
+ // Step 1: Hook fires from inside the myco project (e.g., Claude Code)
+ // Step 2: Global launcher dispatches to the appropriate daemon
+ // Step 3: Daemon identity is determined by detectDevBuild() check
- // GOOD: Explicit checks for global deployment
- const template = installer.readTemplateFile('hook-guard.cjs');
- if (!template) throw new Error(`Template not found: hook-guard.cjs (required for global hooks)`);
+ export function selectDaemonForInvocation(): {
+ daemonServicePath: string;
+ runtimeCommandPath: string;
+ } {
+ // Pattern: detectDevBuild() checks if current binary is dev or production
+ const isDevBuild = detectDevBuild();
+
+ if (isDevBuild) {
+ // Development: use machine-scoped dev daemon
+ // ~/.myco/service-dev/ contains daemon.json for development instance
+ return {
+ daemonServicePath: path.join(os.homedir(), '.myco', 'service-dev', 'daemon.json'),
+ runtimeCommandPath: path.join(os.homedir(), '.myco', 'runtime.command')
+ };
+ } else {
+ // Production: use standard service daemon
+ // ~/.myco/service/ contains daemon.json for production instance
+ return {
+ daemonServicePath: path.join(os.homedir(), '.myco', 'service', 'daemon.json'),
+ runtimeCommandPath: path.join(os.homedir(), '.myco', 'runtime.command')
+ };
+ }
+ }
- const manifest = loader.readManifestFile('claude-code.json');
- if (!manifest) throw new Error(`Manifest not found: claude-code.json (required for global capture)`);
+ // Dev-build detection chain
+ export function detectDevBuild(): boolean {
+ // Priority 1: Check if running inside git worktree with compiled binary
+ const gitDir = findNearestGitDir();
+ const worktreeMarker = path.join(gitDir, '.git', 'worktrees');
+
+ // Priority 2: Check for vendor-src directory (dev artifact not in production binary)
+ const vendorSrcDir = path.join(process.execPath, '..', 'vendor-src');
+ if (fs.existsSync(vendorSrcDir)) {
+ return true;
+ }
+
+ // Priority 3: Check for uncompiled source markers in execution path
+ const sourceMarkers = ['src/', 'packages/myco/src', 'tsconfig.json'];
+ const execDir = path.dirname(process.execPath);
+ for (const marker of sourceMarkers) {
+ if (execDir.includes(marker)) return true;
+ }
+
+ // Default: assume production
+ return false;
+ }
```
- #### Design runtime boundary decisions for global architecture
-
- **Package assets** (bundled via generators for machine-wide deployment): Global hook templates, symbiont manifests, hook configurations, default configs, static strings
- **User assets** (filesystem, project-specific): User configs, generated files, session data, vault contents, runtime logs
-
- When adding new static assets for global deployment, decide the boundary and target generator:
- - **Global hook templates** → add to `src/symbionts/templates/` for `gen-templates.mjs`
- - **Symbiont manifests for global capture** → add to `src/symbionts/manifests/` for `gen-hook-config.ts`
- - **Global hook configurations** → add to `src/hooks/` for `gen-hook-config.ts`
- - **Project-specific or installation-specific** → read from filesystem at runtime
-
- ### Virtual Filesystem Handling for Global Binary Distribution
-
- Bun binaries use a `/$bunfs/` virtual filesystem for bundled content. This creates path resolution challenges that require careful native dependency handling, especially for machine-wide deployment.
-
- #### Use resolvePackageRoot() for machine-scoped bundled content
-
- Never rely on `process.cwd()` for package asset resolution in global deployment. The actual implementation uses import.meta.dirname detection:
+ ### Machine-Scoped Service Selection Pattern
```typescript
- // In src/symbionts/detect.ts (machine-scoped package resolution)
- export function resolvePackageRoot(): string {
- // Try import.meta.dirname first — works in dev and old tsup layout
- if (typeof import.meta.dirname === 'string' && !import.meta.dirname.includes('/$bunfs/')) {
- return path.resolve(import.meta.dirname, '..', '..');
- }
+ // In packages/myco/src/grove/paths.ts
+ export class ServicePaths {
+ /**
+ * Production service daemon lives at ~/.myco/service/daemon.json
+ * Development service daemon lives at ~/.myco/service-dev/daemon.json
+ *
+ * The selection happens once at process startup (after detectDevBuild())
+ * and is immutable per process instance.
+ */
+ private static selectedServiceDir: string | null = null;
- // Fall back to process.execPath resolution — compiled binary case for global deployment
- if (process.execPath && process.execPath !== process.argv0) {
- return path.dirname(process.execPath);
+ static selectServiceDirectory(): string {
+ if (ServicePaths.selectedServiceDir !== null) {
+ return ServicePaths.selectedServiceDir;
+ }
+
+ // Determine at startup (after detectDevBuild())
+ const isDev = detectDevBuild();
+ const baseDir = path.join(os.homedir(), '.myco');
+
+ ServicePaths.selectedServiceDir = isDev
+ ? path.join(baseDir, 'service-dev')
+ : path.join(baseDir, 'service');
+
+ return ServicePaths.selectedServiceDir;
}
- return process.cwd();
+ static getDaemonJsonPath(): string {
+ return path.join(ServicePaths.selectServiceDirectory(), 'daemon.json');
+ }
}
- ```
- **Critical gotcha for global deployment**: Import.meta.dirname detection prevents loading stale `/dist/` artifacts when the global binary is run from a development directory with old build output.
+ // Usage in daemon startup
+ export async function loadDaemonIdentity(): Promise<DaemonRecord> {
+ const daemonJsonPath = ServicePaths.getDaemonJsonPath();
+ const content = await fs.promises.readFile(daemonJsonPath, 'utf-8');
+ return JSON.parse(content);
+ }
+ ```
- #### Handle embedded native dependencies for machine-wide operation
+ ### Gotchas in Dogfood Routing
- Bun's file embedding with `import ... with { type: 'file' }` creates virtual paths that must be materialized for global operation:
+ **Stale hook gotcha:** Hooks installed globally may be outdated if Myco binary is updated. Ensure global hooks are regenerated on production upgrade and again on dev-build downgrades.
- ```typescript
- // In src/entries/cli.darwin-arm64.ts (global binary entry point)
- import libsqliteEmbed from '../../vendor-src/libsqlite3/darwin-arm64/libsqlite3.dylib' with { type: 'file' };
- import vec0Embed from 'sqlite-vec-darwin-arm64/vec0.dylib' with { type: 'file' };
- import ripgrepEmbed from '@vscode/ripgrep/bin/rg' with { type: 'file' };
+ **Dev-only service directory:** Contributors running dev builds must ensure `~/.myco/service-dev/` exists and is owned by the development daemon, not the production service.
- await registerEmbeddedNativeDeps({
- libsqliteEmbed, // Resolves to /$bunfs/path at runtime
- vec0Embed, // Must be extracted to real filesystem for global operation
- ripgrepEmbed,
- version: pkg.version,
- });
- ```
+ ---
- The `registerEmbeddedNativeDeps()` function extracts these to temporary files for machine-wide operation:
+ ## Procedure E: Beta Channel Global Replacement Strategy
- ```typescript
- // In src/runtime/native-deps.ts (machine-scoped native dependency management)
- export async function registerEmbeddedNativeDeps(deps) {
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'myco-'));
-
- // Extract each embedded file to temp directory for global operation
- const libsqlitePath = path.join(tempDir, 'libsqlite3.dylib');
- await fs.writeFile(libsqlitePath, await fs.readFile(deps.libsqliteEmbed));
-
- // Register with the runtime for machine-wide availability
- process.env.MYCO_LIBSQLITE_PATH = libsqlitePath;
- }
- ```
+ ### Decision: Global-Install Beta Model
- ### Build Artifact Packaging for Global Distribution
+ Under Myco's **global-install architecture**, beta channel switching uses **global replacement**: users run a command to download and install the beta package globally, which replaces the production-installed Myco binary system-wide.
- Myco's binary packaging uses target-specific entry points with embedded native dependencies and strict build validation for machine-wide deployment.
+ **Rationale:**
+ - Global installation means Myco operates as a system-wide tool, not per-project
+ - Beta testers opt in by running a global replacement command
+ - No project-level .myco directory changes; only the global installation is affected
+ - Rollback is clean: reinstall the last production release to revert
- #### Use target-specific entry points for global deployment
+ ### Global Beta Channel Workflow
- Each supported platform has a dedicated entry point that embeds the correct native binaries for global operation:
+ #### User Initiates Beta Join
```bash
- # Entry points for each target (global deployment)
- ls packages/myco/src/entries/
- # cli.darwin-arm64.ts
- # cli.darwin-x64.ts
- # cli.linux-x64.ts
- # cli.linux-arm64.ts
- # cli.windows-x64.ts
- # cli.js (shared logic)
+ # User runs command to switch to beta channel
+ myco install:beta
+
+ # Internally, this invokes the global upgrade path:
+ # 1. Download beta-tagged release from artifact store
+ # 2. Verify checksum matches expected beta build
+ # 3. Stop running service daemon (if any)
+ # 4. Replace global Myco binary with beta build
+ # 5. Restart daemon to load new binary
```
- Each entry imports platform-specific native dependencies for machine-wide operation:
+ #### Implementation Pattern
```typescript
- // cli.darwin-arm64.ts embeds macOS ARM64 binaries for global deployment
- import libsqliteEmbed from '../../vendor-src/libsqlite3/darwin-arm64/libsqlite3.dylib' with { type: 'file' };
+ // In packages/myco/src/cli/install.ts (or daemon API endpoint)
+ export async function switchToBetaChannel(): Promise<{ success: boolean; message: string }> {
+ // Step 1: Fetch beta release metadata
+ const betaRelease = await fetchBetaReleaseMetadata();
+ if (!betaRelease) {
+ throw new Error('No beta release available');
+ }
- // cli.linux-x64.ts embeds Linux x64 binaries for global deployment
- import libsqliteEmbed from '../../vendor-src/libsqlite3/linux-x64/libsqlite3.so' with { type: 'file' };
- ```
+ // Step 2: Download beta artifact
+ const betaPath = await downloadBinaryRelease(betaRelease.downloadUrl, {
+ checksumExpected: betaRelease.sha256,
+ tempDir: path.join(os.homedir(), '.myco', 'tmp')
+ });
- #### Build single target binaries for machine-wide installation
+ // Step 3: Stop production daemon before binary replacement
+ // Use graceful shutdown with timeout (critical to avoid ETXTBSY on Windows)
+ await stopDaemonWithTimeout({ timeoutMs: 30000 });
- The build system creates platform-specific binaries in `vendor/{target}/` for global deployment:
+ // Step 4: Global replacement - swap binary atomically
+ const globalBinPath = path.join(os.homedir(), '.myco', 'bin', 'myco');
+ const backupPath = path.join(os.homedir(), '.myco', 'bin', 'myco.production');
+
+ await fsAtomicReplace(globalBinPath, betaPath, {
+ backupPath, // Save production binary for rollback
+ mode: 0o755
+ });
- ```bash
- # Build for current platform (global deployment)
- npm run build:binary
+ // Step 5: Restart daemon with new beta binary
+ // Service daemon will auto-restart via systemd/launchd
+ await startDaemon({
+ waitForHealthy: true,
+ timeoutMs: 30000
+ });
- # Build specific target via env var for machine-wide distribution
- TARGET=darwin-arm64 npm run build:binary
- TARGET=linux-x64 npm run build:binary
- TARGET=windows-x64 npm run build:binary
+ return {
+ success: true,
+ message: `Switched to beta channel. Running ${betaRelease.version}`
+ };
+ }
- # Build all targets (CI use for global distribution)
- npm run build:binaries
+ // Atomic replacement with backup pattern
+ async function fsAtomicReplace(
+ targetPath: string,
+ sourcePath: string,
+ opts: { backupPath: string; mode: number }
+ ): Promise<void> {
+ // Pattern: Backup original, move new to target, verify
+
+ if (fs.existsSync(targetPath)) {
+ await fs.promises.rename(targetPath, opts.backupPath);
+ }
+
+ try {
+ await fs.promises.copyFile(sourcePath, targetPath);
+ await fs.promises.chmod(targetPath, opts.mode);
+
+ // Verify new binary is executable
+ const isExecutable = await isFileExecutable(targetPath);
+ if (!isExecutable) {
+ throw new Error(`New binary not executable: ${targetPath}`);
+ }
+ } catch (err) {
+ // Rollback to backup on failure
+ if (fs.existsSync(opts.backupPath)) {
+ await fs.promises.rename(opts.backupPath, targetPath);
+ }
+ throw err;
+ }
+ }
```
- This runs `scripts/build-single-target.mjs` for global deployment:
-
- ```javascript
- const target = process.env.TARGET ?? detectHostTarget();
- const entry = path.join(pkgRoot, 'src', 'entries', `cli.${target}.ts`);
- const outputDir = path.join(pkgRoot, 'vendor', target);
- const binaryName = target.startsWith('windows-') ? 'myco.exe' : 'myco';
- const outfile = path.join(outputDir, binaryName);
+ ### Beta Channel Rollback Pattern
- const result = spawnSync(
- 'bun',
- ['build', '--compile', `--target=bun-${target}`, entry, '--outfile', outfile],
- { stdio: 'inherit', cwd: pkgRoot }
- );
- ```
+ ```typescript
+ export async function rollbackBetaToPrevious(): Promise<{ success: boolean; version: string }> {
+ const backupPath = path.join(os.homedir(), '.myco', 'bin', 'myco.production');
+ const globalBinPath = path.join(os.homedir(), '.myco', 'bin', 'myco');
- ### Multi-Target Compilation Patterns for Global Distribution
+ if (!fs.existsSync(backupPath)) {
+ throw new Error('No production backup available for rollback');
+ }
- Building for multiple platforms requires handling native dependency differences and target-specific build constraints for machine-wide deployment.
+ // Stop daemon before rollback
+ await stopDaemonWithTimeout({ timeoutMs: 30000 });
- #### Handle platform-specific native dependencies for global operation
+ // Restore production binary
+ await fs.promises.rename(backupPath, globalBinPath);
+ await fs.promises.chmod(globalBinPath, 0o755);
- Each target needs different native binaries embedded for global deployment:
+ // Restart with production binary
+ await startDaemon({ waitForHealthy: true });
- ```bash
- # Install platform-specific dependencies for each target (global deployment)
- # Package structure: vendor-src/libsqlite3/{target}/libsqlite3.{ext}
+ // Verify version
+ const versionOutput = await execSync('myco --version');
+ return {
+ success: true,
+ version: parseVersionFromOutput(versionOutput)
+ };
+ }
+ ```
- # macOS requires .dylib files for global deployment
- packages/myco/vendor-src/libsqlite3/darwin-arm64/libsqlite3.dylib
- packages/myco/vendor-src/libsqlite3/darwin-x64/libsqlite3.dylib
+ ### Beta Channel Configuration State
- # Linux requires .so files for global deployment
- packages/myco/vendor-src/libsqlite3/linux-x64/libsqlite3.so
- packages/myco/vendor-src/libsqlite3/linux-arm64/libsqlite3.so
+ ```typescript
+ // In ~/.myco/myco.yaml
+ export interface BetaChannelState {
+ enabled: boolean;
+ currentRelease: string; // e.g., "2.15.0-beta.2"
+ productionBackupPath: string; // Path to production binary backup
+ switchedAt: number; // Unix timestamp
+ canRollback: boolean;
+ }
- # Windows requires .dll files for global deployment
- packages/myco/vendor-src/libsqlite3/windows-x64/sqlite3.dll
+ // Query current channel
+ export async function getCurrentChannel(): Promise<'production' | 'beta'> {
+ const config = await loadConfig();
+ return config.beta?.enabled ? 'beta' : 'production';
+ }
```
- #### Set up cross-platform CI builds for global distribution
+ ### Gotchas in Beta Channel Switching
- Use matrix builds to compile all targets for machine-wide deployment:
+ **Daemon restart timing gotcha:** On macOS/Linux, the service daemon may be lingering from the old binary. Use `launchctl unload` or `systemctl stop` before binary replacement to avoid ETXTBSY ("text file busy") errors.
- ```yaml
- # In .github/workflows/build.yml (global distribution)
- strategy:
- matrix:
- include:
- - target: darwin-arm64
- os: macos-latest
- - target: linux-x64
- os: ubuntu-latest
- - target: windows-x64
- os: windows-latest
+ **Backup path gotcha:** The backup production binary must be saved before replacement and stored outside the executable location to prevent accidental cleanup. Use `~/.myco/bin/myco.production` as the canonical backup location.
- steps:
- - name: Build target for global deployment
- run: |
- TARGET=${{ matrix.target }} npm run build:binary
- npm run build:verify
- ```
+ **Checksum verification gotcha:** Always verify the beta release checksum before performing binary replacement. Use SHA-256 hashes published alongside the release.
- ### Binary Entry Point Dispatch for Global Hooks
+ ---
- Myco supports runtime resolution via `.myco/runtime.command` with automatic collision detection through the global hook guard system and critical dispatch contract enforcement to prevent version-sync loops across the global capture architecture.
+ ## Procedure F: Bun Compilation and Deployment
- #### Use the global hook guard dispatch pattern
+ ### Launcher Script Quoting and Path Handling
- The `.agents/myco-run.cjs` hook guard provides cross-platform entry point resolution for global hooks:
+ **Critical issue**: Hook dispatcher scripts (.agents/myco-run.cjs) must properly quote the runtime.command binary path to handle spaces and special characters in launcher paths.
+ #### Quoted Binary Path Pattern
+
```javascript
// In .agents/myco-run.cjs (global hook guard)
+ const path = require('path');
+ const fs = require('fs');
+ const { execFileSync } = require('child_process');
+
let bin = 'myco'; // Default for global installs
try {
const aliasPath = path.resolve(__dirname, '..', '.myco', 'runtime.command');
const alias = fs.readFileSync(aliasPath, 'utf-8').trim();
if (alias) bin = alias; // Override with machine-scoped development binary
} catch { /* missing file → use default for global operation */ }
+ // CRITICAL: Quote the binary path to handle spaces
try {
+ // WRONG: Binary path with spaces will be split into multiple args
execFileSync(bin, process.argv.slice(2), { stdio: 'inherit' });
+
+ // RIGHT: Use shell quoting for binary paths with spaces
+ const { spawnSync } = require('child_process');
+ spawnSync(bin, process.argv.slice(2), {
+ stdio: 'inherit',
+ shell: false, // Direct execution, not through shell
+ windowsHide: true
+ });
} catch (e) {
if (e.code === 'ENOENT') process.exit(0); // Silent no-op for missing myco in global context
process.exit(e.status ?? 1);
}
```
- #### Configure runtime.command for machine-wide development
-
- Point to development binaries for local testing across all projects:
-
- ```bash
- # For global install users (default machine-wide configuration)
- echo "myco" > ~/.myco/runtime.command
-
- # For local development affecting all projects (make dev-link creates this)
- echo "/path/to/myco/packages/myco-darwin-arm64/bin/myco" > ~/.myco/runtime.command
-
- # For npm link workflows affecting global hooks
- echo "myco-dev" > ~/.myco/runtime.command
- ```
-
- #### Handle PATH collision detection for global deployment
+ #### Worktree Vendor Assets and Path Resolution
- Before installation, check for conflicting binaries affecting global hooks:
+ **Issue**: When Myco is run from a git worktree with development assets, the runtime.command binary must correctly locate and load vendor assets (libsqlite3, sqlite-vec, ripgrep) from the development binary's directory.
```typescript
- // In src/cli/doctor.ts (global collision detection)
- export function detectPathCollisions(binaryName: string): string[] {
- const collisions: string[] = [];
- const pathDirs = (process.env.PATH || '').split(path.delimiter);
+ // In src/runtime/resolve-package.ts (package root discovery for worktree)
+ export function resolvePackageRoot(): string {
+ // Pattern: Try import.meta.dirname first (works in dev and compiled),
+ // then fallback to process.execPath (for compiled binaries),
+ // then process.cwd() (last resort)
- for (const dir of pathDirs) {
- const candidates = process.platform === 'win32'
- ? [`${binaryName}.exe`, `${binaryName}.cmd`, `${binaryName}.bat`]
- : [binaryName];
-
- for (const candidate of candidates) {
- const binaryPath = path.join(dir, candidate);
- if (fs.existsSync(binaryPath)) {
- collisions.push(binaryPath);
- }
- }
+ // 1. Development: import.meta.dirname points to source directory
+ if (typeof import.meta.dirname === 'string' && !import.meta.dirname.includes('/$bunfs/')) {
+ return path.resolve(import.meta.dirname, '..', '..');
}
- return collisions;
+ // 2. Compiled binary: process.execPath points to binary location
+ if (process.execPath && process.execPath !== process.argv0) {
+ return path.dirname(process.execPath);
+ }
+
+ // 3. Last resort: working directory (may not have vendor assets)
+ return process.cwd();
}
- ```
- ### Single-File Binary Constraints for Global Operation
-
- 1. **Validate Bun compilation output for machine-wide deployment**:
- ```bash
- # Check compiled binary structure using build scripts
- cd packages/myco
- node scripts/build-all-targets.mjs
- file ./dist/myco-*
- ldd ./dist/myco-* 2>/dev/null || echo "Static binaries (expected for global deployment)"
- ```
-
- 2. **Test asset bundling for global hooks**:
- ```bash
- # Verify required assets are bundled for global operation
- strings ./dist/myco-linux | grep -E "\\.(json|sql|md)$" | head -10
- ```
+ // In src/daemon/main.ts (vendor asset loading for global operation)
+ export async function loadVendorAssets() {
+ const pkgRoot = resolvePackageRoot();
+ const vendorDir = path.join(pkgRoot, 'vendor-src');
+
+ // Load embedded native dependencies from vendor directory
+ const libsqlitePath = path.join(vendorDir, 'libsqlite3', 'darwin-arm64', 'libsqlite3.dylib');
+ process.env.MYCO_LIBSQLITE_PATH = libsqlitePath;
+
+ // Verify vendor assets exist (critical for global deployment)
+ if (!fs.existsSync(libsqlitePath)) {
+ throw new Error(`Vendor asset not found: ${libsqlitePath}`);
+ }
+ }
+ ```
- 3. **Build for different platforms with global deployment support**:
- ```bash
- # Use the established build script for multi-target compilation
- make build # Uses bun build --compile for all targets
- ls -la ./dist/myco-*
- ```
+ #### Virtual Filesystem Handling for Global Binary Distribution
- 4. **Validate cross-platform deployment for global hooks**:
- ```bash
- # Test that each binary works on its target platform with global hooks
- file ./dist/myco-*
- # Deploy with machine-scoped runtime.command pins
- # Test global hook functionality on each platform
- ```
+ Bun binaries use a /$bunfs/ virtual filesystem for bundled content. This creates path resolution challenges that require careful native dependency handling, especially for machine-wide deployment.
## Cross-Cutting Gotchas
- **Machine-scoped runtime masquerade**: Machine-wide pins in `~/.myco/runtime.command` now affect all projects globally. Always check `which myco` vs pinned path when debugging unexpected behavior, as it impacts all registered projects simultaneously.
-
- **Global hook binary synchronization**: After `npm install -g @goondocks/myco@latest`, all global hooks must use the updated binary. The Grove global daemon may continue running with the old binary, causing all projects to serve HTML instead of JSON. Always restart the Grove daemon and verify global hook binary references after package upgrades.
-
- **Machine-wide binary replacement impact**: Replacing binaries while Grove daemon processes are running affects all registered projects simultaneously due to global hook architecture. Stop all myco processes before binary updates to prevent undefined behavior across the entire global capture scope.
-
- **Global-first quarantine mode**: Unknown projects operate in quarantine mode until explicitly registered. Attempting to use myco commands in unregistered projects may fail or provide limited functionality. Always register new projects with `myco projects register` to enable full global hook coverage.
-
- **Machine-scoped configuration inheritance**: Project-level runtime configuration overrides are now anti-pattern in the global-first model. Prefer machine-scoped configuration that applies to all projects unless specific compatibility requirements demand project-level overrides.
-
- **Global hook capture coordination**: When multiple projects are active simultaneously, global hooks coordinate capture through the single machine-scoped daemon. Binary updates, daemon restarts, or configuration changes affect all active projects, not just the current working directory.
-
- **Quarantine mode binary resolution**: Projects in quarantine mode may use different binary resolution logic than registered projects. This can cause confusion when the same machine-scoped binary behaves differently across quarantined vs registered project boundaries.
-
- **Machine-scoped runtime persistence**: The runtime.command file operates at machine scope (`~/.myco/runtime.command`) affecting all global hooks. Project-scoped runtime configuration (`.myco/runtime.command`) is now legacy fallback only. Procedures must prioritize machine-scoped configuration in the global-first model.
-
- **Global capture state coordination**: Multiple registered projects share the same global capture infrastructure. Daemon issues, hook failures, or configuration problems affect all projects simultaneously rather than being isolated to individual project boundaries.
+ **Launcher path quoting gotcha**: Hook dispatcher scripts must use proper shell quoting or direct execution (not through shell) when launching binary paths that contain spaces. Use spawnSync() with shell: false or quote paths explicitly in shell scripts to avoid splitting on spaces.
- **Asset loader null returns in global context**: Each asset loader (templates, manifests, hooks) can return `null` when the filesystem path fails and no bundled fallback exists. This particularly affects global hook installation where missing assets prevent proper global capture setup.
+ **Worktree vendor asset loading**: When running development binaries from git worktrees, the package resolution must correctly locate vendor assets. Use resolvePackageRoot() with import.meta.dirname detection to find assets; don't rely on process.cwd() which may be in a different project entirely.
- **Virtual filesystem path resolution for global deployment**: Bun binaries use `/$bunfs/` virtual paths that require special handling for machine-wide deployment. Never rely on `process.cwd()` for package asset resolution in global hooks — use `resolvePackageRoot()` with import.meta.dirname detection.
+ **Service directory isolation**: Dev and production service daemons must use separate directories (~/.myco/service-dev/ and ~/.myco/service/) to prevent state conflicts. The service directory selection happens once at process startup and is immutable for that instance.
- **Native dependency materialization for machine scope**: Embedded native dependencies must be extracted to temporary files before use across the global architecture. The `registerEmbeddedNativeDeps()` function handles this for libsqlite3, sqlite-vec, and ripgrep binaries across all platforms, ensuring machine-wide availability.
+ **Beta channel backup safeguard**: Always keep a backup of the production binary before beta channel switching. The backup path (~/.myco/bin/myco.production) must be outside normal executable locations to prevent accidental deletion or overwriting.