@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.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/docs/facts.json +1 -1
- package/install/_common.mjs +1 -1
- package/install/_plugin-skills.mjs +13 -9
- package/install/catalog-history.json +1 -1
- package/install/codex.mjs +12 -12
- package/manifest.json +59 -53
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
- package/pipeline/lib/analysis-jira-write.sh +81 -27
- package/pipeline/lib/analysis-quality.mjs +261 -0
- package/pipeline/lib/analysis-sections.mjs +105 -0
- package/pipeline/lib/jira-epic-link.sh +54 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
- package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
- package/pipeline/multi-agent-refs/analysis/render.md +20 -21
- package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
- package/pipeline/multi-agent-refs/analysis/review.md +16 -3
- package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
- package/pipeline/multi-agent-refs/analysis-template.md +21 -14
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
- package/pipeline/schemas/analysis-spec.schema.json +22 -0
- package/pipeline/schemas/prefs.schema.json +22 -1
- package/pipeline/scripts/analysis-story-body.mjs +210 -0
- package/pipeline/scripts/analysis-story-tree.mjs +73 -12
- package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
- package/pipeline/scripts/build-references.mjs +14 -1
- package/pipeline/scripts/confluence-readback.mjs +148 -0
- package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
- package/pipeline/skills/.skill-manifest.json +8 -8
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
- package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
- 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
|
-
//
|
|
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
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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,
|
|
787
|
-
[epBody,
|
|
788
|
-
[dlBody,
|
|
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(
|
|
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
|
|
887
|
-
//
|
|
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
|
-
//
|
|
941
|
-
// threads ids across the document by section
|
|
942
|
-
//
|
|
943
|
-
//
|
|
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:
|
|
1022
|
-
// a literal "6.X" match
|
|
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-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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);
|
|
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 |
|