@mmerterden/multi-agent-pipeline 16.11.0 → 16.12.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 +29 -0
- package/README.md +5 -3
- package/README.tr.md +5 -3
- package/docs/adr/0001-three-model-triage.md +5 -0
- package/docs/features.md +18 -2
- package/package.json +1 -1
- package/pipeline/claude-md-template.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/resume-local/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +1 -1
- package/pipeline/lib/figma-screenshot.sh +107 -7
- package/pipeline/lib/md2confluence-v3.py +133 -25
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -3
- package/pipeline/multi-agent-refs/analysis/render.md +44 -9
- package/pipeline/multi-agent-refs/analysis/review.md +17 -1
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +14 -3
- package/pipeline/multi-agent-refs/knowledge.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +8 -8
- package/pipeline/schemas/analysis-spec.schema.json +2 -0
- package/pipeline/schemas/reviewer-output.schema.json +1 -1
- package/pipeline/schemas/triage-output.schema.json +1 -1
- package/pipeline/scripts/anonymize-findings.mjs +1 -1
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +9 -9
- package/pipeline/scripts/validate-analysis-doc.mjs +85 -23
- package/pipeline/skills/.skills-index.json +2 -2
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +2 -2
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -134,6 +134,16 @@ def parse_front_matter(text: str) -> tuple[dict[str, str], str]:
|
|
|
134
134
|
MERMAID_FENCE_OPEN_RE = re.compile(r"^```mermaid\s*$")
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def code_macro(lang: str, body: str) -> str:
|
|
138
|
+
"""A plain code macro, used to carry mermaid source next to a lossy fallback."""
|
|
139
|
+
return (
|
|
140
|
+
'<ac:structured-macro ac:name="code">'
|
|
141
|
+
f'<ac:parameter ac:name="language">{html.escape(lang, quote=True)}</ac:parameter>'
|
|
142
|
+
f'<ac:plain-text-body><![CDATA[\n{cdata_escape(body)}\n]]></ac:plain-text-body>'
|
|
143
|
+
'</ac:structured-macro>'
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
137
147
|
def mermaid_macro(body: str) -> str:
|
|
138
148
|
return (
|
|
139
149
|
'<ac:structured-macro ac:name="mermaid">'
|
|
@@ -145,18 +155,32 @@ def mermaid_macro(body: str) -> str:
|
|
|
145
155
|
def mermaid_to_numbered_list(body: str) -> str:
|
|
146
156
|
"""Best-effort flowchart node extraction for fallback rendering."""
|
|
147
157
|
nodes: list[str] = []
|
|
148
|
-
edges: list[tuple[str, str]] = []
|
|
158
|
+
edges: list[tuple[str, str, str]] = []
|
|
149
159
|
node_label: dict[str, str] = {}
|
|
150
160
|
node_pattern = re.compile(r"([A-Za-z0-9_]+)\s*(?:\[([^\]]+)\]|\(([^)]+)\)|\{([^}]+)\})")
|
|
151
|
-
|
|
161
|
+
# `A --> B` was the only shape matched, so every labelled branch
|
|
162
|
+
# (`B -- Evet --> C`, `B -->|Hayir| D`) lost both its edge and its label, which
|
|
163
|
+
# is the part of a flow chart that carries the decision. Both label syntaxes are
|
|
164
|
+
# captured now and the label is kept.
|
|
165
|
+
# A node id is usually followed by its shape and label - A[Basla], B{Karar} -
|
|
166
|
+
# so the id is not adjacent to the arrow. The old pattern required adjacency and
|
|
167
|
+
# therefore matched no edge at all in any diagram whose nodes carry labels, which
|
|
168
|
+
# is every real one. Skip an optional [..] (..) {..} on both sides.
|
|
169
|
+
_SHAPE = r"(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\})?"
|
|
170
|
+
edge_pattern = re.compile(
|
|
171
|
+
r"([A-Za-z0-9_]+)" + _SHAPE + r"\s*"
|
|
172
|
+
r"(?:--\s*(?P<lbl_dash>[^->|]+?)\s*-->|-->\s*\|(?P<lbl_pipe>[^|]*)\||-->)"
|
|
173
|
+
r"\s*([A-Za-z0-9_]+)" + _SHAPE
|
|
174
|
+
)
|
|
152
175
|
|
|
153
176
|
for raw in body.splitlines():
|
|
154
177
|
line = raw.strip()
|
|
155
178
|
if not line or line.startswith("%%"):
|
|
156
179
|
continue
|
|
157
180
|
for em in edge_pattern.finditer(line):
|
|
158
|
-
a, b = em.group(1), em.group(
|
|
159
|
-
|
|
181
|
+
a, b = em.group(1), em.group(4)
|
|
182
|
+
label = (em.group("lbl_dash") or em.group("lbl_pipe") or "").strip()
|
|
183
|
+
edges.append((a, b, label))
|
|
160
184
|
for nm in (a, b):
|
|
161
185
|
if nm not in node_label:
|
|
162
186
|
node_label[nm] = nm
|
|
@@ -174,9 +198,9 @@ def mermaid_to_numbered_list(body: str) -> str:
|
|
|
174
198
|
# Could not parse; emit as plain pre block
|
|
175
199
|
return f"<pre>{html.escape(body)}</pre>"
|
|
176
200
|
|
|
177
|
-
next_map: dict[str, list[str]] = {}
|
|
178
|
-
for a, b in edges:
|
|
179
|
-
next_map.setdefault(a, []).append(b)
|
|
201
|
+
next_map: dict[str, list[tuple[str, str]]] = {}
|
|
202
|
+
for a, b, lbl in edges:
|
|
203
|
+
next_map.setdefault(a, []).append((b, lbl))
|
|
180
204
|
|
|
181
205
|
items: list[str] = []
|
|
182
206
|
for idx, key in enumerate(nodes, start=1):
|
|
@@ -184,7 +208,13 @@ def mermaid_to_numbered_list(body: str) -> str:
|
|
|
184
208
|
nxt = next_map.get(key, [])
|
|
185
209
|
suffix = ""
|
|
186
210
|
if nxt:
|
|
187
|
-
|
|
211
|
+
# "next: Evet -> Odeme, Hayir -> Hata" keeps the branch condition, which
|
|
212
|
+
# is the only reason a reader looks at a decision node.
|
|
213
|
+
parts = [
|
|
214
|
+
(f"{lbl} -> {node_label.get(n, n)}" if lbl else node_label.get(n, n))
|
|
215
|
+
for n, lbl in nxt
|
|
216
|
+
]
|
|
217
|
+
suffix = " next: " + ", ".join(parts)
|
|
188
218
|
items.append(f"<li>{html.escape(label)}{html.escape(suffix)}</li>")
|
|
189
219
|
return "<ol>" + "".join(items) + "</ol>"
|
|
190
220
|
|
|
@@ -223,12 +253,33 @@ def cdata_escape(s: str) -> str:
|
|
|
223
253
|
class InlineContext:
|
|
224
254
|
attachments_available: set[str]
|
|
225
255
|
tooltip_macro_supported: bool
|
|
256
|
+
image_width: int = 0
|
|
226
257
|
warnings: list[str] = field(default_factory=list)
|
|
227
258
|
tooltip_fallback_used: bool = False
|
|
228
259
|
images_referenced: list[str] = field(default_factory=list)
|
|
229
260
|
|
|
230
261
|
|
|
262
|
+
# A 2x export is 750x1624 for a phone frame and 2880x2048 for a desktop one, and
|
|
263
|
+
# Confluence renders an <ac:image> at its natural size, so one screenshot took two
|
|
264
|
+
# screen heights on the page. Width is a display attribute only: the attachment
|
|
265
|
+
# stays full resolution and a click still opens the original.
|
|
266
|
+
# 720 is a desktop frame at half of a 2x export and a diagram at readable size;
|
|
267
|
+
# a phone frame overrides itself down to 320 through the title syntax, which the
|
|
268
|
+
# frame-gallery row writes because it is the only place that knows the frame width.
|
|
269
|
+
DEFAULT_IMAGE_WIDTH = 720
|
|
270
|
+
|
|
271
|
+
IMAGE_WIDTH_RE = re.compile(r"\bwidth\s*=\s*(\d{2,4})\b")
|
|
272
|
+
|
|
273
|
+
|
|
231
274
|
def render_image(alt: str, src: str, ctx: InlineContext) -> str:
|
|
275
|
+
# Markdown title syntax carries the per-image override: 
|
|
276
|
+
width = None
|
|
277
|
+
m = re.match(r'^(?P<path>\S+)\s+"(?P<title>[^"]*)"$', src.strip())
|
|
278
|
+
if m:
|
|
279
|
+
src = m.group("path")
|
|
280
|
+
w = IMAGE_WIDTH_RE.search(m.group("title"))
|
|
281
|
+
if w:
|
|
282
|
+
width = int(w.group(1))
|
|
232
283
|
filename = os.path.basename(src)
|
|
233
284
|
# If src is an http(s) URL, render as external image link.
|
|
234
285
|
if re.match(r"^https?://", src):
|
|
@@ -237,9 +288,12 @@ def render_image(alt: str, src: str, ctx: InlineContext) -> str:
|
|
|
237
288
|
if ctx.attachments_available and filename not in ctx.attachments_available:
|
|
238
289
|
ctx.warnings.append(f"image reference has no matching attachment: {filename}")
|
|
239
290
|
return f'<a href="{html.escape(src, quote=True)}">{esc(alt) or esc(filename)}</a>'
|
|
291
|
+
if width is None:
|
|
292
|
+
width = ctx.image_width
|
|
240
293
|
alt_attr = f' ac:alt="{html.escape(alt, quote=True)}"' if alt else ""
|
|
294
|
+
width_attr = f' ac:width="{width}"' if width else ""
|
|
241
295
|
return (
|
|
242
|
-
f"<ac:image{alt_attr}>"
|
|
296
|
+
f"<ac:image{alt_attr}{width_attr}>"
|
|
243
297
|
f'<ri:attachment ri:filename="{html.escape(filename, quote=True)}" />'
|
|
244
298
|
"</ac:image>"
|
|
245
299
|
)
|
|
@@ -339,10 +393,12 @@ def markdown_to_storage(
|
|
|
339
393
|
attachments_available: set[str],
|
|
340
394
|
mermaid_fallback: bool,
|
|
341
395
|
tooltip_macro_supported: bool,
|
|
396
|
+
image_width: int = 0,
|
|
342
397
|
) -> ConvertResult:
|
|
343
398
|
ctx = InlineContext(
|
|
344
399
|
attachments_available=attachments_available,
|
|
345
400
|
tooltip_macro_supported=tooltip_macro_supported,
|
|
401
|
+
image_width=image_width,
|
|
346
402
|
)
|
|
347
403
|
lines = markdown.split("\n")
|
|
348
404
|
out: list[str] = []
|
|
@@ -379,7 +435,11 @@ def markdown_to_storage(
|
|
|
379
435
|
body = "\n".join(mermaid_buf)
|
|
380
436
|
if mermaid_fallback:
|
|
381
437
|
mermaid_fallback_used = True
|
|
438
|
+
# The list is lossy however carefully it is built, so the source
|
|
439
|
+
# goes with it. A reader who needs the real diagram can paste it
|
|
440
|
+
# anywhere that renders mermaid; without it the diagram is gone.
|
|
382
441
|
out.append(mermaid_to_numbered_list(body))
|
|
442
|
+
out.append(code_macro("text", body))
|
|
383
443
|
else:
|
|
384
444
|
out.append(mermaid_macro(body))
|
|
385
445
|
in_mermaid = False
|
|
@@ -643,6 +703,17 @@ def upload_attachment(
|
|
|
643
703
|
"""Attach file to page. Uses POST .../child/attachment, which overwrites if
|
|
644
704
|
Confluence is configured to accept the same filename. Wrapped in retry so
|
|
645
705
|
transient 5xx/429/network errors recover before the call site sees them."""
|
|
706
|
+
# Look before creating. Re-attaching a filename that is already on the page is
|
|
707
|
+
# a duplicate, and which status a Confluence tells you that with is not a
|
|
708
|
+
# constant: Cloud answers 400, this Server answers 500. Keying the
|
|
709
|
+
# already-exists path off a status code meant the update path was unreachable
|
|
710
|
+
# on Server, so every re-run logged one warning per attachment while the files
|
|
711
|
+
# were in fact fine. Asking first removes the guess, and leaves the error path
|
|
712
|
+
# as what it should have been all along: a race, not the normal case.
|
|
713
|
+
existing = find_existing_attachment(auth, page_id, file_path.name)
|
|
714
|
+
if existing:
|
|
715
|
+
return update_attachment_data(auth, page_id, existing, file_path)
|
|
716
|
+
|
|
646
717
|
data = file_path.read_bytes()
|
|
647
718
|
body, content_type = build_multipart([("file", data, file_path.name)])
|
|
648
719
|
url = f"{auth.base_url}/rest/api/content/{page_id}/child/attachment"
|
|
@@ -661,23 +732,23 @@ def upload_attachment(
|
|
|
661
732
|
def _do() -> tuple[bool, str]:
|
|
662
733
|
with urllib.request.urlopen(req) as resp:
|
|
663
734
|
resp.read()
|
|
664
|
-
return True, "
|
|
735
|
+
return True, "created"
|
|
665
736
|
|
|
666
737
|
try:
|
|
667
738
|
return _retry_http(_do)
|
|
668
739
|
except urllib.error.HTTPError as e:
|
|
669
|
-
#
|
|
670
|
-
|
|
740
|
+
# Race: something attached the same filename between the check and here.
|
|
741
|
+
# Both codes are treated the same because both mean the same thing.
|
|
742
|
+
if e.code in (400, 409, 500):
|
|
743
|
+
existing = find_existing_attachment(auth, page_id, file_path.name)
|
|
744
|
+
if existing:
|
|
745
|
+
return update_attachment_data(auth, page_id, existing, file_path)
|
|
671
746
|
err_body = ""
|
|
672
747
|
try:
|
|
673
748
|
err_body = e.read().decode("utf-8", errors="replace")
|
|
674
749
|
except Exception:
|
|
675
750
|
pass
|
|
676
|
-
|
|
677
|
-
if existing:
|
|
678
|
-
ok, msg = update_attachment_data(auth, page_id, existing, file_path)
|
|
679
|
-
return ok, msg
|
|
680
|
-
return False, f"HTTP 400: {err_body[:300]}"
|
|
751
|
+
return False, f"HTTP {e.code}: {err_body[:300]}"
|
|
681
752
|
return False, f"HTTP {e.code}"
|
|
682
753
|
except urllib.error.URLError as e:
|
|
683
754
|
return False, f"URLError: {e.reason}"
|
|
@@ -688,19 +759,24 @@ def upload_attachments_parallel(
|
|
|
688
759
|
page_id: str,
|
|
689
760
|
attachments: dict[str, Path],
|
|
690
761
|
max_workers: int = 4,
|
|
691
|
-
) -> tuple[int, list[str]]:
|
|
762
|
+
) -> tuple[int, int, list[str]]:
|
|
692
763
|
"""Upload many attachments concurrently via a small thread pool.
|
|
693
764
|
|
|
694
|
-
Returns (
|
|
765
|
+
Returns (created_count, updated_count, warning_messages). Created and updated
|
|
766
|
+
are counted apart because they are not the same event: a re-run of the same
|
|
767
|
+
document updates every attachment and creates none, and reporting that as
|
|
768
|
+
"0 uploaded, 12 warnings" reads as data loss when nothing was lost.
|
|
769
|
+
Sequential ordering is not
|
|
695
770
|
preserved (the prior implementation processed `sorted(referenced)`), but
|
|
696
771
|
that ordering had no semantic meaning - it was just deterministic for
|
|
697
772
|
debugging. The thread pool keeps per-task retry semantics intact via
|
|
698
773
|
upload_attachment().
|
|
699
774
|
"""
|
|
700
775
|
if not attachments:
|
|
701
|
-
return 0, []
|
|
776
|
+
return 0, 0, []
|
|
702
777
|
warnings: list[str] = []
|
|
703
|
-
|
|
778
|
+
created = 0
|
|
779
|
+
updated = 0
|
|
704
780
|
workers = max(1, min(max_workers, len(attachments)))
|
|
705
781
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
|
706
782
|
future_to_name = {
|
|
@@ -712,12 +788,15 @@ def upload_attachments_parallel(
|
|
|
712
788
|
try:
|
|
713
789
|
ok, msg = future.result()
|
|
714
790
|
if ok:
|
|
715
|
-
|
|
791
|
+
if msg == "updated":
|
|
792
|
+
updated += 1
|
|
793
|
+
else:
|
|
794
|
+
created += 1
|
|
716
795
|
else:
|
|
717
796
|
warnings.append(f"attachment upload failed for {name}: {msg}")
|
|
718
797
|
except Exception as e:
|
|
719
798
|
warnings.append(f"attachment upload exception for {name}: {e}")
|
|
720
|
-
return
|
|
799
|
+
return created, updated, warnings
|
|
721
800
|
|
|
722
801
|
|
|
723
802
|
def find_existing_attachment(
|
|
@@ -829,6 +908,7 @@ def build_storage(
|
|
|
829
908
|
attachments_dir: Path | None,
|
|
830
909
|
mermaid_fallback: bool,
|
|
831
910
|
tooltip_macro_supported: bool = True,
|
|
911
|
+
image_width: int = DEFAULT_IMAGE_WIDTH,
|
|
832
912
|
) -> tuple[ConvertResult, dict[str, str], dict[str, Path], list[str]]:
|
|
833
913
|
front_matter, body = parse_front_matter(markdown_text)
|
|
834
914
|
language = detect_language(body, front_matter.get("language"))
|
|
@@ -839,6 +919,7 @@ def build_storage(
|
|
|
839
919
|
attachments_available=set(attachments.keys()),
|
|
840
920
|
mermaid_fallback=mermaid_fallback,
|
|
841
921
|
tooltip_macro_supported=tooltip_macro_supported,
|
|
922
|
+
image_width=image_width,
|
|
842
923
|
)
|
|
843
924
|
result.warnings = punct_warnings + result.warnings
|
|
844
925
|
return result, front_matter, attachments, []
|
|
@@ -855,6 +936,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
855
936
|
md_text,
|
|
856
937
|
attachments_dir=attachments_dir,
|
|
857
938
|
mermaid_fallback=args.mermaid_fallback,
|
|
939
|
+
image_width=getattr(args, "image_width", DEFAULT_IMAGE_WIDTH),
|
|
858
940
|
)
|
|
859
941
|
|
|
860
942
|
if args.dry_run:
|
|
@@ -864,6 +946,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
864
946
|
"page_id": None,
|
|
865
947
|
"page_url": None,
|
|
866
948
|
"attachments_uploaded": 0,
|
|
949
|
+
"attachments_updated": 0,
|
|
867
950
|
"attachments_available": len(attachments),
|
|
868
951
|
"mermaid_macro_fallback_used": result.mermaid_fallback_used,
|
|
869
952
|
"tooltip_macro_fallback_used": result.tooltip_fallback_used,
|
|
@@ -893,6 +976,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
893
976
|
md_text,
|
|
894
977
|
attachments_dir=attachments_dir,
|
|
895
978
|
mermaid_fallback=True,
|
|
979
|
+
image_width=getattr(args, "image_width", DEFAULT_IMAGE_WIDTH),
|
|
896
980
|
)
|
|
897
981
|
create_payload["body"]["storage"]["value"] = result.storage_xml
|
|
898
982
|
status, body, raw = http_json(auth, "POST", "/rest/api/content", create_payload)
|
|
@@ -905,6 +989,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
905
989
|
|
|
906
990
|
# 2. Upload attachments referenced by markdown - parallel thread pool.
|
|
907
991
|
uploaded = 0
|
|
992
|
+
updated = 0
|
|
908
993
|
if attachments and result.images_referenced:
|
|
909
994
|
to_upload = {
|
|
910
995
|
name: attachments[name]
|
|
@@ -912,7 +997,9 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
912
997
|
if name in attachments
|
|
913
998
|
}
|
|
914
999
|
if to_upload:
|
|
915
|
-
uploaded, upload_warnings = upload_attachments_parallel(
|
|
1000
|
+
uploaded, updated, upload_warnings = upload_attachments_parallel(
|
|
1001
|
+
auth, page_id, to_upload
|
|
1002
|
+
)
|
|
916
1003
|
warnings.extend(upload_warnings)
|
|
917
1004
|
|
|
918
1005
|
# 3. Apply labels from front matter.
|
|
@@ -925,6 +1012,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
925
1012
|
"page_id": page_id,
|
|
926
1013
|
"page_url": page_url,
|
|
927
1014
|
"attachments_uploaded": uploaded,
|
|
1015
|
+
"attachments_updated": updated,
|
|
928
1016
|
"attachments_available": len(attachments),
|
|
929
1017
|
"mermaid_macro_fallback_used": result.mermaid_fallback_used,
|
|
930
1018
|
"tooltip_macro_fallback_used": result.tooltip_fallback_used,
|
|
@@ -941,6 +1029,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
941
1029
|
md_text,
|
|
942
1030
|
attachments_dir=attachments_dir,
|
|
943
1031
|
mermaid_fallback=args.mermaid_fallback,
|
|
1032
|
+
image_width=getattr(args, "image_width", DEFAULT_IMAGE_WIDTH),
|
|
944
1033
|
)
|
|
945
1034
|
|
|
946
1035
|
if args.dry_run:
|
|
@@ -950,6 +1039,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
950
1039
|
"page_id": args.page_id,
|
|
951
1040
|
"page_url": None,
|
|
952
1041
|
"attachments_uploaded": 0,
|
|
1042
|
+
"attachments_updated": 0,
|
|
953
1043
|
"attachments_available": len(attachments),
|
|
954
1044
|
"mermaid_macro_fallback_used": result.mermaid_fallback_used,
|
|
955
1045
|
"tooltip_macro_fallback_used": result.tooltip_fallback_used,
|
|
@@ -989,6 +1079,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
989
1079
|
md_text,
|
|
990
1080
|
attachments_dir=attachments_dir,
|
|
991
1081
|
mermaid_fallback=True,
|
|
1082
|
+
image_width=getattr(args, "image_width", DEFAULT_IMAGE_WIDTH),
|
|
992
1083
|
)
|
|
993
1084
|
update_payload["body"]["storage"]["value"] = result.storage_xml
|
|
994
1085
|
status, body, raw = http_json(
|
|
@@ -1003,6 +1094,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
1003
1094
|
page_url = page_web_url(auth, body, page_id)
|
|
1004
1095
|
|
|
1005
1096
|
uploaded = 0
|
|
1097
|
+
updated = 0
|
|
1006
1098
|
if attachments and result.images_referenced:
|
|
1007
1099
|
to_upload = {
|
|
1008
1100
|
name: attachments[name]
|
|
@@ -1010,7 +1102,9 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
1010
1102
|
if name in attachments
|
|
1011
1103
|
}
|
|
1012
1104
|
if to_upload:
|
|
1013
|
-
uploaded, upload_warnings = upload_attachments_parallel(
|
|
1105
|
+
uploaded, updated, upload_warnings = upload_attachments_parallel(
|
|
1106
|
+
auth, page_id, to_upload
|
|
1107
|
+
)
|
|
1014
1108
|
warnings.extend(upload_warnings)
|
|
1015
1109
|
|
|
1016
1110
|
labels = build_labels(front_matter)
|
|
@@ -1022,6 +1116,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
1022
1116
|
"page_id": page_id,
|
|
1023
1117
|
"page_url": page_url,
|
|
1024
1118
|
"attachments_uploaded": uploaded,
|
|
1119
|
+
"attachments_updated": updated,
|
|
1025
1120
|
"attachments_available": len(attachments),
|
|
1026
1121
|
"mermaid_macro_fallback_used": result.mermaid_fallback_used,
|
|
1027
1122
|
"tooltip_macro_fallback_used": result.tooltip_fallback_used,
|
|
@@ -1065,6 +1160,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1065
1160
|
create.add_argument("--markdown", required=True)
|
|
1066
1161
|
create.add_argument("--attachments-dir")
|
|
1067
1162
|
create.add_argument("--mermaid-fallback", action="store_true")
|
|
1163
|
+
create.add_argument(
|
|
1164
|
+
"--image-width",
|
|
1165
|
+
type=int,
|
|
1166
|
+
default=DEFAULT_IMAGE_WIDTH,
|
|
1167
|
+
help="Display width in px for embedded attachment images (0 = natural size). "
|
|
1168
|
+
"Per-image override: .",
|
|
1169
|
+
)
|
|
1068
1170
|
create.add_argument("--dry-run", action="store_true")
|
|
1069
1171
|
create.set_defaults(func=cmd_create)
|
|
1070
1172
|
|
|
@@ -1073,6 +1175,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1073
1175
|
update.add_argument("--title", required=True)
|
|
1074
1176
|
update.add_argument("--markdown", required=True)
|
|
1075
1177
|
update.add_argument("--attachments-dir")
|
|
1178
|
+
update.add_argument(
|
|
1179
|
+
"--image-width",
|
|
1180
|
+
type=int,
|
|
1181
|
+
default=DEFAULT_IMAGE_WIDTH,
|
|
1182
|
+
help="Display width in px for embedded attachment images (0 = natural size).",
|
|
1183
|
+
)
|
|
1076
1184
|
update.add_argument("--mermaid-fallback", action="store_true")
|
|
1077
1185
|
update.add_argument("--dry-run", action="store_true")
|
|
1078
1186
|
update.set_defaults(func=cmd_update)
|
|
@@ -29,14 +29,14 @@ When citing a Locked decision in code or docs, prefer `Locked <n> (<short label>
|
|
|
29
29
|
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.
|
|
30
30
|
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`.
|
|
31
31
|
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").
|
|
32
|
-
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]]`.
|
|
32
|
+
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.
|
|
33
33
|
13. **Gherkin user stories.** Section 4 scenarios use Given / When / Then. Plain-prose user stories are rejected at render time.
|
|
34
34
|
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.
|
|
35
35
|
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.
|
|
36
36
|
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.
|
|
37
37
|
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.
|
|
38
|
-
18. **Screenshots embedded, not linked.** 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. 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.
|
|
39
|
-
19. **All Figma variants drilled.** 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.
|
|
38
|
+
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.
|
|
39
|
+
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 (in one run a frame carried 88 text layers of which 13 were real, the rest default flight-card and currency-widget filler). 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. One run did exactly that: a frame named `08 Payment` was logged as ambiguous and questioned, while its text spelled out the whole payment step, and the use case shipped incomplete.
|
|
40
40
|
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.
|
|
41
41
|
- **`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.
|
|
42
42
|
- **`externally-owned`**: per-locale values are owned by the authoring system named in `localization.authoringPipeline` and MUST NOT be hand-filled. Section 10 lists `key | status | copy source | base value | ownership` only; the gate becomes "the `localization.baseLanguage` value is present (sourced from the Figma annotation, Locked 3) AND the ownership reference is cited". A key missing its base-language value is a blocker (it renders the raw key at runtime). Fabricated per-locale values are rejected.
|
|
@@ -56,3 +56,4 @@ When citing a Locked decision in code or docs, prefer `Locked <n> (<short label>
|
|
|
56
56
|
33. **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.
|
|
57
57
|
34. **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.
|
|
58
58
|
35. **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.
|
|
59
|
+
36. **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-4-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`.
|
|
@@ -42,6 +42,43 @@
|
|
|
42
42
|
```
|
|
43
43
|
User can inspect drafts before choosing output destinations in Phase 3.5.
|
|
44
44
|
|
|
45
|
+
### Phase 3.2 - Draft review (BLOCKING, runs in every mode)
|
|
46
|
+
|
|
47
|
+
Locked 36 carries the rule. Operationally:
|
|
48
|
+
|
|
49
|
+
1. Run `phases/phase-4-review.md` Step 0, the analysis-mode branch. It already defines
|
|
50
|
+
the strict validator, the host's three-reviewer set with its model routing, and the
|
|
51
|
+
triage. Do not restate it here; a second definition is the one that rots.
|
|
52
|
+
2. Dispatch reviewers as subagents, each given the draft path, the state JSON and
|
|
53
|
+
`analysis/review.md`. Never review from this context: it cannot notice a search it
|
|
54
|
+
never thought to run.
|
|
55
|
+
3. Blocking -> back to Phase 2b, dispatch closed, never an open question. Advisory ->
|
|
56
|
+
Phase 3.3. Cap at two returns, then stop and surface what blocks.
|
|
57
|
+
|
|
58
|
+
Autopilot included. State: `phase = "reviewing_draft"`.
|
|
59
|
+
|
|
60
|
+
### Phase 3.3 - Close the gaps (BLOCKING)
|
|
61
|
+
|
|
62
|
+
Phase 3.2 produced the list. Sort every entry and act:
|
|
63
|
+
|
|
64
|
+
| Bucket | Meaning | Action |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| A. Not searched | The evidence is reachable and this run did not look | Run the search; it closes, and never becomes a question |
|
|
67
|
+
| B. The user knows | Channel scope, which service backs a screen, what is in scope | Ask, `AskUserQuestion`, at most 4 per call |
|
|
68
|
+
| C. External | Final copy, a legal basis, a contract nobody has written | Record as `AS-NN` in Section 20 |
|
|
69
|
+
|
|
70
|
+
**Bucket A must be empty before Phase 3.5.** A gap reaches C only with a stamp:
|
|
71
|
+
`searched, not found` naming what was searched, or `asked, external`. Unstamped
|
|
72
|
+
`EKLENECEK` fails the dispatch gate. A is where most open questions came from in
|
|
73
|
+
practice, and none were questions - they were searches nobody ran.
|
|
74
|
+
|
|
75
|
+
B is asked in every interactive mode; autopilot moves B to C stamped `autopilot: could
|
|
76
|
+
not ask`, so an unanswerable gap reads differently from an unasked one. A stays required
|
|
77
|
+
under autopilot: a search needs no human.
|
|
78
|
+
|
|
79
|
+
Re-render affected sections, then report gaps closed by search, by asking, and entered.
|
|
80
|
+
State: `phase = "reviewing_gaps"`.
|
|
81
|
+
|
|
45
82
|
### Phase 3.5 - Output destination picker
|
|
46
83
|
|
|
47
84
|
AskUserQuestion (multiSelect=true), at least one selection required. `Local file` pre-selected per Locked decision 5.
|
|
@@ -169,15 +206,13 @@ Per the `analysis-output-confluence-on-request` memory, Confluence post is NEVER
|
|
|
169
206
|
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`).
|
|
170
207
|
|
|
171
208
|
**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.
|
|
172
|
-
3. **Publish with `md2confluence-v3.py`, never a hand-rolled conversion + curl.**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
warns when a body references an image no attachment matched, which is the signal
|
|
180
|
-
that a screenshot never downloaded.
|
|
209
|
+
3. **Publish with `md2confluence-v3.py`, never a hand-rolled conversion + curl.** It
|
|
210
|
+
owns the three steps a hand-rolled path has already dropped: storage-XML conversion
|
|
211
|
+
(`channels/confluence.md`), the multipart upload of every PNG in `--attachments-dir`,
|
|
212
|
+
and the `` to `<ac:image><ri:attachment/>` injection that makes a frame
|
|
213
|
+
gallery render as pictures rather than filenames (Locked 18). It also warns when a
|
|
214
|
+
body references an image no attachment matched, which is how a missing screenshot
|
|
215
|
+
announces itself.
|
|
181
216
|
|
|
182
217
|
```bash
|
|
183
218
|
python3 "$HOME/.claude/lib/md2confluence-v3.py" create \
|
|
@@ -31,9 +31,25 @@ node "$HOME/.claude/scripts/build-references.mjs" "<state>" --check "<doc>"
|
|
|
31
31
|
|
|
32
32
|
**No state JSON, no references gate.** Say so in the report rather than passing silently: the coverage claim ("every source the run read is listed") is exactly the claim nobody can verify from the document alone, and marking it unchecked is the honest output.
|
|
33
33
|
|
|
34
|
+
## Phase 1.5 - What did the run skip?
|
|
35
|
+
|
|
36
|
+
Ask this before rubric quality. A document can be well-formed and still be wrong
|
|
37
|
+
because the run never looked. Every row here is a finding class observed in a real run.
|
|
38
|
+
|
|
39
|
+
| Skip class | Evidence it happened |
|
|
40
|
+
|---|---|
|
|
41
|
+
| A declared-missing input nothing searched for | `evidence.fetchErrors[]` has no entry for it and the document cites no scan |
|
|
42
|
+
| An open question about evidence never read | a Section 20 row names a node, page or file with no extracted content behind it |
|
|
43
|
+
| A gap with no owner | `EKLENECEK` with no `AS-NN`, or an `AS-NN` naming nobody |
|
|
44
|
+
| A scope call made without asking | the document narrows what the request asked for and no picker answer records it |
|
|
45
|
+
|
|
46
|
+
A skip is blocking, not advisory: the fix is a search or a question, and both are
|
|
47
|
+
cheaper before publication than a correction after it. Report each with what should
|
|
48
|
+
have been searched, so Phase 3.3 can put it in the right bucket.
|
|
49
|
+
|
|
34
50
|
## Phase 2 - Rubric
|
|
35
51
|
|
|
36
|
-
Parallel model review, same shape as `/multi-agent:review`:
|
|
52
|
+
Parallel model review, same shape as `/multi-agent:review`: 3 models on Claude Code, 3 on Copilot CLI, then triage. Each reviewer answers the rubric below against the resolved profile and returns findings with `Locked <n>` or a rubric id, a quote from the document, and what a reader cannot do because of it.
|
|
37
53
|
|
|
38
54
|
**A. Buildability** - could an implementer start from this alone?
|
|
39
55
|
|
|
@@ -283,11 +283,22 @@ One table per service. Contracts come from the live specification, never from pr
|
|
|
283
283
|
| **Method** | GET / POST / PUT / DELETE |
|
|
284
284
|
| **Description** | <what it does> |
|
|
285
285
|
| **Success and error codes** | **200** <meaning> **401** <meaning> **403** <meaning> |
|
|
286
|
-
| **Request** |
|
|
287
|
-
| **Response** |
|
|
286
|
+
| **Request** | Asagidaki govde / Govde yok / EKLENECEK (AS-NN) |
|
|
287
|
+
| **Response** | Asagidaki govde / Govde yok / EKLENECEK (AS-NN) |
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
**Payloads live below the table, never inside a cell.** Each is a fenced ```json block,
|
|
291
|
+
beautified, preceded by a label line: `Request:`, `Response:`, or a status code
|
|
292
|
+
(`200:`). Field notes get their own table (`Alan | Tip | Zorunlu | Aciklama`); a note
|
|
293
|
+
pasted after inline JSON in the same cell is what made one document render `Request`
|
|
294
|
+
and `Response` in two different formats in the same table. A `{` or `}` inside a
|
|
295
|
+
service cell is a validator ERROR.
|
|
296
|
+
|
|
297
|
+
A session-bound service states its token requirement as one sentence under the field
|
|
298
|
+
table, not inside the request payload. When only the name is unknown the table still
|
|
299
|
+
renders; when the whole contract is unknown, write one line instead of six `EKLENECEK`
|
|
300
|
+
cells: `Bu servisin sozlesmesi tanimli degil (AS-NN).` Render the table only once
|
|
301
|
+
`Name` and `Path` are both known.
|
|
291
302
|
|
|
292
303
|
### 5.(N+5) Servis - Fonksiyonel Gereksinim Eşleştirmesi
|
|
293
304
|
|
|
@@ -80,7 +80,7 @@ Phase 3: Dev
|
|
|
80
80
|
Context: Phase 2 plan + task dependencies
|
|
81
81
|
|
|
82
82
|
Phase 4: Review (CLI-aware parallel + triage)
|
|
83
|
-
Claude Code (
|
|
83
|
+
Claude Code (3 parallel):
|
|
84
84
|
|-- Reviewer (opus) -> security + architecture
|
|
85
85
|
+-- Reviewer (sonnet) -> quality + correctness + edge cases
|
|
86
86
|
Copilot CLI (3 parallel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
### Phase 4: Review (deterministic gates + parallel + triage)
|
|
2
2
|
|
|
3
|
-
> **TLDR** - Three-stage review. Stage 1: deterministic gates (build + lint + test + secret scan) that MUST pass. Stage 2: AI models in parallel -
|
|
3
|
+
> **TLDR** - Three-stage review. Stage 1: deterministic gates (build + lint + test + secret scan) that MUST pass. Stage 2: AI models in parallel - every host dispatches 3 reviewers; only the second slot is **CLI-aware**: Claude Code dispatches Fable + Opus + Sonnet; Copilot CLI dispatches 3 reviewers (GPT-5.4 + Opus + Sonnet - Fable 5 is not offered on Copilot CLI). Stage 3: Fable triage (Opus on Copilot CLI) - evaluates raw findings, filters false-positives/out-of-scope, keeps only actionable items. Only triage-accepted blocking items loop back to Phase 3.
|
|
4
4
|
|
|
5
5
|
<!-- progress-contract: applied -->
|
|
6
6
|
Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` - lines for each gate, each reviewer dispatch + finish, triage start, triage verdict, fix dispatch.
|
|
@@ -264,18 +264,18 @@ Phase 4 sends the same diff to every reviewer and then to triage, so the diff is
|
|
|
264
264
|
|
|
265
265
|
#### Step 2 - Parallel AI Review (CLI-aware reviewer set)
|
|
266
266
|
|
|
267
|
-
Launch Agent instances **in parallel** using the shared `code-reviewer` subagent definition (`~/.claude/agents/code-reviewer.md`).
|
|
267
|
+
Launch Agent instances **in parallel** using the shared `code-reviewer` subagent definition (`~/.claude/agents/code-reviewer.md`). Every host runs three reviewers; only the second slot differs, because GPT-5.4 exists on Copilot and Codex but not on Claude Code, where Opus fills it. Three reviewers on Claude Code cost more than two, and the cost buys the thing a second opinion cannot: a finding two independent readers both miss is what triage has no chance to catch.
|
|
268
268
|
|
|
269
269
|
**Scope from Step 1.77.** `$REVIEW_SCOPE == "single"` → dispatch **Reviewer 1 only**, and skip Step 2.5 + 3.6 (both no-ops with one reviewer). `"full"` (default + fail-safe) → the whole set below. Either way record the count in `consensus.reviewerCount`.
|
|
270
270
|
|
|
271
271
|
| Reviewer | subagent_type | Claude Code | Copilot CLI | Codex CLI | Focus | Skills Referenced |
|
|
272
272
|
| ---------- | --------------- | --- | --- | --- | --- | --- |
|
|
273
273
|
| Reviewer 1 | `code-reviewer` | `claude-fable-5` | `claude-opus-5` | `gpt-5.6` @ `xhigh` | Deep security + architecture | `api-security-best-practices`, `architecture` |
|
|
274
|
-
| Reviewer 2 | `code-reviewer` |
|
|
274
|
+
| Reviewer 2 | `code-reviewer` | `claude-opus-5` | `gpt-5.4` | `gpt-5.4` @ `high` | Edge cases, different perspective | cross-model diversity |
|
|
275
275
|
| Reviewer 3 | `code-reviewer` | `claude-sonnet-5` | `claude-sonnet-5` | `gpt-5.6` @ `medium` | Quality + correctness + naming | `ai-backend-toolkit:clean-code`, stack-specific skill |
|
|
276
276
|
| Triage | triage persona | `claude-fable-5` | `claude-opus-5` | `gpt-5.6` @ `max` | Filter false positives + out-of-scope | - |
|
|
277
277
|
|
|
278
|
-
Reviewer count per host: **Claude Code
|
|
278
|
+
Reviewer count per host: **Claude Code 3, Copilot CLI 3, Codex CLI 3**.
|
|
279
279
|
|
|
280
280
|
#### Codex CLI - two constraints that fail silently
|
|
281
281
|
|
|
@@ -302,11 +302,11 @@ in the triage note when all three agree on a borderline finding.
|
|
|
302
302
|
|
|
303
303
|
Each reviewer inherits the `code-reviewer` agent's focus areas (Security, Architecture, Quality, Performance) and output contract. The orchestrator overrides only the model and the stack-specific skill per-reviewer - no prompt duplication.
|
|
304
304
|
|
|
305
|
-
**Model override wiring:** `code-reviewer.md` declares `preferredModel: fable`, so Reviewer 1 uses the persona default (Fable 5). Reviewer 2 (
|
|
305
|
+
**Model override wiring:** `code-reviewer.md` declares `preferredModel: fable`, so Reviewer 1 uses the persona default (Fable 5). Reviewer 2 (`claude-opus-5` on Claude Code, `gpt-5.4` elsewhere) and Reviewer 3 (`claude-sonnet-5`) set `PHASE_MODEL_OVERRIDE=<model>` before dispatch - the orchestrator exports `CLAUDE_CODE_SUBAGENT_MODEL` on Claude Code, or passes `--model` on Copilot CLI. Full precedence rule: `skills/shared/core/multi-agent/SKILL.md#agent-dispatch--per-persona-model-routing-v610`. Fable dispatches are subject to the fallback contract (`$HOME/.claude/multi-agent-refs/features/model-fallback.md`): dispatch-error retry walks `fable -> opus -> sonnet` and budget-ceiling downgrade.
|
|
306
306
|
|
|
307
|
-
**Stack-specific skills loaded per reviewer** (from Phase 1 `detectedStack`).
|
|
307
|
+
**Stack-specific skills loaded per reviewer** (from Phase 1 `detectedStack`). All three columns are used on every host; Reviewer 2 reads them as Opus on Claude Code and as GPT-5.4 elsewhere.
|
|
308
308
|
|
|
309
|
-
| Stack | Reviewer 1 (Fable / Opus on Copilot) | Reviewer 2 (GPT-5.4
|
|
309
|
+
| Stack | Reviewer 1 (Fable / Opus on Copilot) | Reviewer 2 (Opus on Claude Code, GPT-5.4 elsewhere) | Reviewer 3 (Sonnet) |
|
|
310
310
|
|-------|-------------------|-----------------------------------------|---------------------|
|
|
311
311
|
| iOS/Swift | `ai-ios-toolkit:ios-security`, `ai-ios-toolkit:swiftui-performance`, `ai-ios-toolkit:hig-patterns` | `ai-ios-toolkit:swift-concurrency`, `ai-ios-toolkit:ios-accessibility` | `ai-ios-toolkit:swiftui-pro`, `ai-ios-toolkit:swift-testing` |
|
|
312
312
|
| Android/Kotlin | `ai-android-toolkit:android-security`, `ai-android-toolkit:android-performance` | `ai-android-toolkit:compose-testing`, `ai-android-toolkit:android-architecture` | `ai-android-toolkit:compose-components`, `ai-android-toolkit:kotlin-coroutines-expert` |
|
|
@@ -557,7 +557,7 @@ emit() { # $1=event $2=model $3=duration $4=tokens_in $5=tokens_out
|
|
|
557
557
|
}
|
|
558
558
|
emit review.reviewer_call fable "$R1_DURATION" "$R1_IN" "$R1_OUT" # opus on Copilot CLI
|
|
559
559
|
emit review.reviewer_call sonnet "$SONNET_DURATION" "$SONNET_IN" "$SONNET_OUT"
|
|
560
|
-
# Reviewer 2 is GPT-5.4
|
|
560
|
+
# Reviewer 2 is Opus on Claude Code and GPT-5.4 elsewhere:
|
|
561
561
|
[ "${CLI_HOST:-claude}" = "copilot" ] && \
|
|
562
562
|
emit review.reviewer_call gpt-5.4 "$GPT_DURATION" "$GPT_IN" "$GPT_OUT"
|
|
563
563
|
emit review.triage_call fable "$TRIAGE_DURATION" "$TRIAGE_IN" "$TRIAGE_OUT"
|