@mmerterden/multi-agent-pipeline 20.11.1 → 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 (71) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +1 -1
  3. package/README.tr.md +1 -1
  4. package/docs/facts.json +2 -2
  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/claude.mjs +9 -0
  9. package/install/codex.mjs +12 -12
  10. package/manifest.json +74 -67
  11. package/package.json +5 -5
  12. package/pipeline/agents/code-reviewer.md +1 -1
  13. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  14. package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
  15. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
  16. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
  18. package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
  19. package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
  20. package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
  21. package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/model/SKILL.md +7 -0
  25. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  26. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
  27. package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
  28. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
  29. package/pipeline/commands/multi-agent/sync/SKILL.md +5 -6
  30. package/pipeline/lib/agent-model-sync.mjs +95 -0
  31. package/pipeline/lib/analysis-jira-write.sh +81 -27
  32. package/pipeline/lib/analysis-quality.mjs +261 -0
  33. package/pipeline/lib/analysis-sections.mjs +105 -0
  34. package/pipeline/lib/jira-epic-link.sh +54 -0
  35. package/pipeline/lib/model-rung.sh +7 -0
  36. package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
  37. package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
  38. package/pipeline/multi-agent-refs/analysis/render.md +20 -21
  39. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  40. package/pipeline/multi-agent-refs/analysis/review.md +16 -3
  41. package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
  42. package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
  43. package/pipeline/multi-agent-refs/analysis-template.md +21 -14
  44. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
  45. package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
  46. package/pipeline/multi-agent-refs/features/doctor.md +10 -0
  47. package/pipeline/multi-agent-refs/features/model-fallback.md +10 -0
  48. package/pipeline/multi-agent-refs/features/phone-api.md +1 -1
  49. package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
  50. package/pipeline/multi-agent-refs/features/usage-reporting.md +13 -0
  51. package/pipeline/schemas/analysis-spec.schema.json +22 -0
  52. package/pipeline/schemas/prefs.schema.json +22 -1
  53. package/pipeline/scripts/analysis-story-body.mjs +210 -0
  54. package/pipeline/scripts/analysis-story-tree.mjs +73 -12
  55. package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
  56. package/pipeline/scripts/build-references.mjs +14 -1
  57. package/pipeline/scripts/confluence-readback.mjs +148 -0
  58. package/pipeline/scripts/doctor.mjs +48 -0
  59. package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
  60. package/pipeline/scripts/migrate-prefs.mjs +14 -0
  61. package/pipeline/scripts/unattended_policy.py +12 -5
  62. package/pipeline/scripts/usage-report.mjs +84 -9
  63. package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
  64. package/pipeline/skills/.skill-manifest.json +8 -8
  65. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
  66. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
  67. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
  68. package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
  69. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
  70. package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
  71. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * analysis-quality.mjs - document-quality checks on a rendered analysis
3
+ * document: every use case says how it fails, the document does not contradict
4
+ * itself, each layer carries only its own kind of content, and a design frame
5
+ * belongs to a use case.
6
+ *
7
+ * Each check takes the document's lines and returns `{errors, warns}` so the
8
+ * validator stays the only place that prints and decides the exit code.
9
+ *
10
+ * @module pipeline/lib/analysis-quality
11
+ */
12
+
13
+ import { sectionBody, tableDataRows } from "./analysis-sections.mjs";
14
+
15
+ const cellsOf = (row) =>
16
+ row
17
+ .trim()
18
+ .replace(/^\||\|$/g, "")
19
+ .split("|")
20
+ .map((c) => c.trim());
21
+ const unbold = (s) => s.replace(/\*\*/g, "").trim();
22
+ const isHeading = (l) => /^#{1,6}\s/.test(l);
23
+ const isNumberedHeading = (l) => /^#{2,4}\s+\d+(\.\d+)*\.?\s/.test(l);
24
+ const UC_HEADING = /\bUC-\d{2,3}\b/;
25
+
26
+ const MAIN_FLOW = ["Ana Akış", "Ana Akis", "Main Flow"];
27
+ const EXCEPTION_FLOW = ["İstisna Akışı", "Istisna Akisi", "Exception Flow"];
28
+ const EXPLICIT_NONE = /^(yok|none)\s*:\s*\S/i;
29
+
30
+ /** Use-case sections: from a heading naming `UC-NNN` up to the next numbered heading. */
31
+ function useCaseBlocks(lines) {
32
+ const blocks = [];
33
+ let current = null;
34
+ for (const l of lines) {
35
+ if (isHeading(l)) {
36
+ const uc = l.match(UC_HEADING);
37
+ if (uc && isNumberedHeading(l)) {
38
+ current = { id: uc[0], lines: [] };
39
+ blocks.push(current);
40
+ continue;
41
+ }
42
+ if (isNumberedHeading(l) || /^#\s/.test(l)) current = null;
43
+ }
44
+ if (current) current.lines.push(l);
45
+ }
46
+ return blocks;
47
+ }
48
+
49
+ function fieldRow(lines, names) {
50
+ for (const l of lines) {
51
+ if (!l.trim().startsWith("|")) continue;
52
+ const c = cellsOf(l);
53
+ if (c.length >= 2 && names.some((n) => unbold(c[0]).toLowerCase() === n.toLowerCase())) {
54
+ return c[1];
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * A use case with a main flow states its exception flow. Missing is an error on
62
+ * a final document and a warning on a draft; "-" or empty is a warning;
63
+ * "Yok: <reason>" / "None: <reason>" is an explicit answer.
64
+ */
65
+ export function exceptionFlow(lines, { final = false } = {}) {
66
+ const errors = [];
67
+ const warns = [];
68
+ for (const uc of useCaseBlocks(lines)) {
69
+ if (fieldRow(uc.lines, MAIN_FLOW) === undefined) continue;
70
+ const v = fieldRow(uc.lines, EXCEPTION_FLOW);
71
+ if (v === undefined) {
72
+ const msg = `${uc.id} has a main flow but no exception flow row (İstisna Akışı / Exception Flow)`;
73
+ (final ? errors : warns).push(msg);
74
+ } else if (!v || /^\p{Pd}$/u.test(v)) {
75
+ warns.push(
76
+ `${uc.id} exception flow is empty; write the failure path, or "Yok: <reason>" when there is none`,
77
+ );
78
+ } else if (/^(yok|none)\.?$/i.test(v) && !EXPLICIT_NONE.test(v)) {
79
+ warns.push(`${uc.id} exception flow says none without a reason ("Yok: <reason>")`);
80
+ }
81
+ }
82
+ return { errors, warns };
83
+ }
84
+
85
+ const normalize = (s) =>
86
+ s
87
+ .toLocaleLowerCase("tr")
88
+ .replace(/[.,;:!?"'`*_]/g, "")
89
+ .replace(/\s+/g, " ")
90
+ .trim();
91
+
92
+ const GOALS_KEYWORDS = [
93
+ "Hedefler ve Karşı Hedefler",
94
+ "Hedefler ve Karsi Hedefler",
95
+ "Goals and Non-Goals",
96
+ "Goals",
97
+ ];
98
+
99
+ /** The same item on both sides of the goals table. */
100
+ export function scopeOverlap(lines) {
101
+ const errors = [];
102
+ const body = sectionBody(lines, GOALS_KEYWORDS);
103
+ if (!body) return { errors, warns: [] };
104
+ const goals = new Map();
105
+ const nonGoals = new Set();
106
+ for (const row of tableDataRows(body)) {
107
+ const c = cellsOf(row);
108
+ if (c.length < 2) continue;
109
+ if (c[0] && c[0] !== "-") goals.set(normalize(c[0]), c[0]);
110
+ if (c[1] && c[1] !== "-") nonGoals.add(normalize(c[1]));
111
+ }
112
+ for (const [key] of goals) {
113
+ if (key && nonGoals.has(key)) {
114
+ errors.push(`"${key}" is both in scope and out of scope in the goals table (Locked 14)`);
115
+ }
116
+ }
117
+ return { errors, warns: [] };
118
+ }
119
+
120
+ const CITABLE = [
121
+ /\bBR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+\b/gi,
122
+ /\bCB-[a-z0-9]+(?:-[a-z0-9]+)*-\d+\b/gi,
123
+ /\b(?:IG|FG)-\d{2,3}\b/g,
124
+ /\bUC-\d{2,3}\b/g,
125
+ ];
126
+
127
+ /**
128
+ * Ids the text cites but never defines. An id is defined by the first cell of a
129
+ * table row or by a heading. Section 20 `AS-NN` ids have their own pairing check.
130
+ */
131
+ export function citedUndefined(lines) {
132
+ const defined = new Set();
133
+ const cited = new Set();
134
+ for (const l of lines) {
135
+ const t = l.trim();
136
+ const sources = [];
137
+ if (isHeading(t)) {
138
+ sources.push([t, defined]);
139
+ } else if (t.startsWith("|")) {
140
+ const c = cellsOf(t);
141
+ sources.push([unbold(c[0] || ""), defined], [c.slice(1).join(" | "), cited]);
142
+ } else {
143
+ sources.push([t, cited]);
144
+ }
145
+ for (const [chunk, set] of sources) {
146
+ for (const re of CITABLE) {
147
+ for (const m of chunk.matchAll(re)) set.add(m[0].toUpperCase());
148
+ }
149
+ }
150
+ }
151
+ const errors = [...cited]
152
+ .filter((id) => !defined.has(id))
153
+ .sort()
154
+ .map((id) => `${id} is cited but never defined (no table row or heading names it)`);
155
+ return { errors, warns: [] };
156
+ }
157
+
158
+ const FIGMA_FRAME = /https:\/\/(?:www\.)?figma\.com\/[^\s)|\]]*node-id=([0-9]+[-:][0-9]+)/g;
159
+ const REFERENCES = /Referans|References/i;
160
+
161
+ /** A Figma frame linked outside every use case and outside the references (corporate). */
162
+ export function designFrameUnmapped(lines) {
163
+ const warns = [];
164
+ let inUc = false;
165
+ let inRefs = false;
166
+ for (const l of lines) {
167
+ if (isHeading(l)) {
168
+ if (isNumberedHeading(l) || /^#\s/.test(l)) {
169
+ inUc = UC_HEADING.test(l) && isNumberedHeading(l);
170
+ inRefs = REFERENCES.test(l) && /^##\s/.test(l);
171
+ } else if (/^##\s/.test(l)) {
172
+ inUc = false;
173
+ inRefs = false;
174
+ }
175
+ continue;
176
+ }
177
+ if (inUc || inRefs) continue;
178
+ for (const m of l.matchAll(FIGMA_FRAME)) {
179
+ warns.push(
180
+ `design frame ${m[1]} is linked outside every use case; list it under the use case it serves`,
181
+ );
182
+ }
183
+ }
184
+ return { errors: [], warns };
185
+ }
186
+
187
+ const PART_A = new Set(["1", "2", "3", "4", "18", "19", "20"]);
188
+ const REDESIGN_EVIDENCE = [
189
+ "Mevcut Davranış",
190
+ "Mevcut Davranis",
191
+ "Current Behaviour",
192
+ "Current Behavior",
193
+ "Endpoint Eşlemesi",
194
+ "Endpoint Eslemesi",
195
+ "Endpoint Mapping",
196
+ ];
197
+ const CODE_PATH =
198
+ /[\w.-]+(?:\/[\w.-]+)+\.(?:swift|kt|kts|java|tsx?|jsx?|mjs|py|mm?|go|rb|cs|dart)\b/g;
199
+ const ENDPOINT = /\b(?:GET|POST|PUT|PATCH|DELETE)\s+\/[\w{}/.-]*/g;
200
+
201
+ /**
202
+ * File paths and endpoints in the business layer (Part A: sections 1-4, 18-20) of
203
+ * the global profile. Collapsible `<details>` blocks and the redesign evidence
204
+ * tables are where such evidence belongs, so they are not read.
205
+ */
206
+ export function businessLayerTechnical(lines) {
207
+ const warns = [];
208
+ let section = null;
209
+ let skip = false;
210
+ let details = 0;
211
+ for (const l of lines) {
212
+ const top = l.match(/^##\s+(\d+)\.?\s/);
213
+ if (top) {
214
+ section = top[1];
215
+ skip = false;
216
+ continue;
217
+ }
218
+ if (/^#\s/.test(l)) {
219
+ section = null;
220
+ continue;
221
+ }
222
+ if (/^#{3,4}\s/.test(l)) {
223
+ skip = REDESIGN_EVIDENCE.some((k) => l.includes(k));
224
+ continue;
225
+ }
226
+ details += (l.match(/<details\b/gi) || []).length;
227
+ const closes = (l.match(/<\/details>/gi) || []).length;
228
+ const inDetails = details > 0;
229
+ details = Math.max(0, details - closes);
230
+ if (!section || !PART_A.has(section) || skip || inDetails) continue;
231
+ for (const re of [CODE_PATH, ENDPOINT]) {
232
+ for (const m of l.matchAll(re)) {
233
+ warns.push(
234
+ `Section ${section} (business layer) names "${m[0].trim()}"; move it to Part B/C or into a <details> evidence block`,
235
+ );
236
+ }
237
+ }
238
+ }
239
+ return { errors: [], warns };
240
+ }
241
+
242
+ const PANEL = /^>\s*\*\*(Missing inputs|Eksik girdiler)\s*:?\s*\*\*/i;
243
+
244
+ /** A final document that still lists missing inputs. */
245
+ export function missingInputsFinal(lines, { final = false } = {}) {
246
+ if (!final) return { errors: [], warns: [] };
247
+ const start = lines.findIndex((l) => PANEL.test(l.trim()));
248
+ if (start < 0) return { errors: [], warns: [] };
249
+ let items = 0;
250
+ for (const l of lines.slice(start + 1)) {
251
+ const t = l.trim();
252
+ if (!t.startsWith(">")) break;
253
+ if (/^>\s*[-*]\s+\S/.test(t)) items++;
254
+ }
255
+ return {
256
+ errors: [],
257
+ warns: items
258
+ ? [`the document is final but its missing inputs panel lists ${items} input(s)`]
259
+ : [],
260
+ };
261
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * analysis-sections.mjs - locate sections and table rows in a rendered
3
+ * analysis document.
4
+ *
5
+ * Shared by the document validator and the story-tree planner, so both read a
6
+ * section the same way. Sections are found by heading keyword, not by number:
7
+ * the global and corporate profiles number the same content differently, and a
8
+ * rendered document keeps canonical numbers with gaps.
9
+ *
10
+ * @module pipeline/lib/analysis-sections
11
+ */
12
+
13
+ /**
14
+ * Body lines of the first numbered heading at one of `levels` whose text holds
15
+ * one of `keywords`. Closed by the next heading at the same level or shallower,
16
+ * so the last sub-section of a parent does not run into the following section.
17
+ *
18
+ * @param {string[]} lines
19
+ * @param {string[]} keywords
20
+ * @param {number|number[]} levels
21
+ * @returns {string[]|null}
22
+ */
23
+ export function sectionBody(lines, keywords, levels = 2) {
24
+ for (const level of [].concat(levels)) {
25
+ const open = new RegExp(`^#{${level}}\\s+\\d+(\\.\\d+)*\\.?\\s`);
26
+ const close = new RegExp(`^#{1,${level}}\\s`);
27
+ const start = lines.findIndex((l) => open.test(l) && keywords.some((k) => l.includes(k)));
28
+ if (start < 0) continue;
29
+ let end = lines.length;
30
+ for (let i = start + 1; i < lines.length; i++) {
31
+ if (close.test(lines[i])) {
32
+ end = i;
33
+ break;
34
+ }
35
+ }
36
+ return lines.slice(start + 1, end);
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Data rows of the tables in `bodyLines`: the pipe-rows after a separator row,
43
+ * which is the only shape that tells them from the header without reading cells.
44
+ *
45
+ * @param {string[]} bodyLines
46
+ * @returns {string[]}
47
+ */
48
+ export function tableDataRows(bodyLines) {
49
+ const rows = [];
50
+ let afterSeparator = false;
51
+ for (const line of bodyLines) {
52
+ const t = line.trim();
53
+ if (!t.startsWith("|")) {
54
+ afterSeparator = false;
55
+ continue;
56
+ }
57
+ if (/^\|[\s\-:|]+\|$/.test(t)) {
58
+ afterSeparator = true;
59
+ continue;
60
+ }
61
+ if (afterSeparator) rows.push(t);
62
+ }
63
+ return rows;
64
+ }
65
+
66
+ const CB_KEYWORDS = ["Mevcut Davranış", "Mevcut Davranis", "Current Behaviour", "Current Behavior"];
67
+ const EP_KEYWORDS = ["Endpoint Eşlemesi", "Endpoint Eslemesi", "Endpoint Mapping"];
68
+ const DL_KEYWORDS = ["Fark Listesi", "Difference List"];
69
+
70
+ /**
71
+ * Where each redesign artefact lives per profile. The global profile has them as
72
+ * sub-sections 4.5, 4.6 and 9.5; the corporate profile nests them under its own
73
+ * spine (2.1.1, 2.3.1 and the endpoint mapping under Section 5 service details).
74
+ */
75
+ export const REDESIGN_SECTIONS = {
76
+ global: {
77
+ cb: { keywords: CB_KEYWORDS, levels: [3], label: "4.5 Current Behaviour" },
78
+ ep: { keywords: EP_KEYWORDS, levels: [3], label: "4.6 Endpoint Mapping" },
79
+ dl: { keywords: DL_KEYWORDS, levels: [3], label: "9.5 Difference List" },
80
+ },
81
+ corporate: {
82
+ cb: { keywords: CB_KEYWORDS, levels: [4], label: "2.1.1 Mevcut Davranış / Current Behaviour" },
83
+ ep: {
84
+ keywords: EP_KEYWORDS,
85
+ levels: [4],
86
+ label: "5.(N+4).1 Endpoint Eşlemesi / Endpoint Mapping",
87
+ },
88
+ dl: { keywords: DL_KEYWORDS, levels: [4], label: "2.3.1 Fark Listesi / Difference List" },
89
+ },
90
+ };
91
+
92
+ /**
93
+ * The three redesign artefact bodies for a profile, plus their labels.
94
+ *
95
+ * @param {string[]} lines
96
+ * @param {string} profile
97
+ */
98
+ export function redesignSections(lines, profile) {
99
+ const spec = REDESIGN_SECTIONS[profile] || REDESIGN_SECTIONS.global;
100
+ const out = {};
101
+ for (const [key, s] of Object.entries(spec)) {
102
+ out[key] = { body: sectionBody(lines, s.keywords, s.levels), label: s.label };
103
+ }
104
+ return out;
105
+ }
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # jira-epic-link.sh - which fields link a new story to an existing epic.
3
+ #
4
+ # Jira links a story to its epic in two different ways. Cloud uses the standard
5
+ # `parent` field; Server and Data Center use an "Epic Link" custom field whose id
6
+ # differs per site. The id is discovered from the field list by its schema type,
7
+ # never assumed. When no way is found the story is created without a link and a
8
+ # warning says so, rather than writing a guessed field.
9
+ #
10
+ # Pure: the caller fetches /rest/api/2/serverInfo and /rest/api/2/field and
11
+ # passes the JSON in, so the decision is testable offline.
12
+ #
13
+ # epic_link_fields <mode> <epicKey> <fieldId> <serverInfoJson> <fieldsJson>
14
+ # mode: auto | parent | epicLinkField | none
15
+ # Prints a JSON object to merge into the create body's `fields`.
16
+
17
+ EPIC_LINK_SCHEMA="com.pyxis.greenhopper.jira:gh-epic-link"
18
+
19
+ epic_link_fields() {
20
+ local mode="${1:-auto}" epic="$2" field_id="$3" server="$4" fields="$5" deployment found
21
+ command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required to link an epic" >&2; return 3; }
22
+ if [ -z "$epic" ] || [ "$mode" = "none" ]; then
23
+ printf '{}\n'
24
+ return 0
25
+ fi
26
+ case "$mode" in
27
+ parent)
28
+ jq -cn --arg e "$epic" '{parent: {key: $e}}'
29
+ return 0
30
+ ;;
31
+ epicLinkField)
32
+ if [ -z "$field_id" ]; then
33
+ echo "WARN: epicLinkField mode needs issueTree.epicLinkFieldId; the epic is not linked" >&2
34
+ printf '{}\n'
35
+ return 0
36
+ fi
37
+ jq -cn --arg f "$field_id" --arg e "$epic" '{($f): $e}'
38
+ return 0
39
+ ;;
40
+ esac
41
+ deployment="$(printf '%s' "$server" | jq -r '.deploymentType // empty' 2>/dev/null || true)"
42
+ if [ "$deployment" = "Cloud" ]; then
43
+ jq -cn --arg e "$epic" '{parent: {key: $e}}'
44
+ return 0
45
+ fi
46
+ found="${field_id:-$(printf '%s' "$fields" | jq -r --arg s "$EPIC_LINK_SCHEMA" \
47
+ '[.[]? | select(.schema.custom == $s) | .id] | first // empty' 2>/dev/null || true)}"
48
+ if [ -z "$found" ]; then
49
+ echo "WARN: no epic-link field on this site; the stories are created without an epic" >&2
50
+ printf '{}\n'
51
+ return 0
52
+ fi
53
+ jq -cn --arg f "$found" --arg e "$epic" '{($f): $e}'
54
+ }
@@ -128,6 +128,13 @@ fi
128
128
  mv "$TMP" "$PREFS"
129
129
  trap - EXIT
130
130
 
131
+ # Agents spawned without an explicit model run on their frontmatter, so the
132
+ # installed fable-tier agents follow the switch in the same step.
133
+ if command -v node >/dev/null 2>&1 && [ -f "$_MA_MR_HERE/agent-model-sync.mjs" ]; then
134
+ node "$_MA_MR_HERE/agent-model-sync.mjs" --prefs "$PREFS" ||
135
+ echo "model-rung: installed agents could not be updated; run node $_MA_MR_HERE/agent-model-sync.mjs" >&2
136
+ fi
137
+
131
138
  echo "fable rung: $CUR -> $WANT"
132
139
  echo "cost pricing: $PRICING -> $WANT_PRICING"
133
140
  echo ""
@@ -46,7 +46,7 @@ behind.
46
46
  ### Full list
47
47
 
48
48
  1. **One feature per run.** Every Figma URL, Confluence page, Jira ID, and Standards source the user supplies belongs to the **same feature**. The command never asks "which feature is this for?" or "which URL is primary?". Mixed inputs covering multiple features are treated as user error: surface the conflict, stop, and ask the user to split into separate runs.
49
- 2. **Section omission rule.** Sections with zero evidence are dropped entirely; no `TBD` placeholder section. **A rendered section keeps its canonical template number**, so the set has gaps and that is correct: `1, 2, 4, 9, 13, 14, 21` is a valid rendered document. Numbering does NOT re-flow sequentially `1..N`: that cannot hold alongside Locked 30, which threads ids across sections BY NUMBER (`Section 15.1 scenario`, `Section 4.4`, `Section 3/5 layout cells`), so a re-flowed document sends every one of those cross-references to the wrong section or to nothing. What IS checked: every rendered number is a real template number, and the numbers ascend without repeating.
49
+ 2. **Section omission rule.** Sections with zero evidence are dropped entirely; no `TBD` placeholder section. **A rendered section keeps its canonical template number**, so the set has gaps and that is correct: `1, 2, 4, 9, 13, 14, 21` is a valid rendered document. Numbers are never rewritten into a sequential `1..N`, because Locked 30 threads ids across sections BY NUMBER (`Section 15.1 scenario`, `Section 4.4`, `Section 3/5 layout cells`) and renumbering would send every one of those cross-references to the wrong section or to nothing. What IS checked: every rendered number is a real template number, and the numbers ascend without repeating.
50
50
  3. **Citation discipline.** Every quoted UI string, endpoint path, error code, or analytics event name in Sections 2-4 must carry an inline citation: `[Figma annotation <nodeId>]` for copy taken from a Dev Mode annotation, `[Figma <nodeId>]` (MCP) or `[figma-export: <project-slug>/<screen-slug>:<nodeId>]` (local source) for design strings; `file:line` for repo evidence; `Confluence:<pageId>:<heading-slug>` for spec text. **Annotation-as-copy precedence:** when the project's `figma-config` has `annotations.enabled` and a node carries a Dev Mode annotation, that annotation is the authoritative copy for the node and the visible text layer is treated as a placeholder; cite the annotation, not the layer text. Never invent copy - blank beats a guess; a node whose annotation has the base language but is missing a target language emits a Section 20 (Risks) row rather than a fabricated value. Uncited quotes are downgraded to `[label TBD - see Open Questions]` and a row is added to Section 7 Risks. Subagent prose and Code Connect snippets are not citations.
51
51
  4. **The spec is forward-looking.** Section bodies describe the new feature as drawn / specified. Findings that exist only in legacy code or the existing branch appear as `> Legacy reference: <text> (file:line)` blockquotes inside the relevant section, never as the lead sentence or a primary table row. A legacy-only finding with no forward counterpart goes to Section 7 Risks as a decision item: "current code does X; should the new feature keep, change, or drop this?".
52
52
  5. **Output default = Local file.** The Phase 3.5 output picker keeps `Local file` pre-selected. Confluence and Jira outputs are never default-selected (see `analysis-output-confluence-on-request` memory).
@@ -56,13 +56,13 @@ behind.
56
56
  9. **Per-platform output split.** One markdown file is produced per selected platform under `analysis/<feature>-<platform>.md`. There is no merged single file. Section duplication follows the A3 hybrid table in `$HOME/.claude/multi-agent-refs/analysis-template.md` (Sections 1 + 4 duplicated verbatim; Sections 2, 3, 5, 6, 7 projected per platform). Each file carries a YAML front-matter header naming the feature, platform, generated-at timestamp, sibling file list, and binding standards source.
57
57
  10. **Output destination is asked after drafts exist.** Phase 3 renders each per-platform markdown to `/tmp/analysis-<feature-slug>-<timestamp>/` first. Only then does Phase 3.5 surface the output-destination picker (Local / Confluence / Jira). Drafts in `/tmp/` are the source of truth on resume; `/multi-agent:resume` re-uses them when `phase == awaiting_output_decision`.
58
58
  11. **Repo-evidence reuse-first.** Phase 1b runs a repo-evidence collector against each selected repo, producing 13 buckets (services, dtos, useCases, validationRules, domainEntities, routes, coordinators, diConfigurators, uiComponents, tokens, localizationKeys, testingIdentifiers, analyticsEvents) with each row tagged `direct-match | same-domain | cross-cutting`. Section 7 emits `Reuse existing X (file:line)` rows for `direct-match` items and advisory rows for `cross-cutting` items. New-write tasks for items that have a `direct-match` are downgraded to Risks ("existing X candidate found; reuse or document why a new one is needed").
59
- 12. **MUST: Figma access - 3-tier fallback chain, pipeline-wide.** When any Figma URL or node ID is supplied (Phase 0 Step 5), Phase 1 establishes a Figma ground-truth artefact via the 3-tier chain before any UI synthesis runs: (1) Figma MCP `get_design_context` / `get_screenshot` / `get_metadata` for every referenced frame, with one re-auth retry on auth failure; (2) Figma REST API (`GET /v1/files/{fileKey}/nodes`, `GET /v1/images/{fileKey}`) using the PAT resolved through `~/.claude/lib/credential-store.sh get <logical-key>` where `<logical-key>` = `prefs.global.keychainMapping.figma`; (3) a user-attached screenshot as last resort. The tier in use is persisted as `state.figmaAccess.tier`. On Tier 1 the `CodeConnectSnippet` blocks name the exact target component consumed verbatim in Section 2 and Section 7. On Tier 2 the canonical-component decision falls back to the repo's `*.figma.swift` / `*.figma.kt` mappings keyed by `fileKey` + `nodeId`. On Tier 3 the canonical-component decision becomes "tier-3 best-fit pending design review" and produces a forced Open Question in Section 7 plus a Phase 4 `review_blocking` flag. Sound-alike alternatives, "more flexible" wrappers, and extrapolating from Confluence text-only "Component Kompozisyonu" / "Component Inventory" tables are forbidden in every tier. If all three tiers fail, halt the run and ask the user for access; never proceed with text-derived guesses. Section 7 architecture decisions must cite the node ID (Tier 1 or Tier 2) or the user-screenshot reference (Tier 3) for every UI atom row. Violations cost rebuild rounds (raw-primitive substitution; sound-alike-component swap). Generic rule rationale and checklist: see `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain (BLOCKING, pipeline-wide)". Memory: `[[figma-no-guesswork]]`. **A channel's design may not be called missing until the file tree has been scanned.** Run `figma-screenshot.sh --discover-sections --file-key <k> --feature-slug <s>`; sibling sections it returns join `evidence.figma[]` as their own entries and are never re-asked (Locked 1). Only an empty `candidates` list lets a channel be recorded as having no design, and the record must cite the scan (`fileKey`, `pagesScanned`, `tokensTried`). Declaring a channel missing without scanning fails the dispatch gate: one run did, and the sibling was sitting in the same file with nine frames in it.
59
+ 12. **MUST: Figma access - 3-tier fallback chain, pipeline-wide.** When any Figma URL or node ID is supplied (Phase 0 Step 5), Phase 1 establishes a Figma ground-truth artefact via the 3-tier chain before any UI synthesis runs: (1) Figma MCP `get_design_context` / `get_screenshot` / `get_metadata` for every referenced frame, with one re-auth retry on auth failure; (2) Figma REST API (`GET /v1/files/{fileKey}/nodes`, `GET /v1/images/{fileKey}`) using the PAT resolved through `~/.claude/lib/credential-store.sh get <logical-key>` where `<logical-key>` = `prefs.global.keychainMapping.figma`; (3) a user-attached screenshot as last resort. The tier in use is persisted as `state.figmaAccess.tier`. On Tier 1 the `CodeConnectSnippet` blocks name the exact target component consumed verbatim in Section 2 and Section 7. On Tier 2 the canonical-component decision falls back to the repo's `*.figma.swift` / `*.figma.kt` mappings keyed by `fileKey` + `nodeId`. On Tier 3 the canonical-component decision becomes "tier-3 best-fit pending design review" and produces a forced Open Question in Section 7 plus a Phase 4 `review_blocking` flag. Sound-alike alternatives, "more flexible" wrappers, and extrapolating from Confluence text-only "Component Kompozisyonu" / "Component Inventory" tables are forbidden in every tier. If all three tiers fail, halt the run and ask the user for access; never proceed with text-derived guesses. Section 7 architecture decisions must cite the node ID (Tier 1 or Tier 2) or the user-screenshot reference (Tier 3) for every UI atom row. Violations cost rebuild rounds (raw-primitive substitution; sound-alike-component swap). Generic rule rationale and checklist: see `$HOME/.claude/rules/figma-pipeline.md` "MUST: Figma access - 3-tier fallback chain (BLOCKING, pipeline-wide)". Memory: `[[figma-no-guesswork]]`. **A channel's design may not be called missing until the file tree has been scanned.** Run `figma-screenshot.sh --discover-sections --file-key <k> --feature-slug <s>`; sibling sections it returns join `evidence.figma[]` as their own entries and are never re-asked (Locked 1). Only an empty `candidates` list lets a channel be recorded as having no design, and the record must cite the scan (`fileKey`, `pagesScanned`, `tokensTried`). Declaring a channel missing without scanning fails the dispatch gate, because a channel's frames often sit in a sibling section of the same file.
60
60
  13. **Gherkin user stories.** Section 4 scenarios use Given / When / Then. Plain-prose user stories are rejected at render time.
61
61
  14. **Goals and Non-Goals are paired.** Section 2 always carries both columns. A row in only the goal column without an explicit non-goal counterpart is rejected. Vague "out of scope" phrasing does not satisfy the non-goal column; each non-goal names what is excluded.
62
62
  15. **New assets default to SVG.** Section 8 entries marked `new` are SVG unless a documented exception is captured in the rationale column (Lottie for motion design, optimized PNG for raster-only icons). PDF, JPG, and unoptimized PNG are rejected.
63
63
  16. **Files-to-Add tag mandatory.** Every row in Section 14 carries one tag from `Reuse | Add new | Modify`. Untagged rows fail the dispatch gate.
64
64
  17. **API response variants exhaustive.** Section 9 lists every HTTP status code returned by the endpoint with at least one example body and the matching UI outcome. Phrases like "other errors" or "various 4xx" are rejected.
65
- 18. **Screenshots embedded, not linked, and every inventory row gets one.** Section 5 frame galleries reference local PNG files. Dispatch uploads them as Confluence multipart attachments and injects `<ac:image><ri:attachment ri:filename="..." /></ac:image>` into the page body. The gallery is generated from the inventory, never hand-picked: one row, one embedded image, ordered by node id and titled `<nodeId> <frame name>`. A bare filename in a table cell is text to the reader, not a picture, which is how a run that uploaded 34 attachments and embedded 14 of them was reported as "frames not uploaded" - the files were there and invisible. Narrowing to a canonical subset for page weight is not a call the run makes on its own; the display-width cap (`md2confluence-v3.py --image-width`, phone frames overriding to 320) already solves readability without dropping evidence. Dispatch reports the embedded-over-inventory ratio and warns below 1. URL-only Figma references in Section 5 are a review finding, not a validator ERROR: the check belongs to the Confluence dispatch step, which is the only place that knows whether an attachment upload succeeded.
65
+ 18. **Screenshots embedded, not linked, and every inventory row gets one.** Section 5 frame galleries reference local PNG files. Dispatch uploads them as Confluence multipart attachments and injects `<ac:image><ri:attachment ri:filename="..." /></ac:image>` into the page body. The gallery is generated from the inventory, never hand-picked: one row, one embedded image, ordered by node id and titled `<nodeId> <frame name>`. A bare filename in a table cell is text to the reader, not a picture: an attachment that is uploaded but not embedded is invisible on the page. Narrowing to a canonical subset for page weight is not a call the run makes on its own; the display-width cap (`md2confluence-v3.py --image-width`, phone frames overriding to 320) already solves readability without dropping evidence. Dispatch reports the embedded-over-inventory ratio and warns below 1. URL-only Figma references in Section 5 are a review finding, not a validator ERROR: the check belongs to the Confluence dispatch step, which is the only place that knows whether an attachment upload succeeded.
66
66
  19. **All Figma variants drilled, and drilled means read.** When a Figma section URL is supplied, all child frames are drilled, not just the canonical default. The renderer enumerates child frames via `mcp__claude_ai_Figma__get_metadata` (Tier 1) or `figma-screenshot.sh --section` (Tier 2) and produces one Section 5.1 row per child frame. Counting them is not drilling them: extract the visible `TEXT` layers inside each frame's own bounds, dropping hidden layers and component-instance boilerplate that overflows the frame (default card and widget filler can outnumber the real text several times over). A frame may be called out of scope only by quoting its own extracted text; "the name looked unrelated" is not a reason. Opening a Section 20 question about a frame requires that its text was extracted first, and asking about a frame nobody read fails the dispatch gate: a frame whose name looks ambiguous can spell out a whole step in its text.
67
67
  20. **Localization mode (ownership-aware).** Section 10's shape is driven by the project `figma-config` `localization.ownership` (default `in-repo`). The locale set comes from `localization.locales` (default `tr, en, ar, de, es, fr, it, ru`); do not hardcode a locale list in the render.
68
68
  - **`in-repo`** (default, historical behavior): new keys carry filled cells for every configured locale. Placeholder values `[bekleniyor: çeviri ekibi]` / `[pending: translation team]` are a soft state and the dispatch report flags `i18n_pending: <count>` as a blocker. Empty cells are rejected outright.
@@ -82,5 +82,5 @@ behind.
82
82
  32. **Corporate backbone always renders.** In the `corporate` profile the Locked 2 omission rule is replaced for Part A and the footer: those sections render even with zero evidence, carrying `N/A` when the section is genuinely out of scope for the feature and `EKLENECEK` when evidence is expected but missing. This is the point of a requirements document - a reader has to be able to tell "we considered hardware needs and there are none" from "nobody looked". Every `EKLENECEK` emits a matching Section 20 Risks and Open Questions row naming what is missing and who can answer it; an `EKLENECEK` with no such row fails the dispatch gate, because an unanswered question nobody owns is how a placeholder reaches production. **Missing inputs never block the run**: the corporate source practice of halting until every input arrives is deliberately not adopted - the document is produced with `EKLENECEK` in the gaps and the gaps are raised in Section 20. Part B follows the global omission table unchanged. In the `global` profile Locked 2 applies as written, with no placeholder of any kind.
83
83
  33. **References are built from the evidence record, not written.** Section 21 is emitted by `$HOME/.claude/scripts/build-references.mjs` from `state.analysisSpec.evidence.*` in both profiles. Each row carries a precision anchor in its `Sürüm / Ref` column - Figma node id, Confluence `pageId` plus page version, the commit SHA a repo was read at, the Swagger spec version - because a reference with no anchor points at a moving target. Each row carries an `Erişim / Access` cell: a declared source that could not be fetched still gets a row reading `erişilemedi (<reason>)`, since a silently dropped source reads to the next person as a source that never existed. User statements from the conversation that no fetched source contains are recorded as `Serbest metin` rows, quoted verbatim, with the decision they settled. **Coverage gate**: every entry in `evidence.figma[]`, `confluence[]`, `jira[]`, `swagger[]`, `repo[]`, `standards[]`, `firebase[]`, `documents[]`, `outside[]`, `freeText[]` and every entry in `evidence.fetchErrors[]` must appear as a row, and every row must map to an evidence entry. A source that shaped the document but is missing from References fails the dispatch gate; so does an invented row with no evidence behind it.
84
84
  34. **Stack-optional render.** Platform and repo selection are optional. When `state.analysisSpec.platforms[]` is empty, the run still completes: the analysis layers that do not need a target repository render in full - Part A and Part B in the corporate profile, Sections 1-12 and 16-17 in the global profile - and only the development layer is dropped (corporate Part C; global Sections 13, 14, 15) along with the Pass B projection, since there are no conventions to project onto. A Section 20 row records that the development analysis awaits a repo selection. **The channel split survives the missing repo.** Channels are derived from the evidence instead of repo stack tags (`intake.md` Step 3 carries the signal table) and one document is emitted per derived channel - `mobile`, `web`, or both. A phone screen and a browser screen carry different requirements before anyone has picked a repository; the split (Locked 9) exists to carry that difference and only its *projection* half needs conventions. `mobile` stays one channel rather than iOS plus Android, since without conventions nothing tells the two apart. Evidence with no interface at all yields a single channel-agnostic `<feature>.md`. Files land under `~/Desktop/multiAgentAnalysis/<feature-name>/`, named `<feature>-<channel>.md` (or `<feature>.md` for the channel-agnostic case): the repo-relative `analysis/` path has nothing to be relative to without a repo, and the current working directory is never used, since for a repo-less run it is arbitrary. Desktop rather than a hidden directory because the document is a deliverable somebody is meant to open and hand over, and `multiAgentAnalysis` rather than a bare `Analysis` because a generic word collides with whatever else is on a desktop while the producer name groups every run this command ever writes. The Phase 3.5 picker shows the resolved path and takes an override through its Other input. A requirements document is useful before anyone has decided which repository will hold the code, and refusing to produce one until that decision exists inverts the order the work actually happens in.
85
- 35. **The document is reviewed before it is published.** An analysis run used to go from draft straight to dispatch behind a deterministic validator, so nothing read what it was about to publish: one run put a channel it never searched for, an open question about a frame it never opened, and twenty-three unowned `EKLENECEK` markers onto a live page. Every one is what a reader catches on the first pass. Phase 3.2 runs `phases/phase-3-review.md` Step 0 (strict validator, the host's three-reviewer set, triage) on the draft before the destination is chosen: a finding is cheap while nothing is written. Reviewers are subagents holding `analysis/review.md`, never the context that wrote the document, which cannot notice a search it never thought to run. A blocking finding returns to Phase 2b with dispatch closed and never becomes an open question, since "the document is wrong" is not something to ask the reader; capped at two returns. Phase 3.3 sorts every remaining gap into searched-and-closed, asked-and-answered, or `AS-NN`; an unstamped gap fails the dispatch gate. Autopilot runs both; only the asking degrades, into rows stamped `autopilot: could not ask`.
85
+ 35. **The document is reviewed before it is published.** A deterministic validator checks structure, not content: a channel nobody searched for, an open question about a frame nobody opened, and an unowned `EKLENECEK` marker all pass it, and every one is what a reader catches on the first pass. Phase 3.2 runs `phases/phase-3-review.md` Step 0 (strict validator, the host's three-reviewer set, triage) on the draft before the destination is chosen: a finding is cheap while nothing is written. Reviewers are subagents holding `analysis/review.md`, never the context that wrote the document, which cannot notice a search it never thought to run. A blocking finding returns to Phase 2b with dispatch closed and never becomes an open question, since "the document is wrong" is not something to ask the reader; capped at two returns. Phase 3.3 sorts every remaining gap into searched-and-closed, asked-and-answered, or `AS-NN`; an unstamped gap fails the dispatch gate. Autopilot runs both; only the asking degrades, into rows stamped `autopilot: could not ask`.
86
86
  36. **A redesign records v1 before it plans v2.** `options.redesign` is an opt-in on the `options.uiTests` axis, never a third `mode` value: `mode` says how many sections, `redesign` says which content, and a `mode: redesign` would switch off the Full-mode traceability, Test Plan and rule-to-test gates in exactly the documents that need them. It adds three sub-sections and no top-level section: current behaviour with `CB-<slug>-NN` ids and `repo/file:line` citations, the v1 to v2 endpoint mapping, and a difference list over a closed status vocabulary. Every `Missing` and `Partial` owes a Section 20 row by `AS-NN`, and a `CB-` id in one table but not the other fails in both directions - a behaviour that is in the code and on nobody's difference list is what a redesign loses and production finds. Evidence and certainty are derived from `repoEvidence`, never graded by the writer (Locked 24, same reason), and `options.redesign` is an `evidence_digest` input. Contract and the eight checks: `analysis/redesign.md`, loaded only on a redesign run.
@@ -41,7 +41,9 @@ intake opt-in, written into the front-matter, enforced by the validator's
41
41
  ## The three artefacts
42
42
 
43
43
  Section numbers are per profile. The global profile uses sub-sections of existing
44
- sections (4.5, 4.6, 9.5) and the corporate profile uses 2.1, 2.3 and 5.N+4; no
44
+ sections (4.5, 4.6, 9.5) and the corporate profile nests them one level deeper in
45
+ its own spine (2.1.1 current behaviour, 2.3.1 difference list, 5.(N+4).1 endpoint
46
+ mapping); the validator looks each one up by profile. No
45
47
  new top-level section is introduced in either, because section numbers are quoted
46
48
  in 172 places and renumbering them to add a mode is a worse trade than nesting.
47
49
 
@@ -8,9 +8,11 @@
8
8
 
9
9
  1b. **Template selection** (Locked 31): read `state.analysisSpec.profile`. `global` renders against `$HOME/.claude/multi-agent-refs/analysis-template.md`; `corporate` renders against `$HOME/.claude/multi-agent-refs/analysis-template-corporate.md`. The evidence is the same either way - this step chooses the projection, nothing else. In the corporate profile the Part A backbone and the footer render even with zero evidence, carrying `N/A` or `EKLENECEK`, and each `EKLENECEK` emits its Section 20 row (Locked 32).
10
10
 
11
- 2. **Markdown render**: for each platform in `state.analysisSpec.platforms[]`, concatenate the per-platform spec into one markdown file. Tables in pipe-syntax. Numbering uses plain `## 1.`, `## 2.`, ... - omitted sections do **not** create gaps. Visible numbering is sequential 1..N over the rendered set. In the corporate profile the Part A and footer numbers are fixed and do not re-flow, since the backbone never drops; Part B and Part C follow the omission table as usual.
11
+ 2. **Markdown render**: for each platform in `state.analysisSpec.platforms[]`, concatenate the per-platform spec into one markdown file. Tables in pipe-syntax. Each rendered section keeps its canonical number, so omitted sections leave gaps (Locked 2). Corporate Part A and footer never drop; Parts B and C follow the omission table.
12
12
 
13
- **When `platforms[]` is empty** (Locked 34), the channels come from the evidence instead of from repo stack tags, so this loop runs once per derived channel (`mobile`, `web`, or one channel-agnostic pass). What drops is the development layer (corporate Part C, global Sections 13, 14, 15) and the Pass B projection are skipped, Section 20 carries a row recording that they await a repo selection, and the front-matter `platform` key reads `none`. Everything that does not need a target repository still renders in full.
13
+ **Missing inputs panel** above Section 1: `build-references.mjs "$STATE" --lang "$LANG" --missing-panel` (prints nothing when every source was read). "Not reachable" is never written as "does not exist".
14
+
15
+ **When `platforms[]` is empty** (Locked 34), the loop runs once per channel derived from the evidence (`mobile`, `web`, or one channel-agnostic pass). The development layer (corporate Part C, global Sections 13-15) and the Pass B projection are skipped, Section 20 records that they await a repo selection, and front-matter `platform` reads `none`. Everything else renders in full.
14
16
 
15
17
  3. **Humanizer pass (required: actually invoke the `ai-common-toolkit:humanizer` skill on the rendered markdown - the punctuation grep alone does NOT satisfy this step)** (`technical-explanatory` tone for the scratch buffer; per-channel re-humanize happens in Phase 4 when actually emitting):
16
18
  ```
@@ -21,7 +23,7 @@
21
23
  content: <markdown>
22
24
  ```
23
25
 
24
- **Explicit punctuation policy** (enforced by `stripFancyPunctuation: true`): no em-dash (U+2014), no en-dash (U+2013), no horizontal ellipsis (U+2026), no curly quotes (U+2018, U+2019, U+201C, U+201D), no section sign (U+00A7). The humanizer replaces these with ASCII equivalents (`-`, `:`, `,`, `...`, `'`, `"`, and `bölüm` / `section` for the section sign per `outputLanguage`) before emit. Tables, code blocks, URLs, and front-matter YAML are exempt. Post-emit verification runs through `node $HOME/.claude/scripts/validate-analysis-doc.mjs <file>`, which checks this policy deterministically in Node on all three operating systems. Do NOT verify with `grep -P`: BSD grep (macOS, the pipeline's primary platform) has no `-P`, so that command never runs and "zero matches" is trivially true. The per-language readability rules (Turkish sentence shape, nominalization chains, which terms stay untranslated) live in the humanizer skill itself, not here; this paragraph owns only the punctuation policy the validator enforces. **Diacritics are PRESERVED, not stripped: this policy targets ONLY the listed fancy-punctuation codepoints. Turkish letters (ş/Ş, ç/Ç, ğ/Ğ, ı/I, İ, ö/Ö, ü/Ü) and all other `outputLanguage` letters MUST stay verbatim. Never ASCII-fold the prose - emit `Geliştirme Özeti`, `için`, `Kullanıcı Hikayeleri`, NOT `Gelistirme Ozeti`, `icin`, `Kullanici`. ASCII-folded Turkish is a humanizer-skipped smell and fails review.**
26
+ **Explicit punctuation policy** (enforced by `stripFancyPunctuation: true`): no em-dash (U+2014), no en-dash (U+2013), no horizontal ellipsis (U+2026), no curly quotes (U+2018, U+2019, U+201C, U+201D), no section sign (U+00A7). The humanizer replaces these with ASCII equivalents (`-`, `:`, `,`, `...`, `'`, `"`, and `bölüm` / `section` for the section sign per `outputLanguage`) before emit. Tables, code blocks, URLs, and front-matter YAML are exempt. Verify with `node $HOME/.claude/scripts/validate-analysis-doc.mjs <file>`, not a Perl-regex grep (BSD grep lacks it, so "zero matches" is trivially true). Readability rules live in the humanizer skill; this paragraph owns only the punctuation policy. **Diacritics are PRESERVED: only the listed codepoints are replaced. Turkish letters (ş, ç, ğ, ı, İ, ö, ü) stay verbatim; never ASCII-fold (`Geliştirme Özeti`, not `Gelistirme Ozeti`). ASCII-folded Turkish fails review.**
25
27
 
26
28
  3b. **Build Section 21 References** (Locked 33): emit the table with
27
29
 
@@ -207,36 +209,33 @@ Provenance:
207
209
 
208
210
  ## Confluence write (on-request only)
209
211
 
210
- Per the `analysis-output-confluence-on-request` memory, Confluence post is NEVER default-selected at Phase 3.5 (the output destination picker). Only when the user explicitly asks ("post this to Confluence", "create a page under the Coding Documentation parent", etc.) does the command:
212
+ Confluence is NEVER default-selected at Phase 3.5. Only when the user explicitly asks to post does the command:
211
213
 
212
214
  1. Resolve the token via `~/.claude/lib/credential-store.sh get <key>`, where `<key>` is read from `prefs.global.keychainMapping.confluence` (per-user mapping; never hardcode the service name in this doc - see channel adapter doc `$HOME/.claude/multi-agent-refs/channels/confluence.md` for the lookup contract).
213
215
  2. Use parent page URL from user input. No default parent is hardcoded here; the user picks one at the prompt (LRU recents come from `prefs.projects[<project>].confluenceUrls`).
214
216
 
215
- **Corporate profile exception.** When `state.analysisSpec.profile == "corporate"` and the bindings are configured, the destination is already settled and the prompt is skipped: the space comes from `prefs.global.analysisProfile.corporate.confluenceSpaceKey`, the parent from `prefs.global.analysisProfile.corporate.confluenceParentPageId`, and the page title is built from `prefs.global.analysisProfile.corporate.titleFormat` with `prefs.global.analysisProfile.corporate.titlePrefix` filling its `{prefix}` placeholder. A corporate analysis always lands in the same tree, so asking each time is a question whose answer never changes. Any of the four missing falls back to the prompt above rather than guessing, and a title that would collide with an existing page becomes an update (PUT with version bump), never a second page.
217
+ **Corporate profile exception.** With the bindings configured, the prompt is skipped: space `prefs.global.analysisProfile.corporate.confluenceSpaceKey`, parent `prefs.global.analysisProfile.corporate.confluenceParentPageId`, title from `prefs.global.analysisProfile.corporate.titleFormat` with `prefs.global.analysisProfile.corporate.titlePrefix` filling `{prefix}`. Any of the four missing falls back to the prompt; a colliding title becomes an update (PUT with version bump), never a second page.
216
218
  3. **Publish with `md2confluence-v3.py`, never a hand-rolled conversion + curl.** It
217
- owns the three steps a hand-rolled path has already dropped: storage-XML conversion
218
- (`channels/confluence.md`), the multipart upload of every PNG in `--attachments-dir`,
219
- and the `![](file.png)` to `<ac:image><ri:attachment/>` injection that makes a frame
220
- gallery render as pictures rather than filenames (Locked 18). It also warns when a
221
- body references an image no attachment matched, which is how a missing screenshot
222
- announces itself.
219
+ does the storage-XML conversion (`channels/confluence.md`), uploads every PNG in
220
+ `--attachments-dir`, injects `<ac:image><ri:attachment/>` so a frame gallery renders
221
+ as pictures (Locked 18), and warns on an image reference no attachment matched.
223
222
 
224
223
  ```bash
224
+ CONFLUENCE_BASE_URL="https://$(jq -r '.global.hosts.confluence // empty' "$HOME/.claude/multi-agent-preferences.json")"
225
+ export CONFLUENCE_BASE_URL
225
226
  python3 "$HOME/.claude/lib/md2confluence-v3.py" create \
226
227
  --space "$SPACE" --parent-page-id "$PARENT" --title "$TITLE" \
227
228
  --markdown "$DRAFT" --attachments-dir "$(dirname "$DRAFT")"
228
229
  # updating an existing page: `update --page-id <ID>` instead of `create --space ...`
229
230
  ```
230
231
 
231
- Screenshots must already sit in `--attachments-dir` before this runs. Point
232
- `figma-screenshot.sh --output-dir` at the draft directory during evidence
233
- collection rather than letting it default to its own `/tmp` folder, because a
234
- frame gallery whose PNGs live somewhere else converts to `<a href="frame-NNN.png">`
235
- and the page shows filenames where the pictures should be. MCP asset URLs expire
236
- after 7 days, so the cell must reference the local file, never the remote URL.
237
- 4. Read the script's JSON envelope: it reports the page id and URL, every attachment
238
- uploaded, and the `image reference has no matching attachment` warnings. A non-empty
239
- warning list means the page shipped with missing pictures; surface it rather than
240
- reporting a clean publish. Surface the page URL in the Phase 4 report.
232
+ Screenshots must already sit in `--attachments-dir`: point `figma-screenshot.sh
233
+ --output-dir` at the draft directory during evidence collection, or the gallery
234
+ renders filenames. MCP asset URLs expire after 7 days; cells reference the local file.
235
+ **Draft title:** while `status` is not `final`, prefix `[TASLAK] ` / `[DRAFT] ` (or fill the corporate `titleFormat` `{status}`); the final publish drops it.
236
+ 4. Read the script's JSON envelope (page id and URL, attachments, `image reference has
237
+ no matching attachment` warnings). A warning means missing pictures: surface it, not a
238
+ clean publish. Surface the page URL in the Phase 4 report.
239
+ 5. **Read the page back:** `confluence-readback.mjs --page-id "$PAGE_ID" --markdown "$DRAFT"`. Exit 1 names missing headings and unattached images; report it, never republish automatically.
241
240
 
242
241
  Token miss handling: surface a single line `WARN: Confluence token not configured (prefs.global.keychainMapping.confluence). Run /multi-agent:setup or skip Confluence output.` and continue with local-only output.
@@ -4,7 +4,7 @@
4
4
  >
5
5
  > The same three source labels drive `research/engine.md`, which generalises this walk to any gap list (maturity blockers, open questions) and adds a deterministic check of every cited source (`research-gate.mjs`) for runs nobody watches.
6
6
  >
7
- > The 31 inherited decisions are in `locked.md`; the ones below are this engine's own. Core invariant: the document is authoritative and forward-looking. The engine never invents an answer - when no source produces a credible candidate the only options are Defer and Other.
7
+ > The 36 inherited decisions are in `locked.md`; the ones below are this engine's own. Core invariant: the document is authoritative and forward-looking. The engine never invents an answer - when no source produces a credible candidate the only options are Defer and Other.
8
8
 
9
9
  ## New Locked decisions (this command)
10
10