From 41254a9b7974e41a477c37a184e752d85cec2682 Mon Sep 17 00:00:00 2001 From: bradygaster Date: Sun, 15 Feb 2026 01:13:12 -0800 Subject: [PATCH] fix: split Init Mode into two phases with ask_user gate (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: Init Mode numbered list triggers LLM completion impulse — model executes all steps in one turn, skipping confirmation at step 5. - Split Init Mode into Phase 1 (propose) and Phase 2 (create) - Step 5 now uses ask_user tool with choices for selectable menu - Added explicit STOP gate between Phase 1 and Phase 2 - Added Init Mode exception to Eager Execution Philosophy - New test file: test/init-flow.test.js (8 tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/squad.agent.md | 23 +++- test/init-flow.test.js | 249 ++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 test/init-flow.test.js diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 80c4e7af2..910a5a39b 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -25,9 +25,9 @@ Check: Does `.ai-team/team.md` exist? --- -## Init Mode +## Init Mode — Phase 1: Propose the Team -No team exists yet. Build one. +No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** 1. **Identify the user.** Run `git config user.name` and `git config user.email` to learn who you're working with. Use their name in conversation (e.g., *"Hey Brady, what are you building?"*). Store both in `team.md` under Project Context. 2. Ask: *"What are you building? (language, stack, what it does)"* @@ -49,8 +49,21 @@ No team exists yet. Build one. 🔄 Ralph — (monitor) Work queue, backlog, keep-alive ``` -5. Ask: *"Look right? Say **yes**, **add someone**, or **change a role**. (Or just give me a task to start!)"* -6. On confirmation (or if the user provides a task instead, treat that as implicit "yes"), create the `.ai-team/` directory structure (see `.ai-team-templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). +5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: + - **question:** *"Look right?"* + - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` + +**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** + +--- + +## Init Mode — Phase 2: Create the Team + +**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). + +> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. + +6. Create the `.ai-team/` directory structure (see `.ai-team-templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). **Casting state initialization:** Copy `.ai-team-templates/casting-policy.json` to `.ai-team/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). @@ -479,6 +492,8 @@ Users configure MCP servers at these locations (checked in priority order): ### Eager Execution Philosophy +> **⚠️ Exception:** Eager Execution does NOT apply during Init Mode Phase 1. Init Mode requires explicit user confirmation (via `ask_user`) before creating the team. Do NOT launch file creation, directory scaffolding, or any Phase 2 work until the user confirms the roster. + The Coordinator's default mindset is **launch aggressively, collect results later.** - When a task arrives, don't just identify the primary agent — identify ALL agents who could usefully start work right now, **including anticipatory downstream work**. diff --git a/test/init-flow.test.js b/test/init-flow.test.js new file mode 100644 index 000000000..ec8edaecb --- /dev/null +++ b/test/init-flow.test.js @@ -0,0 +1,249 @@ +const { describe, it, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const os = require('os'); + +const CLI = path.join(__dirname, '..', 'index.js'); + +function runSquad(args, cwd) { + try { + const result = execFileSync(process.execPath, [CLI, ...args], { + cwd, + encoding: 'utf8', + timeout: 15000, + env: { ...process.env, NO_COLOR: '1' }, + }); + return { stdout: result, exitCode: 0 }; + } catch (err) { + return { + stdout: (err.stdout || '') + (err.stderr || ''), + exitCode: err.status ?? 1, + }; + } +} + +function makeTempDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'squad-init-flow-test-')); +} + +function cleanDir(dir) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch {} +} + +function initSquad(dir) { + const result = runSquad([], dir); + assert.equal(result.exitCode, 0, `init should succeed: ${result.stdout}`); + return result; +} + +// Read the generated squad.agent.md from an initialized temp dir +function readGeneratedAgent(dir) { + const agentPath = path.join(dir, '.github', 'agents', 'squad.agent.md'); + assert.ok(fs.existsSync(agentPath), 'squad.agent.md should exist after init'); + return fs.readFileSync(agentPath, 'utf8'); +} + +// Extract ALL Init Mode sections from squad.agent.md (handles two-phase split) +function extractInitMode(content) { + // Match either "## Init Mode" or "## Init Mode — Phase 1" as the start + const phase1Match = content.match(/## Init Mode[^\n]*/); + assert.ok(phase1Match, 'squad.agent.md should contain an Init Mode heading'); + const initStart = phase1Match.index; + + // Find Phase 2 if it exists, and include it + const phase2Match = content.indexOf('## Init Mode — Phase 2'); + if (phase2Match !== -1) { + // Find the next non-Init-Mode ## heading after Phase 2 + const afterPhase2 = content.indexOf('\n## ', phase2Match + 22); + return afterPhase2 !== -1 + ? content.slice(initStart, afterPhase2) + : content.slice(initStart); + } + + // Single-section fallback + const afterStart = content.indexOf('\n## ', initStart + 12); + return afterStart !== -1 + ? content.slice(initStart, afterStart) + : content.slice(initStart); +} + +describe('Init Mode prompt structure (#66)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = makeTempDir(); + }); + + afterEach(() => { + cleanDir(tmpDir); + }); + + describe('init creates squad.agent.md with Init Mode section', () => { + it('squad.agent.md is generated by init', () => { + initSquad(tmpDir); + const agentPath = path.join(tmpDir, '.github', 'agents', 'squad.agent.md'); + assert.ok(fs.existsSync(agentPath), 'squad.agent.md should exist after init'); + }); + + it('generated file contains Init Mode section', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + assert.ok( + content.includes('## Init Mode'), + 'squad.agent.md should contain an "## Init Mode" section' + ); + }); + }); + + describe('Init Mode has explicit stop instruction', () => { + it('Init Mode contains a STOP or WAIT gate between proposing and creating', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // The prompt should contain an explicit instruction to stop and wait + // for user confirmation before creating files. + const hasStopGate = + /\bSTOP\b/i.test(initMode) || + /\bWAIT\b/i.test(initMode) || + /\bDO NOT (proceed|continue|create)/i.test(initMode) || + /\bpause\b.*\b(confirm|response|input)\b/i.test(initMode) || + /\bmust (wait|stop|pause)\b/i.test(initMode); + + assert.ok( + hasStopGate, + 'Init Mode should contain an explicit STOP/WAIT instruction between proposing the team and creating files. ' + + 'Without this, LLMs will execute steps 4-6 in a single turn, skipping user confirmation.' + ); + }); + }); + + describe('Init Mode step 5 exists and contains confirmation question', () => { + it('step 5 asks for user confirmation', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // Check that step 5 exists with a confirmation question + const hasConfirmationQuestion = + initMode.includes('Look right?') || + initMode.includes('look right?') || + /does this (look|seem) (right|good|correct)/i.test(initMode); + + assert.ok( + hasConfirmationQuestion, + 'Init Mode step 5 should ask "Look right?" or equivalent confirmation question' + ); + }); + + it('confirmation step is numbered before file creation step', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // Find the step number that contains confirmation (ask_user or "Look right?") + // Step 5 may reference ask_user with "Look right?" on a following line + const confirmMatch = + initMode.match(/(\d+)\.\s.*(?:Look right\?|look right\?|ask_user.*confirm)/i) || + initMode.match(/(\d+)\.\s.*ask_user/i); + // Find the step number that contains creating .ai-team/ + // Match lines like "6. Create the `.ai-team/` directory structure" + const createMatch = initMode.match(/^(\d+)\.\s.*(?:create|Create).*\.ai-team/m); + + assert.ok(confirmMatch, 'Should find a numbered step with confirmation question'); + assert.ok(createMatch, 'Should find a numbered step that creates .ai-team/'); + + const confirmStep = parseInt(confirmMatch[1], 10); + const createStep = parseInt(createMatch[1], 10); + + assert.ok( + confirmStep < createStep, + `Confirmation step (${confirmStep}) should come before file creation step (${createStep})` + ); + }); + }); + + describe('no tool calls between propose and confirm', () => { + it('prompt does not instruct creating files in the same step as proposing', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // Find step 4 (propose) through step 5 (confirm) — there should be + // no file creation instructions in between. + const proposeMatch = initMode.match(/4\.\s+Propose/); + const confirmMatch = initMode.match(/5\.\s+(Ask|Use)/); + + assert.ok(proposeMatch, 'Step 4 should propose the team'); + assert.ok(confirmMatch, 'Step 5 should ask for confirmation (via Ask or ask_user)'); + + const betweenProposeAndConfirm = initMode.slice( + proposeMatch.index, + confirmMatch.index + ); + + const hasFileCreation = + /create.*\.ai-team/i.test(betweenProposeAndConfirm) || + /mkdir/i.test(betweenProposeAndConfirm) || + /writeFile/i.test(betweenProposeAndConfirm) || + /fs\./i.test(betweenProposeAndConfirm); + + assert.ok( + !hasFileCreation, + 'No file creation instructions should appear between proposing the team (step 4) and asking for confirmation (step 5)' + ); + }); + + it('step 6 (Phase 2) is gated on user confirmation', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // Phase 2 should have a trigger condition requiring user confirmation + const hasPhase2Gate = + /Phase 2/.test(initMode) && + (/Trigger.*user repl/i.test(initMode) || + /user repl.*confirm/i.test(initMode) || + /after.*confirm/i.test(initMode) || + /on confirmation/i.test(initMode)); + + // Also accept the old single-section format + const step6Match = initMode.match(/^6\.\s+(.+)/m); + const step6Conditional = step6Match && + (/on confirmation/i.test(step6Match[1]) || + /after.*confirm/i.test(step6Match[1]) || + /create/i.test(step6Match[1])); + + assert.ok( + hasPhase2Gate || step6Conditional, + 'Step 6 / Phase 2 should be gated on user confirmation' + ); + }); + }); + + describe('ask_user referenced in Init Mode for confirmation', () => { + it('Init Mode instructs coordinator to use ask_user for confirmation', () => { + initSquad(tmpDir); + const content = readGeneratedAgent(tmpDir); + const initMode = extractInitMode(content); + + // The prompt should tell the coordinator to use ask_user (or equivalent + // explicit tool call) to get confirmation before proceeding. + const hasAskUser = + initMode.includes('ask_user') || + initMode.includes('askUser') || + /use.*ask.*tool/i.test(initMode) || + /\bask\b.*\band\b.*\bwait\b/i.test(initMode); + + assert.ok( + hasAskUser, + 'Init Mode should reference ask_user (or equivalent) to ensure the coordinator ' + + 'explicitly waits for user input before creating .ai-team/ files' + ); + }); + }); +});