@mmerterden/multi-agent-pipeline 20.12.0 → 20.13.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 (56) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/README.tr.md +1 -1
  4. package/docs/facts.json +1 -1
  5. package/install/_common.mjs +1 -1
  6. package/install/_plugin-skills.mjs +13 -9
  7. package/install/catalog-history.json +1 -1
  8. package/install/codex.mjs +12 -12
  9. package/manifest.json +59 -53
  10. package/package.json +1 -1
  11. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
  13. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
  14. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  15. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
  16. package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
  17. package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
  18. package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
  19. package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
  20. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
  21. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
  25. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
  26. package/pipeline/lib/analysis-jira-write.sh +81 -27
  27. package/pipeline/lib/analysis-quality.mjs +261 -0
  28. package/pipeline/lib/analysis-sections.mjs +105 -0
  29. package/pipeline/lib/jira-epic-link.sh +54 -0
  30. package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
  31. package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
  32. package/pipeline/multi-agent-refs/analysis/render.md +20 -21
  33. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  34. package/pipeline/multi-agent-refs/analysis/review.md +16 -3
  35. package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
  36. package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
  37. package/pipeline/multi-agent-refs/analysis-template.md +21 -14
  38. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
  39. package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
  40. package/pipeline/schemas/analysis-spec.schema.json +22 -0
  41. package/pipeline/schemas/prefs.schema.json +22 -1
  42. package/pipeline/scripts/analysis-story-body.mjs +210 -0
  43. package/pipeline/scripts/analysis-story-tree.mjs +73 -12
  44. package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
  45. package/pipeline/scripts/build-references.mjs +14 -1
  46. package/pipeline/scripts/confluence-readback.mjs +148 -0
  47. package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
  48. package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
  49. package/pipeline/skills/.skill-manifest.json +8 -8
  50. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
  51. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
  52. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
  53. package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
  54. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
  55. package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
  56. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -0
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * confluence-readback.mjs - verify a published analysis page by reading it back.
4
+ *
5
+ * The publisher reports what it sent; this reads what the page holds. Every
6
+ * heading of the markdown must be on the page, and every local image the
7
+ * markdown embeds must be attached to it. A mismatch is reported, never retried:
8
+ * whether to republish is the caller's decision.
9
+ *
10
+ * Auth follows md2confluence-v3.py: CONFLUENCE_BASE_URL, and the token from the
11
+ * credential store under prefs .global.keychainMapping.confluence, else
12
+ * CONFLUENCE_TOKEN. The token travels only in the Authorization header.
13
+ *
14
+ * Usage:
15
+ * confluence-readback.mjs --page-id ID --markdown FILE
16
+ *
17
+ * Prints a JSON report. Exit: 0 match, 1 mismatch, 2 usage or auth, 5 a call failed.
18
+ */
19
+
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { spawnSync } from "node:child_process";
22
+ import { homedir } from "node:os";
23
+ import { basename, join } from "node:path";
24
+ import { runMain } from "../lib/fatal.mjs";
25
+
26
+ function arg(argv, flag) {
27
+ const i = argv.indexOf(flag);
28
+ return i >= 0 ? argv[i + 1] : undefined;
29
+ }
30
+
31
+ function resolveToken() {
32
+ const home = homedir();
33
+ const store = join(home, ".claude", "lib", "credential-store.sh");
34
+ const prefsPath = join(home, ".claude", "multi-agent-preferences.json");
35
+ if (existsSync(store) && existsSync(prefsPath)) {
36
+ try {
37
+ const key = JSON.parse(readFileSync(prefsPath, "utf8"))?.global?.keychainMapping?.confluence;
38
+ if (key) {
39
+ const r = spawnSync("bash", [store, "get", key], { encoding: "utf8" });
40
+ const token = (r.stdout || "").trim();
41
+ if (r.status === 0 && token) return token;
42
+ }
43
+ } catch {
44
+ // fall through to the environment
45
+ }
46
+ }
47
+ return process.env.CONFLUENCE_TOKEN || null;
48
+ }
49
+
50
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
51
+ const decode = (s) =>
52
+ s
53
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
54
+ .replace(/&([a-z]+);/gi, (m, e) => ENTITIES[e.toLowerCase()] ?? m);
55
+ const norm = (s) => s.replace(/\s+/g, " ").trim();
56
+
57
+ /** Heading texts of the markdown, outside fenced code. */
58
+ export function markdownHeadings(md) {
59
+ const out = [];
60
+ let fenced = false;
61
+ for (const line of md.split("\n")) {
62
+ if (/^\s*(```|~~~)/.test(line)) fenced = !fenced;
63
+ if (fenced) continue;
64
+ const m = line.match(/^#{1,6}\s+(.+?)\s*$/);
65
+ if (!m) continue;
66
+ const text = norm(
67
+ m[1]
68
+ .replace(/<!--.*?-->/g, "")
69
+ .replace(/[*_`]/g, "")
70
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"),
71
+ );
72
+ if (text) out.push(text);
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** File names of the local images the markdown embeds. */
78
+ export function markdownImages(md) {
79
+ const names = new Set();
80
+ for (const m of md.matchAll(/!\[[^\]]*\]\(([^)\s]+)[^)]*\)/g)) {
81
+ if (!/^[a-z]+:\/\//i.test(m[1])) names.add(basename(m[1]));
82
+ }
83
+ return [...names];
84
+ }
85
+
86
+ /** Heading texts of a storage-format body. */
87
+ export function pageHeadings(storage) {
88
+ return [...storage.matchAll(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi)].map((m) =>
89
+ norm(decode(m[2].replace(/<[^>]+>/g, ""))),
90
+ );
91
+ }
92
+
93
+ async function getJson(base, token, path) {
94
+ const res = await fetch(`${base}${path}`, {
95
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
96
+ });
97
+ if (!res.ok) throw new Error(`GET ${path} -> ${res.status}`);
98
+ return res.json();
99
+ }
100
+
101
+ async function main() {
102
+ const argv = process.argv.slice(2);
103
+ const pageId = arg(argv, "--page-id");
104
+ const mdPath = arg(argv, "--markdown");
105
+ if (!pageId || !mdPath || !existsSync(mdPath)) {
106
+ process.stderr.write("usage: confluence-readback.mjs --page-id ID --markdown FILE\n");
107
+ process.exitCode = 2;
108
+ return;
109
+ }
110
+ const base = (process.env.CONFLUENCE_BASE_URL || "").replace(/\/+$/, "");
111
+ const token = resolveToken();
112
+ if (!base || !token) {
113
+ process.stderr.write(
114
+ "ERR: CONFLUENCE_BASE_URL and a Confluence token (credential store or CONFLUENCE_TOKEN) are required\n",
115
+ );
116
+ process.exitCode = 2;
117
+ return;
118
+ }
119
+ const md = readFileSync(mdPath, "utf8");
120
+ const id = encodeURIComponent(pageId);
121
+ let content;
122
+ let attachments;
123
+ try {
124
+ content = await getJson(base, token, `/rest/api/content/${id}?expand=body.storage,version`);
125
+ attachments = await getJson(base, token, `/rest/api/content/${id}/child/attachment?limit=500`);
126
+ } catch (err) {
127
+ process.stderr.write(`ERR: ${err.message}\n`);
128
+ process.exitCode = 5;
129
+ return;
130
+ }
131
+ const onPage = new Set(pageHeadings(content?.body?.storage?.value || ""));
132
+ const attached = new Set((attachments?.results || []).map((a) => a.title));
133
+ const missingHeadings = markdownHeadings(md).filter((h) => !onPage.has(h));
134
+ const missingAttachments = markdownImages(md).filter((n) => !attached.has(n));
135
+ const ok = missingHeadings.length === 0 && missingAttachments.length === 0;
136
+ process.stdout.write(
137
+ `${JSON.stringify({
138
+ ok,
139
+ pageId,
140
+ version: content?.version?.number ?? null,
141
+ missingHeadings,
142
+ missingAttachments,
143
+ })}\n`,
144
+ );
145
+ if (!ok) process.exitCode = 1;
146
+ }
147
+
148
+ runMain("confluence-readback", main);
@@ -72,6 +72,46 @@ export function findUnescaped(text) {
72
72
  return hits;
73
73
  }
74
74
 
75
+ // Every character that opens wiki markup: macros {..}, links and mentions [..],
76
+ // table cells |, images !..!, and inline formatting * _ ^ ~ + - #.
77
+ const MARKUP = /[\\{}[\]|!*_^~+\-#]/g;
78
+
79
+ /**
80
+ * Escape plain text taken from a document so Jira renders it literally. The
81
+ * generator writes its own markup around the result; nothing inside the result
82
+ * can open a macro, a mention, an image, a link or formatting.
83
+ *
84
+ * @param {string} text
85
+ * @returns {string}
86
+ */
87
+ export function escapeJiraWiki(text) {
88
+ const escaped = String(text ?? "")
89
+ .replace(MARKUP, (c) => `\\${c}`)
90
+ .replace(/^(h[1-6]|bq)\./gm, "$1\\.");
91
+ return escapeJiraEmoticons(escaped);
92
+ }
93
+
94
+ /**
95
+ * A wiki link to an http(s) URL, or the escaped label alone for any other
96
+ * scheme. The URL is percent-encoded where a character would end the link.
97
+ *
98
+ * @param {string} label
99
+ * @param {string} url
100
+ * @returns {string}
101
+ */
102
+ export function wikiLink(label, url) {
103
+ const text = escapeJiraWiki(label);
104
+ let u;
105
+ try {
106
+ u = new URL(url);
107
+ } catch {
108
+ return text;
109
+ }
110
+ if (u.protocol !== "https:" && u.protocol !== "http:") return text;
111
+ const safe = u.href.replace(/[|[\]{}!\s]/g, (c) => encodeURIComponent(c));
112
+ return `[${text}|${safe}]`;
113
+ }
114
+
75
115
  async function readStdin() {
76
116
  const chunks = [];
77
117
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -42,6 +42,15 @@
42
42
 
43
43
  import { readFileSync } from "node:fs";
44
44
  import { runMain } from "../lib/fatal.mjs";
45
+ import { redesignSections, sectionBody, tableDataRows } from "../lib/analysis-sections.mjs";
46
+ import {
47
+ businessLayerTechnical,
48
+ citedUndefined,
49
+ designFrameUnmapped,
50
+ exceptionFlow,
51
+ missingInputsFinal,
52
+ scopeOverlap,
53
+ } from "../lib/analysis-quality.mjs";
45
54
 
46
55
  // A repo-less run (Locked 34) still splits by channel, derived from the evidence:
47
56
  // "mobile" and "web" are its platform values. "none" is the narrower case where the
@@ -51,7 +60,7 @@ const KNOWN_PLATFORMS = new Set(["ios", "android", "web", "backend", "mobile", "
51
60
  const REQUIRED_FM = ["feature", "platform", "language", "mode", "template_version"];
52
61
 
53
62
  // Never-omitted sections (Locked 2), matched by bilingual title keyword so the
54
- // re-flowed section numbering does not matter.
63
+ // profile's own numbering does not matter.
55
64
  const REQUIRED_SECTIONS = [
56
65
  { key: "summary", any: ["Summary", "Özet", "Ozet"] },
57
66
  { key: "goals", any: ["Goals", "Hedefler"] },
@@ -108,6 +117,43 @@ const BANNED_PUNCT = [
108
117
  // that blocked a correctly closed document. English has the same trap in
109
118
  // "reopened" and "opened".
110
119
  const OPEN_STATUS = ["açık", "acik", "open", "girdi bekleniyor", "pending input"];
120
+ // Words that make a requirement untestable because they carry no threshold.
121
+ // Matched as whole words in either language, case-insensitively.
122
+ const VAGUE_WORDS = [
123
+ "appropriate",
124
+ "appropriately",
125
+ "as needed",
126
+ "as appropriate",
127
+ "user-friendly",
128
+ "fast",
129
+ "quickly",
130
+ "etc.",
131
+ "and/or",
132
+ "various",
133
+ "if possible",
134
+ "reasonable",
135
+ "uygun şekilde",
136
+ "gerektiğinde",
137
+ "gerekirse",
138
+ "hızlı",
139
+ "hızlıca",
140
+ "vb.",
141
+ "vs.",
142
+ "çeşitli",
143
+ "mümkünse",
144
+ "kullanıcı dostu",
145
+ "makul",
146
+ ];
147
+
148
+ function vagueWords(text) {
149
+ const found = [];
150
+ for (const w of VAGUE_WORDS) {
151
+ const esc = w.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
152
+ if (new RegExp(`(?<![\\p{L}\\p{N}])${esc}(?![\\p{L}\\p{N}])`, "iu").test(text)) found.push(w);
153
+ }
154
+ return found;
155
+ }
156
+
111
157
  function isOpenStatus(cell) {
112
158
  return String(cell)
113
159
  .split("/")
@@ -141,51 +187,6 @@ function parseFrontMatter(text) {
141
187
  return { fm, bodyStart: end + 1 };
142
188
  }
143
189
 
144
- function sectionBody(lines, keywords, level = 2) {
145
- // Opened by a heading at exactly `level`; closed by the next heading at that
146
- // level OR ANY SHALLOWER one. A sub-section that is the last of its parent has
147
- // no sibling after it, so terminating only on equal depth runs the body into
148
- // the following section and audits its rows as if they belonged here.
149
- const open = new RegExp(`^#{${level}}\\s+\\d+(\\.\\d+)*\\.?\\s`);
150
- const close = new RegExp(`^#{1,${level}}\\s`);
151
- let start = -1;
152
- for (let i = 0; i < lines.length; i++) {
153
- if (open.test(lines[i]) && keywords.some((k) => lines[i].includes(k))) {
154
- start = i;
155
- break;
156
- }
157
- }
158
- if (start < 0) return null;
159
- let end = lines.length;
160
- for (let i = start + 1; i < lines.length; i++) {
161
- if (close.test(lines[i])) {
162
- end = i;
163
- break;
164
- }
165
- }
166
- return lines.slice(start + 1, end);
167
- }
168
-
169
- // Data rows are the pipe-rows that follow a table's separator row, which is the
170
- // only shape that distinguishes them from the header without guessing at cell text.
171
- function tableDataRows(bodyLines) {
172
- const rows = [];
173
- let afterSeparator = false;
174
- for (const line of bodyLines) {
175
- const t = line.trim();
176
- if (!t.startsWith("|")) {
177
- afterSeparator = false;
178
- continue;
179
- }
180
- if (/^\|[\s\-:|]+\|$/.test(t)) {
181
- afterSeparator = true;
182
- continue;
183
- }
184
- if (afterSeparator) rows.push(t);
185
- }
186
- return rows;
187
- }
188
-
189
190
  // The report registry. `--report` prints one line per check, and the reason a
190
191
  // check did not run is part of the answer: the traceability matrix is corporate
191
192
  // only, so on a global document it never executes - and until this existed the
@@ -763,29 +764,42 @@ function main() {
763
764
 
764
765
  mark("rule-to-test");
765
766
 
767
+ // 2g-2. Vague wording. A requirement two readers can pass differently is not
768
+ // testable, and these words carry no threshold. Only id-bearing requirement
769
+ // rows are read; summary prose may speak loosely. Code spans are skipped.
770
+ {
771
+ const ROW_ID = /\b(BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+|IG-\d{2,3}|FG-\d{2,3})\b/i;
772
+ for (const line of allLines) {
773
+ const t = line.trim();
774
+ if (!t.startsWith("|")) continue;
775
+ const id = t.match(ROW_ID);
776
+ if (!id) continue;
777
+ const words = vagueWords(t.replace(/`[^`]*`/g, ""));
778
+ if (words.length) {
779
+ warns.push(
780
+ `vague wording in ${id[1]}: ${words.map((w) => `"${w}"`).join(", ")} - state a threshold, a list or a condition instead`,
781
+ );
782
+ }
783
+ }
784
+ mark("vague-wording");
785
+ }
786
+
766
787
  // 2h. Redesign mode (Locked 36). Eight checks, live only when the front-matter
767
788
  // opts in. The contract they enforce is analysis/redesign.md, which loads only
768
789
  // on a redesign run; what is here is the enforcement, not the explanation.
769
790
  const redesign = String(parsed?.fm?.redesign || "false").toLowerCase() === "true";
770
- const cbBody = sectionBody(
771
- allLines,
772
- ["Mevcut Davranış", "Mevcut Davranis", "Current Behaviour", "Current Behavior"],
773
- 3,
774
- );
775
- const epBody = sectionBody(
776
- allLines,
777
- ["Endpoint Eşlemesi", "Endpoint Eslemesi", "Endpoint Mapping"],
778
- 3,
779
- );
780
- const dlBody = sectionBody(allLines, ["Fark Listesi", "Difference List"], 3);
791
+ const rd = redesignSections(allLines, profile);
792
+ const cbBody = rd.cb.body;
793
+ const epBody = rd.ep.body;
794
+ const dlBody = rd.dl.body;
781
795
  if (redesign) {
782
796
  // 1. The sections themselves. Without this every row check below passes over
783
797
  // nothing, which is the specific defect that shows a gate green on an empty
784
798
  // document - the same shape the corporate matrix check guards against.
785
799
  for (const [body, name] of [
786
- [cbBody, "4.5 Current Behaviour"],
787
- [epBody, "4.6 Endpoint Mapping"],
788
- [dlBody, "9.5 Difference List"],
800
+ [cbBody, rd.cb.label],
801
+ [epBody, rd.ep.label],
802
+ [dlBody, rd.dl.label],
789
803
  ]) {
790
804
  if (!body) errors.push(`redesign: true but Section ${name} is missing (Locked 36)`);
791
805
  }
@@ -866,7 +880,7 @@ function main() {
866
880
  const cellCount = (line) =>
867
881
  line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").length;
868
882
  if (!header) {
869
- errors.push("Section 4.6 has no v1/v2 header row (Locked 36)");
883
+ errors.push(`Section ${rd.ep.label} has no v1/v2 header row (Locked 36)`);
870
884
  } else {
871
885
  const cols = cellCount(header);
872
886
  for (const row of tableDataRows(epBody)) {
@@ -883,8 +897,8 @@ function main() {
883
897
  //
884
898
  // The heading match alone is not enough to say that. sectionBody tests the
885
899
  // keyword as a raw substring of the heading line - deliberately, since Locked
886
- // 2 re-flows section numbers and the corporate profile numbers them
887
- // differently - so an unrelated `### 4.9 Notes on the old Current Behaviour
900
+ // 2 drops sections and the corporate profile numbers them differently - so
901
+ // an unrelated `### 4.9 Notes on the old Current Behaviour
888
902
  // audit process` matched, and Phase 3 runs --strict, which turns this warning
889
903
  // into a blocked document. The artefact itself is the honest signal: a
890
904
  // rendered current-behaviour table always carries `CB-<slug>-NN` ids, and a
@@ -900,6 +914,43 @@ function main() {
900
914
  mark("redesign-artifacts", "not a redesign");
901
915
  }
902
916
 
917
+ // 2e. Document quality (analysis-quality.mjs): each check returns its own
918
+ // errors and warnings, so it is testable without this file.
919
+ {
920
+ const isFinal = (parsed?.fm?.status || "draft").toLowerCase() === "final";
921
+ const corporate = profile === "corporate";
922
+ const run = (id, check, skipped) => {
923
+ if (!skipped) {
924
+ const r = check();
925
+ errors.push(...r.errors);
926
+ warns.push(...r.warns);
927
+ }
928
+ mark(id, skipped);
929
+ };
930
+ run(
931
+ "exception-flow",
932
+ () => exceptionFlow(allLines, { final: isFinal }),
933
+ corporate ? undefined : "global profile",
934
+ );
935
+ run("scope-overlap", () => scopeOverlap(allLines));
936
+ run("cited-undefined", () => citedUndefined(allLines));
937
+ run(
938
+ "design-frame-unmapped",
939
+ () => designFrameUnmapped(allLines),
940
+ corporate ? undefined : "global profile",
941
+ );
942
+ run(
943
+ "business-layer-technical",
944
+ () => businessLayerTechnical(allLines),
945
+ corporate ? "corporate profile" : undefined,
946
+ );
947
+ run(
948
+ "missing-inputs",
949
+ () => missingInputsFinal(allLines, { final: isFinal }),
950
+ isFinal ? undefined : "draft document",
951
+ );
952
+ }
953
+
903
954
  // 3. Humanizer punctuation. Scope mirrors the humanizer pass exactly
904
955
  // (analysis/render.md): front-matter, fenced code blocks, table rows and
905
956
  // URLs are exempt there, so flagging them here would raise an ERROR that no
@@ -937,11 +988,10 @@ function main() {
937
988
 
938
989
  // 4b. Locked 2's numbering half.
939
990
  //
940
- // It is NOT "numbering re-flows 1..N", which could not hold: Locked 30
941
- // threads ids across the document by section
942
- // NUMBER ("Section 15.1 scenario", "Section 4.4", "Section 3/5 layout cells"),
943
- // so a re-flowed document sends every one of those references to the wrong
944
- // section. No emitted document ever re-flowed. The rule was the wrong half.
991
+ // Rendered sections keep their canonical template numbers and may leave
992
+ // gaps, because Locked 30 threads ids across the document by section NUMBER
993
+ // ("Section 15.1 scenario", "Section 4.4", "Section 3/5 layout cells"), and
994
+ // renumbering would send every one of those references to the wrong section.
945
995
  //
946
996
  // What holds instead: a rendered section keeps its canonical template number,
947
997
  // so gaps are correct and `1, 2, 4, 9, 13, 14, 21` is a valid document. Two
@@ -1018,8 +1068,8 @@ function main() {
1018
1068
  // audited - "used subset" means nothing without the set it is a subset of.
1019
1069
  let inVariant = false;
1020
1070
  for (const line of text.split("\n")) {
1021
- // Keyed on the heading text, not the number: Locked 2 re-flows numbering, so
1022
- // a literal "6.X" match silently stops running the moment the section moves.
1071
+ // Keyed on the heading text, not the number: the corporate profile numbers
1072
+ // this section differently, so a literal "6.X" match would skip it there.
1023
1073
  if (/^#{2,3}\s+\d+(\.[0-9X]+)*\.?\s/.test(line) && /Varyant|Variant/i.test(line)) {
1024
1074
  inVariant = true;
1025
1075
  continue;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-29T13:31:43Z",
3
+ "generatedAt": "2026-10-01T20:47:44Z",
4
4
  "skillCount": 218,
5
5
  "entries": [
6
6
  {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "shared/core/multi-agent-analysis-jira/SKILL.md",
16
- "sha256": "8c5916a32156bc4c75f97cac1b57dbfe9e49f16027178128d12e50f6e326ae12"
16
+ "sha256": "130543d1017115de351a1175b6b179778642ba0b3207034b54114b39d0546407"
17
17
  },
18
18
  {
19
19
  "path": "shared/core/multi-agent-analysis-resolve/SKILL.md",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  {
23
23
  "path": "shared/core/multi-agent-analysis/SKILL.md",
24
- "sha256": "fff7f2c2d4aa769a5c08beb776a848f91caf485c1d84dd5a4a3571ff02c35e0e"
24
+ "sha256": "a7cf677df00433583d5e2e1578223d1e1797a1bd4d07ff0d1d8596d94baa013a"
25
25
  },
26
26
  {
27
27
  "path": "shared/core/multi-agent-autopilot-off/SKILL.md",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  {
39
39
  "path": "shared/core/multi-agent-autopilot/SKILL.md",
40
- "sha256": "426158ddbb5e8d6e594cd2e02aceb197e1769d692602dfbd2a368a6707081bd3"
40
+ "sha256": "a9731771fbc1f45260f214f069b976eff16e7b25e2f1375b0d9a213575271bc6"
41
41
  },
42
42
  {
43
43
  "path": "shared/core/multi-agent-build-optimize/SKILL.md",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  {
67
67
  "path": "shared/core/multi-agent-doctor/SKILL.md",
68
- "sha256": "1b1450ead2c6ee6ff64a8cfd4fcefeba1084a46f2a2e5d8a8a1077b7ec3f4130"
68
+ "sha256": "2f6222cdef9c6b2b4e2e330ba0acee33ab0fb797029bb16c50bbb244be5fcbe6"
69
69
  },
70
70
  {
71
71
  "path": "shared/core/multi-agent-feedback/SKILL.md",
@@ -145,7 +145,7 @@
145
145
  },
146
146
  {
147
147
  "path": "shared/core/multi-agent-review-analysis/SKILL.md",
148
- "sha256": "f55d3b28266001d150d0438e92f8bb5dfa9cdce517892cf12fe94beee8440c07"
148
+ "sha256": "5f90d2da227e1ffb7fedd00c3a0d094bb0e0e32a26aa25a7ec503ff6e4865503"
149
149
  },
150
150
  {
151
151
  "path": "shared/core/multi-agent-review-issue/SKILL.md",
@@ -157,7 +157,7 @@
157
157
  },
158
158
  {
159
159
  "path": "shared/core/multi-agent-review/SKILL.md",
160
- "sha256": "23b38684ac14c65522ff3f1889ae2f8bab89f10a62db1d7f01f6b46228136157"
160
+ "sha256": "f1bad66314a55f35b7ac056a2f97e44c30702bebfbacc886b8efe7887eec5685"
161
161
  },
162
162
  {
163
163
  "path": "shared/core/multi-agent-route-off/SKILL.md",
@@ -201,7 +201,7 @@
201
201
  },
202
202
  {
203
203
  "path": "shared/core/multi-agent-setup/SKILL.md",
204
- "sha256": "f73388e0a64ba8a1606e3364441b078f3df6f7e99e589fb9bc8b93269a2733b2"
204
+ "sha256": "38fdbe979ab2da87dcee55d1ce988cbda5bab92f5f751f448e2e64b18113c50e"
205
205
  },
206
206
  {
207
207
  "path": "shared/core/multi-agent-stack/SKILL.md",
@@ -15,6 +15,11 @@ Produces a stakeholder-ready, platform-agnostic feature-spec document set (one m
15
15
 
16
16
  > **Language**: Per `refs/rules.md` Language Application matrix - instruction prose stays English. `AskUserQuestion.question`, `.options[].label` and `.options[].description` follow `outputLanguage`; only `header` stays English (<=12-char chip). The emitted document body follows `outputLanguage` (`tr` or `en`).
17
17
 
18
+ ## Gotchas
19
+
20
+ - Design is fetched only inside this command (Locked 29). A variant missing from the document is added by re-running analysis, never by a later phase.
21
+ - `status: final` fails the validator while any `AS-NN` is still open; publish drafts as `draft`.
22
+
18
23
  ## When to use
19
24
 
20
25
  - Before kicking off a new feature: design + API + business rules + architecture plan + test strategy crystallized into one per-platform document set
@@ -40,7 +45,7 @@ One run emits one profile. A missing input never blocks either: the gap is writt
40
45
 
41
46
  The template has 23 main sections plus footer (Glossary, Changelog). The rendered set is chosen from evidence by the omission table (Locked 2), not by a mode: a section with no evidence drops, and 19 Alternatives, 22 Glossary and 18 Rollout are default-drop.
42
47
 
43
- **Section omission**: zero-evidence sections are dropped entirely (no "TBD" placeholder); numbering re-flows to stay sequential. Sections 1, 2, 4, 9, 13, 14, 20, 21 are never omitted.
48
+ **Section omission**: zero-evidence sections are dropped entirely (no "TBD" placeholder); a rendered section keeps its canonical template number, so the rendered set has gaps. Sections 1, 2, 4, 9, 13, 14, 20, 21 are never omitted.
44
49
 
45
50
  **Two-pass render**: Pass A computes the shared logical content once; Pass B projects it onto each selected platform using conventions extracted from the repos (Phase 1c, seven pattern groups with confidence levels). Every projected cell carries an evidence footnote. A convention preview gate lets the user approve or override cells before any file renders.
46
51
 
@@ -2,7 +2,7 @@
2
2
  name: multi-agent-analysis-jira
3
3
  description: "Turn a rendered analysis document into a Jira story tree: derive stories from the document's own rule ids, check coverage both ways, preview every byte, then create only what does not already exist. Use when an analysis is final and the work needs tickets."
4
4
  user-invocable: true
5
- argument-hint: "[analysis.md] [--project KEY] - optional; with no argument, pick from the documents this run emitted"
5
+ argument-hint: "[analysis.md] [--project KEY] [--epic KEY] - optional; with no argument, pick from the documents this run emitted"
6
6
  metadata:
7
7
  language: en
8
8
  not-for: create-jira, jira
@@ -18,13 +18,26 @@ invents a story: every node comes from an id the document defines.
18
18
  Contract, severities and reasoning:
19
19
  `$HOME/.claude/multi-agent-refs/features/analysis-jira.md`.
20
20
 
21
+ ## Gotchas
22
+
23
+ - An issue whose label already exists is skipped, never updated. Editing the document and re-running creates only what is missing; existing tickets keep their old text.
24
+ - A document with no `BR-`/`FG-` ids still plans, from its `4.N` sub-sections, but its coverage verdict is `unverifiable`: there is nothing to check the tree against.
25
+ - An open `EKLENECEK`/`TBD` marker or an open Section 20 row stops planning with exit 4. Resolve them with `/multi-agent-analysis-resolve` first.
26
+ - The epic must already exist: stories are linked to it, never an epic created. On Server/Data Center the Epic Link field is discovered from the site; when none is found the stories are created unlinked and the writer says so.
27
+ - Channel clones (`issueTree.cloneByChannel`) carry their own labels and are searched like stories, so a re-run never doubles them or their links.
28
+ - Story bodies hold only document text. A body warning (no user story, fewer than 2 criteria, no negative criterion) is a gap in the document; fix the document, not the ticket.
29
+
21
30
  ## Phase 1 - Plan, offline
22
31
 
23
32
  ```bash
24
- node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" --json > /tmp/ma-plan.json
25
- node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>"
33
+ node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" --json $PAGE > /tmp/ma-plan.json
34
+ node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" $PAGE
26
35
  ```
27
36
 
37
+ `$PAGE` is `--page-url <url>` when the document was published (the run's
38
+ `outputs.confluencePages[]`); without it the front-matter `confluence_url` is
39
+ used, and with neither the story bodies carry no analysis link.
40
+
28
41
  Exit 4 means the document still carries an open placeholder. Stop and report it:
29
42
  the step is `/multi-agent:analysis-resolve`, and a tree built from an open
30
43
  question publishes the gap as work somebody is now assigned.
@@ -48,12 +61,19 @@ what makes a preview meaningful:
48
61
  - every field beside the pref key it came from, so a wrong setting shows here
49
62
  rather than in Jira afterwards
50
63
  - the write count
64
+ - every story's full description, as it will be written, with its body
65
+ warnings; `--brief` gives the one-line-per-node view
66
+
67
+ Then ask for the epic, once. `AskUserQuestion`:
68
+
69
+ - **Link to an existing epic** - the user types its key (`--epic <KEY>`)
70
+ - **No epic** - the stories are created unlinked
51
71
 
52
72
  Then the dry run, which is what proves the writer agrees with the plan:
53
73
 
54
74
  ```bash
55
75
  bash "$HOME/.claude/lib/analysis-jira-write.sh" \
56
- --plan /tmp/ma-plan.json --project "<KEY>" --dry-run
76
+ --plan /tmp/ma-plan.json --project "<KEY>" [--epic "<EPIC>"] --dry-run
57
77
  ```
58
78
 
59
79
  ## Phase 3 - Approve
@@ -79,11 +99,14 @@ be reachable.
79
99
  invented id means the plan cites something the document does not define - treat
80
100
  that as a defect in the plan, not a warning to click past.
81
101
 
102
+ Body warnings are printed before the question too. They do not block: the
103
+ ticket says only what the document says, and the warning names the gap.
104
+
82
105
  ## Phase 4 - Write
83
106
 
84
107
  ```bash
85
108
  bash "$HOME/.claude/lib/analysis-jira-write.sh" \
86
- --plan /tmp/ma-plan.json --project "<KEY>"
109
+ --plan /tmp/ma-plan.json --project "<KEY>" [--epic "<EPIC>"]
87
110
  ```
88
111
 
89
112
  It searches by label first and skips what exists. Re-running is safe and is the
@@ -93,9 +116,36 @@ findable rather than duplicable.
93
116
  **Never pass `--force`-like flags, and never update an existing node.** Jira has
94
117
  no backup path for fields other than description.
95
118
 
119
+ ### Write the tickets back
120
+
121
+ The document lists what it produced. Local and idempotent: the table sits
122
+ between `<!-- tickets:start -->` and `<!-- tickets:end -->`, a re-run replaces
123
+ it, and only nodes with a key appear.
124
+
125
+ ```bash
126
+ LEDGER="$HOME/.claude/logs/multi-agent/_analysis-jira/$(jq -r '.document.id // "unidentified"' /tmp/ma-plan.json).jsonl"
127
+ node "$HOME/.claude/scripts/analysis-tickets-writeback.mjs" \
128
+ --doc "<analysis.md>" --ledger "$LEDGER" --plan /tmp/ma-plan.json
129
+ ```
130
+
131
+ When the document has a Confluence page, updating it is a **separate**
132
+ `AskUserQuestion`, never part of the tree approval:
133
+
134
+ - **Update the Confluence page** - publish the document with its new table
135
+ - **Keep it local** - the page stays as it is
136
+
137
+ ```bash
138
+ python3 "$HOME/.claude/lib/md2confluence-v3.py" update \
139
+ --page-id "<pageId>" --title "<page title>" \
140
+ --markdown "<analysis.md>" --attachments-dir "$(dirname "<analysis.md>")"
141
+ ```
142
+
143
+ Read its JSON envelope as the analysis render step does, and surface any
144
+ missing-attachment warning.
145
+
96
146
  ## Phase 5 - Report
97
147
 
98
- Keys created, keys skipped, the coverage verdict, and the ledger path. If the
148
+ Keys created, keys skipped, the epic linked (or none), the coverage verdict, and the ledger path. If the
99
149
  verdict was `unverifiable`, say that in the report too - not only at approval
100
150
  time, because the report is what gets pasted elsewhere.
101
151
 
@@ -13,6 +13,12 @@ metadata:
13
13
 
14
14
  Run the task end-to-end with no confirmations.
15
15
 
16
+ ## Gotchas
17
+
18
+ - The guard decides within a 5-second internal deadline and fails closed when unattended. On a heavily loaded machine a correct command can be refused for time, not for policy.
19
+ - An unattended run never pushes or opens a PR itself: it records the request in `pr-request.json` and the runner publishes it.
20
+ - An unattended run cannot run `/multi-agent-stack` or write `.claude/` in the repo; a missing stack toolkit is read from a local marketplace clone instead.
21
+
16
22
  ## What changes
17
23
 
18
24
  | Phase | Normal | Autopilot |