fix(triage): slugify agent names for labels + pre-create missing labels#785
fix(triage): slugify agent names for labels + pre-create missing labels#785tamirdresher merged 1 commit intodevfrom
Conversation
…ls (#784) Bug 1: Watch triage derived labels from display names ('Steve Rogers' → 'squad:steve rogers') instead of directory slugs ('squad:steve-rogers'). Fix: Added shared slugify() utility, applied across triage.ts, templates, and label sync workflows. Bug 2: gh issue edit --add-label fails with exit code 1 when label doesn't exist. Watch swallowed the error silently. Fix: Added ensureTag() to PlatformAdapter + pre-create all squad:{member} labels at watch startup. 12 new tests: slugify utility (10) + triage label verification (2). All 41 tests pass. Build clean. Closes #784 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🛫 PR Readiness Check
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | No Copilot review threads |
| ❌ | CI passing | 11 check(s) still running |
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
Pull request overview
Fixes squad watch triage getting stuck due to invalid/missing squad:{member} labels by standardizing label slugs and adding best-effort label pre-creation.
Changes:
- Introduces an SDK
slugify()utility and uses it for roster-derived labels (squad:steve-rogersvssquad:steve rogers). - Adds
PlatformAdapter.ensureTag()plus a GitHub implementation, and calls it at watch startup to pre-create squad labels. - Updates triage-related templates/workflows to use the same slug logic; adds/extends tests for slug + roster parsing.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/slugify.test.ts | Adds unit tests for the new SDK slugify() utility. |
| test/ralph-triage.test.ts | Extends roster parsing tests to assert slugified label output. |
| templates/workflows/sync-squad-labels.yml | Slugifies member label names when creating/syncing labels (template copy). |
| templates/workflows/squad-triage.yml | Slugifies displayed/assigned member labels (template copy). |
| templates/ralph-triage.js | Slugifies roster-derived labels in the standalone triage script (template copy). |
| packages/squad-sdk/templates/workflows/sync-squad-labels.yml | Slugifies member label names when creating/syncing labels (SDK template mirror). |
| packages/squad-sdk/templates/workflows/squad-triage.yml | Slugifies displayed/assigned member labels (SDK template mirror). |
| packages/squad-sdk/templates/ralph-triage.js | Slugifies roster-derived labels (SDK template mirror). |
| packages/squad-sdk/src/utils/slugify.ts | Adds canonical slugify() implementation in the SDK. |
| packages/squad-sdk/src/ralph/triage.ts | Uses slugify() for TeamMember.label generation and re-exports it. |
| packages/squad-sdk/src/platform/types.ts | Extends PlatformAdapter with optional ensureTag() hook. |
| packages/squad-sdk/src/platform/github.ts | Implements ensureTag() via gh label create --force. |
| packages/squad-cli/templates/workflows/sync-squad-labels.yml | Slugifies member label names when creating/syncing labels (CLI template mirror). |
| packages/squad-cli/templates/workflows/squad-triage.yml | Slugifies displayed/assigned member labels (CLI template mirror). |
| packages/squad-cli/templates/ralph-triage.js | Slugifies roster-derived labels (CLI template mirror). |
| packages/squad-cli/src/cli/commands/watch/index.ts | Pre-creates squad:{member} labels at watch startup using ensureTag(). |
| index.js | Slugifies roster-derived labels in the legacy/root watch subcommand implementation. |
| .squad-templates/workflows/sync-squad-labels.yml | Slugifies member label names when creating/syncing labels (canonical templates). |
| .squad-templates/workflows/squad-triage.yml | Slugifies displayed/assigned member labels (canonical templates). |
| .squad-templates/ralph-triage.js | Slugifies roster-derived labels (canonical templates). |
| .github/workflows/sync-squad-labels.yml | Slugifies member label names when creating/syncing labels (repo workflow). |
| .github/workflows/squad-triage.yml | Slugifies displayed/assigned member labels (repo workflow). |
| .changeset/fix-triage-label-slug.md | Changeset documenting the slug fix + label pre-creation behavior. |
| try { | ||
| this.gh(args); | ||
| } catch { | ||
| // Label already exists or creation failed — continue either way | ||
| } |
There was a problem hiding this comment.
ensureTag() swallows all gh label create failures. Since the command is run with --force, “already exists” shouldn’t be an error, so this catch is likely to hide real problems (e.g., missing repo scope / insufficient permissions), and watch will later fail to add labels with little diagnostic info. Consider only ignoring the specific “already exists” case (if needed) or rethrowing/logging unexpected errors so callers can surface them (at least in verbose mode).
| try { | |
| this.gh(args); | |
| } catch { | |
| // Label already exists or creation failed — continue either way | |
| } | |
| this.gh(args); |
| for (const member of roster) { | ||
| try { | ||
| await adapter.ensureTag(member.label, { color: 'd4c5f9', description: `Squad triage: ${member.name}` }); | ||
| } catch { /* best-effort — continue if label creation fails */ } | ||
| } | ||
| try { | ||
| await adapter.ensureTag('squad:copilot', { color: 'd4c5f9', description: 'Squad triage: Copilot coding agent' }); | ||
| } catch { /* best-effort */ } | ||
| console.log(`${DIM}Labels: ensured ${roster.length + 1} squad labels exist${RESET}`); |
There was a problem hiding this comment.
The startup message says labels were “ensured … exist”, but label creation is best-effort and both ensureTag() and this caller swallow errors. This can produce a misleading success message even when none of the labels were created. Consider tracking successes and reporting “attempted to ensure” (or only printing the message when all creations succeed / when verbose is enabled).
| for (const member of roster) { | |
| try { | |
| await adapter.ensureTag(member.label, { color: 'd4c5f9', description: `Squad triage: ${member.name}` }); | |
| } catch { /* best-effort — continue if label creation fails */ } | |
| } | |
| try { | |
| await adapter.ensureTag('squad:copilot', { color: 'd4c5f9', description: 'Squad triage: Copilot coding agent' }); | |
| } catch { /* best-effort */ } | |
| console.log(`${DIM}Labels: ensured ${roster.length + 1} squad labels exist${RESET}`); | |
| const totalSquadLabels = roster.length + 1; | |
| let ensuredSquadLabels = 0; | |
| for (const member of roster) { | |
| try { | |
| await adapter.ensureTag(member.label, { color: 'd4c5f9', description: `Squad triage: ${member.name}` }); | |
| ensuredSquadLabels += 1; | |
| } catch { /* best-effort — continue if label creation fails */ } | |
| } | |
| try { | |
| await adapter.ensureTag('squad:copilot', { color: 'd4c5f9', description: 'Squad triage: Copilot coding agent' }); | |
| ensuredSquadLabels += 1; | |
| } catch { /* best-effort */ } | |
| if (ensuredSquadLabels === totalSquadLabels) { | |
| console.log(`${DIM}Labels: ensured ${totalSquadLabels} squad labels exist${RESET}`); | |
| } else { | |
| console.log(`${DIM}Labels: attempted to ensure ${totalSquadLabels} squad labels exist (${ensuredSquadLabels} succeeded)${RESET}`); | |
| } |
| export { slugify } from '../utils/slugify.js'; | ||
|
|
There was a problem hiding this comment.
This module now re-exports slugify from utils/slugify, which expands the public surface of ralph/triage beyond triage concerns. If the intent is to make slugify generally available, consider exporting it from a more appropriate public location (e.g., a utils/barrel) rather than from the triage module to avoid confusing API organization.
| export { slugify } from '../utils/slugify.js'; | |
| /** | ||
| * Convert a display name to a URL/label-safe slug. | ||
| * | ||
| * "Steve Rogers" → "steve-rogers" | ||
| * "Tony Stark (Iron Man)" → "tony-stark-iron-man" | ||
| * "Doctor Strange (Stephen Strange)" → "doctor-strange-stephen-strange" | ||
| * "Thor" → "thor" | ||
| * "already-slugified" → "already-slugified" | ||
| */ | ||
| export function slugify(name: string): string { | ||
| return name | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, '-') | ||
| .replace(/^-|-$/g, ''); | ||
| } |
There was a problem hiding this comment.
There are already multiple identical private slugify() implementations in the SDK (e.g., under src/marketplace/*). Introducing another canonical implementation is fine, but it increases duplication unless the existing ones are migrated to import this utility. Consider either reusing an existing shared slugify helper (if one is intended) or planning a follow-up to deduplicate so behavior can’t drift across features.
| members.push({ | ||
| name, | ||
| role, | ||
| label: `squad:${name.toLowerCase()}`, | ||
| label: `squad:${slugify(name)}`, | ||
| }); |
There was a problem hiding this comment.
parseRoster() now derives label via slugify(name), but the TeamMember.label field doc comment still describes it as squad:{lowercase name}. Since this function defines the canonical label format, the type-level documentation should be updated to match the slugified behavior to avoid misleading SDK consumers.
diberry
left a comment
There was a problem hiding this comment.
LGTM
Review Summary
Verdict: ✅ Approve
What's Good
- Real bug fix — \squad:steve rogers\ (space) → \squad:steve-rogers\ (slugified). Root cause of watch triage looping forever on 'Untriaged: 1'.
- Thorough coverage — Fixed slug in ALL locations: SDK triage.ts, CLI watch/index.ts, 3 template copies of ralph-triage.js, 3 workflow yml copies (triage + sync-labels), plus live .github/workflows/\ copies. No orphan instances.
- \�nsureTag()\ well-designed — Optional on interface (\�nsureTag?), uses \gh label create --force\ (idempotent), best-effort with swallowed errors. Pre-creates at watch startup.
- Canonical \slugify()\ utility — Single source of truth in \packages/squad-sdk/src/utils/slugify.ts. 10 tests covering multi-word, parentheses, already-slugified, single-word.
- Changeset included — Patch for both packages. Tested on real repo.
Suggestions (non-blocking)
- Add // Keep in sync with slugify.ts\ comments to the 6+ inline copies in workflows/templates
- Consider one \console.warn\ in the inner \�nsureTag\ catch for easier debugging
- Add \slugify('@copilot') === 'copilot'\ test case to document the @ stripping behavior
…ls (#784) (#785) Bug 1: Watch triage derived labels from display names ('Steve Rogers' → 'squad:steve rogers') instead of directory slugs ('squad:steve-rogers'). Fix: Added shared slugify() utility, applied across triage.ts, templates, and label sync workflows. Bug 2: gh issue edit --add-label fails with exit code 1 when label doesn't exist. Watch swallowed the error silently. Fix: Added ensureTag() to PlatformAdapter + pre-create all squad:{member} labels at watch startup. 12 new tests: slugify utility (10) + triage label verification (2). All 41 tests pass. Build clean. Closes #784 Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix: Triage label slug + missing label pre-creation
Fixes the root cause of
squad watchshowingUntriaged: 1forever without assigning.Bug 1: Wrong label slug
Watch triage derived labels from display names instead of directory slugs:
"Steve Rogers"→squad:steve rogers(space, doesn't exist)"Steve Rogers"→squad:steve-rogers(slugified, matches directory)Fix: Added
slugify()utility applied across:triage.ts(core routing)Bug 2: Missing label pre-creation
gh issue edit --add-labelfails with exit code 1 when the label doesn't exist. Error was swallowed silently.Fix:
ensureTag()toPlatformAdapterinterface + GitHub implementationsquad:{member}labels from team.md rosterTests
Reproduction
Verified
tsc --noEmit✅npm run build✅Closes #784