ira-review 3.1.1 → 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 |`);
@@ -3429,15 +3461,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
3429
3461
  if (acGeneration.parseWarning) {
3430
3462
  warnings.push(acGeneration.parseWarning);
3431
3463
  }
3432
- 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) {
3433
3466
  try {
3434
3467
  const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
3435
3468
  await jiraClient.addComment(this.config.jiraTicket, commentBody);
3436
3469
  console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
3470
+ acGeneration.postedToJira = true;
3437
3471
  } catch (error) {
3438
3472
  const msg = error instanceof Error ? error.message : "Unknown error";
3439
3473
  warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
3474
+ acGeneration.postedToJira = false;
3440
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;
3441
3481
  }
3442
3482
  }
3443
3483
  }
@@ -3651,6 +3691,8 @@ function resolveConfigFromEnv(overrides = {}) {
3651
3691
  }
3652
3692
  const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
3653
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;
3654
3696
  const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
3655
3697
  if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
3656
3698
  throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
@@ -3684,6 +3726,7 @@ function resolveConfigFromEnv(overrides = {}) {
3684
3726
  ...overrides.generateTests && { generateTests: overrides.generateTests },
3685
3727
  ...overrides.testFramework && { testFramework: overrides.testFramework },
3686
3728
  ...jiraAcSource && { jiraAcSource },
3729
+ ...postAcsToJira === false && { postAcsToJira: false },
3687
3730
  ...commentStyle && { commentStyle },
3688
3731
  ...rulesUrl && { rulesUrl }
3689
3732
  };
@@ -4037,7 +4080,7 @@ function logEnvironmentInfo(config) {
4037
4080
  }
4038
4081
  var program = new Command();
4039
4082
  program.name("ira-review").description("AI-powered PR review tool with SonarQube + GitHub/Bitbucket integration").version("3.1.0");
4040
- 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) => {
4041
4084
  try {
4042
4085
  console.log(`
4043
4086
  \u{1F50D} IRA \u2014 Scanning PR before your reviewers do
@@ -4072,6 +4115,11 @@ program.command("review").description("Run AI-powered review on a pull request")
4072
4115
  ...opts.jiraTicket && { jiraTicket: opts.jiraTicket.toUpperCase() },
4073
4116
  ...opts.jiraAcField && { jiraAcField: opts.jiraAcField },
4074
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 },
4075
4123
  ...opts.slackWebhook && { slackWebhook: opts.slackWebhook },
4076
4124
  ...opts.teamsWebhook && { teamsWebhook: opts.teamsWebhook },
4077
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 |`);
@@ -4243,15 +4275,23 @@ Cause: ${warnings[warnings.length - 1]}` : "";
4243
4275
  if (acGeneration.parseWarning) {
4244
4276
  warnings.push(acGeneration.parseWarning);
4245
4277
  }
4246
- 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) {
4247
4280
  try {
4248
4281
  const commentBody = formatACsForJiraComment(acGeneration, pullRequestId);
4249
4282
  await jiraClient.addComment(this.config.jiraTicket, commentBody);
4250
4283
  console.log(` Posted ${acGeneration.totalCriteria} suggested ACs to ${this.config.jiraTicket}`);
4284
+ acGeneration.postedToJira = true;
4251
4285
  } catch (error) {
4252
4286
  const msg = error instanceof Error ? error.message : "Unknown error";
4253
4287
  warnings.push(`Failed to post AC suggestions to JIRA: ${msg}`);
4288
+ acGeneration.postedToJira = false;
4254
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;
4255
4295
  }
4256
4296
  }
4257
4297
  }
@@ -4465,6 +4505,8 @@ function resolveConfigFromEnv(overrides = {}) {
4465
4505
  }
4466
4506
  const jiraAcSource = overrides.jiraAcSource ?? optionalEnv("IRA_JIRA_AC_SOURCE");
4467
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;
4468
4510
  const commentStyleRaw = overrides.commentStyle ?? optionalEnv("IRA_COMMENT_STYLE");
4469
4511
  if (commentStyleRaw && commentStyleRaw !== "compact" && commentStyleRaw !== "detailed") {
4470
4512
  throw new Error(`Invalid comment-style: "${commentStyleRaw}". Must be "compact" or "detailed".`);
@@ -4498,6 +4540,7 @@ function resolveConfigFromEnv(overrides = {}) {
4498
4540
  ...overrides.generateTests && { generateTests: overrides.generateTests },
4499
4541
  ...overrides.testFramework && { testFramework: overrides.testFramework },
4500
4542
  ...jiraAcSource && { jiraAcSource },
4543
+ ...postAcsToJira === false && { postAcsToJira: false },
4501
4544
  ...commentStyle && { commentStyle },
4502
4545
  ...rulesUrl && { rulesUrl }
4503
4546
  };