diff --git a/.changeset/feat-1314-ralph-instructions-template.md b/.changeset/feat-1314-ralph-instructions-template.md new file mode 100644 index 000000000..1ad70d92a --- /dev/null +++ b/.changeset/feat-1314-ralph-instructions-template.md @@ -0,0 +1,28 @@ +--- +"@bradygaster/squad-cli": patch +--- + +Fix #1314: ship `ralph-instructions.md` template for `squad watch --execute` + +The `.squad/ralph-instructions.md` escape hatch used by `squad watch --execute` +had no canonical template, no documentation, and was never created by `squad init` +or `squad upgrade`. This left users who discovered the code path inventing their +own format with no contract or safety guidance. + +This patch promotes the escape hatch to a documented feature: + +- Adds `packages/squad-cli/templates/ralph-instructions.md` — a comment-documented + stub that explains the override format, trust implications, and the stable contract + (what CAN vs CANNOT be customized via this file). +- Registers it in `TEMPLATE_MANIFEST` as a user-owned entry + (`overwriteOnUpgrade: false`) so `squad init` and `squad upgrade` install it + without clobbering user customizations. The destination path + (`ralph-instructions.md` under `.squad/`) exactly matches the `existsSync` lookup + in `execute.ts`. +- Adds three regression tests in `init-upgrade-parity.test.ts`: install on init, + non-overwrite on upgrade (preservation), and install-when-missing on upgrade. +- Adds one new test in `watch-capabilities.test.ts` asserting the `existsSync` + path includes the correct `.squad/ralph-instructions.md` segment derived from + `teamRoot`. + +Closes #1314 diff --git a/.squad-templates/ralph-instructions.md b/.squad-templates/ralph-instructions.md new file mode 100644 index 000000000..cfa653adc --- /dev/null +++ b/.squad-templates/ralph-instructions.md @@ -0,0 +1,67 @@ +# Ralph Instructions + + + + +## Ralph, Go! + +Read this file for your full instructions. Follow ALL sections. +MAXIMIZE PARALLELISM — spawn agents for ALL actionable issues simultaneously. + +### Issue Selection + +Work on every open, unblocked, unassigned issue labeled `squad` or `squad:{member}`. +Skip issues that are assigned to a human, blocked, or marked `status:on-hold`. + +### Post-Task Actions + + + +### Escalation + +If you are blocked on an issue, comment on it explaining why, add a `status:blocked` +label, and move to the next actionable item. Do not halt the loop. diff --git a/docs/src/content/docs/features/consult-mode.md b/docs/src/content/docs/features/consult-mode.md index 4c0b8e3af..7030e9965 100644 --- a/docs/src/content/docs/features/consult-mode.md +++ b/docs/src/content/docs/features/consult-mode.md @@ -258,6 +258,6 @@ All consultations are tracked in your personal squad at `consultations/{project} ## Next Steps -- **Set up a personal squad:** See [Your Personal Squad](../guide/personal-squad.md) for initial setup with `squad init --global` +- **Set up a personal squad:** See [Team Setup](../features/team-setup.md) for initial setup with `squad init --global` - **Learn about sharing:** See [Export & Import](./export-import.md) for portable team snapshots - **Upstream inheritance:** See [Upstream Inheritance](./upstream-inheritance.md) for knowledge sharing across teams diff --git a/docs/src/content/docs/features/squad-rc.md b/docs/src/content/docs/features/squad-rc.md index 998d0803c..3b1b73087 100644 --- a/docs/src/content/docs/features/squad-rc.md +++ b/docs/src/content/docs/features/squad-rc.md @@ -35,7 +35,7 @@ squad rc --tunnel **When to use `squad start`:** You want to mirror a terminal session to your phone (demos, pairing, watching long-running processes). -> 💡 **Looking for terminal mirroring?** See [squad start](./remote-control.md). +> 💡 **Looking for terminal mirroring?** See [squad start](./squad-rc.md). --- @@ -471,7 +471,7 @@ squad rc --port 8080 # Specific port ## See Also -- [squad start](./remote-control.md) — PTY mirror mode for terminal streaming +- squad start — PTY mirror mode for terminal streaming - [CLI Reference](../reference/cli.md) — All squad commands - [Remote Control Protocol](https://github.com/bradygaster/squad/blob/main/packages/squad-sdk/src/remote/protocol.ts) — Wire protocol types - [RemoteBridge SDK](https://github.com/bradygaster/squad/blob/main/packages/squad-sdk/src/remote/bridge.ts) — Server implementation diff --git a/docs/src/content/docs/features/vscode.md b/docs/src/content/docs/features/vscode.md index 6d35f3d3a..ebbb0e79d 100644 --- a/docs/src/content/docs/features/vscode.md +++ b/docs/src/content/docs/features/vscode.md @@ -241,5 +241,5 @@ const decision = await coordinator.route(userTask, { - [Getting Started](../get-started/installation.md) — Installation and setup guide - [Parallel Execution](parallel-execution.md) — How Squadron fan-outs agents - [Model Selection](model-selection.md) — Cost-first routing strategy -- [Interactive Shell](../guide/shell.md) — Shell commands and features +- [CLI Reference](../reference/cli.md) — Shell commands and features - [SDK API Reference](../reference/api-reference.md) — Full SDK type and function reference diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index 35d6e39b9..0041c86c9 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -118,7 +118,7 @@ squad start --tunnel --yolo squad start --tunnel --model gpt-4 --no-config ``` -For details on architecture, security, mobile keyboard, and troubleshooting, see [Remote Control Guide](../features/remote-control.md). +For details on architecture, security, mobile keyboard, and troubleshooting, see [Remote Control Guide](../features/squad-rc.md). --- diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index fe74f89cb..48d296804 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -15,7 +15,8 @@ import { initSquad as sdkInitSquad, cleanupOrphanInitPrompt, ensurePersonalSquad import { installGitHooks } from '../commands/install-hooks.js'; import { liftInitMutableStateOntoOrphan } from '../commands/migrate-backend.js'; import { resolveSquadStateMcpSpec } from './mcp-spec.js'; -import { describeMcpSpec } from './upgrade.js'; +import { describeMcpSpec, ensureUserOwnedTemplates } from './upgrade.js'; +import { getTemplatesDir } from './templates.js'; import { ensureSquadStateMcpInRoot, tombstoneStaleSquadStateInProjectMcp } from './mcp-root.js'; import { readTeamMd, @@ -326,6 +327,17 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi // immediately instead of after the 5-second TTL. clearResolveSquadCache(); + // Install user-owned template files that are not yet present on disk. + // These were not created by sdkInitSquad (which owns ceremonies.md, routing.md, etc. + // via the SDK templates path). ensureUserOwnedTemplates reads TEMPLATE_MANIFEST + // entries with overwriteOnUpgrade: false and copies each one only if absent. + try { + const templatesDir = getTemplatesDir(); + ensureUserOwnedTemplates(dest, templatesDir); + } catch { + // Non-fatal: if templates dir is not found (unusual install), skip silently. + } + // ── Personal squad linking ───────────────────────────────────────── // When a personal squad directory exists and this is a repo init (not global), // set teamRoot in config.json to point to the global squad root that contains diff --git a/packages/squad-cli/src/cli/core/templates.ts b/packages/squad-cli/src/cli/core/templates.ts index 39a249de7..621cf86f6 100644 --- a/packages/squad-cli/src/cli/core/templates.ts +++ b/packages/squad-cli/src/cli/core/templates.ts @@ -167,6 +167,15 @@ export const TEMPLATE_MANIFEST: TemplateFile[] = [ }, // User-owned files (never overwrite) + { + // ralph-instructions.md is read by execute.ts when `squad watch --execute` is run. + // User-owned so customizations survive upgrade. Install path must match the + // existsSync lookup in execute.ts: path.join(teamRoot, '.squad', 'ralph-instructions.md'). + source: 'ralph-instructions.md', + destination: 'ralph-instructions.md', + overwriteOnUpgrade: false, + description: 'Ralph autonomous-execution instructions (user-customizable override)', + }, { source: 'ceremonies.md', destination: 'ceremonies.md', diff --git a/packages/squad-cli/src/cli/core/upgrade.ts b/packages/squad-cli/src/cli/core/upgrade.ts index a2f8ca5d6..45cbc8e95 100644 --- a/packages/squad-cli/src/cli/core/upgrade.ts +++ b/packages/squad-cli/src/cli/core/upgrade.ts @@ -853,6 +853,12 @@ async function runEnsureChecks(dest: string, templatesDir: string, filesUpdated: filesUpdated.push(...builtinAgents); } + const userOwned = ensureUserOwnedTemplates(dest, templatesDir); + if (userOwned.length > 0) { + success(`installed ${userOwned.length} missing user-owned template(s): ${userOwned.join(', ')}`); + filesUpdated.push(...userOwned); + } + const skillMigration = migrateLegacyCopilotSkills(dest); if (skillMigration.migrated.length > 0) { success(`migrated ${skillMigration.migrated.length} skill(s) from .copilot/skills/ → .github/skills/`); @@ -1024,6 +1030,39 @@ export function ensureBuiltinAgents(dest: string, templatesDir: string): string[ return created; } +/** + * Install user-owned template files that are missing on disk. + * + * Iterates TEMPLATE_MANIFEST entries with `overwriteOnUpgrade: false` and + * copies each source template to its destination only if the destination + * does not yet exist. Existing user-customized files are never touched. + * + * Called during both `squad init` (post-SDK scaffolding) and `squad upgrade` + * (via runEnsureChecks) so that newly-added user-owned templates are installed + * for existing squads on upgrade without overwriting any prior customization. + */ +export function ensureUserOwnedTemplates(dest: string, templatesDir: string): string[] { + const created: string[] = []; + const squadDir = path.join(dest, '.squad'); + const userOwned = TEMPLATE_MANIFEST.filter(f => !f.overwriteOnUpgrade && !f.source.startsWith('skills/')); + + for (const entry of userOwned) { + const srcPath = path.join(templatesDir, entry.source); + // Destination paths can be relative to .squad/ or can use '../' for repo root + const destPath = entry.destination.startsWith('../') + ? path.join(dest, entry.destination.slice(3)) + : path.join(squadDir, entry.destination); + + if (!storage.existsSync(srcPath)) continue; + if (storage.existsSync(destPath)) continue; + + storage.mkdirSync(path.dirname(destPath), { recursive: true }); + storage.copySync(srcPath, destPath); + created.push(entry.destination); + } + return created; +} + /** * Run the upgrade command */ diff --git a/packages/squad-cli/templates/ralph-instructions.md b/packages/squad-cli/templates/ralph-instructions.md new file mode 100644 index 000000000..cfa653adc --- /dev/null +++ b/packages/squad-cli/templates/ralph-instructions.md @@ -0,0 +1,67 @@ +# Ralph Instructions + + + + +## Ralph, Go! + +Read this file for your full instructions. Follow ALL sections. +MAXIMIZE PARALLELISM — spawn agents for ALL actionable issues simultaneously. + +### Issue Selection + +Work on every open, unblocked, unassigned issue labeled `squad` or `squad:{member}`. +Skip issues that are assigned to a human, blocked, or marked `status:on-hold`. + +### Post-Task Actions + + + +### Escalation + +If you are blocked on an issue, comment on it explaining why, add a `status:blocked` +label, and move to the next actionable item. Do not halt the loop. diff --git a/packages/squad-sdk/templates/ralph-instructions.md b/packages/squad-sdk/templates/ralph-instructions.md new file mode 100644 index 000000000..cfa653adc --- /dev/null +++ b/packages/squad-sdk/templates/ralph-instructions.md @@ -0,0 +1,67 @@ +# Ralph Instructions + + + + +## Ralph, Go! + +Read this file for your full instructions. Follow ALL sections. +MAXIMIZE PARALLELISM — spawn agents for ALL actionable issues simultaneously. + +### Issue Selection + +Work on every open, unblocked, unassigned issue labeled `squad` or `squad:{member}`. +Skip issues that are assigned to a human, blocked, or marked `status:on-hold`. + +### Post-Task Actions + + + +### Escalation + +If you are blocked on an issue, comment on it explaining why, add a `status:blocked` +label, and move to the next actionable item. Do not halt the loop. diff --git a/templates/ralph-instructions.md b/templates/ralph-instructions.md new file mode 100644 index 000000000..cfa653adc --- /dev/null +++ b/templates/ralph-instructions.md @@ -0,0 +1,67 @@ +# Ralph Instructions + + + + +## Ralph, Go! + +Read this file for your full instructions. Follow ALL sections. +MAXIMIZE PARALLELISM — spawn agents for ALL actionable issues simultaneously. + +### Issue Selection + +Work on every open, unblocked, unassigned issue labeled `squad` or `squad:{member}`. +Skip issues that are assigned to a human, blocked, or marked `status:on-hold`. + +### Post-Task Actions + + + +### Escalation + +If you are blocked on an issue, comment on it explaining why, add a `status:blocked` +label, and move to the next actionable item. Do not halt the loop. diff --git a/test/cli/init-upgrade-parity.test.ts b/test/cli/init-upgrade-parity.test.ts index 60879351e..c47768698 100644 --- a/test/cli/init-upgrade-parity.test.ts +++ b/test/cli/init-upgrade-parity.test.ts @@ -238,4 +238,41 @@ describe('Init / Upgrade parity', () => { expect(count).toBe(1); } }); + + // ------------------------------------------------------------------------- + // 8. ralph-instructions.md: init installs, upgrade preserves + // ------------------------------------------------------------------------- + it('init installs .squad/ralph-instructions.md', async () => { + await runInit(TEST_ROOT); + const filePath = join(TEST_ROOT, '.squad', 'ralph-instructions.md'); + expect(existsSync(filePath)).toBe(true); + const content = await readFile(filePath, 'utf-8'); + expect(content).toContain('ralph-instructions.md'); + expect(content).toContain('squad watch --execute'); + }); + + it('upgrade does not overwrite a customized .squad/ralph-instructions.md', async () => { + await runInit(TEST_ROOT); + const filePath = join(TEST_ROOT, '.squad', 'ralph-instructions.md'); + const custom = '# My Custom Ralph Instructions\nDo something special.'; + await writeFile(filePath, custom, 'utf-8'); + + await runUpgrade(TEST_ROOT); + + const afterUpgrade = await readFile(filePath, 'utf-8'); + expect(afterUpgrade).toBe(custom); + }); + + it('upgrade installs .squad/ralph-instructions.md when it does not exist', async () => { + // init first to get a valid .squad dir, then remove the file to simulate + // a repo that predates this template entry. + await runInit(TEST_ROOT); + const filePath = join(TEST_ROOT, '.squad', 'ralph-instructions.md'); + const { rm: rmFile } = await import('fs/promises'); + if (existsSync(filePath)) await rmFile(filePath); + + await runUpgrade(TEST_ROOT); + + expect(existsSync(filePath)).toBe(true); + }); }); \ No newline at end of file diff --git a/test/cli/watch-capabilities.test.ts b/test/cli/watch-capabilities.test.ts index 3d2689a5d..308d88e15 100644 --- a/test/cli/watch-capabilities.test.ts +++ b/test/cli/watch-capabilities.test.ts @@ -194,6 +194,26 @@ describe('Watch Capabilities', () => { expect(prompt).not.toContain('Ralph, Go!'); }); + it('checks .squad/ralph-instructions.md inside teamRoot', () => { + // Verify that existsSync is called with the correct path so that the + // TEMPLATE_MANIFEST destination ('ralph-instructions.md' under .squad/) + // matches the lookup in execute.ts. + mockFsExistsSync.mockImplementation((p: unknown) => { + return typeof p === 'string' && p.endsWith('.squad/ralph-instructions.md'); + }); + const issues: ExecutableWorkItem[] = [ + { number: 1, title: 'Task', labels: [{ name: 'squad' }], assignees: [] }, + ]; + const prompt = buildAgentPrompt(issues, '/some/repo'); + expect(mockFsExistsSync).toHaveBeenCalledWith( + expect.stringContaining('.squad/ralph-instructions.md'), + ); + // Path must be constructed from teamRoot, not a global path + const [calledPath] = mockFsExistsSync.mock.calls[0] as [string]; + expect(calledPath).toContain('/some/repo'); + expect(prompt).toContain('Ralph, Go!'); + }); + it('formats labels and assignees in issue list', () => { const issues: ExecutableWorkItem[] = [{ number: 42, diff --git a/test/docs-build.test.ts b/test/docs-build.test.ts index 6d1a4031f..cb840883c 100644 --- a/test/docs-build.test.ts +++ b/test/docs-build.test.ts @@ -23,9 +23,9 @@ function docsBuildSkipReason(): string | null { } // Expected content directories in src/content/docs/ -const EXPECTED_GET_STARTED = ['installation', 'first-session', 'five-minute-start', 'choosing-your-path', 'migration']; +const EXPECTED_GET_STARTED = ['installation', 'first-session', 'five-minute-start', 'choose-your-interface', 'migration']; -const EXPECTED_GUIDES = ['build-autonomous-agent', 'building-extensions', 'building-resilient-agents', 'contributing', 'contributors', 'extensibility', 'faq', 'github-auth-setup', 'personal-squad', 'sample-prompts', 'shell', 'tips-and-tricks']; +const EXPECTED_GUIDES = ['build-autonomous-agent', 'building-extensions', 'building-resilient-agents', 'contributing', 'contributors', 'extensibility', 'faq', 'github-auth-setup', 'sample-prompts', 'tips-and-tricks']; const EXPECTED_REFERENCE = ['cli', 'sdk', 'config', 'api-reference', 'integration', 'tools-and-hooks', 'glossary']; @@ -82,7 +82,6 @@ const EXPECTED_FEATURES = [ 'project-boards', 'ralph', 'rate-limiting', - 'remote-control', 'response-modes', 'reviewer-protocol', 'routing',