You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a team member has linked their GitHub account to their Extra Chill user, Roadie writes (issues via file_feature_request, PRs via propose_code_change → apply_code_change) should be authored as that human on GitHub, not as homeboy-ci[bot]. Contributors get real attribution on their GitHub graph; reviewers can see at a glance who proposed what.
When a user has not linked GitHub (e.g. Chris Gardner), the existing bot-with-WP-attribution path remains the default. The bot path is the floor, not the ceiling.
So the WP side knows who's asking; the resolver side knows how to pick profiles; the missing piece is the glue that connects them.
Architecture
Three small changes across three repos:
Roadie tool invocation
(file_feature_request | apply_code_change)
│
▼
resolver.resolve( user_id: $calling_user_id, repo: $repo ) [extrachill-roadie]
│
▼
selectProfile() — new branch: user_id selector [data-machine-code]
│
▼
user_meta('_github_credential_profile_id') [extrachill-users]
→ returns 'user-<wp_id>' profile slug, or null
│
┌─────────────────┴─────────────────┐
│ │
profile exists profile missing
resolve THAT fall back to repo / default
→ authored by HUMAN → authored by homeboy-ci[bot]
The fallback chain (user_id → repo → default) means callers always get some credential, and behavior gracefully degrades from "perfect per-user attribution" to "bot with attribution footer" depending on what's stored.
Work breakdown
Sub-task 1 — data-machine-code: add user_id selector to GitHubCredentialResolver
Note on the get_user_meta() call inside the resolver: today the resolver is pure / WordPress-API-light (just PluginSettings::get). Adding get_user_meta() is a small WP coupling but it's the cleanest seam — the alternative is forcing every caller to do the user-meta lookup themselves and pass a resolved profile_id, which loses the elegance of "the resolver picks." Worth the trade.
Tests: extend the existing resolver smoke to cover:
user_id selector with configured profile → returns user profile.
user_id selector with no user meta → falls through to default (no WP_Error).
user_id selector with stale profile id (meta points at deleted profile) → falls through, no WP_Error.
user_id + repo both passed → user_id wins when configured.
GitHub OAuth flow via gh_app_oauth or similar. User clicks "Connect GitHub", goes through GitHub's OAuth, comes back with a user-scoped App installation token.
Token refresh on expiry handled by the resolver (already supports App-style expiry with expires_at).
This is the right long-term answer but is enough work to deserve its own follow-up issue.
Security: PAT input fields must be password-typed, never echoed in HTML responses, written to github_credential_profiles via the existing sanitizer, and never exposed back to the user after save (re-entry required to change). Audit existing extrachill-users sensitive-field handling for the pattern to follow.
Sub-task 3 — extrachill-roadie: prefer per-user identity in tool calls
Where:inc/tools/class-file-feature-request.php and inc/tools/class-apply-code-change.php.
Change: when calling abilities that ultimately resolve through GitHubCredentialResolver, pass user_id as a selector hint. The abilities don't accept the selector directly today — they call getPat(['repo' => $repo]) internally. Two ways:
A. Extend the ability input schema (in data-machine-code/inc/Abilities/GitHubAbilities.php) to accept an optional user_id field that flows into getPat(['user_id' => $user_id, 'repo' => $repo]). Cleanest.
B. Resolve the token at the tool layer in Roadie via GitHubCredentialResolver::resolve() directly, then pass a one-off profile_id to the ability if such a path exists, or set up a transient filter override. Hackier.
Recommend A.
After the wiring lands, both Roadie tools call abilities with user_id => $acting_user_id (already resolved by resolve_acting_user_id() on ECRoadie_PlatformTool). Add automatic fallback to bot identity when the per-user resolution returns the default profile (i.e., user has no linked account).
Attribution footer behavior:
When user_id resolved to a user-specific profile → the GitHub author IS the user, so the WP-attribution footer becomes redundant. Suppress it via the existing extrachill_roadie_feature_request_attribution_lines filter, returning an empty array when the resolved profile_id starts with user-.
When user_id fell through to bot → keep the footer as today (still useful as proposer attribution).
Acceptance criteria
data-machine-code: GitHubCredentialResolver accepts user_id selector, falls through gracefully when no profile is configured.
data-machine-code: ability inputs (create-github-issue, create-github-pull-request, comment-github-issue, etc.) accept an optional user_id field that maps to the resolver selector.
extrachill-users: user profile screen has a "GitHub Account" section with PAT link/unlink. v1 lo-fi shape; OAuth tracked separately.
extrachill-users: per-user PAT is stored as a github_credential_profiles entry with id user-<wp_id>, and the user_meta _github_credential_profile_id points at it.
extrachill-roadie: file_feature_request and apply_code_change pass user_id => $acting_user_id to GitHub ability calls.
extrachill-roadie: when the resolved credential is user-scoped, suppress the WP-attribution footer (the GitHub author IS the human, no need for a redundant body line).
Smoke: linked user files an issue → author is the user's GitHub account, no WP footer.
Smoke: unlinked user files an issue → author is homeboy-ci[bot], WP footer present (current behavior).
Smoke: PAT becomes invalid (revoked) → ability surfaces a clean error, does NOT silently fall back to bot.
Docs: extrachill-roadie/docs/contribute-code.md updated to document the dual-identity model; extrachill-users gets a short "Linking your GitHub" user-facing doc.
Out of scope
GitHub OAuth flow for linking (v2). v1 ships the PAT-paste path; OAuth gets its own issue once the PAT path proves the surface.
App-style per-user installation tokens (would require a separate Roadie/Extra-Chill GitHub App installed per-user). PATs are sufficient for v1.
Cross-org write surfaces. Per-user PATs scoped to the user's own accessible repos only; if they happen to be able to push to Extra-Chill/* that's fine, but Roadie's tool path still validates the target repo against the existing allowlist.
Bot fallback policy when per-user is configured but the token fails. Default behavior is "surface the failure to the user, do not silently bot-fallback" — they presumably linked the account because they want their identity, so falling back is surprising.
Notes
Chris Gardner (qrisg) does not have a GitHub account. He never links anything. His path stays exactly as it is today: homeboy-ci[bot] files the issue, WP footer credits him. This issue is for other contributors who DO have GitHub.
The work is small per-repo but crosses three repos. Land in this order to avoid breakage: resolver selector first, then extrachill-users storage, then Roadie wiring last.
This will need a coordinated release across the three plugins. Tag a sub-task list when implementing.
Goal
When a team member has linked their GitHub account to their Extra Chill user, Roadie writes (issues via
file_feature_request, PRs viapropose_code_change→apply_code_change) should be authored as that human on GitHub, not ashomeboy-ci[bot]. Contributors get real attribution on their GitHub graph; reviewers can see at a glance who proposed what.When a user has not linked GitHub (e.g. Chris Gardner), the existing bot-with-WP-attribution path remains the default. The bot path is the floor, not the ceiling.
Current state (post-#22)
GitHubCredentialResolveris profile-aware: many credential profiles, eachpatorappmode, with optionalallowed_reposrouting.profile_idorrepo. Nouser_idselector.homeboy-ci(GitHub App, ID 3034937, installation 114752821). Verified end-to-end via smoke issue [smoke test] homeboy-ci[bot] App auth wired up #20.calling_user_idpropagation lands on every tool invocation (feat(agent-mode): adopt AgentModeRegistry for roadie mode + calling_user_id propagation #19, merged). Tools have the human's WP user ID in their$parameters.file_feature_request(feat(tools): file_feature_request chat tool for filing GitHub issues from Roadie #18) andapply_code_change(apply_code_change: use GitHubCredentialResolver instead of GITHUB_TOKEN env var #22) both call abilities that resolve throughGitHubCredentialResolver. They will pick up auser_idselector as soon as one exists.So the WP side knows who's asking; the resolver side knows how to pick profiles; the missing piece is the glue that connects them.
Architecture
Three small changes across three repos:
The fallback chain (
user_id→repo→ default) means callers always get some credential, and behavior gracefully degrades from "perfect per-user attribution" to "bot with attribution footer" depending on what's stored.Work breakdown
Sub-task 1 —
data-machine-code: adduser_idselector to GitHubCredentialResolverWhere:
inc/Support/GitHubCredentialResolver.php,selectProfile()method.Diff sketch:
Selector precedence:
user_id(when profile exists) →profile_id→repo→ default.Note on the
get_user_meta()call inside the resolver: today the resolver is pure / WordPress-API-light (justPluginSettings::get). Addingget_user_meta()is a small WP coupling but it's the cleanest seam — the alternative is forcing every caller to do the user-meta lookup themselves and pass a resolvedprofile_id, which loses the elegance of "the resolver picks." Worth the trade.Tests: extend the existing resolver smoke to cover:
user_idselector with configured profile → returns user profile.user_idselector with no user meta → falls through to default (no WP_Error).user_idselector with stale profile id (meta points at deleted profile) → falls through, no WP_Error.user_id+repoboth passed →user_idwins when configured.Sub-task 2 —
extrachill-users: per-user GitHub account linkingTwo storage paths, ship the simpler one first.
v1 (lo-fi, ship first):
/wp-admin/profile.phpand the user-edit screen).GET /user, capture the GitHub login + user_id, store as a per-user credential profile ingithub_credential_profiles:user_meta('_github_credential_profile_id', 'user-38').wp extrachill users github-link <user_id> --pat=ghp_...andwp extrachill users github-unlink <user_id>.v2 (proper, follow-up):
gh_app_oauthor similar. User clicks "Connect GitHub", goes through GitHub's OAuth, comes back with a user-scoped App installation token.expires_at).Security: PAT input fields must be password-typed, never echoed in HTML responses, written to
github_credential_profilesvia the existing sanitizer, and never exposed back to the user after save (re-entry required to change). Audit existingextrachill-userssensitive-field handling for the pattern to follow.Sub-task 3 —
extrachill-roadie: prefer per-user identity in tool callsWhere:
inc/tools/class-file-feature-request.phpandinc/tools/class-apply-code-change.php.Change: when calling abilities that ultimately resolve through
GitHubCredentialResolver, passuser_idas a selector hint. The abilities don't accept the selector directly today — they callgetPat(['repo' => $repo])internally. Two ways:A. Extend the ability input schema (in
data-machine-code/inc/Abilities/GitHubAbilities.php) to accept an optionaluser_idfield that flows intogetPat(['user_id' => $user_id, 'repo' => $repo]). Cleanest.B. Resolve the token at the tool layer in Roadie via
GitHubCredentialResolver::resolve()directly, then pass a one-offprofile_idto the ability if such a path exists, or set up a transient filter override. Hackier.Recommend A.
After the wiring lands, both Roadie tools call abilities with
user_id => $acting_user_id(already resolved byresolve_acting_user_id()onECRoadie_PlatformTool). Add automatic fallback to bot identity when the per-user resolution returns the default profile (i.e., user has no linked account).Attribution footer behavior:
user_idresolved to a user-specific profile → the GitHub author IS the user, so the WP-attribution footer becomes redundant. Suppress it via the existingextrachill_roadie_feature_request_attribution_linesfilter, returning an empty array when the resolved profile_id starts withuser-.user_idfell through to bot → keep the footer as today (still useful as proposer attribution).Acceptance criteria
data-machine-code:GitHubCredentialResolveracceptsuser_idselector, falls through gracefully when no profile is configured.data-machine-code: ability inputs (create-github-issue,create-github-pull-request,comment-github-issue, etc.) accept an optionaluser_idfield that maps to the resolver selector.extrachill-users: user profile screen has a "GitHub Account" section with PAT link/unlink. v1 lo-fi shape; OAuth tracked separately.extrachill-users: CLI commandswp extrachill users github-link <user_id> --pat=...andgithub-unlink <user_id>.extrachill-users: per-user PAT is stored as agithub_credential_profilesentry with iduser-<wp_id>, and the user_meta_github_credential_profile_idpoints at it.extrachill-roadie:file_feature_requestandapply_code_changepassuser_id => $acting_user_idto GitHub ability calls.extrachill-roadie: when the resolved credential is user-scoped, suppress the WP-attribution footer (the GitHub author IS the human, no need for a redundant body line).homeboy-ci[bot], WP footer present (current behavior).extrachill-roadie/docs/contribute-code.mdupdated to document the dual-identity model;extrachill-usersgets a short "Linking your GitHub" user-facing doc.Out of scope
Extra-Chill/*that's fine, but Roadie's tool path still validates the target repo against the existing allowlist.Notes
homeboy-ci[bot]files the issue, WP footer credits him. This issue is for other contributors who DO have GitHub.extrachill-usersstorage, then Roadie wiring last.