raptor-aios 0.7.1 → 0.8.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 CHANGED
@@ -3,6 +3,22 @@
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.8.0] - 2026-06-09
7
+
8
+ ### Added
9
+
10
+ - **`raptor new --jira` now seeds the spec from the WHOLE card, not just its description.** A ticket's scenarios, requirements, acceptance criteria, test caveats and design/documentation links — wherever they live — are captured, kept verbatim, and routed into the spec for the agent to redistribute (enforced by `gate.spec.ready`, not a brittle extractor). Four parts:
11
+ - **Fetch-all** — the `mcp-atlassian` dialect requests `fields="*all"` with `expand="names,renderedFields"` instead of six fixed fields, so custom fields and the id→label map come back.
12
+ - **Link preservation** — `flattenAdf` now surfaces a link mark's `href` (as `[text](url)`) and smart-link card URLs (`inlineCard`/`blockCard`/`embedCard`), so a Figma link embedded in the body survives flattening and re-enables the `design/` scaffold + `gate.design.ready`.
13
+ - **Rich custom fields** — `JiraIssue` gains `customFields[]` (id, human label, value, canonical bucket). A PT/EN classifier (`classifyField`) routes "Critérios de aceite", "Cenários", "Requisitos", "Fora do escopo", "Documentação"… into the right section; `flattenFieldValue` tolerates ADF/select/multi-select shapes. Nothing is dropped — an unrecognised field falls to an appendix.
14
+ - **Verbatim ticket body** — `mapIssueToSpecContext` builds `jiraTicketBody` (description + labelled `### <field>` sections), folds acceptance-bucket fields into the AC list (de-duped, feeding `acceptance.ids`/M7), and the `spec.md` Problem Statement dumps it with explicit redistribution instructions. `raptor jira pull` and the clarify-time `jira-refresh.md` surface the same fields.
15
+ - **New `jira.custom_fields` config** — deterministic `id`/`label → bucket` overrides for servers that don't honour `expand=names`.
16
+ - **New docs** — `docs/jira-spec-enrichment.md` documents how the card is captured, how the agent is instructed, and how the gates validate it (anchored on a real card).
17
+
18
+ ### Internal
19
+
20
+ - New `extractCustomFields` / `classifyField` / `flattenFieldValue` and `MapIssueOptions` in `jira/mapper.ts`; `JiraCustomField` / `JiraFieldBucket` types. A test proves `gate.spec.ready` blocks an approved-but-un-redistributed Jira seed on three independent axes (leftover `[PREENCHER]`, uncovered user stories, unmirrored `acceptance.ids`) — locking in "Raptor delivers verbatim, the agent redistributes, the gate enforces". Verified end-to-end against the real KAN-2 card.
21
+
6
22
  ## [0.7.1] - 2026-06-08
7
23
 
8
24
  ### Fixed
package/README.md CHANGED
@@ -471,6 +471,8 @@ raptor jira pull APP-1234 # importa a issue
471
471
  raptor new login --jira APP-1234 # semeia a spec a partir da issue
472
472
  ```
473
473
 
474
+ > 📖 Como o card é capturado **por inteiro** (campos ricos, links de Figma) e redistribuído na spec — com os gates cobrando: veja [docs/jira-spec-enrichment.md](docs/jira-spec-enrichment.md).
475
+
474
476
  ---
475
477
 
476
478
  ## 📖 Referência de comandos CLI
@@ -52,7 +52,8 @@ export const MCP_ATLASSIAN_DIALECT = {
52
52
  args: {
53
53
  getIssue: (_cloudId, key) => ({
54
54
  issue_key: key,
55
- fields: "summary,description,status,issuetype,labels,comment",
55
+ fields: "*all",
56
+ expand: "names,renderedFields",
56
57
  comment_limit: 10,
57
58
  }),
58
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, } from "./mcp-client.js";
5
5
  export { ROVO_DIALECT, MCP_ATLASSIAN_DIALECT, DIALECTS, resolveDialect, } from "./dialects.js";
6
- export { parseJiraIssue, parseCreatedIssue, extractComments, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
6
+ export { parseJiraIssue, parseCreatedIssue, extractComments, extractCustomFields, classifyField, flattenFieldValue, mapIssueToSpecContext, flattenAdf, extractAcceptanceCriteria, } from "./mapper.js";
@@ -17,6 +17,12 @@ export function parseJiraIssue(raw, baseUrl) {
17
17
  nestedName(fields["issue_type"]);
18
18
  const labels = strArray(fields["labels"]);
19
19
  const url = resolveUrl(issue, fields, key, baseUrl);
20
+ const names = isRecord(issue["names"])
21
+ ? issue["names"]
22
+ : isRecord(obj["names"])
23
+ ? obj["names"]
24
+ : {};
25
+ const customFields = extractCustomFields(fields, names);
20
26
  return {
21
27
  key,
22
28
  summary,
@@ -26,9 +32,57 @@ export function parseJiraIssue(raw, baseUrl) {
26
32
  labels,
27
33
  acceptanceCriteria: extractAcceptanceCriteria(fields, description),
28
34
  comments: extractComments(fields),
35
+ customFields,
29
36
  ...(url ? { url } : {}),
30
37
  };
31
38
  }
39
+ export function extractCustomFields(fields, names) {
40
+ const out = [];
41
+ for (const [id, raw] of Object.entries(fields)) {
42
+ if (!/^customfield_/i.test(id))
43
+ continue;
44
+ const value = flattenFieldValue(raw);
45
+ if (!value)
46
+ continue;
47
+ const name = str(names[id]) || id;
48
+ out.push({ id, name, value, bucket: classifyField(name) });
49
+ }
50
+ return out;
51
+ }
52
+ export function classifyField(label) {
53
+ const l = label.toLowerCase();
54
+ if (/aceite|aceita[çc][ãa]o|acceptance/.test(l))
55
+ return "acceptance";
56
+ if (/cen[áa]rio|scenario/.test(l))
57
+ return "scenarios";
58
+ if (/n[ãa]o[-\s]?funcional|non[-\s]?functional|\brnf\b/.test(l))
59
+ return "nonFunctional";
60
+ if (/requisito|requirement|\brf\b/.test(l))
61
+ return "functional";
62
+ if (/fora do escopo|out of scope|n[ãa]o escopo/.test(l))
63
+ return "outOfScope";
64
+ if (/documenta|documentation|link|refer[êe]ncia|design|figma/.test(l))
65
+ return "docs";
66
+ return "other";
67
+ }
68
+ export function flattenFieldValue(raw) {
69
+ if (raw == null)
70
+ return "";
71
+ if (typeof raw === "string")
72
+ return raw.trim();
73
+ if (Array.isArray(raw)) {
74
+ return raw.map(flattenFieldValue).filter(Boolean).join("\n").trim();
75
+ }
76
+ if (isRecord(raw)) {
77
+ if ("content" in raw || raw["type"] === "doc")
78
+ return flattenAdf(raw).trim();
79
+ const opt = str(raw["value"]) || str(raw["name"]);
80
+ if (opt)
81
+ return opt;
82
+ return flattenAdf(raw).trim();
83
+ }
84
+ return "";
85
+ }
32
86
  export function parseCreatedIssue(raw) {
33
87
  const obj = coerceObject(raw);
34
88
  if (!obj)
@@ -58,11 +112,38 @@ export function extractComments(fields) {
58
112
  }
59
113
  return out;
60
114
  }
61
- export function mapIssueToSpecContext(issue) {
62
- const ac = issue.acceptanceCriteria;
115
+ export function mapIssueToSpecContext(issue, opts = {}) {
116
+ const overrides = opts.fieldBuckets ?? {};
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
+ });
121
+ const ac = [];
122
+ const seen = new Set();
123
+ const pushAc = (line) => {
124
+ const t = line.trim();
125
+ if (t && !seen.has(t)) {
126
+ seen.add(t);
127
+ ac.push(t);
128
+ }
129
+ };
130
+ issue.acceptanceCriteria.forEach(pushAc);
131
+ for (const cf of custom)
132
+ if (cf.bucket === "acceptance")
133
+ toLines(cf.value).forEach(pushAc);
134
+ const sections = custom
135
+ .filter((cf) => cf.bucket !== "acceptance")
136
+ .map((cf) => ({ title: cf.name, body: cf.value, bucket: cf.bucket }));
137
+ const ticketBody = [
138
+ issue.description,
139
+ ...sections.map((s) => `### ${s.title}\n${s.body}`),
140
+ ]
141
+ .filter((s) => s && s.trim())
142
+ .join("\n\n");
63
143
  return {
64
144
  jiraSummary: issue.summary,
65
145
  jiraDescription: issue.description,
146
+ jiraTicketBody: ticketBody,
66
147
  jiraStatus: issue.status ?? "",
67
148
  jiraIssueType: issue.issueType ?? "",
68
149
  jiraUrl: issue.url ?? "",
@@ -71,6 +152,7 @@ export function mapIssueToSpecContext(issue) {
71
152
  jiraAcceptanceNumbered: ac.map((text, i) => ({ n: i + 1, text })),
72
153
  jiraAcceptanceNextN: ac.length + 1,
73
154
  jiraHasAcceptance: ac.length > 0,
155
+ jiraCustomSections: sections,
74
156
  };
75
157
  }
76
158
  export function flattenAdf(node) {
@@ -82,8 +164,17 @@ export function flattenAdf(node) {
82
164
  return node.map(flattenAdf).join("");
83
165
  if (!isRecord(node))
84
166
  return "";
85
- if (typeof node["text"] === "string")
86
- return node["text"];
167
+ if (typeof node["text"] === "string") {
168
+ const text = node["text"];
169
+ const href = linkHref(node["marks"]);
170
+ return href && href !== text ? `[${text}](${href})` : text;
171
+ }
172
+ const type = str(node["type"]);
173
+ if (type === "inlineCard" || type === "blockCard" || type === "embedCard") {
174
+ const url = cardUrl(node["attrs"]);
175
+ if (url)
176
+ return type === "inlineCard" ? url : `${url}\n`;
177
+ }
87
178
  const inner = flattenAdf(node["content"]);
88
179
  const blockTypes = new Set([
89
180
  "paragraph",
@@ -94,7 +185,6 @@ export function flattenAdf(node) {
94
185
  "codeBlock",
95
186
  "blockquote",
96
187
  ]);
97
- const type = str(node["type"]);
98
188
  if (type === "hardBreak")
99
189
  return "\n";
100
190
  if (type === "listItem")
@@ -103,16 +193,40 @@ export function flattenAdf(node) {
103
193
  return `${inner}\n`;
104
194
  return inner;
105
195
  }
196
+ function linkHref(marks) {
197
+ if (!Array.isArray(marks))
198
+ return undefined;
199
+ for (const m of marks) {
200
+ if (isRecord(m) && m["type"] === "link" && isRecord(m["attrs"])) {
201
+ const href = m["attrs"]["href"];
202
+ if (typeof href === "string" && href)
203
+ return href;
204
+ }
205
+ }
206
+ return undefined;
207
+ }
208
+ function cardUrl(attrs) {
209
+ if (!isRecord(attrs))
210
+ return undefined;
211
+ const url = attrs["url"];
212
+ if (typeof url === "string" && url)
213
+ return url;
214
+ const data = attrs["data"];
215
+ if (isRecord(data) && typeof data["url"] === "string" && data["url"]) {
216
+ return data["url"];
217
+ }
218
+ return undefined;
219
+ }
106
220
  export function extractAcceptanceCriteria(fields, description) {
107
221
  for (const [k, v] of Object.entries(fields)) {
108
- if (!/acceptance/i.test(k))
222
+ if (!/acceptance|aceite|aceita[çc]/i.test(k))
109
223
  continue;
110
224
  const flat = flattenAdf(v);
111
225
  const lines = toLines(flat);
112
226
  if (lines.length)
113
227
  return lines;
114
228
  }
115
- const m = description.match(/acceptance\s+criteria\s*:?\s*\n([\s\S]+?)(?:\n\s*\n|$)/i);
229
+ const m = description.match(/(?:acceptance\s+criteria|crit[ée]rios?\s+de\s+aceita[çc][ãa]o|crit[ée]rios?\s+de\s+aceite)\s*:?\s*\n([\s\S]+?)(?:\n\s*\n|$)/i);
116
230
  if (m && m[1]) {
117
231
  const lines = toLines(m[1]);
118
232
  if (lines.length)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raptor/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js"
6
6
  }
@@ -88,18 +88,22 @@ and assets are incorporated, catalogued and present on disk — no loose ends.
88
88
  {{/if}}
89
89
 
90
90
  ## Problem Statement
91
- {{#if jiraDescription}}
92
- {{jiraDescription}}
91
+ {{#if jiraTicketBody}}
92
+ {{jiraTicketBody}}
93
93
 
94
94
  <!--
95
- Imported VERBATIM from Jira {{jira}} — this is the full ticket body, the authoritative source for this feature.
96
- The agent MUST redistribute this content into the canonical sections below, LOSING NOTHING:
95
+ Imported VERBATIM from Jira {{jira}} — the description PLUS every rich custom field
96
+ (scenarios, requirements, test caveats, documentation/design links) the card holds.
97
+ This is the authoritative source for this feature. The agent MUST redistribute ALL of
98
+ it into the canonical sections below, LOSING NOTHING (each `### <field>` block too):
97
99
  - each user story / "História" → ## User Stories (as [US#] with priority, why, independent test)
98
100
  - each functional requirement / "Requisito Funcional / RF" → ## Functional Requirements (as [FR-#])
99
101
  - each non-functional requirement / "Requisito Não Funcional / RNF" → ## Non-Functional Requirements (as [NFR-#])
102
+ - each applicable scenario / "Cenário aplicável" → ## User Scenarios & Testing (and a covering [AC-#])
100
103
  - each acceptance scenario / "Critério de Aceitação / Cenário" → ## Acceptance Criteria (as [AC-#] Given/When/Then, covering a [US#])
104
+ - test caveats / "Ressalvas de testes" → ## Edge Cases (or the relevant [NFR-#])
101
105
  - the ticket's out-of-scope / "Fora do Escopo" list → ## Out of Scope
102
- - design links (Figma etc.) → ## Dependencies & Assumptions
106
+ - design / documentation links (Figma etc.) → ## Dependencies & Assumptions
103
107
  Then REDUCE this section to a crisp problem statement (who is affected today and how). Do not leave the raw dump here.
104
108
  -->
105
109
  {{else}}{{#if description}}
@@ -36,7 +36,7 @@ export default class JiraPull extends BaseCommand {
36
36
  this.log(JSON.stringify(issue, null, 2));
37
37
  return;
38
38
  }
39
- const ctx = mapIssueToSpecContext(issue);
39
+ const ctx = mapIssueToSpecContext(issue, { fieldBuckets: conn.customFields });
40
40
  this.log(`=== ${issue.key} — ${issue.summary} ===\n`);
41
41
  if (issue.issueType)
42
42
  this.log(` Type: ${issue.issueType}`);
@@ -56,5 +56,10 @@ export default class JiraPull extends BaseCommand {
56
56
  for (const ac of ctx.jiraAcceptance)
57
57
  this.log(` - ${ac}`);
58
58
  }
59
+ for (const s of ctx.jiraCustomSections) {
60
+ this.log(`\n ${s.title} [${s.bucket}]:`);
61
+ for (const line of s.body.split("\n"))
62
+ this.log(` ${line}`);
63
+ }
59
64
  }
60
65
  }
@@ -138,7 +138,7 @@ export default class New extends BaseCommand {
138
138
  ? [
139
139
  flags.desc,
140
140
  jiraContext?.jiraSummary,
141
- jiraContext?.jiraDescription,
141
+ jiraContext?.jiraTicketBody,
142
142
  ]
143
143
  .filter(Boolean)
144
144
  .join("\n")
@@ -447,7 +447,12 @@ export default class New extends BaseCommand {
447
447
  }
448
448
  try {
449
449
  const issue = await client.getJiraIssue(conn.cloudId, key);
450
- return { ok: true, context: mapIssueToSpecContext(issue) };
450
+ return {
451
+ ok: true,
452
+ context: mapIssueToSpecContext(issue, {
453
+ fieldBuckets: conn.customFields,
454
+ }),
455
+ };
451
456
  }
452
457
  catch (err) {
453
458
  return {
@@ -12,6 +12,7 @@ export function jiraConn(config) {
12
12
  ...(j.project_key ? { projectKey: j.project_key } : {}),
13
13
  statusSync: j.status_sync !== false,
14
14
  transitions: j.transitions ?? {},
15
+ customFields: j.custom_fields ?? {},
15
16
  };
16
17
  if (provider === "mcp") {
17
18
  if (!j.mcp?.command)
@@ -112,6 +113,9 @@ function renderRefresh(issue) {
112
113
  for (const ac of issue.acceptanceCriteria)
113
114
  lines.push(`- ${ac}`);
114
115
  }
116
+ for (const cf of issue.customFields ?? []) {
117
+ lines.push("", `## ${cf.name}`, "", cf.value);
118
+ }
115
119
  if (issue.comments.length) {
116
120
  lines.push("", "## Recent comments");
117
121
  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.7.1",
3
+ "version": "0.8.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": {
@@ -29,7 +29,7 @@ const CLI = join(ROOT, "packages", "cli");
29
29
  const CORE = join(ROOT, "packages", "core");
30
30
  const OUT = join(ROOT, "build", "npm");
31
31
 
32
- const VERSION = "0.7.1";
32
+ const VERSION = "0.8.0";
33
33
 
34
34
  function log(msg) {
35
35
  process.stdout.write(` ${msg}\n`);