@mmerterden/multi-agent-pipeline 14.1.1 → 14.2.2

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 (129) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +10 -6
  3. package/README.tr.md +143 -0
  4. package/docs/architecture.md +23 -8
  5. package/docs/ecosystem.md +237 -0
  6. package/install/_plugin-skills.mjs +16 -2
  7. package/install/codex.mjs +9 -4
  8. package/install/templates/copilot-instructions.md +12 -9
  9. package/package.json +1 -1
  10. package/pipeline/commands/deploy.md +4 -1
  11. package/pipeline/commands/multi-agent/SKILL.md +8 -5
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +2 -2
  13. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
  14. package/pipeline/commands/multi-agent/channels/SKILL.md +15 -4
  15. package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
  16. package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
  17. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
  18. package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
  19. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
  20. package/pipeline/commands/multi-agent/help/SKILL.md +49 -11
  21. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
  22. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  23. package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
  24. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
  25. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  26. package/pipeline/commands/multi-agent/setup/SKILL.md +1 -1
  27. package/pipeline/commands/multi-agent/ship/SKILL.md +5 -1
  28. package/pipeline/commands/multi-agent/store-ready/SKILL.md +340 -0
  29. package/pipeline/commands/multi-agent/sync/SKILL.md +7 -6
  30. package/pipeline/commands/multi-agent/test/SKILL.md +18 -8
  31. package/pipeline/commands/multi-agent/test-accessibility/SKILL.md +33 -0
  32. package/pipeline/commands/multi-agent/test-dark-mode/SKILL.md +33 -0
  33. package/pipeline/commands/multi-agent/test-dynamic-type/SKILL.md +33 -0
  34. package/pipeline/commands/multi-agent/test-screenshots/SKILL.md +41 -0
  35. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +28 -201
  36. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  37. package/pipeline/commands/sim-test.md +45 -36
  38. package/pipeline/lib/extract-conventions.sh +44 -15
  39. package/pipeline/lib/fetch-figma-annotations.sh +8 -1
  40. package/pipeline/lib/fetch-fortify.sh +23 -8
  41. package/pipeline/lib/figma-screenshot.sh +11 -1
  42. package/pipeline/lib/issue-fetcher.sh +76 -9
  43. package/pipeline/lib/md2confluence-v3.py +16 -2
  44. package/pipeline/lib/plan-todos.sh +5 -2
  45. package/pipeline/lib/post-pr-review.sh +8 -6
  46. package/pipeline/lib/shadow-git.sh +50 -9
  47. package/pipeline/lib/submodule-detector.sh +8 -1
  48. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  49. package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
  50. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  51. package/pipeline/multi-agent-refs/channels/jira.md +13 -2
  52. package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
  53. package/pipeline/multi-agent-refs/channels/pr.md +20 -0
  54. package/pipeline/multi-agent-refs/channels/wiki.md +1 -1
  55. package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -5
  56. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  57. package/pipeline/multi-agent-refs/generate-issue.md +2 -2
  58. package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
  59. package/pipeline/multi-agent-refs/knowledge.md +1 -1
  60. package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
  61. package/pipeline/multi-agent-refs/phases/modes.md +20 -0
  62. package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
  63. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
  64. package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
  65. package/pipeline/multi-agent-refs/phases.md +6 -0
  66. package/pipeline/multi-agent-refs/rules.md +2 -0
  67. package/pipeline/schemas/prefs.schema.json +2 -2
  68. package/pipeline/scripts/audit-log-rotate.sh +10 -0
  69. package/pipeline/scripts/build-stack-plugins.mjs +8 -1
  70. package/pipeline/scripts/check-derived-drift.mjs +13 -1
  71. package/pipeline/scripts/diff-explain.mjs +41 -3
  72. package/pipeline/scripts/diff-risk-score.mjs +72 -8
  73. package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
  74. package/pipeline/scripts/learning-curve.mjs +8 -2
  75. package/pipeline/scripts/output-quality-check.sh +15 -4
  76. package/pipeline/scripts/phase-tracker.sh +21 -8
  77. package/pipeline/scripts/pre-commit-check.sh +69 -22
  78. package/pipeline/scripts/render-agent-log-cost.sh +8 -3
  79. package/pipeline/scripts/render-cost-summary.sh +42 -22
  80. package/pipeline/scripts/render-work-summary.sh +47 -13
  81. package/pipeline/scripts/review-scope.mjs +1 -1
  82. package/pipeline/scripts/run-aggregator.mjs +38 -14
  83. package/pipeline/scripts/smoke-schema-validation.sh +5 -1
  84. package/pipeline/scripts/test-gap-scan.mjs +45 -6
  85. package/pipeline/scripts/uninstall.mjs +39 -4
  86. package/pipeline/scripts/update-issue-progress.sh +12 -16
  87. package/pipeline/scripts/worktree-finalize.sh +23 -2
  88. package/pipeline/skills/.skills-index.json +58 -4
  89. package/pipeline/skills/shared/README.md +12 -7
  90. package/pipeline/skills/shared/core/multi-agent/SKILL.md +13 -17
  91. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
  92. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  93. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
  94. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
  95. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
  96. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
  97. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +50 -12
  98. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  99. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
  100. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
  101. package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +18 -3
  102. package/pipeline/skills/shared/core/multi-agent-ship/SKILL.md +4 -0
  103. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +50 -0
  104. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +6 -5
  105. package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +18 -8
  106. package/pipeline/skills/shared/core/multi-agent-test-accessibility/SKILL.md +37 -0
  107. package/pipeline/skills/shared/core/multi-agent-test-dark-mode/SKILL.md +37 -0
  108. package/pipeline/skills/shared/core/multi-agent-test-dynamic-type/SKILL.md +37 -0
  109. package/pipeline/skills/shared/core/multi-agent-test-screenshots/SKILL.md +44 -0
  110. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +29 -101
  111. package/pipeline/skills/shared/external/firebase/SKILL.md +1 -1
  112. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
  113. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +144 -0
  114. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
  115. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +175 -0
  117. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
  118. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
  129. package/pipeline/skills/skills-index.md +10 -4
@@ -12,7 +12,15 @@
12
12
  # "title": "...",
13
13
  # "type": "Bug" | "Task" | "Story" | "Issue",
14
14
  # "status": "...",
15
- # "description":"...", // raw body (truncated to 4000 chars)
15
+ # "description":"...", // raw body (truncated to 4000 chars); this
16
+ # // issue's OWN description - never auto-substituted
17
+ # "parentKey": "PROJ-1" | null, // jira only; set when own description is empty
18
+ # // AND a parent link exists (single hop, no chasing
19
+ # // the parent's parent)
20
+ # "parentDescription": "..." | null, // jira only; the parent's description (truncated
21
+ # // 4000 chars), only when own is empty and parent
22
+ # // has content - a CANDIDATE, not applied automatically;
23
+ # // the caller must confirm before using it as "description"
16
24
  # "url": "https://...",
17
25
  # "host": "jira.example.com" | "github.com",
18
26
  # "owner": "org" (gh) | null,
@@ -21,11 +29,17 @@
21
29
  # "maturity": {
22
30
  # "score": 0..100,
23
31
  # "blockers": ["status_closed", "description_empty"],
24
- # "warnings": ["short_description", "no_repro_steps"],
32
+ # "warnings": ["short_description", "no_repro_steps", "description_empty_parent_available"],
25
33
  # "summary": "Description boş - pipeline başlatılamaz." (human-readable, tr|en)
26
34
  # }
27
35
  # }
28
36
  #
37
+ # "description_empty_parent_available": own description is empty but parentDescription is
38
+ # non-empty. This is a WARNING, not a blocker - the caller must ask the user (or, in
39
+ # autopilot, auto-accept per the same "warnings auto-continue" rule) whether to proceed
40
+ # using parentDescription as the working description. See jira/SKILL.md's maturity
41
+ # section for the exact question wording.
42
+ #
29
43
  # Required env:
30
44
  # ACCOUNT_JIRA_TOKEN_KEY keychain service name (optional)
31
45
  # ACCOUNT_JIRA_HOST e.g. jira.example.com (optional)
@@ -51,8 +65,16 @@ detect_kind() {
51
65
  case "$in" in
52
66
  *atlassian.net/browse/*|*jira*/browse/*|http*://*/browse/*) echo "jira-url"; return ;;
53
67
  *github.com/*/issues/*) echo "gh-url"; return ;;
54
- [A-Z]*-[0-9]*) echo "jira-id"; return ;;
55
68
  esac
69
+ # A bare Jira key is PROJECT-NUMBER, anchored end-to-end. The case glob this
70
+ # replaced ([A-Z]*-[0-9]*) matches whenever that shape appears ANYWHERE in a
71
+ # longer string (case patterns match the whole string, but `*` is greedy
72
+ # enough to swallow surrounding words) - "Add iOS-16 support" contains
73
+ # "S-16" and satisfied it, silently misrouting a free-text task description
74
+ # into a Jira lookup instead of the freetext path below.
75
+ if [[ "$in" =~ ^[A-Z][A-Z0-9]*-[0-9]+$ ]]; then
76
+ echo "jira-id"; return
77
+ fi
56
78
  # Bare issue number ("316" or "#316") must resolve BEFORE the repo#N glob:
57
79
  # `*\#[0-9]*` also matches "#316" with an empty repo prefix, which used to
58
80
  # misroute it to gh-short and build https://github.com/<owner>//issues/316.
@@ -140,7 +162,7 @@ fetch_jira() {
140
162
  return 1
141
163
  fi
142
164
  curl -sf -K <(jira_auth_cfg "$token") \
143
- "https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions" \
165
+ "https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions,parent" \
144
166
  || { echo "ERR: Jira fetch failed for $key" >&2; return 1; }
145
167
  }
146
168
 
@@ -197,6 +219,8 @@ descriptor = {
197
219
  "resolution": resolution or None,
198
220
  "fixVersions": fix_versions,
199
221
  "description": desc,
222
+ "parentKey": fields.get("parentKey") or None,
223
+ "parentDescription": fields.get("parentDescription") or None,
200
224
  "host": fields.get("host") or None,
201
225
  "url": fields.get("url") or None,
202
226
  "owner": fields.get("owner") or None,
@@ -213,6 +237,9 @@ title = (descriptor.get("title") or "").strip()
213
237
  status = (descriptor.get("status") or "").lower()
214
238
  itype = (descriptor.get("type") or "").lower()
215
239
 
240
+ parent_desc = (descriptor.get("parentDescription") or "").strip()
241
+ parent_key = descriptor.get("parentKey") or ""
242
+
216
243
  blockers, warnings = [], []
217
244
  closed = {"done","closed","cancelled","canceled","resolved"}
218
245
  if status in closed:
@@ -221,7 +248,14 @@ if status in closed:
221
248
  if resolution:
222
249
  blockers.append("already_resolved")
223
250
  if not desc.strip():
224
- blockers.append("description_empty")
251
+ # A parent with actual content is a candidate, not an auto-fix - surface it
252
+ # as a warning so the caller asks the user before substituting it in (or,
253
+ # under autopilot, auto-accepts per the same "warnings auto-continue" rule).
254
+ # No parent / an equally-empty parent keeps the hard blocker as before.
255
+ if parent_desc:
256
+ warnings.append("description_empty_parent_available")
257
+ else:
258
+ blockers.append("description_empty")
225
259
  elif len(desc.strip()) < 80:
226
260
  warnings.append("short_description")
227
261
  if len(title) < 10:
@@ -239,6 +273,7 @@ labels_tr = {
239
273
  "status_closed": "Issue kapalı veya iptal edilmiş",
240
274
  "already_resolved": "Issue zaten resolved/merged ({})".format(resolution or " - "),
241
275
  "description_empty": "Açıklama boş",
276
+ "description_empty_parent_available":"Açıklama boş ama parent'ta ({}) içerik var - oradan devam edilsin mi?".format(parent_key or " - "),
242
277
  "short_description": "Açıklama çok kısa (<80 karakter)",
243
278
  "short_title": "Başlık çok kısa (<10 karakter)",
244
279
  "no_repro_steps": "Bug için repro adımları yok",
@@ -248,6 +283,7 @@ labels_en = {
248
283
  "status_closed": "Issue is closed/cancelled",
249
284
  "already_resolved": "Issue already resolved/merged ({})".format(resolution or " - "),
250
285
  "description_empty": "Description is empty",
286
+ "description_empty_parent_available":"Description is empty but the parent ({}) has content - continue from there?".format(parent_key or " - "),
251
287
  "short_description": "Description too short (<80 chars)",
252
288
  "short_title": "Title too short (<10 chars)",
253
289
  "no_repro_steps": "Bug missing reproduction steps",
@@ -309,21 +345,52 @@ description = (f.get("description") or "")
309
345
  priority = _name(f.get("priority"))
310
346
  resolution = _name(f.get("resolution"))
311
347
  fixversions = ",".join((v or {}).get("name", "") for v in (f.get("fixVersions") or []))
312
- parts = [title, itype, status, description, priority, resolution, fixversions]
348
+ parentkey = (f.get("parent") or {}).get("key", "") or ""
349
+ parts = [title, itype, status, description, priority, resolution, fixversions, parentkey]
313
350
  parts = [v.replace("\x1f", " ") for v in parts]
314
351
  sys.stdout.write("\x1f".join(parts))
315
352
  ')
316
- IFS=$'\x1f' read -r -d '' title itype status description priority resolution fixversions <<< "$parsed" || true
353
+ IFS=$'\x1f' read -r -d '' title itype status description priority resolution fixversions parentkey <<< "$parsed" || true
354
+ # `<<<` appends a trailing newline to its input; `read -d ''` has no
355
+ # delimiter to stop on, so that newline folds into whichever field
356
+ # comes last - now parentkey. Strip it, or a genuinely-empty parent
357
+ # (no fallback needed) or a real key gets a stray \n appended, which
358
+ # breaks the parent lookup's URL and misreports parentKey in output.
359
+ parentkey="${parentkey%$'\n'}"
317
360
  else
318
361
  title="(unfetched)"; itype="Task"; status="Unknown"; description=""
319
- priority=""; resolution=""; fixversions=""
362
+ priority=""; resolution=""; fixversions=""; parentkey=""
363
+ fi
364
+ # Development sub-tasks are frequently filed with an empty description while
365
+ # the real requirements live on the parent (story/epic). Single hop only -
366
+ # if the parent is also empty/unreachable, description stays empty and
367
+ # description_empty blocks below, same as before this existed. This fetcher
368
+ # only surfaces the parent's description as a CANDIDATE (parentDescription)
369
+ # - it never substitutes it into "description" itself. Whether to actually
370
+ # use it is a decision for the caller to put in front of the user (or
371
+ # auto-accept under autopilot), same as any other maturity warning.
372
+ parentdesc=""
373
+ if [ -z "$(printf '%s' "$description" | tr -d '[:space:]')" ] && [ -n "${parentkey:-}" ]; then
374
+ praw=$(fetch_jira "$parentkey") || praw=""
375
+ if [ -n "$praw" ]; then
376
+ pdesc=$(printf '%s' "$praw" | python3 -c '
377
+ import json, sys
378
+ d = json.load(sys.stdin)
379
+ f = d.get("fields", {}) or {}
380
+ sys.stdout.write((f.get("description") or "").replace("\x1f", " "))
381
+ ')
382
+ if [ -n "$(printf '%s' "$pdesc" | tr -d '[:space:]')" ]; then
383
+ parentdesc="$pdesc"
384
+ fi
385
+ fi
320
386
  fi
321
387
  branch=$(branch_for "$KEY" "$itype" "$title")
322
388
  emit_descriptor \
323
389
  "kind=jira" "key=$KEY" "title=$title" "type=$itype" "status=$status" \
324
390
  "description=$description" "host=$JIRA_HOST" \
325
391
  "url=https://$JIRA_HOST/browse/$KEY" "branchHint=$branch" \
326
- "priority=$priority" "resolution=$resolution" "fixVersions=$fixversions"
392
+ "priority=$priority" "resolution=$resolution" "fixVersions=$fixversions" \
393
+ "parentKey=$parentkey" "parentDescription=$parentdesc"
327
394
  ;;
328
395
 
329
396
  jira-url)
@@ -137,7 +137,7 @@ MERMAID_FENCE_OPEN_RE = re.compile(r"^```mermaid\s*$")
137
137
  def mermaid_macro(body: str) -> str:
138
138
  return (
139
139
  '<ac:structured-macro ac:name="mermaid">'
140
- f'<ac:plain-text-body><![CDATA[\n{body}\n]]></ac:plain-text-body>'
140
+ f'<ac:plain-text-body><![CDATA[\n{cdata_escape(body)}\n]]></ac:plain-text-body>'
141
141
  '</ac:structured-macro>'
142
142
  )
143
143
 
@@ -205,6 +205,20 @@ def esc(s: str) -> str:
205
205
  return html.escape(s, quote=False)
206
206
 
207
207
 
208
+ def cdata_escape(s: str) -> str:
209
+ """Make `s` safe to embed inside a CDATA section.
210
+
211
+ A code fence or Mermaid diagram can legitimately contain the literal
212
+ three-character sequence "]]>" (a real XML CDATA example, a shell case
213
+ pattern, ...), which is the CDATA terminator - embedding it unescaped
214
+ closes the section early and leaves the rest of the storage-format XML
215
+ unparsed as markup. Splitting it across a close/reopen pair is the
216
+ standard CDATA-in-CDATA escape: it renders identically, but is no longer
217
+ a literal terminator to the XML parser.
218
+ """
219
+ return s.replace("]]>", "]]]]><![CDATA[>")
220
+
221
+
208
222
  @dataclass
209
223
  class InlineContext:
210
224
  attachments_available: set[str]
@@ -388,7 +402,7 @@ def markdown_to_storage(
388
402
  out.append(
389
403
  '<ac:structured-macro ac:name="code">'
390
404
  f'<ac:parameter ac:name="language">{esc(code_lang)}</ac:parameter>'
391
- f'<ac:plain-text-body><![CDATA[{body}]]></ac:plain-text-body>'
405
+ f'<ac:plain-text-body><![CDATA[{cdata_escape(body)}]]></ac:plain-text-body>'
392
406
  '</ac:structured-macro>'
393
407
  )
394
408
  i += 1
@@ -45,8 +45,11 @@ usage() {
45
45
  resolve_state() {
46
46
  local task_id="$1"
47
47
  local found=""
48
- for dir in "$HOME"/.claude/projects/*; do
49
- local candidate="$dir/state/$task_id/agent-state.json"
48
+ # multi-repo-pipeline.sh writes agent-state.json under a per-project
49
+ # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/) - not the
50
+ # ~/.claude/projects/*/state/ path this used to glob, which no writer in
51
+ # the pipeline ever populates.
52
+ for candidate in "$HOME"/.claude/logs/multi-agent/*/"$task_id"/agent-state.json; do
50
53
  if [ -f "$candidate" ]; then
51
54
  found="$candidate"
52
55
  break
@@ -45,10 +45,12 @@ if [ -z "$TASK_ID" ]; then
45
45
  exit 64
46
46
  fi
47
47
 
48
- # Resolve agent-state.json (matches phase-tracker convention).
48
+ # Resolve agent-state.json. multi-repo-pipeline.sh writes it under a
49
+ # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
50
+ # - not the ~/.claude/projects/*/state/ path this used to glob, which no
51
+ # writer in the pipeline ever populates.
49
52
  AGENT_STATE=""
50
- for dir in "$HOME"/.claude/projects/*; do
51
- candidate="$dir/state/$TASK_ID/agent-state.json"
53
+ for candidate in "$HOME"/.claude/logs/multi-agent/*/"$TASK_ID"/agent-state.json; do
52
54
  if [ -f "$candidate" ]; then
53
55
  AGENT_STATE="$candidate"
54
56
  break
@@ -302,11 +304,11 @@ post_bitbucket_server() {
302
304
 
303
305
  # Resolve credential keys from prefs.keychainMapping - never hardcode.
304
306
  prefs="$HOME/.claude/multi-agent-preferences.json"
305
- user_key=$(jq -r '.keychainMapping.bitbucket_user // empty' "$prefs" 2>/dev/null || true)
306
- token_key=$(jq -r '.keychainMapping.bitbucket_token // empty' "$prefs" 2>/dev/null || true)
307
+ user_key=$(jq -r '.global.keychainMapping.bitbucket_user // empty' "$prefs" 2>/dev/null || true)
308
+ token_key=$(jq -r '.global.keychainMapping.bitbucket_token // empty' "$prefs" 2>/dev/null || true)
307
309
 
308
310
  if [ -z "$user_key" ] || [ -z "$token_key" ]; then
309
- echo "post-pr-review: keychainMapping.bitbucket_user / .bitbucket_token unset in $prefs" >&2
311
+ echo "post-pr-review: global.keychainMapping.bitbucket_user / .bitbucket_token unset in $prefs" >&2
310
312
  return 4
311
313
  fi
312
314
 
@@ -222,19 +222,60 @@ do_prune() {
222
222
  rm -rf "$sd"
223
223
  printf 'shadow-git: pruned shadow repo for %s\n' "$task_id" >&2
224
224
  else
225
- # Trim history (truncate to last N snapshots within the window). For now,
226
- # implement as "rewind to oldest commit within window".
227
225
  [ ! -d "$sd/.git" ] && { err "no shadow for $task_id"; exit 1; }
226
+ # Retention never touches a real work-tree: this used to run
227
+ # `reset --hard` with --work-tree=$(pwd), silently rewriting whatever
228
+ # directory prune happened to be invoked from, and it kept the OLDEST
229
+ # commit inside the window while discarding every newer one - backwards
230
+ # for a "keep the last N days" policy. Every git call below is
231
+ # git-dir-only plumbing (log/rev-parse/commit-tree/update-ref/gc - no
232
+ # checkout, no reset, no clean); the one --work-tree `sg` still wants
233
+ # points at $sd itself, which prune already owns.
234
+ local branch
235
+ branch=$(sg "$sd" "$sd" symbolic-ref --short HEAD)
228
236
  local cutoff_epoch
229
237
  cutoff_epoch=$(($(date +%s) - older * 86400))
230
- local keep_sha
231
- keep_sha=$(sg "$sd" "$(pwd)" log --pretty='%H %ct' | awk -v c="$cutoff_epoch" '$2 >= c {print $1}' | tail -1)
232
- if [ -n "$keep_sha" ]; then
233
- printf 'shadow-git: oldest snapshot kept within window: %s\n' "$keep_sha" >&2
234
- # Hard-reset history to that snapshot. (Conservative: GC unreachable.)
235
- sg "$sd" "$(pwd)" reset --hard --quiet "$keep_sha" || true
236
- sg "$sd" "$(pwd)" gc --prune=now --quiet 2>/dev/null || true
238
+ # Commits strictly within the window, oldest first: what survives.
239
+ local keep_shas
240
+ keep_shas=$(sg "$sd" "$sd" log --reverse --pretty='%H %ct' "$branch" \
241
+ | awk -v c="$cutoff_epoch" '$2 >= c {print $1}')
242
+ if [ -z "$keep_shas" ]; then
243
+ printf 'shadow-git: no snapshot for %s is within %s day(s); nothing pruned\n' "$task_id" "$older" >&2
244
+ return 0
237
245
  fi
246
+ # Replay the surviving commits onto a fresh chain (oldest becomes the new
247
+ # root) so everything older than the cutoff becomes unreachable and
248
+ # gc-able, while every kept snapshot's tree/message/author/committer date
249
+ # is preserved exactly (committer date matters: it's what the NEXT prune
250
+ # run's cutoff math reads, so it must stay the original snapshot time,
251
+ # not "now").
252
+ local prev="" sha tree msg adate cdate aname aemail cname cemail
253
+ while IFS= read -r sha; do
254
+ [ -z "$sha" ] && continue
255
+ tree=$(sg "$sd" "$sd" rev-parse "$sha^{tree}")
256
+ msg=$(sg "$sd" "$sd" log -1 --format=%B "$sha")
257
+ adate=$(sg "$sd" "$sd" log -1 --format=%ad --date=raw "$sha")
258
+ cdate=$(sg "$sd" "$sd" log -1 --format=%cd --date=raw "$sha")
259
+ aname=$(sg "$sd" "$sd" log -1 --format=%an "$sha")
260
+ aemail=$(sg "$sd" "$sd" log -1 --format=%ae "$sha")
261
+ cname=$(sg "$sd" "$sd" log -1 --format=%cn "$sha")
262
+ cemail=$(sg "$sd" "$sd" log -1 --format=%ce "$sha")
263
+ if [ -n "$prev" ]; then
264
+ prev=$(printf '%s' "$msg" \
265
+ | GIT_AUTHOR_NAME="$aname" GIT_AUTHOR_EMAIL="$aemail" GIT_AUTHOR_DATE="$adate" \
266
+ GIT_COMMITTER_NAME="$cname" GIT_COMMITTER_EMAIL="$cemail" GIT_COMMITTER_DATE="$cdate" \
267
+ git --git-dir="$sd/.git" commit-tree "$tree" -p "$prev")
268
+ else
269
+ prev=$(printf '%s' "$msg" \
270
+ | GIT_AUTHOR_NAME="$aname" GIT_AUTHOR_EMAIL="$aemail" GIT_AUTHOR_DATE="$adate" \
271
+ GIT_COMMITTER_NAME="$cname" GIT_COMMITTER_EMAIL="$cemail" GIT_COMMITTER_DATE="$cdate" \
272
+ git --git-dir="$sd/.git" commit-tree "$tree")
273
+ fi
274
+ done <<<"$keep_shas"
275
+ git --git-dir="$sd/.git" update-ref "refs/heads/$branch" "$prev"
276
+ sg "$sd" "$sd" gc --prune=now --quiet 2>/dev/null || true
277
+ printf 'shadow-git: pruned snapshots older than %s day(s) for %s (kept %s)\n' \
278
+ "$older" "$task_id" "$(printf '%s\n' "$keep_shas" | wc -l | tr -d ' ')" >&2
238
279
  fi
239
280
  }
240
281
 
@@ -168,10 +168,17 @@ case "$MODE" in
168
168
  echo "[]"
169
169
  exit 0
170
170
  fi
171
+ # emit_json already prints a valid "[]" on empty/unparseable stdin (a
172
+ # failed `gh api` call, under pipefail, still runs it through the rest of
173
+ # this pipe with empty input) - the `|| echo "[]"` fallback used to ALSO
174
+ # fire on the pipeline's own (gh api's) non-zero exit, printing a SECOND
175
+ # "[]" line and leaving invalid double-JSON on stdout. `|| true` keeps
176
+ # the local-mode contract (unreachable submodule data -> empty list,
177
+ # exit 0, never a hard error) without re-printing anything.
171
178
  gh api "repos/$TARGET/contents/.gitmodules" \
172
179
  --jq '.content' 2>/dev/null \
173
180
  | tr -d '\n' \
174
181
  | base64 --decode 2>/dev/null \
175
- | emit_json || echo "[]"
182
+ | emit_json || true
176
183
  ;;
177
184
  esac
@@ -6,7 +6,7 @@ description: "Internal - input type detection for multi-agent dispatcher."
6
6
 
7
7
  The top-level `multi-agent` command classifies user arguments per the rules below.
8
8
 
9
- > **Language**: Schema reference only - no user-facing text. Picker `label` + `header` always English (`promptLanguage` is locked to `"en"`); `question` + `description` follow `outputLanguage` per the `rules.md` matrix. Commit/PR/Jira payloads stay English.
9
+ > **Language**: Schema reference only - no user-facing text. Picker `label` + `header` always English (`promptLanguage` is locked to `"en"`); `question` + `description` follow `outputLanguage` per the `rules.md` matrix. Commit messages, branch names and PR titles stay English; PR/Jira/Confluence **bodies** follow `outputLanguage`.
10
10
 
11
11
  ## Type Table
12
12
 
@@ -116,11 +116,14 @@ Auto-suggested as `"{jiraId} - {taskTitle}"` - user can edit at the prompt.
116
116
  | Inline `code` | `<code>code</code>` |
117
117
  | Links `[t](u)` | `<a href="u">t</a>` |
118
118
  | Lists | `<ul><li>...</li></ul>` / `<ol><li>...</li></ol>` |
119
+ | ` ```mermaid ` fences | `ac:name="mermaid"` macro (`md2confluence-v3.py` implements it, with a numbered-list fallback when the space lacks the plugin) - not the generic code macro |
119
120
 
120
121
  Body is run through humanizer before XML conversion - Confluence tone is the most formal of the four channels (target audience: stakeholders, PMs, designers).
121
122
 
122
123
  ## POST contract
123
124
 
125
+ The paths below are relative to the API base, which depends on the Confluence flavor - resolve it the way `lib/fetch-confluence.sh` already does: hosts matching `*atlassian.net` (Cloud) use `https://<host>/wiki/rest/api`, everything else (Server / Data Center) uses `https://<host>/rest/api`. On Cloud the un-prefixed form 404s on every call. (Cloud also serves a v2 API at `/wiki/api/v2` with ADF bodies; this adapter stays on v1 + storage format, which both flavors accept.)
126
+
124
127
  ```
125
128
  POST /rest/api/content
126
129
  {
@@ -43,7 +43,7 @@ The template (this doc) is English because `promptLanguage="en"` is locked. At w
43
43
  <one line per repo touched; never collapse multiple repos into one>
44
44
 
45
45
  ### Acceptance Criteria
46
- - [<x|·>] <AC text - `x` if delivered, `·` if intentionally deferred (with `(deferred: <reason>)` suffix)>
46
+ - [<x| >] <AC text - `x` if delivered, space if intentionally deferred (with `(deferred: <reason>)` suffix)>
47
47
  <one bullet per AC item from the issue body, or per implicit AC if the issue had none>
48
48
 
49
49
  ### Build & Test
@@ -65,7 +65,7 @@ Ref: #<issueNumber>
65
65
  - **`title`** - always wraps the component or task name in backticks. No emoji.
66
66
  - **`summary`** - 1-3 paragraphs, **why** before **what**. No bullet lists in the summary block - those belong in `acceptance_criteria`.
67
67
  - **`prs_branches`** - every repo the run touched gets exactly ONE line. If the PR was deferred, write the branch + commit SHA explicitly so the reader can `gh pr create` from there. Never collapse multiple repos into one line.
68
- - **`acceptance_criteria`** - copy from the issue body verbatim when an `### Acceptance Criteria` section exists. If the section is `_No response_`, the agent enumerates the implicit ACs it satisfied during Phase 1/3. `[x]` only when delivered and verified; `[·]` for deferred (with reason in parens).
68
+ - **`acceptance_criteria`** - copy from the issue body verbatim when an `### Acceptance Criteria` section exists. If the section is `_No response_`, the agent enumerates the implicit ACs it satisfied during Phase 1/3. `[x]` only when delivered and verified; `[ ]` for deferred, with the reason in parens - GitHub's task-list syntax accepts only `[ ]` and `[x]`, any other marker (a `·` was used here once) renders as literal bracket text instead of a checkbox, and the `(deferred: <reason>)` suffix is what distinguishes deferred from merely unchecked.
69
69
  - **`build_and_test`** - required. If a build failure was pre-existing, the line includes the baseline SHA where the failure was reproduced (per `feedback_build_baseline_proof.md`).
70
70
  - **`figma_extras`** - emitted only when `state.taskType == "component"` and the figma sub-pipeline ran.
71
71
  - **`next_steps`** - empty list is fine; the section header itself stays. Empty signals "fully complete, no follow-ups".
@@ -64,14 +64,25 @@ Body markdown is rewritten to Jira wiki markup before POST:
64
64
 
65
65
  | Markdown | Jira wiki |
66
66
  |---|---|
67
- | `### Heading` | `*Heading*` |
67
+ | `## Heading` | `h2. Heading` |
68
+ | `### Heading` | `h3. Heading` |
69
+ | `**bold**` | `*bold*` |
70
+ | `1. item` / `2. item` | `# item` (Jira does not render `1.` as an ordered list) |
71
+ | `- item` | `* item` |
68
72
  | `- [ ] task` / `- [x] task` | `# task` (numbered list - Jira renders TODO checkboxes inconsistently) |
69
73
  | `` `code` `` | `{{code}}` |
70
- | Tables (` | h | h | / | --- | --- |`) | `||h||h||` then `|c|c|` rows |
74
+ | `[text](url)` | `[text\|url]` |
75
+ | Tables (`\| h \| h \|` / `\| --- \| --- \|`) | `\|\|h\|\|h\|\|` then `\|c\|c\|` rows |
71
76
  | Code fences ` ```lang ` | `{code:lang} ... {code}` |
72
77
 
78
+ The `\|` sequences in the two link/table rows are this table's own escape for a literal pipe - the emitted Jira markup carries `|`, never `\|`.
79
+
73
80
  Lines outside these patterns pass through verbatim. Multi-paragraph blocks are joined with one blank line.
74
81
 
82
+ Every row above is load-bearing, including the ones that look cosmetic. The table must cover each construct the section templates in this file actually emit - `##` for the three required headings, `**Given**` / `**When**` / `**Then**` in the test-scenario skeleton, and `1.`-numbered scenarios. A missing row does not degrade gracefully: the "pass through verbatim" fallback POSTs `## Test Senaryoları` and `**Given**` as literal text, so the comment renders with visible `##` and stray asterisks. Single `*bold*` in Markdown means *italic* in Jira wiki - never map `**bold**` to `*bold*` by dropping one asterisk mechanically without checking the source was bold, not italic.
83
+
84
+ There is no markdown→Jira-wiki converter program in the pipeline (`lib/` ships `md2confluence-v3.py` for Confluence only). This table is applied by the model, by hand, which is exactly why it has to be complete.
85
+
75
86
  ## Cross-link injection
76
87
 
77
88
  The adapter prepends the linked PR URL on the **first line** so reviewers can jump to the PR with one click:
@@ -78,7 +78,7 @@ If a finding's `line` falls outside the diff (review of unchanged code), the anc
78
78
  | Provider | Approve | Needs Work / Request Changes |
79
79
  |---|---|---|
80
80
  | GitHub | `gh pr review <N> --approve` | `gh pr review <N> --request-changes --body "<empty>"` |
81
- | Bitbucket Server | `PUT /pull-requests/{id}/participants/{userSlug}` body `{"status":"APPROVED"}` | `PUT /pull-requests/{id}/participants/{userSlug}` body `{"status":"NEEDS_WORK"}` |
81
+ | Bitbucket Server | `PUT /rest/api/1.0/projects/{key}/repos/{slug}/pull-requests/{id}/participants/{userSlug}` body `{"status":"APPROVED"}` | same path, body `{"status":"NEEDS_WORK"}` |
82
82
 
83
83
  The Bitbucket needs-work pattern keeps the user as REVIEWER role; status flip is the visible "Request changes" signal in the UI.
84
84
 
@@ -75,6 +75,22 @@ Never use `Closes #N`, `Fixes #N`, `Resolves PROJ-X`. Issues require 4-approval
75
75
  5. Dispatch per the Behaviour-by-remote table.
76
76
  ```
77
77
 
78
+ ## Markup dialect (per surface, not per pipeline)
79
+
80
+ The PR body is **Markdown** on every supported remote - GitHub, Bitbucket Server, and GitLab all render Markdown in the description field. Emit the assembled markdown verbatim; there is no conversion step on this adapter.
81
+
82
+ Jira wiki markup in a PR body is a defect, not a style choice. The Jira adapter's conversion table (`channels/jira.md` "Wiki markup conversion") applies to the **Jira comment only** and must never be reached from here. Concretely, in a PR body:
83
+
84
+ | Never in a PR body | Renders as | Use instead |
85
+ |---|---|---|
86
+ | `h2. Title` / `h3. Title` | literal text `h2. Title` | `## Title` / `### Title` |
87
+ | `{{identifier}}` | literal braces `{{identifier}}` | `` `identifier` `` |
88
+ | `# item` for a numbered list | an H1 heading, one giant line per item | `1. item` |
89
+ | `{code:swift} ... {code}` | literal braces | fenced ` ```swift ` block |
90
+ | `*bold*` | italic in Markdown, not bold | `**bold**` |
91
+
92
+ The two adapters run in the same phase over the same source markdown, so the failure mode is a converter applied to the wrong target: the Jira comment is correct and the PR body ships raw wiki markup. Assemble once in markdown, then convert **only** on the Jira branch.
93
+
78
94
  ## Behaviour by remote
79
95
 
80
96
  | Remote | API | Reviewer handling |
@@ -106,6 +122,10 @@ curl -s -X PUT -u "$BB_USER:$BB_TOKEN" -H "Content-Type: application/json" \
106
122
  --data-binary @/tmp/channels-$TASK_ID-pr-payload.json "$PR_URL"
107
123
  ```
108
124
 
125
+ Verify after every PUT: refetch the PR and compare `reviewers | length` against the pre-PUT count. A drop means the payload lost the field - repair immediately.
126
+
127
+ `POST /pull-requests/{id}/participants` is **repair-only**, never the primary path. It accepts one user per call, so restoring N reviewers writes N separate "added 1 reviewer" rows into the PR activity feed, and Bitbucket activity entries cannot be deleted. A PR that opened with 22 reviewers in one clean create call and then shows 22 individual re-adds is a visible, permanent record of a dropped-reviewer PUT. Carry `reviewers` through the payload above instead.
128
+
109
129
  ## Multi-repo cross-links
110
130
 
111
131
  When `state.projects[].length > 1`, `channels-multi-repo.sh render-pr <state> <body> <repoName>` prepends a `## Related PRs` block:
@@ -62,6 +62,6 @@ Adapter implementation chooses the right git push target + commit message format
62
62
  - Wiki adapter is the **only** channel allowed to skip silently when its precondition gate fails. PR / Jira / Confluence always run when selected.
63
63
  - Per the `figma-pipeline.md` rules, every component wiki page must include a variant matrix, accessibility identifiers, analytics events, and 3-layer test summary.
64
64
  - Screenshots cover light/dark + LTR/RTL - wiki adapter refuses to commit if any quadrant is missing.
65
- - Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, design token names, and wiki markup stay verbatim across languages. The template (this doc) is English because `promptLanguage="en"` is locked.
65
+ - Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, design token names, and Markdown formatting stay verbatim across languages (wiki pages are `.md` files - "wiki markup" in this doc set means Jira's dialect, which never appears here). The template (this doc) is English because `promptLanguage="en"` is locked.
66
66
  - No decorative/emotive emoji or smileys (😊 🙂 🎉 👍 🚀 ✨) in the page prose. Only functional/structural marks a fixed template defines are allowed.
67
67
  - Autopilot always pauses at the channels menu (per `phase-7-report.md` autopilot contract) - even in autopilot mode the user gets to confirm wiki scope.
@@ -6,14 +6,15 @@
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Command Inventory (44 commands)
9
+ ## 1. Command Inventory (49 commands)
10
10
 
11
11
  ```
12
12
  analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, design-check, dev,
13
13
  dev-autopilot, dev-local, dev-local-autopilot, diff-explain, forget, garbage-collect,
14
14
  help, ios-coding-standard, issue, jira, kill, language, local,
15
15
  local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
16
- routines, save, scan, search, setup, ship, stack, status, sync, test, testflight-validation, uninstall, update
16
+ routines, save, scan, search, setup, ship, stack, status, store-ready, sync, test, test-accessibility,
17
+ test-dark-mode, test-dynamic-type, test-screenshots, testflight-validation, uninstall, update
17
18
  ```
18
19
 
19
20
  Categories:
@@ -163,10 +164,10 @@ skills took the block from 11 skills / 4,710 bytes to 83 skills / 22,111 bytes
163
164
  only **75 of the 142** surfaced, **and an unrelated user-scope skill was evicted**.
164
165
  Removing the plugin brought it back.
165
166
 
166
- So shipping the 43 sub-commands as peer skills on Codex would silently lose
167
+ So shipping the 49 sub-commands as peer skills on Codex would silently lose
167
168
  pipeline commands next to any stack toolkit, with no error anywhere. The pipeline
168
169
  therefore contributes **exactly one** skill on Codex (`multi-agent`) and keeps the
169
- 43 sub-command specs as reference files that cost nothing until read.
170
+ 49 sub-command specs as reference files that cost nothing until read.
170
171
 
171
172
  **Do not "fix" this by adding per-command skills on Codex.** The layout is
172
173
  capability-derived, and `smoke-install-layout.sh` fails if the Codex skills tree
@@ -175,7 +176,7 @@ gains a second pipeline entry.
175
176
  ### Parity axis differs per host
176
177
 
177
178
  Claude Code and Copilot CLI are compared on their **skill directory sets**. Codex is
178
- compared on its **ref set**: the 43 command specs must all exist under
179
+ compared on its **ref set**: the 49 command specs must all exist under
179
180
  `~/.codex/multi-agent-refs/commands/<cmd>/SKILL.md`, and
180
181
  `smoke-codex-install.sh` asserts the count against the source tree. Comparing Codex
181
182
  on skill directories would demand exactly the layout that breaks it.
@@ -51,7 +51,7 @@ This is why the removal is safe:
51
51
  |---|---|---|
52
52
  | Phase 7 triage-memory ingest | `triage-output.json` | `[ -f ]`-guarded, so it degrades **silently**: the triage corpus and learnings ledger stop being fed and no error appears |
53
53
  | Phase 7 learnings-ledger distill | same file | same silent degradation |
54
- | `render-work-summary.sh` | `agent-state.json`, `phase-tracker.json` | exits 2, so the Work Summary vanishes from the PR body and the Jira comment |
54
+ | `render-work-summary.sh` | `agent-state.json`, `phase-tracker.json` (falls back to `logs/multi-agent/<task>/tracker-state.json`, which survives removal) | loses the salvaged copies but keeps the tracker via the logs fallback |
55
55
  | `:resume` | `agent-state.json` | cannot continue a Phase 7 pause |
56
56
  | `:status`, `:log` | `agent-state.json` | the task becomes invisible |
57
57
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **TLDR** - Shared 12-step flow for `/multi-agent:create-jira`. Asks the issue type (Task / Bug / Story), mines the target project's existing same-type issues to learn team conventions, detects the active sprint, drafts a standards-compliant issue from a fixed standard template with auto-sizing sections, asks the user about every genuinely unknown field, renders a full preview, and creates the Jira issue only after explicit approval. Creates exactly one Jira issue per run - no branches, no commits, no worktrees.
4
4
 
5
- Consumed by `generate.md`. This ref is never invoked directly.
5
+ Consumed by `create-jira/SKILL.md`. This ref is never invoked directly.
6
6
 
7
7
  ## Hard rules (must not regress)
8
8
 
@@ -10,7 +10,7 @@ Consumed by `generate.md`. This ref is never invoked directly.
10
10
  - **Never invent content.** Anything the user did not supply and mining could not derive stays an open question (step [8/12]) or is omitted entirely - never fabricated repro steps, environments, acceptance criteria, or test pass/fail results.
11
11
  - **Standard template is the baseline.** The description structure comes from the type's standard template (see [7/12]), not from a mined heading set. Mining informs summary prefix, labels, components, priority norm, sprint placement, and test-scenario style - never replaces the standard section skeleton.
12
12
  - **Auto-sizing.** A conditional section renders only when its trigger is present. No empty placeholder headings - if there is nothing to put in a conditional section, it does not appear.
13
- - **Issue content language follows `prefs.global.outputLanguage`.** Summary and description are written in the user's output language (intentional exception to the "external payloads stay English" default - the issue is authored FOR the user's team). Code identifiers, file paths, and URLs stay verbatim. AskUserQuestion `question`/`description` also follow `outputLanguage`; `label`/`header` stay English.
13
+ - **Issue content language follows `prefs.global.outputLanguage`.** Summary and description are written in the user's output language, like every other user-facing payload body (`rules.md` matrix). Code identifiers, file paths, and URLs stay verbatim. AskUserQuestion `question`/`description` also follow `outputLanguage`; `label`/`header` stay English.
14
14
  - **UTF-8 verbatim POST path** (same as `$HOME/.claude/multi-agent-refs/channels/jira.md`): description body goes to a file, `jq -n --rawfile` builds the payload, `curl --data-binary @file` ships it. Never round-trip through `unicode_escape`/`latin-1`, never hand-roll a re-encoding helper. Jira wiki markup (`h3.` headings, `[text|url]` links), real newlines, no HTML entities.
15
15
  - **Humanizer pass** on the description body after composition, before the preview render.
16
16
  - **Read-only until approval.** Steps 1-9 perform only GET requests. The first write of any kind is the `POST /rest/api/2/issue` in step [10/12].
@@ -42,7 +42,7 @@ Decision table:
42
42
  2. Resolve Jira token via `keychainMapping.jira`. Token missing → run Token Save Flow from `setup.md` inline (clipboard-based; user can add now or skip). Skip → treat as `never` for this run.
43
43
  3. Derive Jira fields from the GitHub issue:
44
44
  - `summary` ← GitHub issue title (truncate to 255 chars).
45
- - `description` ← GitHub issue body, prefixed with `h3. From GitHub\n[GH{issueNo}|{url}]\n\n`.
45
+ - `description` ← GitHub issue body run through the markdown → Jira wiki conversion table (`channels/jira.md`) - the body is GitHub Markdown (`###`, `- [ ]`, backticks) and Jira's description field renders wiki markup, so an unconverted body arrives as literal text under a correctly-rendered prefix. Prefix the converted body with the two lines `h3. From GitHub` and `[GH{issueNo}|{url}]`, then a blank line (real newlines in the payload file, never literal `\n`).
46
46
  - `issueType` - infer from labels: `bug` → Bug, `enhancement` → Story, default Task.
47
47
  - `project` - `figmaConfig.jira.projectKey`, or `prefs.global.defaultJiraKey`, or prompt (cached after first use).
48
48
  - `priority` - derive from labels if present (`priority:high` → High, etc.); else leave unset (Jira default).
@@ -66,7 +66,7 @@ Flow:
66
66
 
67
67
  1. Emit `→ posting wiki summary to Jira {jiraId}`.
68
68
  2. Read the first N lines of the primary wiki markdown file (the component's overview page - one of `writtenPaths[0]`).
69
- 3. Render as Jira comment: title line (`h3. Component docs - {componentName}`) + an overview paragraph + a link to the full page (wiki URL, if the adapter returned `pushedRemote`).
69
+ 3. Render as Jira comment: title line (`h3. Component docs - {componentName}`) + an overview paragraph + a link to the full page (wiki URL, if the adapter returned `pushedRemote`). The overview lines come from a Markdown file - convert them via the `channels/jira.md` table before POST, exactly like the title line already is.
70
70
  4. **Run through the humanizer skill** - same policy as Step 4 Confluence: user-facing content must read naturally.
71
71
  5. `POST {jira.baseUrl}/rest/api/2/issue/{jiraId}/comment`.
72
72
  6. Log: `Phase 7: wiki summary posted to Jira {jiraId}`.
@@ -96,7 +96,7 @@ Rationale: autopilot is explicit consent for side-effect creation; muting Jira a
96
96
  Both Claude Code and Copilot CLI implementations MUST:
97
97
 
98
98
  - Resolve the `autoJiraFromGithubIssue` enum the same way.
99
- - Derive Jira fields from GitHub issue metadata identically (title → summary, body → description with `h3. From GitHub` prefix, label → issueType mapping).
99
+ - Derive Jira fields from GitHub issue metadata identically (title → summary, body → description via the markdown → wiki conversion + `h3. From GitHub` prefix, label → issueType mapping).
100
100
  - Patch the GitHub issue body with the `Jira: [KEY](url)` line after create.
101
101
  - Treat failures as non-blocking with the same log shape.
102
102
  - Run the humanizer skill on the wiki-summary Jira comment.
@@ -56,7 +56,7 @@ Knowledge files grow over time. Maintenance rules:
56
56
  - 90-day-old entries are considered "stale" - not used without verification
57
57
  - Stale check: orchestrator checks file mtime during Phase 1 knowledge injection
58
58
  - Stale entries are added to the prompt with a "STALE - verify before relying" tag
59
- - `clear-logs` does not touch knowledge - only deletes logs and state
59
+ - `prune-logs` does not touch knowledge - only deletes logs and state
60
60
  - `purge` does not touch knowledge either - separate command: `/multi-agent clear-knowledge {project}`
61
61
 
62
62
  ---