Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8a072d6
chore: initialize Squad team for dfberry.github.io blog
dfberry Mar 22, 2026
75327f1
chore: recast team from Firefly to Encanto universe
dfberry Mar 22, 2026
bdfe426
feat(blog): colorful image prompts, add Antonio (Storyteller), narrat…
dfberry Mar 22, 2026
55a2826
Scribe: Document mirabel-scaffold session (2026-03-22T2203)
dfberry Mar 22, 2026
34c7f91
Add inline images to Squad blog post
dfberry Mar 22, 2026
fc8eda6
Add SDXL images + set post to draft (published: false)
dfberry Mar 22, 2026
e1b6424
Fix image paths to match blog media convention
dfberry Mar 23, 2026
0bfdbc0
Add engaging H1 to GitHub account cleanup post
dfberry Mar 23, 2026
5b52d96
Generalize inner/open source framing to 'projects'
dfberry Mar 23, 2026
95b093a
Add H1 to squad inner-source blog post
dfberry Mar 23, 2026
4c66fa3
Rewrite Squad post in Geraldine's voice
dfberry Mar 23, 2026
d1dc1db
Capture Geraldine's voice as standing directive for Isabela
dfberry Mar 23, 2026
0a0261e
Fix skills attribution: humanizer and external-comms are Geraldine's …
dfberry Mar 23, 2026
7c7be13
Expand opening paragraph with concrete clone motivations
dfberry Mar 23, 2026
1af6829
Expand Brady quote with plain-language translation
dfberry Mar 23, 2026
368eb36
Remove humanizer skill — not Geraldine's contribution
dfberry Mar 23, 2026
fd3f51f
Expand external-comms section with RFC #426 context: why .squad/ make…
dfberry Mar 23, 2026
1f7f811
Update Isabela history: RFC #426 external-comms expansion logged
dfberry Mar 23, 2026
fb921c6
Add link to external-comms skill on GitHub
dfberry Mar 23, 2026
02efbb9
Restructure OSS section: lead with contributor angle, move external-c…
dfberry Mar 23, 2026
9a676ae
Update Isabela history: document OSS section restructure decision
dfberry Mar 23, 2026
de721c5
Reframe personal examples as supporting context, keep focus on Squad'…
dfberry Mar 23, 2026
4b06d9d
Rewrite OSS maintainer section: focus on contributor conformance to m…
dfberry Mar 23, 2026
a04b927
Update Isabela history: OSS maintainer conformance framing
dfberry Mar 23, 2026
ddb2932
Move external-comms to supporting note in OSS section, remove standal…
dfberry Mar 23, 2026
138d36c
Add experimentation section to squad blog post
dfberry Mar 23, 2026
810ee1f
Add three contributor-benefit sections: approach validation, meta-con…
dfberry Mar 23, 2026
d457694
Add 7 new sections, reorder post, remap images
dfberry Mar 23, 2026
339a74a
Add image-generation skill
dfberry Mar 23, 2026
210e9a8
Add blog-image-generation skill
dfberry Mar 23, 2026
fd8a97e
Remove duplicate image-generation skill (kept blog-image-generation)
dfberry Mar 23, 2026
df3cf80
Remove duplication, fix squad names in blog post
dfberry Mar 23, 2026
794be9c
Replace images 03-05 with no-people versions (animals, plants, landsc…
dfberry Mar 23, 2026
925fd6f
Use Brady's real NASA squad names in post
dfberry Mar 23, 2026
1f48125
Fill out charter section with real Flight example
dfberry Mar 23, 2026
00fd0ef
Remove duplicate 'wrong direction' setup and stale-docs repeat
dfberry Mar 23, 2026
4cd68a6
Remove 'documentation that doesn't go stale' section
dfberry Mar 23, 2026
0425b73
Replace Brady name with 'repo owner' after first intro mention
dfberry Mar 23, 2026
2d054d9
Weave vacation/travel metaphor into blog prose
dfberry Mar 23, 2026
365a48c
Regenerate blog images with vacation travel theme
dfberry Mar 23, 2026
51695a0
Reframe 'What Squad is' using resort staff metaphor
dfberry Mar 23, 2026
03025ef
Fill in 'Without squad' experimentation section
dfberry Mar 23, 2026
f45b0c6
Tighten blog post to ~1,200 words: cut 3 sections, merge 4 sections
dfberry Mar 23, 2026
e534e0d
Weave vacation narrative through all sections
dfberry Mar 23, 2026
5e2dcab
Add Without/With squad structure to all contrast sections
dfberry Mar 23, 2026
77998ce
Add intro sentence to each H2 before H3 subsections
dfberry Mar 23, 2026
f559111
Add images for all H2 sections and place image 02 in post
dfberry Mar 23, 2026
07d3448
Add blog-image-generation skill
dfberry Mar 23, 2026
9b31fe2
Update blog-image-generation skill: one-at-a-time approval workflow
dfberry Mar 23, 2026
87b2a95
feat(squad): inner source blog post with vibrant images, agent histor…
dfberry Apr 12, 2026
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
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Squad: union merge for append-only team state files
.squad/decisions.md merge=union
.squad/agents/*/history.md merge=union
.squad/log/** merge=union
.squad/orchestration-log/** merge=union
1,146 changes: 1,146 additions & 0 deletions .github/agents/squad.agent.md

Large diffs are not rendered by default.

316 changes: 316 additions & 0 deletions .github/workflows/squad-heartbeat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
name: Squad Heartbeat (Ralph)

on:
# DISABLED: Cron heartbeat commented out pre-migration — re-enable when ready
# schedule:
# # Every 30 minutes — adjust or remove if not needed
# - cron: '*/30 * * * *'

# React to completed work or new squad work
issues:
types: [closed, labeled]
pull_request:
types: [closed]

# Manual trigger
workflow_dispatch:

permissions:
issues: write
contents: read
pull-requests: read

jobs:
heartbeat:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Ralph — Check for squad work
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');

// Read team roster — check .squad/ first, fall back to .ai-team/
let teamFile = '.squad/team.md';
if (!fs.existsSync(teamFile)) {
teamFile = '.ai-team/team.md';
}
if (!fs.existsSync(teamFile)) {
core.info('No .squad/team.md or .ai-team/team.md found — Ralph has nothing to monitor');
return;
}

const content = fs.readFileSync(teamFile, 'utf8');

// Check if Ralph is on the roster
if (!content.includes('Ralph') || !content.includes('🔄')) {
core.info('Ralph not on roster — heartbeat disabled');
return;
}

// Parse members from roster
const lines = content.split('\n');
const members = [];
let inMembersTable = false;
for (const line of lines) {
if (line.match(/^##\s+(Members|Team Roster)/i)) {
inMembersTable = true;
continue;
}
if (inMembersTable && line.startsWith('## ')) break;
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
if (cells.length >= 2 && !['Scribe', 'Ralph'].includes(cells[0])) {
members.push({
name: cells[0],
role: cells[1],
label: `squad:${cells[0].toLowerCase()}`
});
}
}
}

if (members.length === 0) {
core.info('No squad members found — nothing to monitor');
return;
}

// 1. Find untriaged issues (labeled "squad" but no "squad:{member}" label)
const { data: squadIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'squad',
state: 'open',
per_page: 20
});

const memberLabels = members.map(m => m.label);
const untriaged = squadIssues.filter(issue => {
const issueLabels = issue.labels.map(l => l.name);
return !memberLabels.some(ml => issueLabels.includes(ml));
});

// 2. Find assigned but unstarted issues (has squad:{member} label, no assignee)
const unstarted = [];
for (const member of members) {
try {
const { data: memberIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: member.label,
state: 'open',
per_page: 10
});
for (const issue of memberIssues) {
if (!issue.assignees || issue.assignees.length === 0) {
unstarted.push({ issue, member });
}
}
} catch (e) {
// Label may not exist yet
}
}

// 3. Find squad issues missing triage verdict (no go:* label)
const missingVerdict = squadIssues.filter(issue => {
const labels = issue.labels.map(l => l.name);
return !labels.some(l => l.startsWith('go:'));
});

// 4. Find go:yes issues missing release target
const goYesIssues = squadIssues.filter(issue => {
const labels = issue.labels.map(l => l.name);
return labels.includes('go:yes') && !labels.some(l => l.startsWith('release:'));
});

// 4b. Find issues missing type: label
const missingType = squadIssues.filter(issue => {
const labels = issue.labels.map(l => l.name);
return !labels.some(l => l.startsWith('type:'));
});

// 5. Find open PRs that need attention
const { data: openPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 20
});

const squadPRs = openPRs.filter(pr =>
pr.labels.some(l => l.name.startsWith('squad'))
);

// Build status summary
const summary = [];
if (untriaged.length > 0) {
summary.push(`🔴 **${untriaged.length} untriaged issue(s)** need triage`);
}
if (unstarted.length > 0) {
summary.push(`🟡 **${unstarted.length} assigned issue(s)** have no assignee`);
}
if (missingVerdict.length > 0) {
summary.push(`⚪ **${missingVerdict.length} issue(s)** missing triage verdict (no \`go:\` label)`);
}
if (goYesIssues.length > 0) {
summary.push(`⚪ **${goYesIssues.length} approved issue(s)** missing release target (no \`release:\` label)`);
}
if (missingType.length > 0) {
summary.push(`⚪ **${missingType.length} issue(s)** missing \`type:\` label`);
}
if (squadPRs.length > 0) {
const drafts = squadPRs.filter(pr => pr.draft).length;
const ready = squadPRs.length - drafts;
if (drafts > 0) summary.push(`🟡 **${drafts} draft PR(s)** in progress`);
if (ready > 0) summary.push(`🟢 **${ready} PR(s)** open for review/merge`);
}

if (summary.length === 0) {
core.info('📋 Board is clear — Ralph found no pending work');
return;
}

core.info(`🔄 Ralph found work:\n${summary.join('\n')}`);

// Auto-triage untriaged issues
for (const issue of untriaged) {
const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase();
let assignedMember = null;
let reason = '';

// Simple keyword-based routing
for (const member of members) {
const role = member.role.toLowerCase();
if ((role.includes('frontend') || role.includes('ui')) &&
(issueText.includes('ui') || issueText.includes('frontend') ||
issueText.includes('css') || issueText.includes('component'))) {
assignedMember = member;
reason = 'Matches frontend/UI domain';
break;
}
if ((role.includes('backend') || role.includes('api') || role.includes('server')) &&
(issueText.includes('api') || issueText.includes('backend') ||
issueText.includes('database') || issueText.includes('endpoint'))) {
assignedMember = member;
reason = 'Matches backend/API domain';
break;
}
if ((role.includes('test') || role.includes('qa')) &&
(issueText.includes('test') || issueText.includes('bug') ||
issueText.includes('fix') || issueText.includes('regression'))) {
assignedMember = member;
reason = 'Matches testing/QA domain';
break;
}
}

// Default to Lead
if (!assignedMember) {
const lead = members.find(m =>
m.role.toLowerCase().includes('lead') ||
m.role.toLowerCase().includes('architect')
);
if (lead) {
assignedMember = lead;
reason = 'No domain match — routed to Lead';
}
}

if (assignedMember) {
// Add member label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [assignedMember.label]
});

// Post triage comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: [
`### 🔄 Ralph — Auto-Triage`,
'',
`**Assigned to:** ${assignedMember.name} (${assignedMember.role})`,
`**Reason:** ${reason}`,
'',
`> Ralph auto-triaged this issue via the squad heartbeat. To reassign, swap the \`squad:*\` label.`
].join('\n')
});

core.info(`Auto-triaged #${issue.number} → ${assignedMember.name}`);
}
}

# Copilot auto-assign step (uses PAT if available)
- name: Ralph — Assign @copilot issues
if: success()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');

let teamFile = '.squad/team.md';
if (!fs.existsSync(teamFile)) {
teamFile = '.ai-team/team.md';
}
if (!fs.existsSync(teamFile)) return;

const content = fs.readFileSync(teamFile, 'utf8');

// Check if @copilot is on the team with auto-assign
const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
if (!hasCopilot || !autoAssign) return;

// Find issues labeled squad:copilot with no assignee
try {
const { data: copilotIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'squad:copilot',
state: 'open',
per_page: 5
});

const unassigned = copilotIssues.filter(i =>
!i.assignees || i.assignees.length === 0
);

if (unassigned.length === 0) {
core.info('No unassigned squad:copilot issues');
return;
}

// Get repo default branch
const { data: repoData } = await github.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo
});

for (const issue of unassigned) {
try {
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['copilot-swe-agent[bot]'],
agent_assignment: {
target_repo: `${context.repo.owner}/${context.repo.repo}`,
base_branch: repoData.default_branch,
custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.`
}
});
core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
} catch (e) {
core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
}
}
} catch (e) {
core.info(`No squad:copilot label found or error: ${e.message}`);
}
Loading