Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .github/agents/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)"*
Expand All @@ -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).

Expand Down Expand Up @@ -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**.
Expand Down
249 changes: 249 additions & 0 deletions test/init-flow.test.js
Original file line number Diff line number Diff line change
@@ -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'
);
});
});
});