raptor-aios 0.9.0 → 0.10.0
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/CHANGELOG.md +19 -0
- package/dist/_core/dist/jira/dialects.js +2 -2
- package/dist/_core/dist/jira/index.js +1 -1
- package/dist/_core/dist/jira/mapper.js +152 -13
- package/dist/_core/dist/jira/mcp-client.js +1 -0
- package/dist/_core/package.json +1 -1
- package/dist/_core/templates/spec.md.hbs +4 -2
- package/dist/commands/jira/pull.js +7 -1
- package/dist/shared/jira.js +18 -7
- package/package.json +1 -1
- package/scripts/prepare-npm.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.10.0] - 2026-06-11
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Jira → spec: nomes de campo resolvidos + filtro de relevância (`packages/core/src/jira/`).** Um card de Jira corporativo carrega centenas de custom fields de configuração (o `CDPOS-6532` real: 1408 campos, 515 não-nulos) que antes inundavam a `## Problem Statement` com blocos `### customfield_NNNNN` ilegíveis. Agora:
|
|
11
|
+
- **Nomes resolvidos.** O dialeto Rovo (hospedado, OAuth) passa a enviar `expand: "names"`, então cada `customfield_NNNNN` ganha seu rótulo humano ("Critérios de Aceite"). O `mcp-atlassian` (≥ 0.11.2) tem o rótulo desembrulhado do wrapper `{value, name}` (ele nunca ecoa o mapa `names`); `renderedFields` deixou de ser pedido (o servidor o descarta e a combinação suprime custom fields em algumas versões).
|
|
12
|
+
- **Filtro de relevância (`filterCustomFields`).** Camadas determinísticas (PT/EN, singular+plural): valor-ruído (enums, datas, placeholders) e rótulos de config (Responsável, Relator, Categorias, Branch, Repositório, EI/EO/EQ, CMDB, Nova Data…) caem; allowlist de história (Requisitos, Cenários, Critérios de Aceite, Ressalva de testes, Figma, Traduções, Documentação, Anexo…) e links de ferramenta de design (figma/miro/zeplin/notion/confluence) ficam — avaliados ANTES do denylist. Desconhecidos só ficam com prosa substantiva (rede *lose-nothing*).
|
|
13
|
+
- **`jira.custom_fields` ganha `exclude` e force-keep** (chaves case-insensitive): `<campo>: exclude` derruba; `<campo>: <bucket>` força a manter.
|
|
14
|
+
- **Anexos de imagem** (`image/*`) entram como `### Attachments (images)` (links, não baixados); outros tipos ficam no card.
|
|
15
|
+
- **Critérios de Aceite agrupados por cenário** (`acceptanceLines`): blocos "Cenário N …" viram um `[AC-#]` cada (não um por linha de Gherkin).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`flattenAdf` rende `taskItem` e `table`.** Uma matriz de aplicabilidade do Jira (grade de checkboxes) agora vira texto legível: **só os itens marcados `[x]` sobrevivem** (os desmarcados são ruído) e tabelas viram uma linha por row com células separadas por `|`. No `CDPOS-6532`, "Cenários aplicáveis" encolheu de 2528 → 607 chars, mostrando o escopo real.
|
|
20
|
+
- **URL do card browsable.** `resolveUrl` prefere o `base_url` do `raptor.yml` ao `self` do gateway `api.atlassian.com` (que não abre no navegador); `jira.base_url` é encanado do `raptor.yml` ao cliente OAuth.
|
|
21
|
+
- **`raptor jira pull`** imprime quantos custom fields foram filtrados (descoberta para o escape hatch); `--json` segue despejando tudo sem filtro. O `jira-refresh.md` (clarify) usa o mesmo filtro — os três consumidores (`new`, `pull`, `jira-refresh`) mostram o mesmo card.
|
|
22
|
+
|
|
23
|
+
Doc: [`docs/jira-spec-enrichment.md`](docs/jira-spec-enrichment.md) (seções F1–F4 + F3½ + §4b card corporativo). Validado E2E contra o `CDPOS-6532` real via OAuth.
|
|
24
|
+
|
|
6
25
|
## [0.9.0] - 2026-06-11
|
|
7
26
|
|
|
8
27
|
### Added
|
|
@@ -12,7 +12,7 @@ export const ROVO_DIALECT = {
|
|
|
12
12
|
transition: ["transitionJiraIssue"],
|
|
13
13
|
},
|
|
14
14
|
args: {
|
|
15
|
-
getIssue: (cloudId, key) => ({ cloudId, issueIdOrKey: key }),
|
|
15
|
+
getIssue: (cloudId, key) => ({ cloudId, issueIdOrKey: key, expand: "names" }),
|
|
16
16
|
search: (cloudId, jql, max) => ({ cloudId, jql, maxResults: max }),
|
|
17
17
|
projects: (cloudId) => ({ cloudId }),
|
|
18
18
|
createIssue: (input) => ({
|
|
@@ -53,7 +53,7 @@ export const MCP_ATLASSIAN_DIALECT = {
|
|
|
53
53
|
getIssue: (_cloudId, key) => ({
|
|
54
54
|
issue_key: key,
|
|
55
55
|
fields: "*all",
|
|
56
|
-
expand: "names
|
|
56
|
+
expand: "names",
|
|
57
57
|
comment_limit: 10,
|
|
58
58
|
}),
|
|
59
59
|
search: (_cloudId, jql, max) => ({ jql, limit: max }),
|
|
@@ -3,4 +3,4 @@ export * from "./credentials.js";
|
|
|
3
3
|
export { AtlassianOAuthProvider, isAccessTokenExpired, openBrowser, startCallbackServer, } from "./oauth.js";
|
|
4
4
|
export { connectJira, connectMcpServer, makeJiraClient, clientToToolCaller, decodeToolResult, unwrapEnvelope, expandEnv, missingEnvRefs, } from "./mcp-client.js";
|
|
5
5
|
export { ROVO_DIALECT, MCP_ATLASSIAN_DIALECT, DIALECTS, resolveDialect, detectDialect, } from "./dialects.js";
|
|
6
|
-
export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, classifyField, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
|
|
6
|
+
export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, extractAttachments, classifyField, filterCustomFields, isBoilerplateValue, isTemplateValue, acceptanceLines, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
|
|
@@ -33,18 +33,49 @@ export function parseJiraIssue(raw, baseUrl) {
|
|
|
33
33
|
acceptanceCriteria: extractAcceptanceCriteria(fields, description),
|
|
34
34
|
comments: extractComments(fields),
|
|
35
35
|
customFields,
|
|
36
|
+
attachments: extractAttachments(fields),
|
|
36
37
|
...(url ? { url } : {}),
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
export function extractAttachments(fields) {
|
|
41
|
+
const list = Array.isArray(fields["attachment"])
|
|
42
|
+
? fields["attachment"]
|
|
43
|
+
: Array.isArray(fields["attachments"])
|
|
44
|
+
? fields["attachments"]
|
|
45
|
+
: [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const a of list) {
|
|
48
|
+
if (!isRecord(a))
|
|
49
|
+
continue;
|
|
50
|
+
const filename = str(a["filename"]);
|
|
51
|
+
if (!filename)
|
|
52
|
+
continue;
|
|
53
|
+
out.push({
|
|
54
|
+
filename,
|
|
55
|
+
mimeType: str(a["mimeType"]) || str(a["content_type"]),
|
|
56
|
+
url: str(a["content"]) || str(a["url"]),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
39
61
|
export function extractCustomFields(fields, names) {
|
|
40
62
|
const out = [];
|
|
41
63
|
for (const [id, raw] of Object.entries(fields)) {
|
|
42
64
|
if (!/^customfield_/i.test(id))
|
|
43
65
|
continue;
|
|
44
|
-
|
|
66
|
+
let label = str(names[id]);
|
|
67
|
+
let inner = raw;
|
|
68
|
+
if (isRecord(raw) &&
|
|
69
|
+
"value" in raw &&
|
|
70
|
+
Object.keys(raw).every((k) => k === "value" || k === "name")) {
|
|
71
|
+
if (!label)
|
|
72
|
+
label = str(raw["name"]);
|
|
73
|
+
inner = raw["value"];
|
|
74
|
+
}
|
|
75
|
+
const value = flattenFieldValue(inner);
|
|
45
76
|
if (!value)
|
|
46
77
|
continue;
|
|
47
|
-
const name =
|
|
78
|
+
const name = label || id;
|
|
48
79
|
out.push({ id, name, value, bucket: classifyField(name) });
|
|
49
80
|
}
|
|
50
81
|
return out;
|
|
@@ -55,21 +86,102 @@ export function classifyField(label) {
|
|
|
55
86
|
return "acceptance";
|
|
56
87
|
if (/cen[áa]rio|scenario/.test(l))
|
|
57
88
|
return "scenarios";
|
|
58
|
-
if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b/.test(l))
|
|
89
|
+
if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b|ressalva/.test(l))
|
|
59
90
|
return "nonFunctional";
|
|
60
91
|
if (/requisito|requirement|\brf\b/.test(l))
|
|
61
92
|
return "functional";
|
|
62
93
|
if (/fora do escopo|out of scope|n[ãa]o escopo/.test(l))
|
|
63
94
|
return "outOfScope";
|
|
64
|
-
if (/documenta|documentation|link|refer[êe]ncia|design|figma/.test(l))
|
|
95
|
+
if (/documenta|documentation|link|refer[êe]ncia|design|figma|tradu[çc]|translation/.test(l))
|
|
65
96
|
return "docs";
|
|
66
97
|
return "other";
|
|
67
98
|
}
|
|
99
|
+
const ENUM_NOISE = /^(?:sim|n[ãa]o|yes|no|true|false|n\/a|na|none|nenhum|classificar|backlog|portal|no prazo|⏳|✅|❌|-+)$/i;
|
|
100
|
+
const PLACEHOLDER_LINE = /^(?:inserir|informar|descrever|preencher|selecionar|indicar|exportar|insira|informe|descreva|preencha|selecione|indique)\b/i;
|
|
101
|
+
const CARD_CONFIG_LABEL = /\b(?:respons[áa]ve(?:l|is)|relator(?:a|es|as)?|reporter|assignee|aprovador(?:a|es|as)?|squads?|categorias?|data\s+limite|nova\s+data|due\s*date|prazos?|branch(?:es)?|reposit[óo]rios?|repositor(?:y|ies)|entradas?\s+externas?|sa[íi]das?\s+externas?|consultas?\s+externas?|\bei\b|\beo\b|\beq\b|cmdb|aplica[çc][ãa]o\s+afetada|epic\s*links?|rank|development|sprints?|itera[çc](?:[ãa]o|[õo]es)|iterations?|quarters?|garantias?|story\s*points?|time\s*tracking)\b/i;
|
|
102
|
+
const ESSENTIAL_LABEL = /aceite|aceita[çc][ãa]o|acceptance|cen[áa]rio|scenario|requisito|requirement|\brnf\b|\brf\b|ressalva|figma|tradu[çc]|translation|documenta[çc]|documentation|refer[êe]ncia|\bdesign\b|anexo|attachment|prot[óo]tipo|prototype|hist[óo]ria|user\s*stor|fora\s+do\s+escopo|out\s+of\s+scope/i;
|
|
103
|
+
const DOC_TOOL_URL = /\b(?:figma\.com|miro\.com|zeplin\.(?:io|app)|notion\.so|atlassian\.net\/wiki)\b/i;
|
|
104
|
+
export function isBoilerplateValue(value) {
|
|
105
|
+
const v = value.trim();
|
|
106
|
+
if (!v)
|
|
107
|
+
return true;
|
|
108
|
+
if (ENUM_NOISE.test(v))
|
|
109
|
+
return true;
|
|
110
|
+
if (!v.includes("\n") && /^[\d|:.,\-T+Z\s/]+$/.test(v))
|
|
111
|
+
return true;
|
|
112
|
+
if (/^(?:espa[çc]o para inser[çc][ãa]o|campos a seguir)/i.test(v))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
export function isTemplateValue(value) {
|
|
117
|
+
const v = value.trim();
|
|
118
|
+
if (v.length < 200)
|
|
119
|
+
return true;
|
|
120
|
+
if (v.includes("{panel") || v.includes("🚨"))
|
|
121
|
+
return true;
|
|
122
|
+
if (/^\{(?:pullrequest|dataType|json)\b/.test(v))
|
|
123
|
+
return true;
|
|
124
|
+
const segments = v
|
|
125
|
+
.split("\n")
|
|
126
|
+
.flatMap((l) => l.split(" | "))
|
|
127
|
+
.map((s) => s.replace(/^[-*\d.)\s]+/, "").trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
return segments.filter((s) => PLACEHOLDER_LINE.test(s)).length >= 2;
|
|
130
|
+
}
|
|
131
|
+
export function filterCustomFields(fields, overrides = {}) {
|
|
132
|
+
const ovByKey = {};
|
|
133
|
+
for (const [k, v] of Object.entries(overrides))
|
|
134
|
+
ovByKey[k.toLowerCase()] = v;
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const cf of fields) {
|
|
137
|
+
const ov = ovByKey[cf.id.toLowerCase()] ?? ovByKey[cf.name.toLowerCase()];
|
|
138
|
+
if (ov && ov.toLowerCase() === "exclude")
|
|
139
|
+
continue;
|
|
140
|
+
if (ov) {
|
|
141
|
+
out.push({ ...cf, bucket: ov });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isBoilerplateValue(cf.value))
|
|
145
|
+
continue;
|
|
146
|
+
if (ESSENTIAL_LABEL.test(cf.name)) {
|
|
147
|
+
out.push(cf);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (DOC_TOOL_URL.test(cf.value)) {
|
|
151
|
+
out.push({ ...cf, bucket: "docs" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (CARD_CONFIG_LABEL.test(cf.name))
|
|
155
|
+
continue;
|
|
156
|
+
if (isTemplateValue(cf.value))
|
|
157
|
+
continue;
|
|
158
|
+
out.push(cf);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
export function acceptanceLines(value) {
|
|
163
|
+
const lines = toLines(value);
|
|
164
|
+
const isStart = (l) => /^(?:cen[áa]rio|scenario)\b/i.test(l);
|
|
165
|
+
const starts = lines.filter(isStart).length;
|
|
166
|
+
const hasGherkin = lines.some((l) => /^(?:dado|quando|ent[ãa]o|given|when|then)\b/i.test(l));
|
|
167
|
+
if (starts === 0 || (starts === 1 && !hasGherkin))
|
|
168
|
+
return lines;
|
|
169
|
+
const blocks = [];
|
|
170
|
+
for (const l of lines) {
|
|
171
|
+
if (isStart(l) || blocks.length === 0)
|
|
172
|
+
blocks.push([l]);
|
|
173
|
+
else
|
|
174
|
+
blocks[blocks.length - 1].push(l);
|
|
175
|
+
}
|
|
176
|
+
return blocks.map((b) => b.join(" "));
|
|
177
|
+
}
|
|
68
178
|
export function flattenFieldValue(raw) {
|
|
69
179
|
if (raw == null)
|
|
70
180
|
return "";
|
|
71
181
|
if (typeof raw === "string")
|
|
72
182
|
return raw.trim();
|
|
183
|
+
if (typeof raw === "number" || typeof raw === "boolean")
|
|
184
|
+
return String(raw);
|
|
73
185
|
if (Array.isArray(raw)) {
|
|
74
186
|
return raw.map(flattenFieldValue).filter(Boolean).join("\n").trim();
|
|
75
187
|
}
|
|
@@ -113,11 +225,7 @@ export function extractComments(fields) {
|
|
|
113
225
|
return out;
|
|
114
226
|
}
|
|
115
227
|
export function mapIssueToSpecContext(issue, opts = {}) {
|
|
116
|
-
const
|
|
117
|
-
const custom = (issue.customFields ?? []).map((cf) => {
|
|
118
|
-
const ov = overrides[cf.id] ?? overrides[cf.name.toLowerCase()];
|
|
119
|
-
return ov ? { ...cf, bucket: ov } : cf;
|
|
120
|
-
});
|
|
228
|
+
const custom = filterCustomFields(issue.customFields ?? [], opts.fieldBuckets);
|
|
121
229
|
const ac = [];
|
|
122
230
|
const seen = new Set();
|
|
123
231
|
const pushAc = (line) => {
|
|
@@ -130,13 +238,19 @@ export function mapIssueToSpecContext(issue, opts = {}) {
|
|
|
130
238
|
issue.acceptanceCriteria.forEach(pushAc);
|
|
131
239
|
for (const cf of custom)
|
|
132
240
|
if (cf.bucket === "acceptance")
|
|
133
|
-
|
|
241
|
+
acceptanceLines(cf.value).forEach(pushAc);
|
|
134
242
|
const sections = custom
|
|
135
243
|
.filter((cf) => cf.bucket !== "acceptance")
|
|
136
244
|
.map((cf) => ({ title: cf.name, body: cf.value, bucket: cf.bucket }));
|
|
245
|
+
const images = (issue.attachments ?? []).filter((a) => /^image\//i.test(a.mimeType));
|
|
137
246
|
const ticketBody = [
|
|
138
247
|
issue.description,
|
|
139
248
|
...sections.map((s) => `### ${s.title}\n${s.body}`),
|
|
249
|
+
images.length
|
|
250
|
+
? `### Attachments (images)\n${images
|
|
251
|
+
.map((a) => `- [${a.filename}](${a.url})`)
|
|
252
|
+
.join("\n")}`
|
|
253
|
+
: "",
|
|
140
254
|
]
|
|
141
255
|
.filter((s) => s && s.trim())
|
|
142
256
|
.join("\n\n");
|
|
@@ -175,6 +289,18 @@ export function flattenAdf(node) {
|
|
|
175
289
|
if (url)
|
|
176
290
|
return type === "inlineCard" ? url : `${url}\n`;
|
|
177
291
|
}
|
|
292
|
+
if (type === "taskItem") {
|
|
293
|
+
const attrs = isRecord(node["attrs"]) ? node["attrs"] : {};
|
|
294
|
+
if (attrs["state"] !== "DONE")
|
|
295
|
+
return "";
|
|
296
|
+
return `- [x] ${flattenAdf(node["content"]).trim()}\n`;
|
|
297
|
+
}
|
|
298
|
+
if (type === "tableRow") {
|
|
299
|
+
const cells = Array.isArray(node["content"])
|
|
300
|
+
? node["content"].map((c) => flattenAdf(c).trim().replace(/\s*\n\s*/g, " · "))
|
|
301
|
+
: [];
|
|
302
|
+
return `${cells.join(" | ")}\n`;
|
|
303
|
+
}
|
|
178
304
|
const inner = flattenAdf(node["content"]);
|
|
179
305
|
const blockTypes = new Set([
|
|
180
306
|
"paragraph",
|
|
@@ -184,6 +310,8 @@ export function flattenAdf(node) {
|
|
|
184
310
|
"listItem",
|
|
185
311
|
"codeBlock",
|
|
186
312
|
"blockquote",
|
|
313
|
+
"table",
|
|
314
|
+
"taskList",
|
|
187
315
|
]);
|
|
188
316
|
if (type === "hardBreak")
|
|
189
317
|
return "\n";
|
|
@@ -256,13 +384,17 @@ function resolveUrl(issue, fields, key, baseUrl) {
|
|
|
256
384
|
if (direct)
|
|
257
385
|
return direct;
|
|
258
386
|
const self = str(issue["self"]);
|
|
259
|
-
|
|
387
|
+
const selfOrigin = (() => {
|
|
260
388
|
try {
|
|
261
|
-
|
|
389
|
+
const u = new URL(self);
|
|
390
|
+
return u.hostname === "api.atlassian.com" ? undefined : u.origin;
|
|
262
391
|
}
|
|
263
392
|
catch {
|
|
393
|
+
return undefined;
|
|
264
394
|
}
|
|
265
|
-
}
|
|
395
|
+
})();
|
|
396
|
+
if (selfOrigin)
|
|
397
|
+
return `${selfOrigin}/browse/${key}`;
|
|
266
398
|
if (baseUrl) {
|
|
267
399
|
try {
|
|
268
400
|
return `${new URL(baseUrl).origin}/browse/${key}`;
|
|
@@ -270,6 +402,13 @@ function resolveUrl(issue, fields, key, baseUrl) {
|
|
|
270
402
|
catch {
|
|
271
403
|
}
|
|
272
404
|
}
|
|
405
|
+
if (self) {
|
|
406
|
+
try {
|
|
407
|
+
return `${new URL(self).origin}/browse/${key}`;
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
273
412
|
return undefined;
|
|
274
413
|
}
|
|
275
414
|
function nestedName(v) {
|
package/dist/_core/package.json
CHANGED
|
@@ -111,8 +111,9 @@ and assets are incorporated, catalogued and present on disk — no loose ends.
|
|
|
111
111
|
{{jiraTicketBody}}
|
|
112
112
|
|
|
113
113
|
<!--
|
|
114
|
-
Imported VERBATIM from Jira {{jira}} — the description PLUS every
|
|
115
|
-
(scenarios, requirements, test caveats, documentation/design links) the card holds
|
|
114
|
+
Imported VERBATIM from Jira {{jira}} — the description PLUS every STORY-relevant custom
|
|
115
|
+
field (scenarios, requirements, test caveats, documentation/design links) the card holds;
|
|
116
|
+
card-config fields (workflow enums, form templates, tracking metadata) were filtered out.
|
|
116
117
|
This is the authoritative source for this feature. The agent MUST redistribute ALL of
|
|
117
118
|
it into the canonical sections below, LOSING NOTHING (each `### <field>` block too):
|
|
118
119
|
- each user story / "História" → ## User Stories (as [US#] with priority, why, independent test)
|
|
@@ -123,6 +124,7 @@ it into the canonical sections below, LOSING NOTHING (each `### <field>` block t
|
|
|
123
124
|
- test caveats / "Ressalvas de testes" → ## Edge Cases (or the relevant [NFR-#])
|
|
124
125
|
- the ticket's out-of-scope / "Fora do Escopo" list → ## Out of Scope
|
|
125
126
|
- design / documentation links (Figma etc.) → ## Dependencies & Assumptions
|
|
127
|
+
- "### Attachments (images)" → ## Design Reference / Dependencies & Assumptions (visual sources)
|
|
126
128
|
Then REDUCE this section to a crisp problem statement (who is affected today and how). Do not leave the raw dump here.
|
|
127
129
|
-->
|
|
128
130
|
{{else}}{{#if description}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from "@oclif/core";
|
|
2
2
|
import { BaseCommand } from "../../base-command.js";
|
|
3
|
-
import { mapIssueToSpecContext } from "../../_core/dist/index.js";
|
|
3
|
+
import { filterCustomFields, mapIssueToSpecContext } from "../../_core/dist/index.js";
|
|
4
4
|
import { readConfig, requireProjectRoot } from "../../shared/project.js";
|
|
5
5
|
import { jiraConn, openJiraInteractive } from "../../shared/jira.js";
|
|
6
6
|
export default class JiraPull extends BaseCommand {
|
|
@@ -61,5 +61,11 @@ export default class JiraPull extends BaseCommand {
|
|
|
61
61
|
for (const line of s.body.split("\n"))
|
|
62
62
|
this.log(` ${line}`);
|
|
63
63
|
}
|
|
64
|
+
const total = (issue.customFields ?? []).length;
|
|
65
|
+
const kept = filterCustomFields(issue.customFields ?? [], conn.customFields).length;
|
|
66
|
+
if (total > kept) {
|
|
67
|
+
this.log(`\n (${total - kept} of ${total} custom fields filtered as card config — ` +
|
|
68
|
+
`inspect with --json; force-keep via jira.custom_fields in raptor.yml)`);
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
}
|
package/dist/shared/jira.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { connectJira, connectMcpServer, hasStoredTokens, parseSpec, } from "../_core/dist/index.js";
|
|
3
|
+
import { connectJira, connectMcpServer, filterCustomFields, hasStoredTokens, parseSpec, } from "../_core/dist/index.js";
|
|
4
4
|
import { featureDir, readConfig } from "./project.js";
|
|
5
5
|
export function jiraConn(config) {
|
|
6
6
|
const j = config.jira;
|
|
@@ -10,6 +10,7 @@ export function jiraConn(config) {
|
|
|
10
10
|
const common = {
|
|
11
11
|
provider,
|
|
12
12
|
...(j.project_key ? { projectKey: j.project_key } : {}),
|
|
13
|
+
...(j.base_url ? { baseUrl: j.base_url } : {}),
|
|
13
14
|
statusSync: j.status_sync !== false,
|
|
14
15
|
transitions: j.transitions ?? {},
|
|
15
16
|
customFields: j.custom_fields ?? {},
|
|
@@ -36,7 +37,7 @@ export async function openJiraClient(conn) {
|
|
|
36
37
|
throw new Error("Jira 'mcp' provider has no server config.");
|
|
37
38
|
return connectMcpServer(conn.mcp);
|
|
38
39
|
}
|
|
39
|
-
return openJiraNonInteractive(conn.serverUrl);
|
|
40
|
+
return openJiraNonInteractive(conn.serverUrl, conn.baseUrl);
|
|
40
41
|
}
|
|
41
42
|
export async function openJiraInteractive(conn) {
|
|
42
43
|
if (conn.provider === "mcp") {
|
|
@@ -44,11 +45,15 @@ export async function openJiraInteractive(conn) {
|
|
|
44
45
|
throw new Error("Jira 'mcp' provider has no server config.");
|
|
45
46
|
return connectMcpServer(conn.mcp);
|
|
46
47
|
}
|
|
47
|
-
return connectJira(
|
|
48
|
+
return connectJira({
|
|
49
|
+
...(conn.serverUrl ? { serverUrl: conn.serverUrl } : {}),
|
|
50
|
+
...(conn.baseUrl ? { baseUrl: conn.baseUrl } : {}),
|
|
51
|
+
});
|
|
48
52
|
}
|
|
49
|
-
export async function openJiraNonInteractive(serverUrl) {
|
|
53
|
+
export async function openJiraNonInteractive(serverUrl, baseUrl) {
|
|
50
54
|
return connectJira({
|
|
51
55
|
...(serverUrl ? { serverUrl } : {}),
|
|
56
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
52
57
|
onAuthorizationUrl: () => {
|
|
53
58
|
throw new Error("re-authorization required");
|
|
54
59
|
},
|
|
@@ -81,7 +86,7 @@ export async function refreshJiraSidecar(cmd, root, feature, dir) {
|
|
|
81
86
|
}
|
|
82
87
|
try {
|
|
83
88
|
const issue = await client.getJiraIssue(conn.cloudId, storyKey);
|
|
84
|
-
writeFileSync(join(dir, "jira-refresh.md"), renderRefresh(issue));
|
|
89
|
+
writeFileSync(join(dir, "jira-refresh.md"), renderRefresh(issue, conn.customFields));
|
|
85
90
|
cmd.log(` ↪ Jira ${storyKey}: refreshed → jira-refresh.md`);
|
|
86
91
|
}
|
|
87
92
|
catch (err) {
|
|
@@ -91,7 +96,7 @@ export async function refreshJiraSidecar(cmd, root, feature, dir) {
|
|
|
91
96
|
await client.close();
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
|
-
function renderRefresh(issue) {
|
|
99
|
+
function renderRefresh(issue, fieldOverrides = {}) {
|
|
95
100
|
const lines = [
|
|
96
101
|
`# Jira refresh — ${issue.key}`,
|
|
97
102
|
"",
|
|
@@ -113,9 +118,15 @@ function renderRefresh(issue) {
|
|
|
113
118
|
for (const ac of issue.acceptanceCriteria)
|
|
114
119
|
lines.push(`- ${ac}`);
|
|
115
120
|
}
|
|
116
|
-
for (const cf of issue.customFields ?? []) {
|
|
121
|
+
for (const cf of filterCustomFields(issue.customFields ?? [], fieldOverrides)) {
|
|
117
122
|
lines.push("", `## ${cf.name}`, "", cf.value);
|
|
118
123
|
}
|
|
124
|
+
const images = (issue.attachments ?? []).filter((a) => /^image\//i.test(a.mimeType));
|
|
125
|
+
if (images.length) {
|
|
126
|
+
lines.push("", "## Attachments (images)");
|
|
127
|
+
for (const a of images)
|
|
128
|
+
lines.push(`- [${a.filename}](${a.url})`);
|
|
129
|
+
}
|
|
119
130
|
if (issue.comments.length) {
|
|
120
131
|
lines.push("", "## Recent comments");
|
|
121
132
|
for (const c of issue.comments.slice(-5)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raptor-aios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Raptor — Spec-Driven Development (SDD) CLI for modern mobile apps. Constitutional gates, audit trail, real verification (a11y/perf/stores/OS matrix), and AI-agent slash commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/prepare-npm.mjs
CHANGED