@mmerterden/multi-agent-pipeline 20.26.0 → 20.27.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -1
  3. package/docs/facts.json +3 -3
  4. package/install/catalog-history.json +1 -1
  5. package/manifest.json +42 -34
  6. package/package.json +5 -5
  7. package/pipeline/commands/multi-agent/channels/SKILL.md +19 -17
  8. package/pipeline/lib/issue-fetcher.sh +40 -2
  9. package/pipeline/lib/jira-remote-links.sh +99 -0
  10. package/pipeline/multi-agent-refs/analysis/intake.md +2 -0
  11. package/pipeline/multi-agent-refs/analysis/render.md +4 -2
  12. package/pipeline/multi-agent-refs/channels/jira.md +10 -8
  13. package/pipeline/multi-agent-refs/channels/pr.md +11 -8
  14. package/pipeline/multi-agent-refs/channels/test-run.md +52 -0
  15. package/pipeline/multi-agent-refs/conventions-defaults.md +2 -0
  16. package/pipeline/multi-agent-refs/features/analysis-issue-links.md +42 -0
  17. package/pipeline/multi-agent-refs/features/localization-reuse.md +25 -0
  18. package/pipeline/multi-agent-refs/features/url-enrichment.md +11 -3
  19. package/pipeline/multi-agent-refs/features/visual-evidence.md +35 -9
  20. package/pipeline/multi-agent-refs/generate-issue.md +9 -11
  21. package/pipeline/multi-agent-refs/phases/phase-0-init.md +10 -6
  22. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -0
  23. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +1 -1
  24. package/pipeline/multi-agent-refs/phases/phase-5-report.md +2 -1
  25. package/pipeline/multi-agent-refs/phases.md +1 -1
  26. package/pipeline/multi-agent-refs/platform-parity.md +1 -1
  27. package/pipeline/multi-agent-refs/tracker-contract.md +10 -6
  28. package/pipeline/schemas/agent-state.schema.json +50 -0
  29. package/pipeline/schemas/prefs.schema.json +11 -0
  30. package/pipeline/schemas/unattended-policy.json +1 -0
  31. package/pipeline/scripts/_sibling-root.mjs +38 -0
  32. package/pipeline/scripts/gen-facts.mjs +5 -3
  33. package/pipeline/scripts/plan-remote-links.mjs +339 -0
  34. package/pipeline/scripts/render-flow-run.mjs +583 -0
  35. package/pipeline/scripts/render-jira-evidence.mjs +213 -0
  36. package/pipeline/skills/.skill-manifest.json +4 -4
  37. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +8 -2
  38. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  39. package/pipeline/skills/shared/external/ios-localization/SKILL.md +5 -0
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * render-jira-evidence.mjs - the evidence lines of a Jira comment.
4
+ *
5
+ * Screenshots and recordings are uploaded as Jira attachments
6
+ * (`jira-attach.sh`, which returns each file's attachment URL). This script
7
+ * turns `state.visualEvidence` into the lines that reference them.
8
+ *
9
+ * Links are the default. One full-size image embedded per scenario makes a
10
+ * comment many screens long; a link line keeps the comment readable and still
11
+ * opens the attachment in one click. `--mode thumbnails` embeds images as small
12
+ * thumbnails instead. Recordings are always links: an embedded player is the
13
+ * same length problem with a larger frame.
14
+ *
15
+ * An attachment without a URL is named and marked as attached, never linked to
16
+ * an address built by hand.
17
+ *
18
+ * Output (JSON): {summary, flow, scenarios: [{id, line}], gaps: [line]}
19
+ * summary the before/after line for the summary section, or null
20
+ * flow the flow recording line for the test scenarios section, or null
21
+ * scenarios one evidence line per scenario, placed under that scenario
22
+ * gaps one line per recorded gap, with its reason
23
+ *
24
+ * Lines are markdown; the Jira adapter's wiki conversion turns `[text](url)`
25
+ * into `[text|url]`. Thumbnail markup is already wiki syntax and passes through.
26
+ *
27
+ * Usage:
28
+ * node render-jira-evidence.mjs --state agent-state.json [--lang tr|en] [--mode links|thumbnails]
29
+ * node render-jira-evidence.mjs --measure <body file> [--limit N]
30
+ */
31
+ import { readFileSync } from "node:fs";
32
+ import { invokedDirectly } from "../lib/invoked-directly.mjs";
33
+
34
+ export const DEFAULT_LIMIT = 8000;
35
+
36
+ const LABELS = {
37
+ tr: {
38
+ evidence: "Kanıt",
39
+ before: "Önce",
40
+ after: "Sonra",
41
+ screen: "Ekran",
42
+ recording: "Kayıt",
43
+ attached: "ekte",
44
+ notUploaded: "yüklenmedi",
45
+ none: "yok",
46
+ video: "Kayıt",
47
+ },
48
+ en: {
49
+ evidence: "Evidence",
50
+ before: "Before",
51
+ after: "After",
52
+ screen: "Screen",
53
+ recording: "Recording",
54
+ attached: "attached",
55
+ notUploaded: "not uploaded",
56
+ none: "none",
57
+ video: "Recording",
58
+ },
59
+ };
60
+
61
+ // Jira wiki markup a free-text value could open: links, embeds, macros, tables.
62
+ function plain(text) {
63
+ return String(text).replace(/[[\]()|!{}]/g, "\\$&");
64
+ }
65
+
66
+ function nameOf(item) {
67
+ return item?.jiraFilename || (item?.file ? String(item.file).split(/[\\/]/).pop() : null);
68
+ }
69
+
70
+ function link(item, labels) {
71
+ const name = nameOf(item);
72
+ if (!name) return null;
73
+ const text = name.replace(/[[\]()|]/g, "\\$&");
74
+ if (item.url) return `[${text}](${item.url})`;
75
+ return `${text} (${item.jiraFilename ? labels.attached : labels.notUploaded})`;
76
+ }
77
+
78
+ // Only an uploaded attachment can be embedded; anything else would render as a broken image.
79
+ function thumb(item) {
80
+ const name = item?.jiraFilename;
81
+ // `!` and `|` would end the embed early; such a name is linked instead.
82
+ return name && !/[!|]/.test(name) ? `!${name}|thumbnail!` : null;
83
+ }
84
+
85
+ function links(items, labels) {
86
+ return (items || []).map((item) => link(item, labels)).filter(Boolean);
87
+ }
88
+
89
+ export function renderEvidence(visualEvidence, { lang = "tr", mode = "links" } = {}) {
90
+ const labels = LABELS[lang] || LABELS.en;
91
+ const empty = { summary: null, flow: null, scenarios: [], gaps: [] };
92
+ if (!visualEvidence || typeof visualEvidence !== "object") return empty;
93
+ const thumbnails = mode === "thumbnails";
94
+
95
+ let summary = null;
96
+ if (thumbnails) {
97
+ // An image that cannot be embedded is still named, as a link or as not uploaded.
98
+ const row = [...(visualEvidence.before || []), ...(visualEvidence.after || [])]
99
+ .map((item) => thumb(item) || link(item, labels))
100
+ .filter(Boolean);
101
+ if (row.length) summary = `${labels.before} / ${labels.after}\n${row.join(" ")}`;
102
+ } else {
103
+ const parts = [];
104
+ const before = links(visualEvidence.before, labels);
105
+ const after = links(visualEvidence.after, labels);
106
+ if (before.length) parts.push(`${labels.before} ${before.join(", ")}`);
107
+ if (after.length) parts.push(`${labels.after} ${after.join(", ")}`);
108
+ if (parts.length) summary = `${labels.evidence}: ${parts.join(" · ")}`;
109
+ }
110
+
111
+ let flow = null;
112
+ const video = link(visualEvidence.video, labels);
113
+ if (video) {
114
+ const tier = Number.isInteger(visualEvidence.videoTier)
115
+ ? ` · tier ${visualEvidence.videoTier}`
116
+ : "";
117
+ flow = `${labels.recording}: ${video}${tier}`;
118
+ }
119
+
120
+ const scenarios = [];
121
+ for (const scenario of visualEvidence.scenarios || []) {
122
+ const parts = [];
123
+ if (thumbnails && thumb(scenario.image)) parts.push(thumb(scenario.image));
124
+ else if (link(scenario.image, labels))
125
+ parts.push(`${labels.screen} ${link(scenario.image, labels)}`);
126
+ const recording = link(scenario.video, labels);
127
+ if (recording) parts.push(`${labels.recording} ${recording}`);
128
+ if (!parts.length) continue;
129
+ const line =
130
+ thumbnails && thumb(scenario.image)
131
+ ? parts.join(" · ")
132
+ : `${labels.evidence}: ${parts.join(" · ")}`;
133
+ scenarios.push({ id: scenario.id || null, line });
134
+ }
135
+
136
+ const gaps = (visualEvidence.gaps || [])
137
+ .filter((gap) => gap?.what)
138
+ .map((gap) => {
139
+ const what = Object.hasOwn(labels, gap.what) ? labels[gap.what] : plain(gap.what);
140
+ const label = what.charAt(0).toUpperCase() + what.slice(1);
141
+ return `${label}: ${labels.none}${gap.reason ? ` - ${plain(gap.reason)}` : ""}`;
142
+ });
143
+
144
+ return { summary, flow, scenarios, gaps };
145
+ }
146
+
147
+ /** Size of a comment body, and how many images it embeds at full size. */
148
+ export function measureBody(body, limit = DEFAULT_LIMIT) {
149
+ const text = String(body);
150
+ const embeds = [...text.matchAll(/!([^!\n|]+)(\|[^!\n]*)?!/g)].filter(
151
+ ([, , params]) => !/thumbnail/i.test(params || ""),
152
+ );
153
+ const embeddedImages = embeds.filter(([, name]) =>
154
+ /\.(?:png|jpe?g|gif|webp)$/i.test(name),
155
+ ).length;
156
+ const embeddedVideos = embeds.filter(([, name]) => /\.(?:mp4|mov|webm)$/i.test(name)).length;
157
+ return { chars: text.length, limit, over: text.length > limit, embeddedImages, embeddedVideos };
158
+ }
159
+
160
+ function main(argv) {
161
+ const opts = {};
162
+ for (let i = 0; i < argv.length; i++) {
163
+ const arg = argv[i];
164
+ if (
165
+ !["--state", "--lang", "--mode", "--measure", "--limit"].includes(arg) ||
166
+ argv[i + 1] === undefined
167
+ ) {
168
+ process.stderr.write(`render-jira-evidence: unknown or incomplete argument: ${arg}\n`);
169
+ return 2;
170
+ }
171
+ opts[arg.slice(2)] = argv[++i];
172
+ }
173
+ const read = (path) => {
174
+ try {
175
+ return readFileSync(path, "utf-8");
176
+ } catch {
177
+ process.stderr.write(`render-jira-evidence: cannot read ${path}\n`);
178
+ return null;
179
+ }
180
+ };
181
+ if (opts.measure) {
182
+ const limit = opts.limit === undefined ? DEFAULT_LIMIT : Number(opts.limit);
183
+ if (!Number.isInteger(limit) || limit <= 0) {
184
+ process.stderr.write(`render-jira-evidence: --limit must be a positive integer\n`);
185
+ return 2;
186
+ }
187
+ const body = read(opts.measure);
188
+ if (body === null) return 2;
189
+ process.stdout.write(`${JSON.stringify(measureBody(body, limit))}\n`);
190
+ return 0;
191
+ }
192
+ if (!opts.state) {
193
+ process.stderr.write("render-jira-evidence: --state or --measure is required\n");
194
+ return 2;
195
+ }
196
+ const raw = read(opts.state);
197
+ if (raw === null) return 2;
198
+ let state;
199
+ try {
200
+ state = JSON.parse(raw);
201
+ } catch {
202
+ process.stderr.write(`render-jira-evidence: ${opts.state} is not JSON\n`);
203
+ return 2;
204
+ }
205
+ const out = renderEvidence(state.visualEvidence, {
206
+ lang: opts.lang || "tr",
207
+ mode: opts.mode || "links",
208
+ });
209
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
210
+ return 0;
211
+ }
212
+
213
+ if (invokedDirectly(import.meta.url)) process.exit(main(process.argv.slice(2)));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-14T16:29:25Z",
3
+ "generatedAt": "2026-10-16T05:27:33Z",
4
4
  "skillCount": 229,
5
5
  "entries": [
6
6
  {
@@ -49,7 +49,7 @@
49
49
  },
50
50
  {
51
51
  "path": "shared/core/multi-agent-channels/SKILL.md",
52
- "sha256": "cb5e1f9e82f3fd3e7d9ad20bff7a066cafff75323044a08726e3af368738b123"
52
+ "sha256": "b55e8461a1bcdc1b4b88c42e086f599fccda2a89d7f56a29ac11e5ebc12a3623"
53
53
  },
54
54
  {
55
55
  "path": "shared/core/multi-agent-complaint-analysis/SKILL.md",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  {
59
59
  "path": "shared/core/multi-agent-create-jira/SKILL.md",
60
- "sha256": "5aad0d841e9e5b7140bb441e07819209ca64c5b6fc209d6ad586b5fbb353e377"
60
+ "sha256": "15efd1a0c661f848c2afa427015cd6c31e9f6680900111a4ab1a2e34ad705e81"
61
61
  },
62
62
  {
63
63
  "path": "shared/core/multi-agent-design-check/SKILL.md",
@@ -577,7 +577,7 @@
577
577
  },
578
578
  {
579
579
  "path": "shared/external/ios-localization/SKILL.md",
580
- "sha256": "3ee1a34bbf6c0b77e1e9f16cb7d8bc3d10f2ba7bf6d0b48e64126afcf9dbe2b6"
580
+ "sha256": "eb1e0dc8f68d4d6c29af6562186060f265ce4fadad633638d18b9aee098bfe4a"
581
581
  },
582
582
  {
583
583
  "path": "shared/external/ios-module-structure/SKILL.md",
@@ -2,7 +2,7 @@
2
2
  name: multi-agent-channels
3
3
  description: "Multi-channel reporter - Jira/Confluence/Wiki/PR description. Multi-select channels + content, humanizer pass, reviewer-preserving Bitbucket PR PUT. Phase 5 delegates to this skill; users can invoke post-hoc by Jira ID / PR URL / branch."
4
4
  user-invocable: true
5
- argument-hint: '<Jira-id | PR-url | #N | Jira-url> [--channels pr,jira,confluence,wiki] [--content normal,technical,test,auto-diff,note] [--message "..."] [--message-file path] [--dry-run] [--append] [--ready]'
5
+ argument-hint: '<Jira-id | PR-url | #N | Jira-url> [--channels pr,jira,confluence,wiki] [--content normal,technical,test,auto-diff,note,test-run] [--flow-run <runId>[,<runId>...]] [--message "..."] [--message-file path] [--dry-run] [--append] [--ready]'
6
6
  metadata:
7
7
  language: en
8
8
  ---
@@ -62,6 +62,7 @@ Rendered in English (`promptLanguage` is locked to `en`).
62
62
  │ [x] Normal analiz (Phase 1+2+4'ten - high-level impact + risks)
63
63
  │ [ ] Teknik analiz (Changes · Architecture · Dependencies)
64
64
  │ [x] Test senaryoları (Phase 1+2+4'ten)
65
+ │ [ ] Test Koşumu (Xray) (flow runner history.jsonl)
65
66
  │ [ ] PR diff'ten auto-gen (PR varsa diff'i summarize eder)
66
67
  │ [ ] Manuel not (--message / --message-file)
67
68
  └───────────────────────────────────────────────────────
@@ -73,6 +74,7 @@ Rendered in English (`promptLanguage` is locked to `en`).
73
74
  - Teknik analiz unselectable if no Phase 1 planning log AND no PR diff.
74
75
  - Auto-diff unselectable if no PR found.
75
76
  - Manuel not always available.
77
+ - Test Koşumu (Xray) unselectable when `render-flow-run.mjs --json` reports `available: false`; its `reason` is shown.
76
78
 
77
79
  ## Channel adapters
78
80
 
@@ -227,6 +229,9 @@ bash "$HOME/.copilot/lib/jira-family-write.sh" apply --state "$STATE_FILE"
227
229
  1. {step}
228
230
  2. **Expected:** {what should still behave as before}
229
231
 
232
+ ### Test Koşumu ← if "Test Koşumu (Xray)" selected
233
+ {`render-flow-run.mjs` output, unedited: Xray Test Execution, Test Set / Plan, run, results, one row per scenario. Read-only; a key the run history does not record is never written. Full contract: `channels/test-run.md`.}
234
+
230
235
  ### Notes ← if "Manuel not" selected
231
236
  {user message}
232
237
 
@@ -264,7 +269,8 @@ Target length: < 80 lines (fits one screen). Auto-diff capped at 5 lines per sec
264
269
  | Flag | Effect |
265
270
  |------|--------|
266
271
  | `--channels pr,jira,confluence,wiki` | Non-interactive channel selection. |
267
- | `--content normal,test,auto-diff,note` | Non-interactive content selection. |
272
+ | `--content normal,test,auto-diff,note,test-run` | Non-interactive content selection. |
273
+ | `--flow-run <runId>[,<runId>...]` | Runs reported by the Test Koşumu (Xray) block; wins over `state.evidence.flowRuns[]` and discovery. |
268
274
  | `--message "..."` | Manual mode - user text as source. Forces `content=note`. |
269
275
  | `--message-file path` | Manual mode, long message from file. |
270
276
  | `--append` | PR description: append instead of replace. |
@@ -44,7 +44,7 @@ A `C` section fires on: Design Reference → Figma URL; Screenshots → pasted i
44
44
  7. **Draft + clarifying questions** - compose summary (`{minedPrefix} {title}`, 255 trunc) + description from the type's **standard template** in Jira wiki markup (`h3.`), auto-sizing conditional sections, humanizer pass, in `outputLanguage`; body to a file for `jq -n --rawfile` (UTF-8 verbatim, `--data-binary @file`, no re-encode), passed through `node "$HOME/.claude/scripts/jira-wiki-escape.mjs"` first - the description field expands `:)` `(x)` `(!)` `(/)` into emoticon images. Ask ONLY genuinely unknown fields: component, epic (when usage rate is high), priority (when norm ambiguous), labels, assignee, sprint vs backlog, required customs, and any always-present section still empty (Bug Steps/Environment, or a Task/Story with no derivable Scope/AC). Never invent content the user did not supply.
45
45
  8. **Full preview + approval gate (never skipped)** - render the complete issue (all fields + full description + attachment plan, uploads default off). Ask: `Approve` / `Edit` (apply → re-render → re-ask, loop) / `Cancel` (nothing created). No flag or mode bypasses this gate.
46
46
  9. **Create** - `POST /rest/api/2/issue` with `jq -n --rawfile` payload + `curl --data-binary @file`. 400 field errors → show verbatim, re-ask those fields, re-preview.
47
- 10. **Post-create** (each failure-isolated: warn + continue): sprint chosen → `POST /rest/agile/1.0/sprint/{id}/issue` `{"issues":["KEY"]}`; Figma → `POST /rest/api/2/issue/{KEY}/remotelink`; Swagger → `remotelink` with title "API contract"; screenshots opt-in → `POST /rest/api/2/issue/{KEY}/attachments` with `X-Atlassian-Token: no-check`.
47
+ 10. **Post-create** (each failure-isolated: warn + continue): sprint chosen → `POST /rest/agile/1.0/sprint/{id}/issue` `{"issues":["KEY"]}`; Figma and Swagger → Issue Links through `plan-remote-links.mjs` + `jira-remote-links.sh apply` (one stable global id per link, so a retry adds nothing); screenshots opt-in → `POST /rest/api/2/issue/{KEY}/attachments` with `X-Atlassian-Token: no-check`.
48
48
  11. **Report** - `Created {KEY}: https://{host}/browse/{KEY}` + which post-steps ran.
49
49
 
50
50
  ## Error paths
@@ -81,6 +81,11 @@ catalogs require Xcode 26, and do not tie them to iOS 17.
81
81
  - Pick stable, hand-written keys for anything reached through a symbol. A key
82
82
  derived from the English copy renames its symbol the moment someone edits
83
83
  the wording, and every call site churns.
84
+ - When the catalog is generated from a source shared with other platforms
85
+ (mirrored by a sync bot, stamped "do not edit"), a new key is added upstream,
86
+ and only after searching that source by key and by display text: a key
87
+ another platform already added is reused, never duplicated. The procedure is
88
+ the stack toolkit's `reference/component/key-naming.md`, "Reuse before mint".
84
89
 
85
90
  ```swift
86
91
  // Localizable.xcstrings: "cart_empty" -> "Your cart is empty"