ira-review 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.github.md CHANGED
@@ -135,6 +135,12 @@ IRA fetches the linked JIRA ticket, extracts the acceptance criteria, and uses t
135
135
 
136
136
  This is the feature that catches "does this actually match the ticket?" before a human has to ask.
137
137
 
138
+ When a JIRA ticket has **no acceptance criteria at all**, IRA generates suggested ACs from the diff and (by default) posts them as a comment on the JIRA ticket for the Product Owner to review and refine. If your CI environment cannot or should not write back to JIRA — for example, the JIRA service account is read-only, or org policy forbids automated JIRA writes — pass `--no-post-acs-to-jira` (or set `IRA_POST_ACS_TO_JIRA=false`). The suggestions still render in the PR summary under **📝 Suggested Acceptance Criteria**, so reviewers see them either way; only the JIRA write is suppressed.
139
+
140
+ ### "All Clear" PR Summary
141
+
142
+ When IRA finishes a review with **zero issues** found and **JIRA acceptance criteria are 100% covered** (or the ticket needs no ACs), the PR summary leads with a celebratory ✅ banner: a confident, specific signal that automated review passed, alongside an explicit reminder that **human reviewer approval is still required before merge**. The banner is suppressed when an AC gap exists so the summary never reads "safe to approve" while a requirements gap is still visible — IRA augments your code review process, it doesn't replace it.
143
+
138
144
  ### Inline AI Comments
139
145
 
140
146
  Each issue is posted as an inline comment on the exact line in the PR, containing:
@@ -436,6 +442,15 @@ Tips for Jenkins / corporate networks:
436
442
  - **Comment style** — use `--comment-style compact` (default) for terse,
437
443
  severity-first inline comments. `--comment-style detailed` keeps the legacy
438
444
  Explanation / Impact / Suggested Fix block.
445
+ - **Don't write to JIRA** — pass `--no-post-acs-to-jira` (or set
446
+ `IRA_POST_ACS_TO_JIRA=false`) when the JIRA token is read-only or org policy
447
+ forbids automated comments on tickets. AI-generated AC suggestions still
448
+ appear in the PR summary; only the JIRA write is suppressed.
449
+ - **PowerShell stability on Windows agents** — wrap the `ira-review` invocation
450
+ with `2>&1 | ForEach-Object { Write-Host $_ }` and check `$LASTEXITCODE`
451
+ explicitly. PowerShell's default `$ErrorActionPreference = 'Stop'` treats any
452
+ native-command stderr as a terminating `NativeCommandError`; merging stderr
453
+ into stdout prevents harmless deprecation notices from aborting the pipeline.
439
454
 
440
455
  ---
441
456
 
package/README.md CHANGED
@@ -42,6 +42,7 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
42
42
  - Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
43
43
  - JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
44
44
  - JIRA AC auto-detection — finds AC from custom field or description automatically
45
+ - "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
45
46
  - Custom team review rules via `.ira-rules.json` (see below)
46
47
  - Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
47
48
  - Comment deduplication across re-runs
@@ -151,6 +152,7 @@ All optional. IRA works with just an SCM token and an AI key.
151
152
  | OpenAI-compatible gateway | `--ai-base-url https://your-llm-proxy/v1` (GitHub Models, LiteLLM, internal proxy…) |
152
153
  | Rules from URL (no checkout) | `--rules-url https://bitbucket.example.com/.../.ira-rules.json` |
153
154
  | Compact / detailed comments | `--comment-style compact` (default) or `--comment-style detailed` |
155
+ | Don't post AI-generated ACs to JIRA | `--no-post-acs-to-jira` (env: `IRA_POST_ACS_TO_JIRA=false`) — suggestions still render in the PR summary; only the JIRA write is skipped |
154
156
 
155
157
  ---
156
158
 
package/README.npm.md CHANGED
@@ -42,6 +42,7 @@ Each issue is posted as an inline comment on the exact PR line with explanation,
42
42
  - Two-pass critical review (`--ai-model-critical`) — bulk pass uses your everyday model; only `CRITICAL`/`BLOCKER` findings are re-run against a stronger model, keeping premium-request cost low while preserving deep analysis on what matters
43
43
  - JIRA acceptance criteria validation with per-criterion pass/fail and edge case detection
44
44
  - JIRA AC auto-detection — finds AC from custom field or description automatically
45
+ - "All Clear" PR summary block — celebratory ✅ banner when zero issues are found and JIRA AC coverage is 100%, with a clear "human reviewer approval is still required before merge" reminder. Suppressed automatically if any AC gap exists, so the summary never claims "safe to approve" while requirements are unmet.
45
46
  - Custom team review rules via `.ira-rules.json` (see below)
46
47
  - Test case generation from JIRA tickets (Jest, Vitest, Playwright, etc.)
47
48
  - Comment deduplication across re-runs
@@ -151,6 +152,7 @@ All optional. IRA works with just an SCM token and an AI key.
151
152
  | OpenAI-compatible gateway | `--ai-base-url https://your-llm-proxy/v1` (GitHub Models, LiteLLM, internal proxy…) |
152
153
  | Rules from URL (no checkout) | `--rules-url https://bitbucket.example.com/.../.ira-rules.json` |
153
154
  | Compact / detailed comments | `--comment-style compact` (default) or `--comment-style detailed` |
155
+ | Don't post AI-generated ACs to JIRA | `--no-post-acs-to-jira` (env: `IRA_POST_ACS_TO_JIRA=false`) — suggestions still render in the PR summary; only the JIRA write is skipped |
154
156
 
155
157
  ---
156
158
 
package/dist/cli.js CHANGED
@@ -1295,10 +1295,17 @@ var BitbucketServerClient = class {
1295
1295
  });
1296
1296
  }
1297
1297
  async getIssueComments(pullRequestId) {
1298
+ const collect = (node, sink) => {
1299
+ if (!node) return;
1300
+ if (typeof node.text === "string" && node.text.length > 0) sink.push(node.text);
1301
+ if (Array.isArray(node.comments)) {
1302
+ for (const child of node.comments) collect(child, sink);
1303
+ }
1304
+ };
1298
1305
  const bodies = [];
1299
1306
  let start = 0;
1300
1307
  while (true) {
1301
- const url = this.prUrl(pullRequestId, `/comments?start=${start}&limit=100`);
1308
+ const url = this.prUrl(pullRequestId, `/activities?start=${start}&limit=100`);
1302
1309
  const data = await withRetry(async () => {
1303
1310
  const response = await fetchWithTimeout(url, { headers: this.headers });
1304
1311
  if (!response.ok) {
@@ -1310,7 +1317,9 @@ var BitbucketServerClient = class {
1310
1317
  }
1311
1318
  return await response.json();
1312
1319
  });
1313
- for (const c of data.values) bodies.push(c.text);
1320
+ for (const activity of data.values) {
1321
+ if (activity.action === "COMMENTED") collect(activity.comment, bodies);
1322
+ }
1314
1323
  if (data.isLastPage) break;
1315
1324
  start = data.nextPageStart ?? start + 100;
1316
1325
  }
@@ -2615,6 +2624,29 @@ function buildSummary2(result) {
2615
2624
  lines.push("");
2616
2625
  }
2617
2626
  }
2627
+ const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
2628
+ if (result.comments.length === 0 && !acGapExists) {
2629
+ const fw = result.framework ?? "your stack";
2630
+ const acLine = result.requirementCompletion ? `Acceptance criteria for **${result.requirementCompletion.jiraKey}**: **${result.requirementCompletion.completionPercentage}% covered** (${result.requirementCompletion.metCriteria}/${result.requirementCompletion.totalCriteria}).` : result.acceptanceValidation ? `Acceptance criteria for **${result.acceptanceValidation.jiraKey}**: ${result.acceptanceValidation.overallPass ? "**all met** \u2705" : "**partially met** \u2014 see the JIRA section above"}.` : result.acGeneration && result.acGeneration.criteria.length > 0 ? result.acGeneration.postedToJira ? `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** and posted them as a comment on the JIRA ticket for the Product Owner / requirement author to review and refine.` : `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** (see the Suggested Acceptance Criteria section below).` : null;
2631
+ lines.push("## \u2705 All Clear \u2014 No Issues Found");
2632
+ lines.push("");
2633
+ lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
2634
+ lines.push(`>`);
2635
+ lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
2636
+ if (acLine) {
2637
+ lines.push(`>`);
2638
+ lines.push(`> ${acLine}`);
2639
+ }
2640
+ if (result.risk) {
2641
+ lines.push(`>`);
2642
+ lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
2643
+ }
2644
+ lines.push(`>`);
2645
+ lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
2646
+ lines.push(`>`);
2647
+ lines.push(`> \u{1F465} **Human reviewer approval is still required before merge.** IRA augments your code review process \u2014 it doesn't replace it. Please ensure your team's review and approval requirements have been met before merging.`);
2648
+ lines.push("");
2649
+ }
2618
2650
  lines.push("## Overview");
2619
2651
  lines.push("");
2620
2652
  lines.push(`| Metric | Value |`);
@@ -2862,7 +2894,10 @@ var Notifier = class {
2862
2894
  import { execSync as execSync2 } from "child_process";
2863
2895
  function resolveGitRoot() {
2864
2896
  try {
2865
- return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
2897
+ return execSync2("git rev-parse --show-toplevel", {
2898
+ encoding: "utf-8",
2899
+ stdio: ["pipe", "pipe", "pipe"]
2900
+ }).trim();
2866
2901
  } catch {
2867
2902
  return process.cwd();
2868
2903
  }
@@ -3403,7 +3438,11 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3403
3438
  let commitMessages = [];
3404
3439
  try {
3405
3440
  const { execSync: execSync6 } = await import("child_process");
3406
- const gitLog = execSync6("git log --oneline -20 --no-decorate", { cwd: repoPath, encoding: "utf-8" });
3441
+ const gitLog = execSync6("git log --oneline -20 --no-decorate", {
3442
+ cwd: repoPath,
3443
+ encoding: "utf-8",
3444
+ stdio: ["pipe", "pipe", "pipe"]
3445
+ });
3407
3446
  commitMessages = gitLog.trim().split("\n").filter(Boolean);
3408
3447
  } catch {
3409
3448
  }
@@ -3422,15 +3461,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3422
3461
  if (acGeneration.parseWarning) {
3423
3462
  warnings.push(acGeneration.parseWarning);
3424
3463
  }
3425
- if (acGeneration.criteria.length > 0 && !this.config.dryRun) {
3464
+ const postingDisabled = this.config.postAcsToJira === false;
3465
+ if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
3426
3466
  try {
3427
3467
  const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
3428
3468
  await jiraClient.addComment(this.config.jiraTicket, commentBody);
3429
3469
  console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
3470
+ acGeneration.postedToJira = true;
3430
3471
  } catch (error) {
3431
3472
  const msg = error instanceof Error ? error.message : "Unknown error";
3432
3473
  warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
3474
+ acGeneration.postedToJira = false;
3433
3475
  }
3476
+ } else if (postingDisabled && acGeneration.criteria.length > 0) {
3477
+ console.log(` Skipped posting ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket} (--no-post-acs-to-jira). See PR summary.`);
3478
+ acGeneration.postedToJira = false;
3479
+ } else {
3480
+ acGeneration.postedToJira = false;
3434
3481
  }
3435
3482
  }
3436
3483
  }
@@ -3644,6 +3691,8 @@ function resolveConfigFromEnv(overrides = {}) {
3644
3691
  }
3645
3692
  const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
3646
3693
  const jiraTicket = overrides.jiraTicket ?? optionalEnv("IRA_JIRA_TICKET");
3694
+ const postAcsToJiraEnv = optionalEnv("IRA_POST_ACS_TO_JIRA");
3695
+ const postAcsToJira = overrides.postAcsToJira === false ? false : postAcsToJiraEnv && /^(false|0|no|off)$/i.test(postAcsToJiraEnv) ? false : void 0;
3647
3696
  const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
3648
3697
  if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
3649
3698
  throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
@@ -3677,6 +3726,7 @@ function resolveConfigFromEnv(overrides = {}) {
3677
3726
  ...overrides.generateTests && { generateTests: overrides.generateTests },
3678
3727
  ...overrides.testFramework && { testFramework: overrides.testFramework },
3679
3728
  ...jiraAcSource && { jiraAcSource },
3729
+ ...postAcsToJira === false && { postAcsToJira: false },
3680
3730
  ...commentStyle && { commentStyle },
3681
3731
  ...rulesUrl && { rulesUrl }
3682
3732
  };
@@ -4030,7 +4080,7 @@ function logEnvironmentInfo(config) {
4030
4080
  }
4031
4081
  var program = new Command();
4032
4082
  program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version("3.1.0");
4033
- program.command("review").description("Run AI-powered review on a pull request").option("--sonar-url <url>", "SonarQube/SonarCloud base URL (or IRA_SONAR_URL)").option("--sonar-token <token>", "SonarQube API token (or IRA_SONAR_TOKEN)").option("--project-key <key>", "Sonar project key (or IRA_PROJECT_KEY)").option("--pr <id>", "Pull request ID (or IRA_PR)").option("--scm-provider <provider>", "SCM provider: bitbucket or github (or IRA_SCM_PROVIDER)").option("--bitbucket-token <token>", "Bitbucket API token (or IRA_BITBUCKET_TOKEN)").option("--repo <repo>", "Bitbucket workspace/repo-slug (Cloud) or PROJECT/repo-slug (Server) (or IRA_REPO)").option("--bitbucket-url <url>", "Bitbucket base URL (or IRA_BITBUCKET_URL)").option("--bitbucket-type <type>", "Bitbucket type: cloud or server (auto-detects from URL if omitted; or IRA_BITBUCKET_TYPE)").option("--github-token <token>", "GitHub API token (or IRA_GITHUB_TOKEN)").option("--github-repo <repo>", "GitHub owner/repo (or IRA_GITHUB_REPO)").option("--github-url <url>", "GitHub Enterprise URL (or IRA_GITHUB_URL)").option("--ai-provider <provider>", "AI provider").option("--ai-model <model>", "AI model to use").option("--dry-run", "Print comments to stdout instead of posting to SCM").option("--min-severity <level>", "Minimum severity to review (BLOCKER|CRITICAL|MAJOR|MINOR|INFO)").option("--jira-url <url>", "JIRA base URL (or IRA_JIRA_URL)").option("--jira-email <email>", "JIRA email (or IRA_JIRA_EMAIL)").option("--jira-token <token>", "JIRA API token (or IRA_JIRA_TOKEN)").option("--jira-type <type>", "JIRA type: cloud or server (auto-detects from URL if omitted)").option("--jira-ticket <key>", "JIRA ticket key (e.g. PROJ-123)").option("--jira-ac-field <field>", "Custom field ID for acceptance criteria").option("--jira-ac-source <source>", "Where to look for AC: customField, description, or both (default: customField)").option("--slack-webhook <url>", "Slack webhook URL for notifications").option("--teams-webhook <url>", "Teams webhook URL for notifications").option("--notify-min-risk <level>", "Only notify when risk is at or above this level: low, medium, high, critical").option("--notify-on-ac-fail", "Send notification when JIRA acceptance criteria validation fails").option("--ai-base-url <url>", "AI provider base URL (Azure endpoint, Ollama URL)").option("--ai-api-key <key>", "AI API key (or IRA_AI_API_KEY / OPENAI_API_KEY)").option("--ai-api-version <version>", "Azure OpenAI API version").option("--ai-deployment <name>", "Azure OpenAI deployment name").option("--ai-model-critical <model>", "Stronger AI model for BLOCKER/CRITICAL issues").option("--generate-tests", "Generate test cases from JIRA acceptance criteria").option("--test-framework <framework>", "Test framework: jest, vitest, mocha, playwright, cypress, gherkin, pytest, junit (default: jest)").option("--config <path>", "Path to config file (default: auto-detect .irarc.json / ira.config.json)").option("--no-config-file", "Disable auto-loading config file from repo").option("--rules-url <url>", "Fetch .ira-rules.json from URL (raw HTTP) \u2014 useful when CI has no full checkout (or IRA_RULES_URL)").option("--comment-style <style>", "Comment formatter style: compact (default) or detailed (or IRA_COMMENT_STYLE)").action(async (opts) => {
4083
+ program.command("review").description("Run AI-powered review on a pull request").option("--sonar-url <url>", "SonarQube/SonarCloud base URL (or IRA_SONAR_URL)").option("--sonar-token <token>", "SonarQube API token (or IRA_SONAR_TOKEN)").option("--project-key <key>", "Sonar project key (or IRA_PROJECT_KEY)").option("--pr <id>", "Pull request ID (or IRA_PR)").option("--scm-provider <provider>", "SCM provider: bitbucket or github (or IRA_SCM_PROVIDER)").option("--bitbucket-token <token>", "Bitbucket API token (or IRA_BITBUCKET_TOKEN)").option("--repo <repo>", "Bitbucket workspace/repo-slug (Cloud) or PROJECT/repo-slug (Server) (or IRA_REPO)").option("--bitbucket-url <url>", "Bitbucket base URL (or IRA_BITBUCKET_URL)").option("--bitbucket-type <type>", "Bitbucket type: cloud or server (auto-detects from URL if omitted; or IRA_BITBUCKET_TYPE)").option("--github-token <token>", "GitHub API token (or IRA_GITHUB_TOKEN)").option("--github-repo <repo>", "GitHub owner/repo (or IRA_GITHUB_REPO)").option("--github-url <url>", "GitHub Enterprise URL (or IRA_GITHUB_URL)").option("--ai-provider <provider>", "AI provider").option("--ai-model <model>", "AI model to use").option("--dry-run", "Print comments to stdout instead of posting to SCM").option("--min-severity <level>", "Minimum severity to review (BLOCKER|CRITICAL|MAJOR|MINOR|INFO)").option("--jira-url <url>", "JIRA base URL (or IRA_JIRA_URL)").option("--jira-email <email>", "JIRA email (or IRA_JIRA_EMAIL)").option("--jira-token <token>", "JIRA API token (or IRA_JIRA_TOKEN)").option("--jira-type <type>", "JIRA type: cloud or server (auto-detects from URL if omitted)").option("--jira-ticket <key>", "JIRA ticket key (e.g. PROJ-123)").option("--jira-ac-field <field>", "Custom field ID for acceptance criteria").option("--jira-ac-source <source>", "Where to look for AC: customField, description, or both (default: customField)").option("--no-post-acs-to-jira", "Don't post AI-generated acceptance criteria back to the JIRA ticket when none exist; keep them in the PR summary only (or IRA_POST_ACS_TO_JIRA=false)").option("--slack-webhook <url>", "Slack webhook URL for notifications").option("--teams-webhook <url>", "Teams webhook URL for notifications").option("--notify-min-risk <level>", "Only notify when risk is at or above this level: low, medium, high, critical").option("--notify-on-ac-fail", "Send notification when JIRA acceptance criteria validation fails").option("--ai-base-url <url>", "AI provider base URL (Azure endpoint, Ollama URL)").option("--ai-api-key <key>", "AI API key (or IRA_AI_API_KEY / OPENAI_API_KEY)").option("--ai-api-version <version>", "Azure OpenAI API version").option("--ai-deployment <name>", "Azure OpenAI deployment name").option("--ai-model-critical <model>", "Stronger AI model for BLOCKER/CRITICAL issues").option("--generate-tests", "Generate test cases from JIRA acceptance criteria").option("--test-framework <framework>", "Test framework: jest, vitest, mocha, playwright, cypress, gherkin, pytest, junit (default: jest)").option("--config <path>", "Path to config file (default: auto-detect .irarc.json / ira.config.json)").option("--no-config-file", "Disable auto-loading config file from repo").option("--rules-url <url>", "Fetch .ira-rules.json from URL (raw HTTP) \u2014 useful when CI has no full checkout (or IRA_RULES_URL)").option("--comment-style <style>", "Comment formatter style: compact (default) or detailed (or IRA_COMMENT_STYLE)").action(async (opts) => {
4034
4084
  try {
4035
4085
  console.log(`
4036
4086
  \u{1F50D} IRA \u2014 Scanning PR before your reviewers do
@@ -4065,6 +4115,11 @@ program.command("review").description("Run AI-powered review on a pull request")
4065
4115
  ...opts.jiraTicket && { jiraTicket: opts.jiraTicket.toUpperCase() },
4066
4116
  ...opts.jiraAcField && { jiraAcField: opts.jiraAcField },
4067
4117
  ...opts.jiraAcSource && { jiraAcSource: opts.jiraAcSource },
4118
+ // commander's --no-X pattern → opts.postAcsToJira is `true` by default,
4119
+ // `false` only when the user passed `--no-post-acs-to-jira`. Only push
4120
+ // through the explicit-disable case; otherwise leave undefined so env
4121
+ // var (or the engine's built-in default of "enabled") wins.
4122
+ ...opts.postAcsToJira === false && { postAcsToJira: false },
4068
4123
  ...opts.slackWebhook && { slackWebhook: opts.slackWebhook },
4069
4124
  ...opts.teamsWebhook && { teamsWebhook: opts.teamsWebhook },
4070
4125
  ...opts.notifyMinRisk && { notifyMinRisk: opts.notifyMinRisk },
package/dist/index.cjs CHANGED
@@ -1796,10 +1796,17 @@ var BitbucketServerClient = class {
1796
1796
  });
1797
1797
  }
1798
1798
  async getIssueComments(pullRequestId) {
1799
+ const collect = (node, sink) => {
1800
+ if (!node) return;
1801
+ if (typeof node.text === "string" && node.text.length > 0) sink.push(node.text);
1802
+ if (Array.isArray(node.comments)) {
1803
+ for (const child of node.comments) collect(child, sink);
1804
+ }
1805
+ };
1799
1806
  const bodies = [];
1800
1807
  let start = 0;
1801
1808
  while (true) {
1802
- const url = this.prUrl(pullRequestId, `/comments?start=${start}&limit=100`);
1809
+ const url = this.prUrl(pullRequestId, `/activities?start=${start}&limit=100`);
1803
1810
  const data = await withRetry(async () => {
1804
1811
  const response = await fetchWithTimeout(url, { headers: this.headers });
1805
1812
  if (!response.ok) {
@@ -1811,7 +1818,9 @@ var BitbucketServerClient = class {
1811
1818
  }
1812
1819
  return await response.json();
1813
1820
  });
1814
- for (const c of data.values) bodies.push(c.text);
1821
+ for (const activity of data.values) {
1822
+ if (activity.action === "COMMENTED") collect(activity.comment, bodies);
1823
+ }
1815
1824
  if (data.isLastPage) break;
1816
1825
  start = data.nextPageStart ?? start + 100;
1817
1826
  }
@@ -3411,6 +3420,29 @@ function buildSummary2(result) {
3411
3420
  lines.push("");
3412
3421
  }
3413
3422
  }
3423
+ const acGapExists = result.requirementCompletion && result.requirementCompletion.completionPercentage < 100 || result.acceptanceValidation && !result.acceptanceValidation.overallPass;
3424
+ if (result.comments.length === 0 && !acGapExists) {
3425
+ const fw = result.framework ?? "your stack";
3426
+ const acLine = result.requirementCompletion ? `Acceptance criteria for **${result.requirementCompletion.jiraKey}**: **${result.requirementCompletion.completionPercentage}% covered** (${result.requirementCompletion.metCriteria}/${result.requirementCompletion.totalCriteria}).` : result.acceptanceValidation ? `Acceptance criteria for **${result.acceptanceValidation.jiraKey}**: ${result.acceptanceValidation.overallPass ? "**all met** \u2705" : "**partially met** \u2014 see the JIRA section above"}.` : result.acGeneration && result.acGeneration.criteria.length > 0 ? result.acGeneration.postedToJira ? `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** and posted them as a comment on the JIRA ticket for the Product Owner / requirement author to review and refine.` : `\u{1F4DD} No acceptance criteria found on **${result.acGeneration.jiraKey}** \u2014 IRA generated **${result.acGeneration.totalCriteria} suggested AC${result.acGeneration.totalCriteria === 1 ? "" : "s"}** (see the Suggested Acceptance Criteria section below).` : null;
3427
+ lines.push("## \u2705 All Clear \u2014 No Issues Found");
3428
+ lines.push("");
3429
+ lines.push(`> \u{1F389} **Nice work on PR #${result.pullRequestId}!**`);
3430
+ lines.push(`>`);
3431
+ lines.push(`> IRA scanned every changed file across **${fw}** and didn't surface a single concern.`);
3432
+ if (acLine) {
3433
+ lines.push(`>`);
3434
+ lines.push(`> ${acLine}`);
3435
+ }
3436
+ if (result.risk) {
3437
+ lines.push(`>`);
3438
+ lines.push(`> Risk score: **${result.risk.score}/${result.risk.maxScore}** (${result.risk.level}).`);
3439
+ }
3440
+ lines.push(`>`);
3441
+ lines.push(`> \u2705 **Safe to approve from an automated-review standpoint.**`);
3442
+ lines.push(`>`);
3443
+ lines.push(`> \u{1F465} **Human reviewer approval is still required before merge.** IRA augments your code review process \u2014 it doesn't replace it. Please ensure your team's review and approval requirements have been met before merging.`);
3444
+ lines.push("");
3445
+ }
3414
3446
  lines.push("## Overview");
3415
3447
  lines.push("");
3416
3448
  lines.push(`| Metric | Value |`);
@@ -3658,7 +3690,10 @@ var Notifier = class {
3658
3690
  var import_node_child_process2 = require("child_process");
3659
3691
  function resolveGitRoot() {
3660
3692
  try {
3661
- return (0, import_node_child_process2.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
3693
+ return (0, import_node_child_process2.execSync)("git rev-parse --show-toplevel", {
3694
+ encoding: "utf-8",
3695
+ stdio: ["pipe", "pipe", "pipe"]
3696
+ }).trim();
3662
3697
  } catch {
3663
3698
  return process.cwd();
3664
3699
  }
@@ -4217,7 +4252,11 @@ Cause: ${warnings[warnings.length - 1]}` : "";
4217
4252
  let commitMessages = [];
4218
4253
  try {
4219
4254
  const { execSync: execSync4 } = await import("child_process");
4220
- const gitLog = execSync4("git log --oneline -20 --no-decorate", { cwd: repoPath, encoding: "utf-8" });
4255
+ const gitLog = execSync4("git log --oneline -20 --no-decorate", {
4256
+ cwd: repoPath,
4257
+ encoding: "utf-8",
4258
+ stdio: ["pipe", "pipe", "pipe"]
4259
+ });
4221
4260
  commitMessages = gitLog.trim().split("\n").filter(Boolean);
4222
4261
  } catch {
4223
4262
  }
@@ -4236,15 +4275,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
4236
4275
  if (acGeneration.parseWarning) {
4237
4276
  warnings.push(acGeneration.parseWarning);
4238
4277
  }
4239
- if (acGeneration.criteria.length > 0 && !this.config.dryRun) {
4278
+ const postingDisabled = this.config.postAcsToJira === false;
4279
+ if (acGeneration.criteria.length > 0 && !this.config.dryRun && !postingDisabled) {
4240
4280
  try {
4241
4281
  const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
4242
4282
  await jiraClient.addComment(this.config.jiraTicket, commentBody);
4243
4283
  console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
4284
+ acGeneration.postedToJira = true;
4244
4285
  } catch (error) {
4245
4286
  const msg = error instanceof Error ? error.message : "Unknown error";
4246
4287
  warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
4288
+ acGeneration.postedToJira = false;
4247
4289
  }
4290
+ } else if (postingDisabled && acGeneration.criteria.length > 0) {
4291
+ console.log(` Skipped posting ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket} (--no-post-acs-to-jira). See PR summary.`);
4292
+ acGeneration.postedToJira = false;
4293
+ } else {
4294
+ acGeneration.postedToJira = false;
4248
4295
  }
4249
4296
  }
4250
4297
  }
@@ -4458,6 +4505,8 @@ function resolveConfigFromEnv(overrides = {}) {
4458
4505
  }
4459
4506
  const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
4460
4507
  const jiraTicket = overrides.jiraTicket ?? optionalEnv("IRA_JIRA_TICKET");
4508
+ const postAcsToJiraEnv = optionalEnv("IRA_POST_ACS_TO_JIRA");
4509
+ const postAcsToJira = overrides.postAcsToJira === false ? false : postAcsToJiraEnv && /^(false|0|no|off)$/i.test(postAcsToJiraEnv) ? false : void 0;
4461
4510
  const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
4462
4511
  if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
4463
4512
  throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
@@ -4491,6 +4540,7 @@ function resolveConfigFromEnv(overrides = {}) {
4491
4540
  ...overrides.generateTests && { generateTests: overrides.generateTests },
4492
4541
  ...overrides.testFramework && { testFramework: overrides.testFramework },
4493
4542
  ...jiraAcSource && { jiraAcSource },
4543
+ ...postAcsToJira === false && { postAcsToJira: false },
4494
4544
  ...commentStyle && { commentStyle },
4495
4545
  ...rulesUrl && { rulesUrl }
4496
4546
  };