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
12 changes: 12 additions & 0 deletions .changeset/preserve-gh-aw-builtins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@bradygaster/squad-cli": patch
"@bradygaster/squad-sdk": patch
---

Standardize the Rai built-in agent on the canonical lowercase directory/template ID `rai` (matching `scribe`, `ralph`, and `fact-checker`) instead of the mixed-case `Rai`, which broke on case-sensitive filesystems (Linux CI) and silently duplicated the agent on case-insensitive ones (macOS/Windows).

`squad upgrade` now detects a legacy case-sensitive `.squad/agents/Rai/` directory and migrates it in place to `.squad/agents/rai/`, preserving the existing charter/history content. If both `Rai/` and `rai/` already exist (a partial upgrade on a case-sensitive filesystem), the canonical `rai/` is kept untouched and only the stale `Rai/` copy is removed — no user customization is ever clobbered, and repeat upgrade runs are idempotent.

The shipped Rai charter template's manifest source/destination were also renamed from `Rai-charter.md` to lowercase `rai-charter.md`, matching the file already shipped under `templates/`.

The `workflows/squad.md` gh-aw coordinator prompt and its paired `squad-cast-validator.mjs` validator now let the generated coordinator name the four permanent built-in support agents (Scribe, Ralph, Rai, Fact Checker) and state their fixed lifecycle duties in a dedicated `## Built-in Support Agents` section, while still forbidding any reference to unrelated standalone lifecycle/log/decision state or paths.
2 changes: 1 addition & 1 deletion .squad-templates/fact-checker-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- **Name:** Fact Checker
- **Role:** Devil's Advocate & Verification Agent
- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive.
- **Casting:** Gets a universe name like any other agent (not exempt like Scribe/Ralph).
- **Casting:** Permanently exempt from casting/universe naming alongside Scribe, Ralph, and Rai; fixed ID `fact-checker`.

## What I Do

Expand Down
2 changes: 1 addition & 1 deletion .squad-templates/rai-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

- `.squad/rai/policy.md` — Canonical RAI policy (terms, anti-patterns, taxonomy)
- `.squad/rai/audit-trail.md` — Evidence log (append-only, redacted)
- `.squad/agents/Rai/history.md` — Learnings across sessions
- `.squad/agents/rai/history.md` — Learnings across sessions

## Traffic Light Verdicts

Expand Down
52 changes: 52 additions & 0 deletions .squad-templates/ralph-charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Ralph

> Keeps the board moving until it is actually clear.

## Identity

- **Name:** Ralph
- **Role:** Work Monitor
- **Style:** Persistent, concise, operational.
- **Mode:** Active on request ("Ralph, go") or idle-watch by default. Never blocks the conversation.

## What I Own

- Scanning for open Squad work: `squad:*` labels, draft PRs, review feedback, CI failures, merge-ready PRs.
- Driving the work queue while active — scan, act, rescan, repeat.
- Reporting compact board status.

## How I Work

**Philosophy:** don't pause for permission between work items while active — keep looping until the
board is clear or the user explicitly says idle/stop. A clear board moves me to idle-watch, not full
shutdown.

1. **Scan** — check for actionable work in priority order: untriaged issues, assigned work, CI
failures, review feedback, approved/merge-ready PRs.
2. **Act** — route each item to the responsible agent; never modify product artifacts directly.
3. **Rescan** — after work lands, check again immediately. Do not wait for the user to ask.
4. **Report** — compact board status only. No narration, no filler.

Use the `gh` CLI when GitHub MCP tooling is unavailable.

## Board Status Format

```
📋 Board: {N} open | {M} in review | {K} merge-ready
```

## Boundaries

**I handle:** Work discovery, board status, issue/PR monitoring, keep-working loops.

**I don't handle:** Feature implementation, security review, content writing, design decisions. I
route work — I don't do it.

## Project Context

**Project:** {project_name}
{project_description}

## Learnings

Initial setup complete. Ready to monitor the board.
32 changes: 25 additions & 7 deletions packages/squad-cli/src/cli/core/cast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ function generateCharter(member: CastMember & { _personality?: string; _backstor
if (catalogCharter) return catalogCharter;

const personality = personalityForRole(member.role, { personality: member._personality });
const nameLower = member.name.toLowerCase();
const nameLower = memberId(member.name);

// If CastingEngine provided a backstory, use it in the charter preamble
const preamble = member._backstory || personality;
Expand Down Expand Up @@ -477,6 +477,24 @@ function generateHistory(member: CastMember, projectDescription: string): string

// ── Built-in agents ────────────────────────────────────────────────

// The four built-in support agents always materialize under these exact
// lowercase kebab-case directory/registry IDs, regardless of display-name
// casing or spacing (`member.name.toLowerCase()` alone turns "Fact Checker"
// into "fact checker" — a space, not a hyphen — and leaves "Rai" ambiguous
// against other built-in scaffolders). Canonical built-in contract: scribe,
// ralph, rai, fact-checker.
const BUILTIN_IDS: Record<string, string> = {
'Scribe': 'scribe',
'Ralph': 'ralph',
'Rai': 'rai',
'Fact Checker': 'fact-checker',
};

/** Resolve a member's directory/registry ID: the authoritative built-in ID when the name is one of the four built-ins, otherwise the existing lowercased-name behavior for specialists. */
function memberId(name: string): string {
return BUILTIN_IDS[name] ?? name.toLowerCase();
}

function scribeMember(): CastMember {
return { name: 'Scribe', role: 'Session Logger', scope: 'Maintaining decisions.md, cross-agent context sharing, orchestration logging, session logging, git commits', emoji: '📋' };
}
Expand Down Expand Up @@ -592,7 +610,7 @@ function RaiCharter(): string {

- \`.squad/rai/policy.md\` — Canonical RAI policy (terms, anti-patterns, taxonomy)
- \`.squad/rai/audit-trail.md\` — Evidence log (append-only, redacted)
- \`.squad/agents/Rai/history.md\` — Learnings across sessions
- \`.squad/agents/rai/history.md\` — Learnings across sessions

## Traffic Light Verdicts

Expand Down Expand Up @@ -640,7 +658,7 @@ Before starting work, run \`git rev-parse --show-toplevel\` to find the repo roo

Read \`.squad/rai/policy.md\` for the canonical check definitions.
Append findings to \`.squad/rai/audit-trail.md\` (redacted — never raw secrets or harmful text).
After making a decision others should know, write it to \`.squad/decisions/inbox/Rai-{brief-slug}.md\`.
After making a decision others should know, write it to \`.squad/decisions/inbox/rai-{brief-slug}.md\`.
`;
}

Expand All @@ -649,7 +667,7 @@ After making a decision others should know, write it to \`.squad/decisions/inbox
function buildMembersTable(allMembers: CastMember[]): string {
let table = `## Members\n\n| Name | Role | Charter | Status |\n|------|------|---------|--------|\n`;
for (const m of allMembers) {
const nameLower = m.name.toLowerCase();
const nameLower = memberId(m.name);
let status = '✅ Active';
if (m.role === 'Session Logger') status = '📋 Silent';
if (m.role === 'Work Monitor') status = '🔄 Monitor';
Expand Down Expand Up @@ -700,7 +718,7 @@ export async function createTeam(teamRoot: string, proposal: CastProposal): Prom

// Create agent directories and files
for (const member of allMembers) {
const nameLower = member.name.toLowerCase();
const nameLower = memberId(member.name);
const agentDir = join(agentsDir, nameLower);

const charterPath = join(agentDir, 'charter.md');
Expand Down Expand Up @@ -805,7 +823,7 @@ export async function createTeam(teamRoot: string, proposal: CastProposal): Prom
const registryAgents: Record<string, object> = {};
const snapshotAgents: string[] = [];
for (const member of allMembers) {
const nameLower = member.name.toLowerCase();
const nameLower = memberId(member.name);
registryAgents[nameLower] = {
created_at: now,
persistent_name: member.name,
Expand Down Expand Up @@ -853,7 +871,7 @@ export async function createTeam(teamRoot: string, proposal: CastProposal): Prom

// Sync new agents into squad.config.ts (if present)
for (const member of allMembers) {
await addAgentToConfig(teamRoot, member.name.toLowerCase(), member.role);
await addAgentToConfig(teamRoot, memberId(member.name), member.role);
}

// Re-advertise the cast in .github/agents/squad.agent.md (#1608). Cast
Expand Down
4 changes: 2 additions & 2 deletions packages/squad-cli/src/cli/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ export const TEMPLATE_MANIFEST: TemplateFile[] = [
description: 'Scribe charter template',
},
{
source: 'Rai-charter.md',
destination: 'templates/Rai-charter.md',
source: 'rai-charter.md',
destination: 'templates/rai-charter.md',
overwriteOnUpgrade: true,
description: 'Rai RAI reviewer charter template',
},
Expand Down
82 changes: 81 additions & 1 deletion packages/squad-cli/src/cli/core/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,15 @@ async function runEnsureChecks(dest: string, templatesDir: string, filesUpdated:
filesUpdated.push(...memoryFiles);
}

const raiMigration = migrateLegacyRaiAgent(dest);
if (raiMigration.migrated.length > 0) {
success('migrated .squad/agents/Rai/ → .squad/agents/rai/');
filesUpdated.push(...raiMigration.migrated);
}
if (raiMigration.tombstoned.length > 0) {
success('removed stale .squad/agents/Rai/ (superseded by .squad/agents/rai/)');
}

const builtinAgents = ensureBuiltinAgents(dest, templatesDir);
if (builtinAgents.length > 0) {
const uniqueAgentNames = Array.from(new Set(builtinAgents.map(p => path.basename(path.dirname(p)))));
Expand Down Expand Up @@ -997,6 +1006,66 @@ export function ensureMemoryGovernanceUpgradeDefaults(dest: string): string[] {
return created;
}

/**
* Migrate the legacy case-sensitive `.squad/agents/Rai/` directory — produced
* by prior releases that scaffolded the built-in with a capitalized directory
* name — to the canonical lowercase `.squad/agents/rai/`, preserving any
* existing charter/history content.
*
* Detection uses `storage.listSync` (on-disk entry names) rather than
* `storage.existsSync('.squad/agents/rai')`, because `existsSync` can report
* a false positive for `rai` on case-insensitive filesystems (macOS, Windows)
* even when only the legacy `Rai` entry physically exists — that false
* positive would make the migration silently no-op on those platforms and
* leave the case-sensitive `.squad/agents/Rai/` behind on Linux checkouts of
* the same repo.
*
* Idempotent and duplicate-safe:
* - Only `Rai/` exists → rename it in place to `rai/` (single `renameSync`,
* same convention as `migrateDirectory`'s `.ai-team/` → `.squad/` rename).
* - Both `Rai/` and `rai/` exist (possible on a case-sensitive filesystem
* after a partial upgrade) → canonical `rai/` wins; the legacy `Rai/` is
* removed without overwriting `rai/`, so no in-place customization at the
* canonical location is ever clobbered.
* - Neither exists, or only `rai/` exists → no-op.
*
* @param dest Root directory containing .squad/
* @returns Paths (relative to dest) affected by the migration
*/
export function migrateLegacyRaiAgent(dest: string): { migrated: string[]; tombstoned: string[] } {
const agentsDir = path.join(dest, '.squad', 'agents');
if (!storage.existsSync(agentsDir)) return { migrated: [], tombstoned: [] };

const entries = storage.listSync(agentsDir);
const legacyDir = path.join(agentsDir, 'Rai');
if (!entries.includes('Rai') || !storage.isDirectorySync(legacyDir)) {
return { migrated: [], tombstoned: [] };
}

const canonicalDir = path.join(agentsDir, 'rai');
const migrated: string[] = [];
const tombstoned: string[] = [];

if (entries.includes('rai')) {
// Both exist — canonical `rai/` wins, drop the legacy copy as-is.
try {
storage.deleteDirSync(legacyDir);
tombstoned.push(path.posix.join('.squad/agents', 'Rai'));
} catch {
// best-effort; leave the legacy dir if we can't remove it
}
return { migrated, tombstoned };
}

try {
storage.renameSync(legacyDir, canonicalDir);
migrated.push(path.posix.join('.squad/agents', 'rai'));
} catch {
// best-effort; leave the legacy dir if the rename fails (e.g. read-only fs)
}
return { migrated, tombstoned };
}

/**
* Scaffold always-on built-in agent charters (Rai, Fact Checker) that ship
* as templates but may be missing from older squads. Idempotent — only writes
Expand All @@ -1009,6 +1078,11 @@ export function ensureMemoryGovernanceUpgradeDefaults(dest: string): string[] {
* cast.ts (no shipped template file). Adding them here would risk overwriting
* customized versions on legacy squads.
*
* Callers should run `migrateLegacyRaiAgent(dest)` before this function so a
* pre-existing legacy `.squad/agents/Rai/` is renamed to `rai/` first — this
* function's idempotency check (`storage.existsSync(agentDir)`) would
* otherwise miss it on case-sensitive filesystems and scaffold a duplicate.
*
* @param dest Root directory containing .squad/
* @param templatesDir Directory containing shipped charter templates
* @returns Paths (relative to dest) of created agent files
Expand All @@ -1023,8 +1097,14 @@ export function ensureBuiltinAgents(dest: string, templatesDir: string): string[
// - dirName: case-preserving directory name under .squad/agents/
// - templateFile: filename under templatesDir (charter template)
// - displayName: shown in history.md header
//
// dirName/templateFile use the authoritative lowercase kebab-case built-in
// ID (`rai`, not `Rai`) so this matches the canonical `.squad-templates/`
// charter filenames and the directory IDs every other built-in scaffolder
// (cast.ts's `memberId()`) now produces. Canonical built-in contract:
// scribe, ralph, rai, fact-checker.
const builtins: Array<{ dirName: string; templateFile: string; displayName: string }> = [
{ dirName: 'Rai', templateFile: 'Rai-charter.md', displayName: 'Rai' },
{ dirName: 'rai', templateFile: 'rai-charter.md', displayName: 'Rai' },
{ dirName: 'fact-checker', templateFile: 'fact-checker-charter.md', displayName: 'Fact Checker' },
];

Expand Down
2 changes: 1 addition & 1 deletion packages/squad-cli/templates/fact-checker-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- **Name:** Fact Checker
- **Role:** Devil's Advocate & Verification Agent
- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive.
- **Casting:** Gets a universe name like any other agent (not exempt like Scribe/Ralph).
- **Casting:** Permanently exempt from casting/universe naming alongside Scribe, Ralph, and Rai; fixed ID `fact-checker`.

## What I Do

Expand Down
2 changes: 1 addition & 1 deletion packages/squad-cli/templates/rai-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

- `.squad/rai/policy.md` — Canonical RAI policy (terms, anti-patterns, taxonomy)
- `.squad/rai/audit-trail.md` — Evidence log (append-only, redacted)
- `.squad/agents/Rai/history.md` — Learnings across sessions
- `.squad/agents/rai/history.md` — Learnings across sessions

## Traffic Light Verdicts

Expand Down
52 changes: 52 additions & 0 deletions packages/squad-cli/templates/ralph-charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Ralph

> Keeps the board moving until it is actually clear.

## Identity

- **Name:** Ralph
- **Role:** Work Monitor
- **Style:** Persistent, concise, operational.
- **Mode:** Active on request ("Ralph, go") or idle-watch by default. Never blocks the conversation.

## What I Own

- Scanning for open Squad work: `squad:*` labels, draft PRs, review feedback, CI failures, merge-ready PRs.
- Driving the work queue while active — scan, act, rescan, repeat.
- Reporting compact board status.

## How I Work

**Philosophy:** don't pause for permission between work items while active — keep looping until the
board is clear or the user explicitly says idle/stop. A clear board moves me to idle-watch, not full
shutdown.

1. **Scan** — check for actionable work in priority order: untriaged issues, assigned work, CI
failures, review feedback, approved/merge-ready PRs.
2. **Act** — route each item to the responsible agent; never modify product artifacts directly.
3. **Rescan** — after work lands, check again immediately. Do not wait for the user to ask.
4. **Report** — compact board status only. No narration, no filler.

Use the `gh` CLI when GitHub MCP tooling is unavailable.

## Board Status Format

```
📋 Board: {N} open | {M} in review | {K} merge-ready
```

## Boundaries

**I handle:** Work discovery, board status, issue/PR monitoring, keep-working loops.

**I don't handle:** Feature implementation, security review, content writing, design decisions. I
route work — I don't do it.

## Project Context

**Project:** {project_name}
{project_description}

## Learnings

Initial setup complete. Ready to monitor the board.
2 changes: 1 addition & 1 deletion packages/squad-sdk/templates/fact-checker-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- **Name:** Fact Checker
- **Role:** Devil's Advocate & Verification Agent
- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive.
- **Casting:** Gets a universe name like any other agent (not exempt like Scribe/Ralph).
- **Casting:** Permanently exempt from casting/universe naming alongside Scribe, Ralph, and Rai; fixed ID `fact-checker`.

## What I Do

Expand Down
2 changes: 1 addition & 1 deletion packages/squad-sdk/templates/rai-charter.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

- `.squad/rai/policy.md` — Canonical RAI policy (terms, anti-patterns, taxonomy)
- `.squad/rai/audit-trail.md` — Evidence log (append-only, redacted)
- `.squad/agents/Rai/history.md` — Learnings across sessions
- `.squad/agents/rai/history.md` — Learnings across sessions

## Traffic Light Verdicts

Expand Down
Loading