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
16 changes: 16 additions & 0 deletions .changeset/fix-fact-checker-auto-scaffold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@bradygaster/squad-cli": patch
---

Fix #1222: Auto-scaffold Fact Checker agent during `squad init`, `squad cast`, and `squad upgrade`

The Fact Checker role was added in v0.10.0 (#789) with its catalog entry, charter template, skill, AGENT_TEMPLATES map entry, and template manifest entry — but it was never wired into the user-facing onboarding flow. Users running `squad init` got Scribe/Ralph/Rai but never saw Fact Checker. Users running `squad upgrade` from older versions never got Rai or Fact Checker scaffolded either (upgrade was intentionally silent on agents).

This change wires Fact Checker (and Rai, as a defensive backfill) into three code paths:

- **`init.ts`** — adds `fact-checker` to the default `agents:` array passed to `sdkInitSquad()`. Fresh `squad init` now produces `.squad/agents/fact-checker/`.
- **`cast.ts`** — adds `factCheckerMember()`, `factCheckerCharter()`, `hasFactChecker` branches in `castTeam()`, and the roster banner line. Interactive `squad cast` now offers Fact Checker as an always-on background agent.
- **`upgrade.ts`** — new `ensureBuiltinAgents()` runs in `runEnsureChecks()`. Idempotently scaffolds `.squad/agents/Rai/` and `.squad/agents/fact-checker/` from shipped charter templates if missing. Never overwrites existing charters or history files. Scribe and Ralph are intentionally NOT scaffolded by upgrade (they predate this fix in all squads, and their charters are inlined in cast.ts).

Result: any squad — fresh init, interactive cast, or upgrade from any prior version — now ends up with Fact Checker available.

86 changes: 86 additions & 0 deletions packages/squad-cli/src/cli/core/cast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,82 @@ function RaiMember(): CastMember {
return { name: 'Rai', role: 'RAI Reviewer', scope: 'Content safety, bias detection, credential scanning, ethical review', emoji: '🛡️' };
}

function factCheckerMember(): CastMember {
return { name: 'Fact Checker', role: 'Fact Checker', scope: 'Claim verification, hallucination detection, counter-hypothesis analysis, source validation', emoji: '🔍' };
}
Comment on lines +501 to +503

function factCheckerCharter(): string {
return `# Fact Checker

> Trust, but verify. Every claim gets a source check.

## Identity

- **Name:** Fact Checker
- **Role:** Devil's Advocate & Verification Agent
- **Emoji:** 🔍
- **Style:** Rigorous but constructive. Flags issues clearly without being abrasive.

## What I Do

Validate claims, detect hallucinations, and run counter-hypotheses on team output before it ships.

## Verification Methodology

For every claim or assertion I review:

1. **Source Check:** What evidence supports this? Can I verify it?
2. **Counter-Hypothesis:** What would disprove this? Is there an alternative explanation?
3. **Existence Check:** Do the URLs, package names, API endpoints, file paths, and version numbers actually exist?
4. **Consistency Check:** Does this contradict anything in \`.squad/decisions.md\` or prior team output?

## Confidence Ratings

Every verified item gets one of:

| Rating | Meaning |
|--------|---------|
| ✅ Verified | Confirmed via source, test, or direct observation |
| ⚠️ Unverified | Plausible but could not confirm — needs human review |
| ❌ Contradicted | Found evidence that contradicts the claim |
| 🔍 Needs Investigation | Requires deeper analysis beyond current scope |

## When I'm Triggered

- **Auto-trigger (via routing):** Tasks tagged with \`review\`, \`verify\`, \`fact-check\`, \`audit\`
- **Pre-publish gate:** Before any artifact is delivered to the user, if configured
- **Manual:** User says "fact-check this", "verify these claims", "double-check"
- **Post-research:** After any agent produces research output or external references

## How I Work

1. **Read the artifact** — understand what's being claimed
2. **Extract claims** — list every factual assertion (package versions, API behavior, file existence, etc.)
3. **Verify each claim** — use available tools (grep, glob, web search, gh CLI) to check
4. **Run counter-hypotheses** — for key assumptions, ask "what if this is wrong?"
5. **Produce a verification report**
6. **Write decision** if I found issues: \`.squad/decisions/inbox/fact-checker-{slug}.md\`

## Boundaries

**I handle:** Verification, fact-checking, counter-hypotheses, hallucination detection.

**I don't handle:** Implementation, design, testing, or docs. I review, not create.

**I am not a blocker by default.** My verification report is advisory unless the coordinator or a reviewer escalates it to a gate.

## Collaboration

Before starting work, run \`git rev-parse --show-toplevel\` to find the repo root, or use the \`TEAM ROOT\` provided in the spawn prompt. All \`.squad/\` paths must be resolved relative to this root.

After making a decision others should know, write it to \`.squad/decisions/inbox/fact-checker-{brief-slug}.md\`.

## Learnings

Initial setup complete. Ready for verification work.
`;
}

function RaiCharter(): string {
return `# Rai — RAI Reviewer

Expand Down Expand Up @@ -618,6 +694,9 @@ export async function createTeam(teamRoot: string, proposal: CastProposal): Prom
const hasRai = proposal.members.some(m => /Rai/i.test(m.name));
if (!hasRai) allMembers.push(RaiMember());

const hasFactChecker = proposal.members.some(m => /fact.?checker/i.test(m.name));
if (!hasFactChecker) allMembers.push(factCheckerMember());

Comment on lines 694 to +699
// Create agent directories and files
for (const member of allMembers) {
const nameLower = member.name.toLowerCase();
Expand All @@ -631,6 +710,8 @@ export async function createTeam(teamRoot: string, proposal: CastProposal): Prom
charter = ralphCharter();
} else if (member.name === 'Rai' && !hasRai) {
charter = RaiCharter();
} else if (member.name === 'Fact Checker' && !hasFactChecker) {
charter = factCheckerCharter();
} else {
charter = generateCharter(member);
Comment on lines 710 to 716
}
Expand Down Expand Up @@ -805,5 +886,10 @@ export function formatCastSummary(proposal: CastProposal): string {
lines.push(`🛡️ ${'Rai'.padEnd(10)} — ${'(background)'.padEnd(15)} RAI awareness, content safety`);
}

const hasFactChecker = proposal.members.some(m => /fact.?checker/i.test(m.name));
if (!hasFactChecker) {
lines.push(`🔍 ${'Fact Checker'.padEnd(10)} — ${'(advisory)'.padEnd(15)} Claim verification, hallucination detection`);
}

return lines.join('\n');
}
5 changes: 5 additions & 0 deletions packages/squad-cli/src/cli/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi
name: 'Rai',
role: 'Rai',
displayName: 'Rai',
},
{
name: 'fact-checker',
role: 'fact-checker',
displayName: 'Fact Checker',
}
],
configFormat: options.sdk ? 'sdk' : 'markdown',
Expand Down
78 changes: 78 additions & 0 deletions packages/squad-cli/src/cli/core/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,13 @@ async function runEnsureChecks(dest: string, templatesDir: string, filesUpdated:
filesUpdated.push(...memoryFiles);
}

const builtinAgents = ensureBuiltinAgents(dest, templatesDir);
if (builtinAgents.length > 0) {
const uniqueAgentNames = Array.from(new Set(builtinAgents.map(p => path.basename(path.dirname(p)))));
success(`scaffolded ${uniqueAgentNames.length} built-in agent(s): ${uniqueAgentNames.join(', ')}`);
filesUpdated.push(...builtinAgents);
}

const skillCount = syncAllSkills(dest, templatesDir);
if (skillCount > 0) {
success(`synced ${skillCount} skills to .copilot/skills/`);
Expand Down Expand Up @@ -757,6 +764,77 @@ export function ensureMemoryGovernanceUpgradeDefaults(dest: string): string[] {
return created;
}

/**
* Scaffold always-on built-in agent charters (Rai, Fact Checker) that ship
* as templates but may be missing from older squads. Idempotent — only writes
* when the agent directory is absent. Never overwrites existing charters or
* history. Sources charter content from the shipped `templates/{name}-charter.md`
* files when available, falling back to a minimal placeholder otherwise.
*
* Scribe and Ralph are intentionally NOT scaffolded here — they should already
* exist in any squad that ran a prior init, and their charters are inlined in
* cast.ts (no shipped template file). Adding them here would risk overwriting
* customized versions on legacy squads.
*
* @param dest Root directory containing .squad/
* @param templatesDir Directory containing shipped charter templates
* @returns Paths (relative to dest) of created agent files
*/
export function ensureBuiltinAgents(dest: string, templatesDir: string): string[] {
const agentsDir = path.join(dest, '.squad', 'agents');
if (!storage.existsSync(agentsDir)) {
storage.mkdirSync(agentsDir, { recursive: true });
}

// Built-in agents that ship as charter templates. Each entry maps to:
// - dirName: case-preserving directory name under .squad/agents/
// - templateFile: filename under templatesDir (charter template)
// - displayName: shown in history.md header
const builtins: Array<{ dirName: string; templateFile: string; displayName: string }> = [
{ dirName: 'Rai', templateFile: 'Rai-charter.md', displayName: 'Rai' },
{ dirName: 'fact-checker', templateFile: 'fact-checker-charter.md', displayName: 'Fact Checker' },
];

const created: string[] = [];
for (const agent of builtins) {
const agentDir = path.join(agentsDir, agent.dirName);
const charterPath = path.join(agentDir, 'charter.md');
const historyPath = path.join(agentDir, 'history.md');

// Idempotent: skip if agent directory already exists. Never overwrite
// existing charters or history files (preserves user customization).
if (storage.existsSync(agentDir)) continue;

// Source charter content from the shipped template; fall back to a
// minimal placeholder if the template file is missing (defensive — should
// not happen in a well-formed install, but better than crashing upgrade).
const tplPath = path.join(templatesDir, agent.templateFile);
let charterContent: string;
if (storage.existsSync(tplPath)) {
charterContent = storage.readSync(tplPath) ?? '';
} else {
charterContent = `# ${agent.displayName}\n\n> Charter template not found in shipped templates. Run \`squad upgrade\` after reinstalling the CLI to repair.\n`;
}

try {
storage.mkdirSync(agentDir, { recursive: true });
storage.writeSync(charterPath, charterContent);
created.push(path.join('.squad', 'agents', agent.dirName, 'charter.md'));

storage.writeSync(historyPath, `# ${agent.displayName} — History\n\n## Learnings\n\nInitial scaffold via \`squad upgrade\`. Ready for work.\n`);
created.push(path.join('.squad', 'agents', agent.dirName, 'history.md'));
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && ['EPERM', 'EACCES'].includes((err as NodeJS.ErrnoException).code ?? '')) {
warn(`Could not scaffold built-in agent ${agent.displayName} (read-only). Create .squad/agents/${agent.dirName}/ manually.`);
continue;
}
throw err;
}
}

return created;
}

/**
* Run the upgrade command
*/
Expand Down
Loading