Skip to content

OCPEDGE-2970: Add edge-contribution plugin - #300

Open
copejon wants to merge 8 commits into
openshift-eng:mainfrom
copejon:ocpedge-2970-edge-contribution-plugin
Open

copejon wants to merge 8 commits into
openshift-eng:mainfrom
copejon:ocpedge-2970-edge-contribution-plugin

Conversation

@copejon

@copejon copejon commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Add the edge-contribution Claude Code plugin: 3 skills providing a manager-user 2 different views of edge-team member work distribution across work streams. Users are provided a data-driven analysis of contribution diversity (how many workstreams a member contributed to) and workstream silo-ing (how many contributions each workstream received). Accepts times spans in FYQ (e.g. 2026Q1), quarter-to-date, and arbitrary time spans.

Skills
/edge-contribution:heatmap
Screenshot 2026-09-30 at 11 28 35 AM

/edge-contribution:summary
Screenshot 2026-09-30 at 12 25 12 PM

/edge-contribution:export Exports the raw data in csv format
Screenshot 2026-09-30 at 12 29 58 PM

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Central YAML (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ba3161ff-9727-4994-b046-7347ad62e966

Comment @coderabbitai help to get the list of available commands.

@openshift-ci

openshift-ci Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: copejon

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Sep 30, 2026
@copejon
copejon force-pushed the ocpedge-2970-edge-contribution-plugin branch from 9ad25e7 to 799f404 Compare October 1, 2026 14:10

@brandisher brandisher left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong test coverage and the collectors are appropriately narrow in what data they pull. But stepping back, this plugin has ~2,700 lines of pipeline code (collectors, metrics, rendering, CSV export) trying to deterministically pre-solve a lot of problems that the model is already good at solving live: calling gh/Jira APIs in a loop per team member, aggregating results, formatting a table or summary. A skill doesn't need a hand-built GraphQL batching engine, a custom Jira REST client, or a 1,100-line renderer with duplicated text/markdown code paths — it can just tell the model what data to gather and how to present it, and let it write and run that logic on the fly each time.

Suggest cutting this down significantly: lean on gh and the mcp-atlassian MCP tools directly instead of reimplementing their transports, and trust the model to loop over the roster, compute the aggregates, and render the output per-invocation rather than shipping a large deterministic pipeline to maintain. Specific spots called out inline, but the overall ask is to simplify broadly, not just at these points.

Comment thread plugins/edge-contribution/bin/load_context.py Outdated
# Managers on the Eng/QE roster by role title who are not IC contributors.
# Filtering them from the team median prevents distortion of allocation signals.
# Stored as the hash256 of the member's jira username
EXCLUDED_MEMBERS = ("82e5282cc07f498ff0daf8352e7403e2f16b017dd17c3e589276e406480bb5be"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are SHA-256 hashes of manager jira_usernames, presumably so the names aren't readable directly in source. But the roster itself (roster.json) has these same people in plaintext, and the comment above already explains why they're excluded (managers skew the team median). Hashing here doesn't add real privacy — it just makes the exclusion list unreviewable at a glance in code review, and brittle if anyone's jira username changes. Would a plaintext list (or pulling the exclusion from the role field already in the roster, e.g. anyone not is_engineering_or_qe) be simpler and equally safe here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent here is to keep the names out of the published source. Obfuscating names in the output artifacts isn't really a concern b/c they aren't published publicly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way of looking at this is that the list of names is an input detail rather than something that needs to be hard coded. For a one-shot approach having the names hardcoded makes sense but I wouldn't consider that a requirement for this since an individual can run it for themselves or it can be run for a team/arbitrary set of people.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. To make that work, the edge-context's roles aren't quite sufficient because they cross the include/exclude boundary we spoke about previously (e.g. Principle Software Engineer covers both ICs as well as architects / tech leads).

The simplest approach would be to add a column to the edge context roster to make the distinction, a la openshift-eng/edge-context#77.

Comment thread plugins/edge-contribution/bin/render.py Outdated
Comment thread plugins/edge-contribution/TODO.md Outdated
return JiraConfig(base_url=base_url, username=username, api_token=api_token)


class JiraClient:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implements a full custom Jira REST client (auth, HTTP, pagination) via requests. This workspace already has mcp-atlassian configured with Jira search tools — could this plugin depend on that instead of maintaining its own HTTP/auth layer? Would cut a meaningful chunk of _common.py plus the test_common.py coverage for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I don't see that wouldn't work. Will try and see

@copejon copejon Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The short answer is "kind of." Models communicate w/ mcps over jsonrpc 2.0. So instead of implementing a REST client here, it would be a jsonrpc2.0 client, add dependency on another plugin, but gain nothing (AFAICT).

I probably misread your comment. Corrected response:
It'd definitely an option to allow the model to make direct mcp calls. That said, the inputs and outputs are deterministic, which makes it's a good candidate for scripting, and saves expending tokens to execute nearly identical mcp calls for nmembers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for this iteration let's err on the side of trusting the model's MCP calls to keep the plugin simple. This is an option for improving the determinism later if we see it can't do a good job consistently.

return f"{window.start.isoformat()}..{window.end.isoformat()}"


def build_batch_query(slots: List[SearchSlot], window: Window) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest dropping the custom batching layer here in favor of plain gh calls — SearchSlot, build_batch_query, parse_batch_response, and graphql_rate_limit_adapter (~350 lines across lines 83–430) amount to a bespoke GraphQL query engine built on top of gh api graphql. gh search prs or per-member gh api calls would get the same data with a fraction of the code and none of the custom rate-limit/parsing logic to maintain. Team-roster-sized workloads shouldn't need this level of batching — let's lean on gh directly and cut this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started out with using gh directly, but ran into some limitations that forced me onto graphql. Namely because running 1 gh search per user was consistently triggering API rate limiting. Batching via graphql has eliminated that problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gh can make graphql requests. Ref

…map)

Introduce the edge-contribution Claude Code plugin: two skills over a
shared, standalone, testable data-collection layer that report
cross-workstream contribution for a quarter or date range.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@copejon
copejon force-pushed the ocpedge-2970-edge-contribution-plugin branch from 799f404 to 5604d33 Compare October 5, 2026 18:38
copejon and others added 7 commits October 5, 2026 20:22
The location and kerberos fields were parsed from the roster and
serialized to roster.json, but nothing downstream (metrics.py,
render.py, report.py, export_csv.py) ever read them.

kerberos is still derived from the Rover URL and used transiently
to build jira_username, but it's no longer persisted as a separate
field. location is dropped entirely.

Addresses code review feedback on PR openshift-eng#300.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously, each summary section had two functions: _foo_text and
_foo_markdown, duplicating logic with only formatting differences
(markdown headers, pipe tables vs plain text). This resulted in
16 functions (8 pairs) with duplicated code.

Now each section has one function with a format parameter that
branches only on the line-formatting tokens. Output is byte-for-byte
identical to the previous implementation.

Functions refactored:
- _summary_header (was _summary_header_text + _summary_header_markdown)
- _summary_team
- _summary_workstreams
- _summary_people
- _summary_data_health
- _summary_what_counted
- _summary_how_attributed
- _summary_what_excluded

Reduces render.py from 1143 to 1112 lines. All 408 tests pass.

Addresses code review feedback on PR openshift-eng#300.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This was a local development planning file that accidentally
slipped into the PR. Removing per code review feedback.

Addresses code review feedback on PR openshift-eng#300.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ion of the contribution plugin is implemented
Replace Python REST client in collect_jira.py with MCP-based collection
orchestrated by skills. Skills now call mcp__mcp-atlassian__jira_search
directly and delegate attribution logic to a new pure Python script.

Changes:
- Add attribute_jira_activities.py: Pure attribution logic (no API calls)
  extracted from collect_jira.py. Takes raw Jira issues collected via
  MCP and applies same attribution chain (component → parent → project).

- Update pipeline.md step 4: Replace collect_jira.py with MCP workflow:
  * Skills query Jira via mcp__mcp-atlassian__jira_search (3 queries per
    member: assignee, QA contact cf[10470], OCPSTRAT SME cf[10475])
  * Extract parent keys and batch-resolve via MCP
  * Write intermediate raw_jira_issues.json
  * Run attribute_jira_activities.py to produce jira_activity.json

- Add mcp__mcp-atlassian__jira_search to allowed-tools in all 3 skills
  (heatmap, summary, export)

Architecture: Skills orchestrate MCP calls → Python handles processing.
Output format unchanged: jira_activity.json has same schema as before.

Keep collect_jira.py for rollback. Can remove after validation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove collect_jira.py and its tests now that Jira collection happens
via MCP tools in skills.

Changes:
- Delete bin/collect_jira.py (replaced by MCP workflow in pipeline.md)
- Delete bin/tests/test_collect_jira.py (tests for removed script)
- Move constants to _common.py: ACTIVITY_PROJECTS, QA_CONTACT_FIELD,
  SME_FIELD, UNATTRIBUTED_NO_PARENT, UNATTRIBUTED_PARENT_EMPTY
- Update report.py imports to use _common instead of collect_jira
- Update attribute_jira_activities.py to import constants from _common
- Update README.md: Remove references to collect_jira.py, document
  new MCP-based architecture

The old REST client is fully replaced. Skills orchestrate MCP queries,
attribute_jira_activities.py handles attribution logic.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update comments that referenced the removed collect_jira.py script.

Changes:
- attribute_jira_activities.py: Remove "as collect_jira.py" reference
  in docstring (line 5) and "same format as collect_jira.py" comment
  (line 213)
- render.py: Change "collect_jira does not gather" to passive voice
  "Jira comments are not collected"

All functional code unchanged. Only comment cleanup after removal of
collect_jira.py.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants