-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Phase 2 - Planner Agent GitHub API Integration #634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8113cfe
43578fe
7ff7a16
f20d67c
4633daf
7093b8d
68d7363
5a79d7d
eaa71cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -105,7 +105,6 @@ jobs: | |||||
| github.event.inputs.report_channel == 'discussions' | ||||||
| ) | ||||||
| runs-on: ubuntu-latest | ||||||
|
|
||||||
| steps: | ||||||
| - name: Checkout repository | ||||||
| uses: actions/checkout@v4 | ||||||
|
|
@@ -136,19 +135,16 @@ jobs: | |||||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
| script: | | ||||||
| const title = `Weekly Metrics Summary — ${new Date().toISOString().split('T')[0]}`; | ||||||
| const body = `${{ steps.read_report.outputs.content }} | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| *Generated by metrics-summary workflow. [View full reports](${{ github.server_url }}/${{ github.repository }}/tree/develop/.github/reports/metrics/)*`; | ||||||
| const reportUrl = `${{ github.server_url }}/${{ github.repository }}/tree/develop/.github/reports/metrics/`; | ||||||
| const body = `${{ steps.read_report.outputs.content }}\n\n---\n\n*Generated by metrics-summary workflow. [View full reports](${reportUrl})*`; | ||||||
|
|
||||||
| try { | ||||||
| const { data } = await github.rest.discussions.createDiscussion({ | ||||||
| owner: context.repo.owner, | ||||||
| repo: context.repo.repo, | ||||||
| title, | ||||||
| body, | ||||||
| category_id: 'general', // Adjust based on your category ID | ||||||
| category_id: 'general', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In GitHub’s REST API for creating a discussion (octokit / GitHub REST endpoint Citations:
Fix discussions At ✅ Suggested direction- category_id: 'general',
+ category_id: process.env.DISCUSSION_CATEGORY_ID,Set 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }); | ||||||
| console.log(`Posted discussion: ${data.html_url}`); | ||||||
| } catch (err) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| 24 | ||
| 22 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,23 @@ | ||
| --- | ||
| name: "Product Requirements Document Generator" | ||
| description: "Generate a comprehensive Product Requirements Document (PRD) in Markdown, detailing user stories, acceptance criteria, technical considerations, and metrics. Optionally create GitHub issues upon user confirmation." | ||
| version: "v1.1" | ||
| last_updated: "2026-05-29" | ||
| owners: ["LightSpeedWP Engineering"] | ||
| tags: ["agent", "mode", "prd", "product-management", "requirements"] | ||
| file_type: "agent" | ||
| status: "active" | ||
| domain: "planning" | ||
| stability: "stable" | ||
| tools: | ||
| - codebase | ||
| - edit/editFiles | ||
| - fetch | ||
| - findTestFiles | ||
| - list_issues | ||
| - githubRepo | ||
| - search | ||
| - add_issue_comment | ||
| - issue_write | ||
| - issue_read | ||
| - search_issues | ||
| permissions: | ||
| - read | ||
| - write | ||
| - github:issues | ||
| [ | ||
| "codebase", | ||
| "edit/editFiles", | ||
| "fetch", | ||
| "findTestFiles", | ||
| "list_issues", | ||
| "githubRepo", | ||
| "search", | ||
| "add_issue_comment", | ||
| "issue_write", | ||
| "issue_read", | ||
| "search_issues", | ||
| ] | ||
| metadata: | ||
| guardrails: Ask clarifying questions first, confirm scope before drafting any PRD, | ||
| only create docs with user approval, and never write issues without explicit consent. | ||
| guardrails: "Ask clarifying questions first, confirm scope before drafting any PRD, only create docs with user approval, and never write issues without explicit consent." | ||
|
|
||
| --- | ||
|
Comment on lines
1
to
21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Frontmatter does not meet required markdown schema Please add the repository-required markdown metadata fields (and As per coding guidelines, "All 🤖 Prompt for AI Agents |
||
|
|
||
| # Create PRD Agent | ||
|
|
@@ -214,3 +204,5 @@ Concise paragraph describing the user's journey and benefits. | |
| - Bullet list of criteria. | ||
|
|
||
| --- | ||
|
|
||
| After generating the PRD, I will ask if you want to proceed with creating GitHub issues for the user stories. If you agree, I will create them using `issue_write` and provide you with the links. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,16 @@ | |
| * Jest suite verifying the baseline behaviour of `project-meta-sync.agent.js`. | ||
| * @see ../project-meta-sync.agent.js | ||
| */ | ||
| // Basic smoke test for project-meta-sync.agent.js | ||
| const agent = require("../project-meta-sync.agent"); | ||
| const agent = require('../project-meta-sync.agent'); | ||
|
|
||
| describe("project-meta-sync.agent", () => { | ||
| it("should be defined", () => { | ||
| expect(agent).toBeDefined(); | ||
| describe('project-meta-sync.agent', () => { | ||
| it('exports a callable function', () => { | ||
| expect(typeof agent).toBe('function'); | ||
| }); | ||
|
|
||
| it('does not execute run() on require (no LS_PROJECT_URL side-effect)', () => { | ||
| // If the module-scope guard is absent, requiring the file calls run() immediately, | ||
| // which throws "LS_PROJECT_URL not set" and sets process.exitCode = 1. | ||
| expect(process.exitCode).not.toBe(1); | ||
| }); | ||
|
Comment on lines
+5
to
16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test assertion relies on shared global state This check can be flaky because Suggested test hardening-const agent = require('../project-meta-sync.agent');
-
describe('project-meta-sync.agent', () => {
it('exports a callable function', () => {
+ const agent = require('../project-meta-sync.agent');
expect(typeof agent).toBe('function');
});
it('does not execute run() on require (no LS_PROJECT_URL side-effect)', () => {
- // If the module-scope guard is absent, requiring the file calls run() immediately,
- // which throws "LS_PROJECT_URL not set" and sets process.exitCode = 1.
+ const originalExitCode = process.exitCode;
+ process.exitCode = undefined;
+ jest.isolateModules(() => {
+ require('../project-meta-sync.agent');
+ });
expect(process.exitCode).not.toBe(1);
+ process.exitCode = originalExitCode;
});
});As per coding guidelines, "Ensure tests are isolated and do not depend on external state." 🤖 Prompt for AI Agents |
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,57 @@ function determinePlanType(title, labels, description) { | |
| return "task"; | ||
| } | ||
|
|
||
| function deriveProjectFromLabels(labels) { | ||
| if (!Array.isArray(labels)) { | ||
| return { | ||
| project: null, | ||
| confidence: "low", | ||
| reason: "No clear project mapping; manual review recommended", | ||
| }; | ||
| } | ||
|
|
||
| // Map area labels to project names | ||
|
Comment on lines
+53
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent runtime errors if function deriveProjectFromLabels(labels) {
if (!Array.isArray(labels)) {
return {
project: null,
confidence: "low",
reason: "No clear project mapping; manual review recommended",
};
}
// Map area labels to project names |
||
| const areaToProject = { | ||
| "area:workflows": "workflows-automation", | ||
| "area:documentation": "documentation", | ||
| "area:testing": "testing-coverage", | ||
| "area:security": "security-hardening", | ||
| }; | ||
|
|
||
| for (const label of labels) { | ||
| if (areaToProject[label]) { | ||
| return { | ||
| project: areaToProject[label], | ||
| confidence: "high", | ||
| reason: `Derived from label '${label}'`, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // Check for type labels to suggest generic projects | ||
| if (labels.includes("type:feature") || labels.includes("type:enhancement")) { | ||
| return { | ||
| project: "feature-development", | ||
| confidence: "medium", | ||
| reason: "Feature/enhancement type; recommend feature-development project", | ||
| }; | ||
| } | ||
|
|
||
| if (labels.includes("type:bug")) { | ||
| return { | ||
| project: "bug-fixes", | ||
| confidence: "medium", | ||
| reason: "Bug type; recommend bug-fixes project", | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| project: null, | ||
| confidence: "low", | ||
| reason: "No clear project mapping; manual review recommended", | ||
| }; | ||
| } | ||
|
|
||
| function generateArchitecturePlan(issue) { | ||
| return `## 📐 Architecture Plan for #${issue.number} | ||
|
|
||
|
|
@@ -178,18 +229,43 @@ async function analyzeContext(octokit, context) { | |
| labels, | ||
| linkedIssues, | ||
| type: determinePlanType(title, labels, description), | ||
| projectAssignment: deriveProjectFromLabels(labels), | ||
| }; | ||
| } | ||
|
|
||
| function generatePlan(context) { | ||
| const baseIssue = { number: context.number }; | ||
| let plan; | ||
|
|
||
| switch (context.type) { | ||
| case "architecture": | ||
| return generateArchitecturePlan({ number: context.number }); | ||
| plan = generateArchitecturePlan(baseIssue); | ||
| break; | ||
| case "implementation": | ||
| return generateImplementationPlan({ number: context.number }); | ||
| plan = generateImplementationPlan(baseIssue); | ||
| break; | ||
| default: | ||
| return generateTaskPlan({ number: context.number }); | ||
| plan = generateTaskPlan(baseIssue); | ||
| } | ||
|
|
||
| // Add project assignment recommendation if available | ||
| if (context.projectAssignment?.project) { | ||
| const projectSection = ` | ||
| ### 📋 Project Assignment | ||
|
|
||
| **Assigned Project:** \`${context.projectAssignment.project}\` | ||
| **Confidence:** ${context.projectAssignment.confidence} | ||
| **Reason:** ${context.projectAssignment.reason} | ||
| `; | ||
| return plan.replace( | ||
| /---\r?\n\*\*Generated by Planner Agent\*\*.*/, | ||
| `${projectSection.trimStart()} | ||
| --- | ||
| **Generated by Planner Agent** <!-- planner-agent-summary -->`, | ||
| ); | ||
|
Comment on lines
+260
to
+265
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a literal string with return plan.replace(
/---\r?\n\*\*Generated by Planner Agent\*\*/,
`${projectSection}\n---\n**Generated by Planner Agent**`
); |
||
| } | ||
|
|
||
| return plan; | ||
| } | ||
|
|
||
| async function run(context = github.context, options = {}) { | ||
|
|
@@ -239,6 +315,11 @@ async function run(context = github.context, options = {}) { | |
| if (dryRun) { | ||
| core.info(`DRY-RUN: Would post plan:\n${plan}`); | ||
| logger.info("Dry-run mode: plan not posted", { event: "dry-run" }); | ||
| if (analysisContext.projectAssignment?.project) { | ||
| core.info( | ||
| `DRY-RUN: Would assign to project: ${analysisContext.projectAssignment.project} (${analysisContext.projectAssignment.confidence})`, | ||
| ); | ||
| } | ||
| } else { | ||
| try { | ||
| const prComments = await octokit.rest.issues.listComments({ | ||
|
|
@@ -277,6 +358,12 @@ async function run(context = github.context, options = {}) { | |
| issueNumber: issue.number, | ||
| }); | ||
| } | ||
|
|
||
| if (analysisContext.projectAssignment?.project) { | ||
| core.info( | ||
| `Project assignment: ${analysisContext.projectAssignment.project} (${analysisContext.projectAssignment.confidence})`, | ||
| ); | ||
| } | ||
|
Comment on lines
+362
to
+366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Project assignment is logged but never actually applied At the moment this only prints a recommendation in apply mode. There is no GitHub API call that adds the issue/PR to a 🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| throw new Error( | ||
| `Failed to post plan on #${issue.number}: ${error.message}`, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In GitHub Actions, is using ${{ ... }} interpolation directly inside actions/github-scriptscript:considered a template/code injection risk, and is passing data viaenvthe recommended mitigation?💡 Result:
Yes, using
${{ ... }}interpolation directly inside the actions/github-script script: parameter is considered a significant security risk and is strongly discouraged [1][2]. Because GitHub Actions evaluates${{ ... }}expressions before passing the resulting string to the script, any untrusted input—such as PR titles, issue bodies, or commit messages—can be manipulated to inject malicious JavaScript code [1][3][2]. This can lead to unauthorized code execution or syntax errors when the injected string is not valid JavaScript [1][2]. The recommended mitigation is to pass data via environment variables instead of direct interpolation [1][4][5]. By storing untrusted input in an env block, you can safely access the values within your script using process.env, which treats the data as a standard variable rather than executable code [2][4][6]. Example of the insecure pattern: - uses: actions/github-script@v8 with: script: console.log("${{ github.event.issue.title }}") # DANGEROUS: Injection risk Example of the recommended, secure pattern: - uses: actions/github-script@v8 env: ISSUE_TITLE: ${{ github.event.issue.title }} with: script: console.log(process.env.ISSUE_TITLE) # SAFE: Treated as a variable Official documentation and the actions/github-script maintainers advise this defensive programming posture to prevent script injection vulnerabilities, similar to how you would protect any other privileged application [3][2][4].Citations:
scriptactions/github-script#603🏁 Script executed:
Repository: lightspeedwp/.github
Length of output: 175
🌐 Web query:
GitHub REST API discussions.createDiscussion category_id expects numeric ID or category slug name?💡 Result:
GitHub REST API endpoint discussions.createDiscussion expects category_id to be the category’s numeric ID (not the category slug name). [1] Evidence: GitHub’s GraphQL API uses categoryId as an opaque ID type (Node IDs) when creating discussions. [2] However, for REST usage the category identifier you pass as category_id is the numeric category ID; slugs are used for discussion category forms/templates (they map to filenames and a “slug”, not an API parameter for createDiscussion). [3] So, pass a numeric category id value for category_id, not the slug string.
Citations:
🏁 Script executed:
Repository: lightspeedwp/.github
Length of output: 1858
🌐 Web query:
https://docs.github.com REST API create a discussion category_id parameter type numeric ID or string slug?💡 Result:
There is no GitHub REST API endpoint for creating repository-level discussions [1]. Repository discussions are managed exclusively through the GitHub GraphQL API [2][3]. In the GraphQL API, the
createDiscussionmutation uses acategoryIdparameter which is defined as anID!type (a string in GraphQL) [2]. This ID refers to the unique, system-generated node ID of theDiscussionCategory(e.g., "DIC_kwDO..."), not a numeric ID or a human-readable string slug [2][4]. If you are looking for REST API documentation regarding "discussions," it is limited to team discussions (within organizations) [5], which are distinct from the repository discussions feature [1]. For repository discussions, you must use the GraphQL API [2][3].Citations:
🌐 Web query:
GitHub REST API POST /repos/{owner}/{repo}/discussions parameter category_id expected type (numeric ID vs slug) general💡 Result:
For GitHub’s REST endpoint POST /repos/{owner}/{repo}/discussions, the request body parameter category_id expects the numeric database ID of the discussion category, not a slug. Evidence: the REST-side discussion category object includes an integer id field (and also a separate slug string), indicating that category selection is done by the integer id rather than the slug. Specifically, the discussion category JSON schema shows id: {type: integer} and slug: {type: string} as separate fields, with id being the numeric identifier you would pass as category_id.[1] Note: GitHub’s discussion-category template syntax uses the slug name (YAML template filenames correspond to the category slug), but that slug mechanism is for custom templates, not the REST API create-discussion input.[2]
Citations:
Fix
metrics-summarydiscussion posting security + correctcategory_id${{ steps.read_report.outputs.content }}is interpolated directly into theactions/github-scriptJavaScript template literal. Untrusted content can break the script (and${...}sequences can get interpreted by JS). Pass it viaenvand readprocess.env.REPORT_CONTENTinstead.category_id: 'general'is likely incorrect—POST /repos/{owner}/{repo}/discussionsexpects the numeric discussion category ID (not the slug/name).🛡️ Safer pattern
- name: Post to discussions if: steps.read_report.outputs.content != '' uses: actions/github-script@v7 + env: + REPORT_CONTENT: ${{ steps.read_report.outputs.content }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const title = `Weekly Metrics Summary — ${new Date().toISOString().split('T')[0]}`; const reportUrl = `${{ github.server_url }}/${{ github.repository }}/tree/develop/.github/reports/metrics/`; - const body = `${{ steps.read_report.outputs.content }}\n\n---\n\n*Generated by metrics-summary workflow. [View full reports](${reportUrl})*`; + const reportContent = process.env.REPORT_CONTENT || ''; + const body = `${reportContent}\n\n---\n\n*Generated by metrics-summary workflow. [View full reports](${reportUrl})*`;📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.25.2)
[info] 139-139: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents