@jam-mcp/server 1.3.0 → 1.3.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 colosair
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 colosair
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,86 +1,90 @@
1
- # @jam-mcp/server
2
-
3
- JAM (Jira Agent MCP) itself: the MCP server, the setup core, and the `jam` CLI.
4
-
5
- JAM is an agent-facing Jira **read** layer. It takes over the decisions an agent
6
- should not be making — which fields to request, when to paginate, when to read
7
- the comment thread, what to do when a result is too big — so everyday reads stay
8
- cheap and important judgements still get full context.
9
-
10
- ## The tools
11
-
12
- The external contract is five tools. Adding or renaming one is a breaking
13
- change.
14
-
15
- | Tool | Use it for |
16
- |---|---|
17
- | `jira_search` | listing, discovery, "what's open", picking candidates |
18
- | `jira_context` | readiness, blockers, dependencies, priority |
19
- | `jira_full` | agreement, contract, approval, closure |
20
- | `jira_write_plan` | work out how to change an issue — changes nothing |
21
- | `jira_write_apply` | apply a plan, then confirm it by reading the issue back |
22
-
23
- A `jira_search` result is never complete issue context — nothing about
24
- agreement, approval, or done-ness follows from it.
25
-
26
- Writing is deliberately two calls. `jira_write_apply` takes a `planId` and no
27
- payload, so a change cannot be made that JAM has not first read the issue for,
28
- checked against the configured project, and described. Before writing it
29
- re-reads the issue and refuses if it moved; after writing it reads again and
30
- refuses to report success unless the intended result is actually there. An
31
- ambiguous failure is reported as uncertain rather than retried — retrying a
32
- write that may have landed is how one comment becomes two.
33
-
34
- Three operations: `comment.add` (plain text, converted to ADF here),
35
- `field.update` (summary, priority, labels, components), and `status.transition`
36
- (matched against the transitions Jira currently offers, never a guessed id).
37
-
38
- ## Evidence boundary
39
-
40
- Every result carries a `meta` block, and nothing is ever truncated silently.
41
-
42
- `meta.complete` means **JAM finished the Jira retrieval with no known loss**. It
43
- is not a statement about the project: not readiness, not "unblocked", not "the
44
- whole story". What JAM did not look at is named in the same block —
45
- `evidenceScope` and `limitations` call out the repository, external sources, and
46
- dependencies that live outside Jira. `provenance` and `source` say where the
47
- records came from.
48
-
49
- So an empty comment thread on a complete read is a complete read *of Jira*, not
50
- proof that nothing was agreed. If the issue points at an external canonical
51
- source, that source is what settles the question.
52
-
53
- ## Running it
54
-
55
- Don't install this package to use JAM. Your coding agent launches
56
- [`@jam-mcp/launcher`](https://www.npmjs.com/package/@jam-mcp/launcher), which
57
- reads your `~/.jam/config.yaml`, decides which JAM build this machine should
58
- run, and dispatches here. Naming the server directly instead pins one machine
59
- to one build and bypasses that choice and it is what keeps a committed
60
- `.mcp.json` free of machine-specific paths.
61
-
62
- The same binary carries the CLI, and every command below is reachable through
63
- the launcher:
64
-
65
- ```text
66
- serve Run the MCP server over stdio
67
- doctor Diagnose config, credentials and Jira connectivity
68
- setup Wire up JAM and verify (personal by default; --shared for the team)
69
- auth login Store Jira credentials in this user's OS secret store
70
- runtime Show or change which JAM build this machine runs
71
- ```
72
-
73
- Written out, that is `npx --yes @jam-mcp/launcher@1.3.0 doctor`, or just `jam
74
- doctor` if you took the launcher's optional global install. Starting from
75
- nothing — no install, no runtime chosen yet — use
76
- `npx --yes @jam-mcp/bootstrap@1.3.0 init` instead.
77
-
78
- Credentials come from the process environment or this user's OS secret store
79
- never from a repository file and never appear in logs, telemetry, or tool
80
- results. `stdout` is reserved for the MCP protocol and for JSON output;
81
- diagnostics go to `stderr`.
82
-
83
- ## More
84
-
85
- - [Repository README](https://github.com/colosair/jam#readme)
86
- - [JAM design of record](https://github.com/colosair/jam/blob/main/docs/architecture/jira-agent-mcp-design.md)
1
+ # @jam-mcp/server
2
+
3
+ JAM (Jira Agent MCP) itself: the MCP server, the setup core, and the `jam` CLI.
4
+
5
+ JAM is an agent-facing Jira **read** layer. It takes over the decisions an agent
6
+ should not be making — which fields to request, when to paginate, when to read
7
+ the comment thread, what to do when a result is too big — so everyday reads stay
8
+ cheap and important judgements still get full context.
9
+
10
+ ## The tools
11
+
12
+ The external contract is five tools. Adding or renaming one is a breaking
13
+ change.
14
+
15
+ | Tool | Use it for |
16
+ |---|---|
17
+ | `jira_search` | listing, discovery, "what's open", picking candidates |
18
+ | `jira_context` | readiness, blockers, dependencies, priority |
19
+ | `jira_full` | agreement, contract, approval, closure |
20
+ | `jira_write_plan` | work out how to change an issue — changes nothing |
21
+ | `jira_write_apply` | apply a plan, then confirm it by reading the issue back |
22
+
23
+ A `jira_search` result is never complete issue context — nothing about
24
+ agreement, approval, or done-ness follows from it.
25
+
26
+ Writing is deliberately two calls. `jira_write_apply` takes a `planId` and no
27
+ payload, so a change cannot be made that JAM has not first read the issue for,
28
+ checked against the configured project, and described. Before writing it
29
+ re-reads the issue and refuses if it moved; after writing it reads again and
30
+ refuses to report success unless the intended result is actually there. An
31
+ ambiguous failure is reported as uncertain rather than retried — retrying a
32
+ write that may have landed is how one comment becomes two.
33
+
34
+ Five operations: `comment.add` (plain text, converted to ADF here),
35
+ `field.update` (summary, priority, labels, components), `status.transition`
36
+ (matched against the transitions Jira currently offers, never a guessed id),
37
+ `assignee.update` (resolved to an account by exact display name or accountId,
38
+ never a substring match), and `issue.create` (planned against the project's own
39
+ create schema, so an unavailable issue type is a refusal here rather than a
40
+ Jira 400 later).
41
+
42
+ ## Evidence boundary
43
+
44
+ Every result carries a `meta` block, and nothing is ever truncated silently.
45
+
46
+ `meta.complete` means **JAM finished the Jira retrieval with no known loss**. It
47
+ is not a statement about the project: not readiness, not "unblocked", not "the
48
+ whole story". What JAM did not look at is named in the same block —
49
+ `evidenceScope` and `limitations` call out the repository, external sources, and
50
+ dependencies that live outside Jira. `provenance` and `source` say where the
51
+ records came from.
52
+
53
+ So an empty comment thread on a complete read is a complete read *of Jira*, not
54
+ proof that nothing was agreed. If the issue points at an external canonical
55
+ source, that source is what settles the question.
56
+
57
+ ## Running it
58
+
59
+ Don't install this package to use JAM. Your coding agent launches
60
+ [`@jam-mcp/launcher`](https://www.npmjs.com/package/@jam-mcp/launcher), which
61
+ reads your `~/.jam/config.yaml`, decides which JAM build this machine should
62
+ run, and dispatches here. Naming the server directly instead pins one machine
63
+ to one build and bypasses that choice — and it is what keeps a committed
64
+ `.mcp.json` free of machine-specific paths.
65
+
66
+ The same binary carries the CLI, and every command below is reachable through
67
+ the launcher:
68
+
69
+ ```text
70
+ serve Run the MCP server over stdio
71
+ doctor Diagnose config, credentials and Jira connectivity
72
+ setup Wire up JAM and verify (personal by default; --shared for the team)
73
+ auth login Store Jira credentials in this user's OS secret store
74
+ runtime Show or change which JAM build this machine runs
75
+ ```
76
+
77
+ Written out, that is `npx --yes @jam-mcp/launcher@1.3.1 doctor`, or just `jam
78
+ doctor` if you took the launcher's optional global install. Starting from
79
+ nothingno install, no runtime chosen yet use
80
+ `npx --yes @jam-mcp/bootstrap@1.3.1 init` instead.
81
+
82
+ Credentials come from the process environment or this user's OS secret store —
83
+ never from a repository file — and never appear in logs, telemetry, or tool
84
+ results. `stdout` is reserved for the MCP protocol and for JSON output;
85
+ diagnostics go to `stderr`.
86
+
87
+ ## More
88
+
89
+ - [Repository README](https://github.com/colosair/jam#readme)
90
+ - [JAM design of record](https://github.com/colosair/jam/blob/main/docs/architecture/jira-agent-mcp-design.md)
@@ -0,0 +1,25 @@
1
+ import type { EditFieldMetadata } from "../../domain/write.js";
2
+ import type { CredentialPort } from "../../ports/credentials.port.js";
3
+ import type { JiraEditMetadataPort } from "../../ports/jira-edit-metadata.port.js";
4
+ /**
5
+ * Jira Cloud REST v3 edit metadata for one issue.
6
+ *
7
+ * `GET /rest/api/3/issue/{key}/editmeta`, `retry: false`. Its answer decides
8
+ * whether a mutation may proceed and what shape it takes, so a
9
+ * retried-and-stale answer is worse than a failure - the same argument that
10
+ * keeps `getTransitions`, the create metadata calls and the assignability
11
+ * check on the non-retrying side.
12
+ *
13
+ * Jira keys the response by field id, and describes each field with a `schema`
14
+ * and a list of `operations`. Both travel, because both are what the decision
15
+ * is made in; the rest of the document does not.
16
+ *
17
+ * Anything JAM cannot read is dropped rather than half-understood. A field
18
+ * that survives here with the wrong shape would be a field JAM claims to
19
+ * understand well enough to write.
20
+ */
21
+ export declare class JiraCloudEditMetadataAdapter implements JiraEditMetadataPort {
22
+ private readonly client;
23
+ constructor(credentials: CredentialPort, fetchImpl?: typeof fetch);
24
+ getEditableFields(issueKey: string): Promise<EditFieldMetadata[]>;
25
+ }
@@ -0,0 +1,84 @@
1
+ import { JiraClient } from "./jira-client.js";
2
+ /**
3
+ * Jira Cloud REST v3 edit metadata for one issue.
4
+ *
5
+ * `GET /rest/api/3/issue/{key}/editmeta`, `retry: false`. Its answer decides
6
+ * whether a mutation may proceed and what shape it takes, so a
7
+ * retried-and-stale answer is worse than a failure - the same argument that
8
+ * keeps `getTransitions`, the create metadata calls and the assignability
9
+ * check on the non-retrying side.
10
+ *
11
+ * Jira keys the response by field id, and describes each field with a `schema`
12
+ * and a list of `operations`. Both travel, because both are what the decision
13
+ * is made in; the rest of the document does not.
14
+ *
15
+ * Anything JAM cannot read is dropped rather than half-understood. A field
16
+ * that survives here with the wrong shape would be a field JAM claims to
17
+ * understand well enough to write.
18
+ */
19
+ export class JiraCloudEditMetadataAdapter {
20
+ client;
21
+ constructor(credentials, fetchImpl) {
22
+ this.client = fetchImpl ? new JiraClient(credentials, fetchImpl) : new JiraClient(credentials);
23
+ }
24
+ async getEditableFields(issueKey) {
25
+ const { data } = await this.client.request({
26
+ path: `rest/api/3/issue/${encodeURIComponent(issueKey)}/editmeta`,
27
+ retry: false,
28
+ });
29
+ const fields = data?.fields;
30
+ if (!fields || typeof fields !== "object")
31
+ return [];
32
+ return Object.entries(fields)
33
+ .map(([id, raw]) => toEditField(id, raw))
34
+ .filter((f) => f !== undefined);
35
+ }
36
+ }
37
+ function toEditField(id, raw) {
38
+ if (!raw || typeof raw !== "object")
39
+ return undefined;
40
+ // A field with no schema type is a field JAM cannot classify, and an
41
+ // unclassifiable field is one it must not decide it can write.
42
+ const type = typeof raw.schema?.type === "string" ? raw.schema.type : undefined;
43
+ if (!type)
44
+ return undefined;
45
+ const allowed = toOptions(raw.allowedValues);
46
+ return {
47
+ id,
48
+ name: typeof raw.name === "string" ? raw.name : id,
49
+ required: raw.required === true,
50
+ operations: Array.isArray(raw.operations)
51
+ ? raw.operations.filter((op) => typeof op === "string")
52
+ : [],
53
+ schema: {
54
+ type,
55
+ ...(typeof raw.schema?.items === "string" ? { items: raw.schema.items } : {}),
56
+ ...(typeof raw.schema?.custom === "string" ? { custom: raw.schema.custom } : {}),
57
+ ...(typeof raw.schema?.customId === "number" ? { customId: raw.schema.customId } : {}),
58
+ },
59
+ ...(allowed ? { allowedValues: allowed } : {}),
60
+ };
61
+ }
62
+ /**
63
+ * The options Jira offers, when it constrains the field at all.
64
+ *
65
+ * Absent and empty mean different things and stay apart: absent is "Jira did
66
+ * not constrain this", empty is "Jira constrains it and offers nothing". The
67
+ * first permits a free value, the second permits none.
68
+ *
69
+ * Jira labels an option `value` on a select and `name` on some other pickers.
70
+ * Both are read; an option with neither an id nor a label is dropped, because
71
+ * it can be neither chosen nor recognised afterwards.
72
+ */
73
+ function toOptions(raw) {
74
+ if (!Array.isArray(raw))
75
+ return undefined;
76
+ return raw
77
+ .map((entry) => {
78
+ const o = entry;
79
+ const id = typeof o?.id === "string" ? o.id : typeof o?.id === "number" ? String(o.id) : undefined;
80
+ const label = typeof o?.value === "string" ? o.value : typeof o?.name === "string" ? o.name : undefined;
81
+ return id && label ? { id, label } : undefined;
82
+ })
83
+ .filter((o) => o !== undefined);
84
+ }
@@ -45,11 +45,21 @@ export class JiraCloudReadAdapter {
45
45
  // Read straight off the raw payload rather than through the mapper: the
46
46
  // mapper's job is the shape the read tools see, and this identity is only
47
47
  // for the write plane. Raw DTOs still stop here.
48
- const assignee = data.fields?.assignee;
48
+ const raw = data.fields;
49
+ const assignee = raw?.["assignee"];
49
50
  const accountId = typeof assignee?.accountId === "string" ? assignee.accountId : undefined;
51
+ // Only the ids that were asked for, and only when some were: a caller that
52
+ // did not request a custom field gets no entry rather than an empty object
53
+ // it has to tell apart from a field that is genuinely unset.
54
+ const customFieldValues = {};
55
+ for (const field of req.fields) {
56
+ if (field.startsWith("customfield_"))
57
+ customFieldValues[field] = raw?.[field] ?? null;
58
+ }
50
59
  return {
51
60
  issue: mapIssueWithMeta(data, this.config).issue,
52
61
  ...(accountId ? { assigneeAccountId: accountId } : {}),
62
+ ...(Object.keys(customFieldValues).length > 0 ? { customFieldValues } : {}),
53
63
  responseBytes: bytes,
54
64
  };
55
65
  }
@@ -1,9 +1,10 @@
1
1
  import { JamError, toJamError } from "../domain/errors.js";
2
2
  import { readModeAfterWrite } from "../policy/consistency-policy.js";
3
3
  import { assertAssignable } from "../policy/assignee-policy.js";
4
+ import { assertCustomFieldUnchanged } from "../policy/custom-field-policy.js";
4
5
  import { assertUnchanged } from "../policy/write-policy.js";
5
6
  import { applyCreateIssue } from "./apply-create-issue.js";
6
- import { readIssue } from "./plan-write.js";
7
+ import { currentCustomFieldView, readIssue } from "./plan-write.js";
7
8
  /**
8
9
  * Execute a plan JAM made, then go and look at what happened.
9
10
  *
@@ -62,10 +63,18 @@ export async function applyWritePlan(deps, request) {
62
63
  * own state, which `assertUnchanged` already compared.
63
64
  */
64
65
  async function revalidate(deps, plan) {
65
- if (plan.mutation.kind !== "assignee")
66
+ if (plan.mutation.kind === "assignee") {
67
+ const target = plan.intendedAfter["assignee"];
68
+ assertAssignable(plan.issueKey, target, await deps.jiraAssignees.isAssignable(plan.issueKey, plan.mutation.accountId));
66
69
  return;
67
- const target = plan.intendedAfter["assignee"];
68
- assertAssignable(plan.issueKey, target, await deps.jiraAssignees.isAssignable(plan.issueKey, plan.mutation.accountId));
70
+ }
71
+ if (plan.mutation.kind === "custom-field" && plan.customFieldRequirements) {
72
+ // A field can be taken off a screen, lose its `set` operation, change type
73
+ // or have an option renamed without the issue's own revision moving, so
74
+ // `assertUnchanged` cannot see any of it. These are the premises the plan
75
+ // actually rested on, re-derived.
76
+ assertCustomFieldUnchanged(plan.issueKey, plan.customFieldRequirements, await deps.jiraEditMetadata.getEditableFields(plan.issueKey));
77
+ }
69
78
  }
70
79
  /**
71
80
  * Send the mutation, once.
@@ -92,6 +101,14 @@ async function mutate(deps, plan) {
92
101
  case "assignee":
93
102
  await deps.jiraWrite.assignIssue(plan.issueKey, plan.mutation.accountId);
94
103
  return {};
104
+ case "custom-field":
105
+ // The ordinary issue edit endpoint. A custom field is a field; what
106
+ // made it need its own operation was deciding whether it may be
107
+ // written and in what shape, and that is already settled here.
108
+ await deps.jiraWrite.updateIssue(plan.issueKey, {
109
+ [plan.mutation.fieldId]: plan.mutation.value,
110
+ });
111
+ return {};
95
112
  case "create":
96
113
  // Unreachable: a create plan is routed to applyCreateIssue above. The
97
114
  // case exists so adding a mutation kind is a compile error here rather
@@ -124,7 +141,7 @@ function isAmbiguous(err) {
124
141
  * ours.
125
142
  */
126
143
  async function verify(deps, plan) {
127
- const snapshot = await readIssue(deps, plan.issueKey);
144
+ const snapshot = await readIssue(deps, plan.issueKey, plan.mutation.kind === "custom-field" ? [plan.mutation.fieldId] : []);
128
145
  const issue = snapshot.issue;
129
146
  if (plan.mutation.kind === "assignee") {
130
147
  // On the accountId, never on the display name. Two people can share a
@@ -156,6 +173,15 @@ async function verify(deps, plan) {
156
173
  }
157
174
  return { comments: comments.length, commentAdded: wanted };
158
175
  }
176
+ if (plan.mutation.kind === "custom-field" && plan.customFieldRequirements) {
177
+ const requirements = plan.customFieldRequirements;
178
+ const expected = plan.intendedAfter["customField"];
179
+ const observedValue = currentCustomFieldView({ id: requirements.fieldId, name: requirements.fieldName }, requirements.kind, snapshot.customFieldValues?.[requirements.fieldId]);
180
+ if (!sameCustomFieldValue(requirements.kind, expected.value, observedValue.value)) {
181
+ throw verificationFailed(plan, { customField: expected }, { customField: observedValue });
182
+ }
183
+ return { customField: observedValue };
184
+ }
159
185
  const observed = observedFor(plan, issue);
160
186
  for (const [field, expected] of Object.entries(plan.intendedAfter)) {
161
187
  if (!sameValue(observed[field], expected)) {
@@ -189,6 +215,27 @@ function observedFor(plan, issue) {
189
215
  }
190
216
  return observed;
191
217
  }
218
+ /**
219
+ * Did the field end up holding what was planned?
220
+ *
221
+ * Options are compared on their ids, never on their labels - an option is
222
+ * identified by its id, and a label is what a person reads. For a multi-select
223
+ * the comparison is set-wise: Jira is free to return the same selection in a
224
+ * different order, and that is not a different selection.
225
+ */
226
+ function sameCustomFieldValue(kind, expected, observed) {
227
+ if (kind === "multi-option") {
228
+ const ids = (v) => (Array.isArray(v) ? v.map((o) => o.id) : []).sort();
229
+ const a = ids(expected);
230
+ const b = ids(observed);
231
+ return a.length === b.length && a.every((id, i) => id === b[i]);
232
+ }
233
+ if (kind === "single-option") {
234
+ const id = (v) => v && typeof v === "object" && !Array.isArray(v) ? v.id : undefined;
235
+ return id(expected) === id(observed);
236
+ }
237
+ return expected === observed;
238
+ }
192
239
  function sameValue(observed, expected) {
193
240
  if (Array.isArray(expected) || Array.isArray(observed)) {
194
241
  const a = Array.isArray(observed) ? [...observed].map(String).sort() : [];
@@ -1,6 +1,6 @@
1
1
  import type { JamDeps } from "../deps.js";
2
2
  import type { FullIssueContext } from "../domain/context.js";
3
- import type { WritePlan, WritePlanReceipt } from "../domain/write.js";
3
+ import type { CustomFieldKind, CustomFieldValueView, WritePlan, WritePlanReceipt } from "../domain/write.js";
4
4
  export type PlanWriteRequest = {
5
5
  /** Absent for `issue.create`, which names a project rather than an issue. */
6
6
  key?: string;
@@ -40,5 +40,18 @@ export type IssueSnapshot = {
40
40
  issue: FullIssueContext;
41
41
  /** Identity of the current assignee, which `issue.assignee` cannot supply. */
42
42
  assigneeAccountId?: string;
43
+ /** Raw values for any custom field ids that were asked for. */
44
+ customFieldValues?: Record<string, unknown>;
43
45
  };
44
- export declare function readIssue(deps: JamDeps, issueKey: string): Promise<IssueSnapshot>;
46
+ export declare function readIssue(deps: JamDeps, issueKey: string, extraFields?: string[]): Promise<IssueSnapshot>;
47
+ /**
48
+ * What the field holds now, in the shape a receipt shows.
49
+ *
50
+ * Jira stores an option as an object and a scalar as itself; a person reading
51
+ * `before` wants the same canonical form they will see in `intendedAfter`, so
52
+ * they can compare the two rather than a payload against a summary.
53
+ */
54
+ export declare function currentCustomFieldView(field: {
55
+ id: string;
56
+ name: string;
57
+ }, kind: CustomFieldKind, raw: unknown): CustomFieldValueView;