Skip to content

Issue-intent handlers use wrong payload shape (close_issue + assign_to_agent): metadata must be nested, not top-level #46833

Description

@alondahari

Summary

Two safe-output handlers send issue-intent metadata (rationale, confidence, suggest) with the wrong payload shape, so the metadata is silently dropped by the GitHub REST API (the mutation succeeds with HTTP 200, no error, the error-only fallback never fires, and no intent is persisted):

  1. close_issue — spreads metadata as top-level siblings of a scalar state: "closed"; the API only reads close rationale when nested inside a state object.
  2. assign_to_agent / assign_agent_helpers — sends assignees as an array of login strings and spreads metadata at the top level; the assignees endpoint only reads rationale from per-assignee objects.

The GraphQL handlers (set_issue_type, set_issue_field) and add_labels nest metadata correctly and are unaffected.

1. close_issue.cjs

closeIssue (~L161-186):

const baseParams = {
  owner, repo, issue_number: issueNumber,
  state: "closed",                                   // scalar
  state_reason: (stateReason || "COMPLETED").toLowerCase(),
};

if (useIssueIntent && hasIntentMetadata) {
  const { data: issue } = await github.request("PATCH /repos/{owner}/{repo}/issues/{issue_number}", {
    ...baseParams,
    ...intentMetadata,                               // ← rationale/confidence/suggest at TOP LEVEL
  });
  ...
}

intentMetadata ({ rationale?, confidence?, suggest? }, from normalizeIssueIntentMetadata) lands next to the scalar state, where the API ignores it. rationale is only honored when nested inside a state object.

Correct payload

{
  "state": {
    "value": "closed",
    "rationale": "Short reason for closing.",
    "confidence": "HIGH",   // optional
    "suggest": false        // optional
  },
  "state_reason": "not_planned"
}

Suggested fix

const { data: issue } = await github.request("PATCH /repos/{owner}/{repo}/issues/{issue_number}", {
  owner, repo, issue_number: issueNumber,
  state: { value: "closed", ...intentMetadata },   // nest under state
  state_reason: baseParams.state_reason,
});

Keep the existing error-only fallback to github.rest.issues.update(baseParams) (scalar state) for older targets that don't accept the object form.

2. assign_agent_helpers.cjs

assignToAgent (~L399-408):

const assignParams = {
  owner: targetOwner,
  repo: targetRepo,
  issue_number: issueNumber,
  assignees: [agentLogin],                          // ← array of login STRINGS
};
if (useIssueIntent && intentMetadata && Object.keys(intentMetadata).length > 0) {
  Object.assign(assignParams, intentMetadata);      // ← rationale/etc. at TOP LEVEL
}
await githubClient.request("POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", assignParams);

The POST /assignees endpoint reads intent metadata per-assignee, from an object with a login key. A plain string entry yields no metadata, and the top-level rationale is ignored.

Correct payload

{
  "assignees": [
    { "login": "<agent-login>", "rationale": "Short reason.", "confidence": "HIGH", "suggest": false }
  ]
}

Suggested fix

const assignee = useIssueIntent && intentMetadata && Object.keys(intentMetadata).length > 0
  ? { login: agentLogin, ...intentMetadata }
  : agentLogin;
const assignParams = { owner: targetOwner, repo: targetRepo, issue_number: issueNumber, assignees: [assignee] };
await githubClient.request("POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", assignParams);

Note the object form keys the assignee by login (mirroring add_labels, which keys by name).

Handlers that are correct (for reference)

  • set_issue_type.cjs — GraphQL updateIssue, nests under issueType: { issueTypeId, ...intentMetadata }.
  • set_issue_field.cjs — GraphQL setIssueFieldValue, nests metadata into each per-field issueFields[] entry.
  • add_labels.cjs — sends per-item { name, rationale, ... } objects.

The common contract: intent metadata must be nested inside the per-field / per-item object it annotates, never a top-level sibling.

Metadata

Metadata

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions