Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,27 @@ tools:
"search_issues",
]
---

# Create GitHub Issue from Specification

Create GitHub Issue for the specification at `${file}`.

## Process

1. Analyse specification file to extract requirements
2. Check existing issues using `search_issues`
3. Create or update the issue using `issue_write`
4. Use `feature_request.yml` template (fallback to default)

## Requirements

- Single issue for the complete specification
- Clear title identifying the specification
- Include only changes required by the specification
- Verify against existing issues before creation

## Issue Content

- Title: Feature name from specification
- Description: Problem statement, proposed solution, and context
- Labels: feature, enhancement (as appropriate)
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,27 @@ tools:
"search_issues",
]
---

# Create GitHub Issue from Implementation Plan

Create GitHub Issues for the implementation plan at `${file}`.

## Process

1. Analyse plan file to identify phases
2. Check existing issues using `search_issues`
3. Create or update one issue per phase using `issue_write`
4. Use `feature_request.yml` or `chore_request.yml` templates (fallback to default)

## Requirements

- One issue per implementation phase
- Clear, structured titles and descriptions
- Include only changes required by the plan
- Verify against existing issues before creation

## Issue Content

- Title: Phase name from implementation plan
- Description: Phase details, requirements, and context
- Labels: Appropriate for issue type (feature/chore)
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,34 @@ tools:
"search_issues",
]
---

# Create GitHub Issues for Unmet Specification Requirements

Create GitHub Issues for unimplemented requirements in the specification at `${file}`.

## Process

1. Analyse specification file to extract all requirements
2. Check codebase implementation status for each requirement
3. Search existing issues using `search_issues` to avoid duplicates
4. Create a new issue per unimplemented requirement using `issue_write`
5. Use `feature_request.yml` template (fallback to default)

## Requirements

- One issue per unimplemented requirement from specification
- Clear requirement ID and description mapping
- Include implementation guidance and acceptance criteria
- Verify against existing issues before creation

## Issue Content

- Title: Requirement ID and brief description
- Description: Detailed requirement, implementation method, and context
- Labels: feature, enhancement (as appropriate)

## Implementation Check

- Search codebase for related code patterns
- Check related specification files in `/spec/` directory
- Verify requirement isn't partially implemented
10 changes: 3 additions & 7 deletions .github/workflows/metrics-summary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ jobs:
github.event.inputs.report_channel == 'discussions'
)
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -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})*`;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In GitHub Actions, is using ${{ ... }} interpolation directly inside actions/github-script script:considered a template/code injection risk, and is passing data viaenv the 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:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/metrics-summary.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '120,170p'

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:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/metrics-summary.yml"

echo "== Show matching lines =="
rg -n "actions/github-script|read_report\.outputs\.content|category_id|createDiscussion" "$FILE" || true

echo
echo "== Show context around line 130-170 =="
# Use sed with line numbers (no nl available)
sed -n '125,170p' "$FILE" | awk '{printf "%05d:%s\n", NR+124, $0}'

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 createDiscussion mutation uses a categoryId parameter which is defined as an ID! type (a string in GraphQL) [2]. This ID refers to the unique, system-generated node ID of the DiscussionCategory (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-summary discussion posting security + correct category_id

  • Line 139: ${{ steps.read_report.outputs.content }} is interpolated directly into the actions/github-script JavaScript template literal. Untrusted content can break the script (and ${...} sequences can get interpreted by JS). Pass it via env and read process.env.REPORT_CONTENT instead.
  • Line 147: category_id: 'general' is likely incorrect—POST /repos/{owner}/{repo}/discussions expects 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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})*`;
🧰 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/metrics-summary.yml at line 139, Change the workflow to
stop interpolating steps.read_report.outputs.content directly into the
actions/github-script template literal and instead pass it via env (e.g., set
env: REPORT_CONTENT: ${{ steps.read_report.outputs.content }}) then read it
inside the github-script step using process.env.REPORT_CONTENT; also update the
discussion payload where you set category_id: 'general' to use the numeric
discussion category ID (the integer expected by POST
/repos/{owner}/{repo}/discussions) or look up the correct ID via the Discussions
API before creating the discussion so the createDiscussion call uses the numeric
category_id value.


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',

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For github.rest.discussions.createDiscussion in Octokit/GitHub REST API, what is the exact expected format for category_id (numeric/database ID vs slug/name), and how should it be retrieved safely in Actions?

💡 Result:

In GitHub’s REST API for creating a discussion (octokit / GitHub REST endpoint github.rest.discussions.createDiscussion), category_id is expected to be the database ID of the discussion category (i.e., the numeric id you get from REST), not the category slug/name. How to retrieve it safely in GitHub Actions: 1) Use the REST API to list discussion categories for the repository. 2) Find the category whose name or slug matches what you want. 3) Pass that category’s numeric id as category_id to createDiscussion. Evidence / what the sources support: - The GraphQL documentation for discussions shows that the discussion category identifier used to create a discussion in GraphQL is categoryId and is a node ID (GraphQL DiscussionCategory.id is the Node ID, not a human-readable name/slug). It explicitly models categoryId as the id of the DiscussionCategory object [1][2]. - The GitHub Marketplace Actions ecosystem that wraps discussion creation distinguishes between category-id (an ID) and category-name (a name), and notes you obtain repository-id/category-id via GraphQL Explorer querying discussionCategories { node { id name } }. This aligns with the notion that the value used as “category-id” is the category’s id from GitHub (Node ID in that context) rather than a slug/name [3][4]. Important nuance: - Your question asks specifically about the REST API parameter category_id for github.rest.discussions.createDiscussion. The sources retrieved in this research round clearly support the GraphQL side (categoryId is a node ID) and the general “ID vs name” distinction in Action wrappers, but they do not include the official REST reference page text showing the exact REST parameter type/format for category_id. So, with the evidence we have, I can confidently answer the “ID vs slug/name” part (use the category’s id, not the slug/name). However, I cannot fully prove from the retrieved sources whether REST category_id is explicitly the numeric database id vs a global node id. If you want, tell me which exact Octokit method/signature you’re using (the URL or the generated OpenAPI schema snippet), and I can do one more targeted search for the official REST reference text that states the exact category_id format (numeric vs Node/global ID).

Citations:


Fix discussions category_id to use the real category ID (not 'general')

At .github/workflows/metrics-summary.yml:147, category_id: 'general' looks like a category slug/name, but github.rest.discussions.createDiscussion expects the discussion category’s numeric id, so discussion creation can fail.

✅ Suggested direction
-                category_id: 'general',
+                category_id: process.env.DISCUSSION_CATEGORY_ID,

Set DISCUSSION_CATEGORY_ID to the category’s id (resolve it by calling the repo’s “list discussion categories” API, match by slug/name, then use the returned id), rather than the slug/name itself.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
category_id: 'general',
category_id: process.env.DISCUSSION_CATEGORY_ID,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/metrics-summary.yml at line 147, The workflow is passing a
slug ('general') to github.rest.discussions.createDiscussion via category_id:
'general' but that API expects the numeric category id; update the workflow to
supply a numeric DISCUSSION_CATEGORY_ID instead of the slug, and add a step (or
script) to call the repository "list discussion categories" API, find the
category by its slug/name (e.g., "general") and export the returned id into
DISCUSSION_CATEGORY_ID; then replace category_id: 'general' with category_id:
${{ env.DISCUSSION_CATEGORY_ID }} (or the equivalent variable reference) so
github.rest.discussions.createDiscussion receives the numeric id.

});
console.log(`Posted discussion: ${data.html_url}`);
} catch (err) {
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ on:
jobs:
check:
runs-on: ubuntu-latest
env:
HUSKY: '0'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: node --version && npm --version
- run: npm ci
- run: npm run check
- run: npm run lint:js
- run: npm run lint:yaml
- run: npm run lint:pkg-json
- run: npm run lint:workflows
- run: npm run lint:md
- run: npm run lint:json
- run: npm run test
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24
22
11 changes: 10 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ tags: ["contributing", "guidelines", "workflow", "standards", "pull-requests"]
## 🚀 Quick Start (TL;DR)

1. **Fork & Clone:** Fork this repo and clone it locally.
2. **Install dependencies:** `npm ci` (runs `prepare` and installs Husky hooks)
2. **Install dependencies:** `npm ci`
3. **Create a branch:** Use the correct prefix (e.g. `feat/`, `fix/`, `docs/`).
4. **Write code & tests:** Follow [Coding Standards](instructions/coding-standards.instructions.md) and add/expand tests.
5. **Lint & test:** Run `npm run lint:all` and `npm test` before committing. On
Expand Down Expand Up @@ -149,3 +149,12 @@ Refer to `.vscode/extensions.json` and `.vscode/settings.json` for the authorita
- [Languages & Linting](instructions/languages.instructions.md)

---

## Licence

By contributing to this project, you agree that your contributions will be licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.

Thank you for helping us make LightSpeed better!

*Maintained with ❤️ by the 🚀 LightSpeedWP Automation Team*
[Org Profile](https://github.com/lightspeedwp/.github/tree/main/profile)
42 changes: 17 additions & 25 deletions agents/mode-prd.agent.md
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

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Frontmatter does not meet required markdown schema

Please add the repository-required markdown metadata fields (and language) to this file’s YAML frontmatter.

As per coding guidelines, "All .md files in this repository should include YAML frontmatter with required fields:
file_type, title, description, version, last_updated, owners, tags, status, stability, domain" and "Specify language in
frontmatter".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/mode-prd.agent.md` around lines 1 - 21, The YAML frontmatter in
agents/mode-prd.agent.md is missing required repository metadata; update the
existing frontmatter block (the top-level --- section) to include file_type,
title, description, version, last_updated, owners, tags, status, stability,
domain and language; populate title/description from the current
name/description values, set sensible defaults for version and last_updated
(e.g., "1.0.0" and an ISO date), add owners as a list, tags as an array of
relevant keywords, and provide status and stability values so the frontmatter
matches the repository schema while preserving the existing name and description
entries.


# Create PRD Agent
Expand Down Expand Up @@ -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.
15 changes: 10 additions & 5 deletions scripts/agents/__tests__/project-meta-sync.agent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test assertion relies on shared global state

This check can be flaky because process.exitCode may be set by another test. Please isolate the module load inside the
test and snapshot/reset process.exitCode locally before assertion.

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agents/__tests__/project-meta-sync.agent.test.js` around lines 5 -
16, The test relies on global process.exitCode and can be flaky; modify the
second spec to snapshot and restore process.exitCode and load the module in
isolation (use jest.isolateModules or require inside a closure) so requiring
'../project-meta-sync.agent' cannot be affected by other tests: save const
originalExitCode = process.exitCode, set process.exitCode = undefined (or a
known safe value), call jest.isolateModules(() => { const agent =
require('../project-meta-sync.agent'); /* assertions */ }), assert typeof agent
=== 'function' and process.exitCode is not 1, then restore process.exitCode =
originalExitCode; this targets the module load and the run() side-effect guard
in project-meta-sync.agent.

});
93 changes: 90 additions & 3 deletions scripts/agents/planner.agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.

medium

To prevent runtime errors if labels is not provided or is not an array (e.g., during testing or if the API response is malformed), add a defensive check at the beginning of deriveProjectFromLabels.

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}

Expand Down Expand Up @@ -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

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.

high

Using a literal string with \n for replacement can fail if the file is checked out with CRLF (\r\n) line endings (which is common on Windows environments). Using a regular expression that handles optional carriage returns (\r?) makes the replacement robust and ensures the project assignment section is always correctly inserted.

    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 = {}) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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
project, and no fallback queue path for low-confidence mappings, so the Phase 2 acceptance criteria are not fully met.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agents/planner.agent.js` around lines 362 - 366, The code in
scripts/agents/planner.agent.js currently only logs
analysisContext.projectAssignment via core.info (inside the planner logic) but
never applies it or handles low-confidence cases; update the planner flow so
that when analysisContext.projectAssignment?.project is present and we are in
apply mode you call the GitHub API to add the issue/PR to the specified project
(use the repository/issue PR identifiers available in the planner context and
the GitHub REST/Octokit projects endpoints), and when confidence is below the
threshold implement a fallback path that enqueues the issue/PR into a fallback
queue (or label) instead of assigning to the project; target the code around the
core.info call and the surrounding apply-mode check so you both perform the
project assignment API call and add the low-confidence fallback handling.

} catch (error) {
throw new Error(
`Failed to post plan on #${issue.number}: ${error.message}`,
Expand Down
Loading