# Dryrun Internal Jira Comment Design

Date: 2026-07-02

## Current Design

`POST /v1/tickets/analyze` is a dry-run API. It fetches or accepts a Jira issue,
creates a replay workspace, asks Codex to generate `commentDraftMarkdown`, and
returns the draft plus `commentBodyAdf`.

Current hard boundary:

- The API never writes Jira.
- The generated customer-facing draft remains behind human review.
- Jira credentials are read-only in practice, although the token may have wider
  permissions.
- The run workspace is the only persisted artifact.

## Goal

Add an explicit dry-run option to also write the generated answer back to Jira as
an internal comment for reviewer visibility.

This must remain opt-in. A normal dry run must not write Jira.

## Jira API Choice

There are two relevant Jira Cloud write paths:

1. Jira platform comment API:
   `POST /rest/api/3/issue/{issueIdOrKey}/comment`
   - Body is Atlassian Document Format (ADF).
   - It supports comment `properties`, but internal/customer visibility for
     Jira Service Management is not the clearest primary contract.

2. Jira Service Management request comment API:
   `POST /rest/servicedeskapi/request/{issueIdOrKey}/comment`
   - Body is a string.
   - `public: false` creates a private/internal comment on the customer request.
   - This is the preferred path for JSM tickets when we need "internal note"
     semantics.

Target: use the JSM request comment API for internal comments. Keep the platform
ADF comment API out of this change unless we later need non-JSM issue support.

Follow-up formatting decision: the JSM request comment API accepts a string body,
not ADF. Before creating the internal comment, convert the review-source
Markdown to Jira wiki-style markup so fenced code blocks, numbered sections,
bullets, and inline code render in Jira instead of being shown as raw Markdown.
For an existing auto-oncall comment with the same idempotency key, update the
existing issue comment body via Jira platform comment update API rather than
adding a duplicate comment.

## Target Request Contract

Extend `AnalyzeTicketRequest`:

```ts
type AnalyzeTicketRequest = {
  issueKey?: string;
  ticket?: JiraIssueSnapshot;
  dryRun?: {
    jiraInternalComment?: {
      mode?: "none" | "preview" | "write";
      idempotencyKey?: string;
    };
  };
};
```

Semantics:

- Omitted / `none`: current behavior, no Jira write and no internal comment body
  is built for the response.
- `preview`: build and return the Markdown review source that would be
  transformed for Jira, but do not call Jira.
- `write`: after successful generation and safety checks, create one internal
  Jira/JSM comment.

The first implementation can also accept a simpler top-level flag:

```json
{
  "issueKey": "APID-29123",
  "writeJiraInternalComment": true
}
```

But the nested shape is preferred because it leaves room for idempotency and
future modes without adding more booleans.

## Internal Comment Content

Do not write the customer-facing draft alone. The internal comment should be a
review artifact:

```text
[auto-oncall dryrun]
Decision: internal_only
Run: <workspaceUrl or runId>
Replay: <mode>, visible comments <indexes>

Draft Jira reply:
<commentDraftMarkdown>

Notes:
- This is generated by auto-oncall dryrun.
- Not posted to customer.
```

For `human_required`, write only:

```text
[auto-oncall dryrun]
Decision: human_required
Reason: <decisionReason or blockedReason>
Run: <workspaceUrl or runId>
```

Do not include secrets, raw full Jira payloads, or oversized traces.

## Safety Gates

`write` is allowed only when all are true:

- Request uses `issueKey`; do not write when caller provides an arbitrary
  `ticket` snapshot.
- `AUTO_ONCALL_ENABLE_JIRA_INTERNAL_COMMENT_WRITE=true`.
- Jira credentials are present.
- The generated output parsed successfully and `commentDraftMarkdown` is not
  empty.
- The internal comment body is below a configured size limit, default 30 KiB.
- The issue belongs to the allowlisted projects initially used by this service:
  `ST`, `TCID`, `APID`, `NAID`, `EMID`.

If any gate fails, return `status=ok` for analysis but include a
`jiraInternalComment.status="skipped"` object with the reason. Do not fail the
whole dry run unless Jira write was explicitly requested and the caller asks for
strict behavior in a future option.

## Idempotency

Avoid duplicate comments when a dry run is retried.

Initial design:

- Generate `idempotencyKey` as:
  `<issueKey>:<promptVersion>:<schemaVersion>:<replayTarget hash>`.
- If caller provides `dryRun.jiraInternalComment.idempotencyKey`, use that.
- Before writing, search recent issue/request comments for:
  `auto-oncall-run-id: <runId>` or `auto-oncall-idempotency-key: <key>`.
- If found, overwrite the existing auto-oncall comment and return the comment
  id/link.

The comment should include both markers at the bottom:

```text
auto-oncall-run-id: <runId>
auto-oncall-idempotency-key: <key>
```

## Response Contract

Extend `AnalyzeTicketResponse`:

```ts
type JiraInternalCommentResult = {
  requested: boolean;
  status: "not_requested" | "preview" | "written" | "updated" | "skipped" | "failed";
  endpoint: "servicedeskapi" | "jira-platform" | null;
  commentId: string | null;
  commentUrl: string | null;
  bodyMarkdown: string | null;
  skippedReason: string | null;
  error: string | null;
};
```

Add:

```ts
jiraInternalComment: JiraInternalCommentResult;
```

## Implementation Steps

1. Add request/response types for `dryRun.jiraInternalComment`.
2. Add `src/jira-comments.ts`:
   - `buildInternalDryRunComment(...)`
   - `createJsmInternalComment(issueKey, body)`
   - optional `findExistingAutoOncallComment(...)`
3. Wire `app.ts` after runner output:
   - build preview always when requested,
   - write only after safety gates pass.
4. Add config helpers:
   - `AUTO_ONCALL_ENABLE_JIRA_INTERNAL_COMMENT_WRITE`
   - `AUTO_ONCALL_JIRA_INTERNAL_COMMENT_MAX_BYTES`
5. Add unit tests with mocked `fetch`:
   - default no write,
   - preview no write,
   - write gate disabled -> skipped,
   - write success sends `public:false`,
   - `ticket` snapshot + write -> skipped,
   - duplicate idempotency -> updated in place.
6. Update README with examples.

## Deferred

Do not implement per-agent environment isolation in this change. The current
runtime may still have Jira credentials available to the process, so the service
keeps the prompt/system contract that Codex must not write Jira directly and
performs all actual writes in backend code. Move credential isolation into the
future unified agent framework.

## Verification Checklist

- `npm run typecheck`
- `npm test`
- Local dry run with `preview` returns internal body and performs no Jira calls.
- Staging dry run with write disabled returns `skipped`.
- Staging dry run with write enabled on one test issue creates an internal JSM
  comment and does not create a customer-visible comment.

## Non-Goals

- Do not post customer-visible Jira comments.
- Do not transition issues or close tickets.
- Do not support attachments in internal comments.
- Do not implement generic Jira platform internal visibility until we confirm a
  non-JSM use case.
