@mmerterden/multi-agent-pipeline 14.2.1 → 15.0.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 +118 -6
- package/README.md +20 -9
- package/README.tr.md +150 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +24 -9
- package/docs/ecosystem.md +237 -0
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +33 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +37 -7
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +15 -12
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/extract-conventions.sh +44 -15
- package/pipeline/lib/fetch-figma-annotations.sh +8 -1
- package/pipeline/lib/fetch-fortify.sh +23 -8
- package/pipeline/lib/figma-screenshot.sh +11 -1
- package/pipeline/lib/issue-fetcher.sh +77 -10
- package/pipeline/lib/md2confluence-v3.py +16 -2
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/lib/plan-todos.sh +5 -2
- package/pipeline/lib/post-pr-review.sh +8 -6
- package/pipeline/lib/shadow-git.sh +50 -9
- package/pipeline/lib/submodule-detector.sh +8 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +13 -2
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +20 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +3 -3
- package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
- package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
- package/pipeline/multi-agent-refs/phases/modes.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
- package/pipeline/multi-agent-refs/phases.md +6 -0
- package/pipeline/multi-agent-refs/rules.md +2 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +277 -67
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +14 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +35 -60
- package/pipeline/scripts/check-derived-drift.mjs +65 -29
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +33 -12
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/pre-commit-check.sh +69 -22
- package/pipeline/scripts/render-agent-log-cost.sh +8 -3
- package/pipeline/scripts/render-cost-summary.sh +42 -22
- package/pipeline/scripts/render-work-summary.sh +47 -13
- package/pipeline/scripts/review-scope.mjs +1 -1
- package/pipeline/scripts/run-aggregator.mjs +43 -14
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +196 -14
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +459 -13
- package/pipeline/skills/shared/README.md +15 -11
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
- package/pipeline/skills/skills-index.md +9 -5
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# parse-complaints.sh
|
|
4
|
+
# Normalize a batch of customer complaints into the uniform JSON list the
|
|
5
|
+
# /multi-agent:complaint-analysis command consumes. Accepts a file export
|
|
6
|
+
# (csv / xlsx / txt / json) or pasted free text on stdin, extracts any
|
|
7
|
+
# transaction / conversation ids embedded in the text, and redacts common
|
|
8
|
+
# PII shapes before anything leaves this script. The raw input is never
|
|
9
|
+
# copied into pipeline state - only the redacted view is.
|
|
10
|
+
#
|
|
11
|
+
# Input forms (exactly one source is required):
|
|
12
|
+
# ./parse-complaints.sh --file <path> [--format csv|xlsx|txt|json|auto]
|
|
13
|
+
# ./parse-complaints.sh --stdin [--format text]
|
|
14
|
+
#
|
|
15
|
+
# Options:
|
|
16
|
+
# --no-redact keep complaint text verbatim (debugging only; the skill
|
|
17
|
+
# never passes this flag)
|
|
18
|
+
#
|
|
19
|
+
# Column mapping (file formats, case-insensitive header match):
|
|
20
|
+
# trxId <- trxId | trx | transaction id / transaction_id / transactionId
|
|
21
|
+
# convId <- convId | conversationId | x-conversationId
|
|
22
|
+
# date <- date | created | createdAt | timestamp
|
|
23
|
+
# platformHint <- platform | channel | os | device
|
|
24
|
+
# text <- comment | complaint | text | description | message, else the
|
|
25
|
+
# longest text column
|
|
26
|
+
#
|
|
27
|
+
# Free-text mode splits on blank lines (or numbered bullets when the paste is
|
|
28
|
+
# one block) and pre-fills ids per block using the same label set as
|
|
29
|
+
# context-link-extractor.sh GRAYLOG_RE (x-conversationId | conversationId |
|
|
30
|
+
# convId | trxId | trx | transaction id).
|
|
31
|
+
#
|
|
32
|
+
# Redaction (default ON, applied after id extraction so ids survive):
|
|
33
|
+
# email -> [REDACTED:email]
|
|
34
|
+
# 13-19 digit card-like runs -> [REDACTED:card]
|
|
35
|
+
# 11 digit national-id-like numbers -> [REDACTED:national-id]
|
|
36
|
+
# 9+ digit phone-like runs -> [REDACTED:phone]
|
|
37
|
+
# 6-char PNR-like tokens (A-Z0-9 mix)-> [REDACTED:pnr]
|
|
38
|
+
#
|
|
39
|
+
# Output (stdout, single JSON object):
|
|
40
|
+
# {
|
|
41
|
+
# "parsedAt": "<ISO8601>",
|
|
42
|
+
# "source": "<path>"|"stdin",
|
|
43
|
+
# "format": "csv|xlsx|txt|json|text",
|
|
44
|
+
# "complaints": [
|
|
45
|
+
# { "id": "C-01", "text": "...", "trxId": "..."|null,
|
|
46
|
+
# "convId": "..."|null, "date": "..."|null,
|
|
47
|
+
# "platformHint": "..."|null }
|
|
48
|
+
# ],
|
|
49
|
+
# "redacted": <bool>, "degraded": <bool>, "degradeReason": "..."|null
|
|
50
|
+
# }
|
|
51
|
+
#
|
|
52
|
+
# Exit codes:
|
|
53
|
+
# 0 success
|
|
54
|
+
# 4 bad usage (no source, unknown flag, unreadable file)
|
|
55
|
+
# 5 format degrade (xlsx requested but openpyxl is not installed; the
|
|
56
|
+
# degraded JSON on stdout tells the user to export as CSV and re-run)
|
|
57
|
+
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
FILE=""
|
|
61
|
+
USE_STDIN="false"
|
|
62
|
+
FORMAT="auto"
|
|
63
|
+
REDACT="true"
|
|
64
|
+
|
|
65
|
+
while [ $# -gt 0 ]; do
|
|
66
|
+
case "$1" in
|
|
67
|
+
--file) FILE="$2"; shift 2 ;;
|
|
68
|
+
--stdin) USE_STDIN="true"; shift ;;
|
|
69
|
+
--format) FORMAT="$2"; shift 2 ;;
|
|
70
|
+
--no-redact) REDACT="false"; shift ;;
|
|
71
|
+
-h|--help)
|
|
72
|
+
echo "usage: $0 --file <path> [--format csv|xlsx|txt|json|auto] [--no-redact] | --stdin [--format text]" >&2
|
|
73
|
+
exit 4 ;;
|
|
74
|
+
*)
|
|
75
|
+
echo "ERR: unexpected arg $1" >&2; exit 4 ;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ -z "$FILE" ] && [ "$USE_STDIN" != "true" ]; then
|
|
80
|
+
echo "ERR: no source (pass --file <path> or --stdin)" >&2
|
|
81
|
+
exit 4
|
|
82
|
+
fi
|
|
83
|
+
if [ -n "$FILE" ] && [ "$USE_STDIN" = "true" ]; then
|
|
84
|
+
echo "ERR: pass either --file or --stdin, not both" >&2
|
|
85
|
+
exit 4
|
|
86
|
+
fi
|
|
87
|
+
if [ -n "$FILE" ] && [ ! -r "$FILE" ]; then
|
|
88
|
+
echo "ERR: file not readable: $FILE" >&2
|
|
89
|
+
exit 4
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [ "$FORMAT" = "auto" ]; then
|
|
93
|
+
if [ "$USE_STDIN" = "true" ]; then
|
|
94
|
+
FORMAT="text"
|
|
95
|
+
else
|
|
96
|
+
case "$FILE" in
|
|
97
|
+
*.csv) FORMAT="csv" ;;
|
|
98
|
+
*.xlsx) FORMAT="xlsx" ;;
|
|
99
|
+
*.json) FORMAT="json" ;;
|
|
100
|
+
*.txt|*.md) FORMAT="txt" ;;
|
|
101
|
+
*)
|
|
102
|
+
echo "ERR: cannot infer format from extension; pass --format" >&2
|
|
103
|
+
exit 4 ;;
|
|
104
|
+
esac
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
case "$FORMAT" in
|
|
109
|
+
csv|xlsx|txt|json|text) : ;;
|
|
110
|
+
*) echo "ERR: unknown format $FORMAT" >&2; exit 4 ;;
|
|
111
|
+
esac
|
|
112
|
+
|
|
113
|
+
STDIN_PAYLOAD=""
|
|
114
|
+
if [ "$USE_STDIN" = "true" ]; then
|
|
115
|
+
STDIN_PAYLOAD=$(cat)
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
PC_FILE="$FILE" PC_FORMAT="$FORMAT" PC_REDACT="$REDACT" PC_STDIN="$STDIN_PAYLOAD" \
|
|
119
|
+
python3 - <<'PY'
|
|
120
|
+
import csv, datetime, io, json, os, re, sys
|
|
121
|
+
|
|
122
|
+
fmt = os.environ["PC_FORMAT"]
|
|
123
|
+
path = os.environ.get("PC_FILE") or ""
|
|
124
|
+
redact_on = os.environ.get("PC_REDACT") == "true"
|
|
125
|
+
|
|
126
|
+
# Same label set as context-link-extractor.sh GRAYLOG_RE.
|
|
127
|
+
ID_LABEL = re.compile(
|
|
128
|
+
r"(?P<label>x-conversationId|conversationId|convId|trxId|trx|transaction[ _-]?id)"
|
|
129
|
+
r"\s*[:=#]?\s*(?P<value>[A-Za-z0-9][A-Za-z0-9._-]{5,63})",
|
|
130
|
+
re.IGNORECASE,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def extract_ids(text):
|
|
134
|
+
trx, conv = None, None
|
|
135
|
+
for m in ID_LABEL.finditer(text or ""):
|
|
136
|
+
label = m.group("label").lower()
|
|
137
|
+
value = m.group("value")
|
|
138
|
+
if "conv" in label:
|
|
139
|
+
conv = conv or value
|
|
140
|
+
else:
|
|
141
|
+
trx = trx or value
|
|
142
|
+
return trx, conv
|
|
143
|
+
|
|
144
|
+
# Order matters: card (13-19 digits, separators allowed) before national-id
|
|
145
|
+
# (exactly 11) before phone (9+); a 16-digit card must not survive as a
|
|
146
|
+
# "phone". PNR-like runs last so redacted placeholders are not re-matched.
|
|
147
|
+
REDACTIONS = [
|
|
148
|
+
(re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[REDACTED:email]"),
|
|
149
|
+
(re.compile(r"\b\d(?:[ -]?\d){12,18}\b"), "[REDACTED:card]"),
|
|
150
|
+
(re.compile(r"\b\d{11}\b"), "[REDACTED:national-id]"),
|
|
151
|
+
(re.compile(r"(?<![\w.-])\+?\d(?:[ ()-]?\d){8,14}\b"), "[REDACTED:phone]"),
|
|
152
|
+
(re.compile(r"\b(?=[A-Z0-9]{6}\b)(?=[A-Z0-9]*[A-Z])(?=[A-Z0-9]*\d)[A-Z0-9]{6}\b"), "[REDACTED:pnr]"),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
def redact(text):
|
|
156
|
+
if not redact_on:
|
|
157
|
+
return text
|
|
158
|
+
for rx, repl in REDACTIONS:
|
|
159
|
+
text = rx.sub(repl, text)
|
|
160
|
+
return text
|
|
161
|
+
|
|
162
|
+
TEXT_KEYS = ("comment", "complaint", "text", "description", "message", "yorum", "sikayet")
|
|
163
|
+
TRX_KEYS = re.compile(r"^(trxid|trx|transaction[ _-]?id)$", re.IGNORECASE)
|
|
164
|
+
CONV_KEYS = re.compile(r"^(convid|conversationid|x-conversationid)$", re.IGNORECASE)
|
|
165
|
+
DATE_KEYS = re.compile(r"^(date|created|createdat|timestamp)$", re.IGNORECASE)
|
|
166
|
+
PLATFORM_KEYS = re.compile(r"^(platform|channel|os|device)$", re.IGNORECASE)
|
|
167
|
+
|
|
168
|
+
def row_to_complaint(row):
|
|
169
|
+
text_val, trx, conv, date, platform = None, None, None, None, None
|
|
170
|
+
longest = ""
|
|
171
|
+
for key, raw in row.items():
|
|
172
|
+
if key is None or raw is None:
|
|
173
|
+
continue
|
|
174
|
+
val = str(raw).strip()
|
|
175
|
+
if not val:
|
|
176
|
+
continue
|
|
177
|
+
k = str(key).strip()
|
|
178
|
+
if TRX_KEYS.match(k):
|
|
179
|
+
trx = val
|
|
180
|
+
elif CONV_KEYS.match(k):
|
|
181
|
+
conv = val
|
|
182
|
+
elif DATE_KEYS.match(k):
|
|
183
|
+
date = val
|
|
184
|
+
elif PLATFORM_KEYS.match(k):
|
|
185
|
+
platform = val
|
|
186
|
+
elif k.lower() in TEXT_KEYS:
|
|
187
|
+
text_val = val
|
|
188
|
+
elif len(val) > len(longest):
|
|
189
|
+
longest = val
|
|
190
|
+
text = text_val or longest
|
|
191
|
+
if not text:
|
|
192
|
+
return None
|
|
193
|
+
found_trx, found_conv = extract_ids(text)
|
|
194
|
+
return {
|
|
195
|
+
"text": text,
|
|
196
|
+
"trxId": trx or found_trx,
|
|
197
|
+
"convId": conv or found_conv,
|
|
198
|
+
"date": date,
|
|
199
|
+
"platformHint": platform,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def blocks_from_text(payload):
|
|
203
|
+
payload = payload.strip()
|
|
204
|
+
if not payload:
|
|
205
|
+
return []
|
|
206
|
+
blocks = [b.strip() for b in re.split(r"\n\s*\n", payload) if b.strip()]
|
|
207
|
+
if len(blocks) == 1 and re.search(r"^\s*(?:\d+[.)]|-)\s+", payload, re.MULTILINE):
|
|
208
|
+
blocks = [
|
|
209
|
+
b.strip()
|
|
210
|
+
for b in re.split(r"^\s*(?:\d+[.)]|-)\s+", payload, flags=re.MULTILINE)
|
|
211
|
+
if b.strip()
|
|
212
|
+
]
|
|
213
|
+
return blocks
|
|
214
|
+
|
|
215
|
+
degraded = False
|
|
216
|
+
degrade_reason = None
|
|
217
|
+
raw_complaints = []
|
|
218
|
+
|
|
219
|
+
if fmt == "text":
|
|
220
|
+
for block in blocks_from_text(os.environ.get("PC_STDIN") or ""):
|
|
221
|
+
trx, conv = extract_ids(block)
|
|
222
|
+
raw_complaints.append(
|
|
223
|
+
{"text": block, "trxId": trx, "convId": conv, "date": None, "platformHint": None}
|
|
224
|
+
)
|
|
225
|
+
elif fmt == "txt":
|
|
226
|
+
with open(path, encoding="utf-8-sig", errors="replace") as f:
|
|
227
|
+
payload = f.read()
|
|
228
|
+
for block in blocks_from_text(payload):
|
|
229
|
+
trx, conv = extract_ids(block)
|
|
230
|
+
raw_complaints.append(
|
|
231
|
+
{"text": block, "trxId": trx, "convId": conv, "date": None, "platformHint": None}
|
|
232
|
+
)
|
|
233
|
+
elif fmt == "csv":
|
|
234
|
+
with open(path, encoding="utf-8-sig", errors="replace", newline="") as f:
|
|
235
|
+
sample = f.read(4096)
|
|
236
|
+
f.seek(0)
|
|
237
|
+
try:
|
|
238
|
+
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t")
|
|
239
|
+
except csv.Error:
|
|
240
|
+
dialect = csv.excel
|
|
241
|
+
for row in csv.DictReader(f, dialect=dialect):
|
|
242
|
+
c = row_to_complaint(row)
|
|
243
|
+
if c:
|
|
244
|
+
raw_complaints.append(c)
|
|
245
|
+
elif fmt == "json":
|
|
246
|
+
with open(path, encoding="utf-8-sig") as f:
|
|
247
|
+
data = json.load(f)
|
|
248
|
+
if isinstance(data, dict):
|
|
249
|
+
data = data.get("complaints") or data.get("items") or []
|
|
250
|
+
if not isinstance(data, list):
|
|
251
|
+
data = []
|
|
252
|
+
for row in data:
|
|
253
|
+
if isinstance(row, str):
|
|
254
|
+
trx, conv = extract_ids(row)
|
|
255
|
+
raw_complaints.append(
|
|
256
|
+
{"text": row.strip(), "trxId": trx, "convId": conv, "date": None, "platformHint": None}
|
|
257
|
+
)
|
|
258
|
+
elif isinstance(row, dict):
|
|
259
|
+
c = row_to_complaint(row)
|
|
260
|
+
if c:
|
|
261
|
+
raw_complaints.append(c)
|
|
262
|
+
elif fmt == "xlsx":
|
|
263
|
+
try:
|
|
264
|
+
import openpyxl
|
|
265
|
+
except ImportError:
|
|
266
|
+
degraded = True
|
|
267
|
+
degrade_reason = "openpyxl not installed; export the sheet as CSV and re-run with --format csv"
|
|
268
|
+
if not degraded:
|
|
269
|
+
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
|
270
|
+
ws = wb.active
|
|
271
|
+
rows = ws.iter_rows(values_only=True)
|
|
272
|
+
header = next(rows, None)
|
|
273
|
+
if header:
|
|
274
|
+
keys = [str(h).strip() if h is not None else "" for h in header]
|
|
275
|
+
for values in rows:
|
|
276
|
+
row = {k: v for k, v in zip(keys, values) if k}
|
|
277
|
+
c = row_to_complaint(row)
|
|
278
|
+
if c:
|
|
279
|
+
raw_complaints.append(c)
|
|
280
|
+
wb.close()
|
|
281
|
+
|
|
282
|
+
complaints = []
|
|
283
|
+
for i, c in enumerate(raw_complaints, start=1):
|
|
284
|
+
complaints.append(
|
|
285
|
+
{
|
|
286
|
+
"id": "C-%02d" % i,
|
|
287
|
+
"text": redact(c["text"]),
|
|
288
|
+
"trxId": c["trxId"],
|
|
289
|
+
"convId": c["convId"],
|
|
290
|
+
"date": c["date"],
|
|
291
|
+
"platformHint": (c["platformHint"] or None) and str(c["platformHint"]).strip().lower(),
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
result = {
|
|
296
|
+
"parsedAt": datetime.datetime.utcnow().isoformat() + "Z",
|
|
297
|
+
"source": path or "stdin",
|
|
298
|
+
"format": fmt,
|
|
299
|
+
"complaints": complaints,
|
|
300
|
+
"redacted": redact_on,
|
|
301
|
+
"degraded": degraded,
|
|
302
|
+
"degradeReason": degrade_reason,
|
|
303
|
+
}
|
|
304
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
305
|
+
sys.exit(5 if degraded else 0)
|
|
306
|
+
PY
|
|
@@ -45,8 +45,11 @@ usage() {
|
|
|
45
45
|
resolve_state() {
|
|
46
46
|
local task_id="$1"
|
|
47
47
|
local found=""
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 ||
|
|
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
|
|
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
|
|
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; `[
|
|
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
|
-
|
|
|
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
|
-
|
|
|
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"}` |
|
|
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:
|
|
@@ -34,9 +34,9 @@ The selected scope routes to a platform skill:
|
|
|
34
34
|
|
|
35
35
|
| Platform | Skill | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| iOS / SwiftUI | `ai-ios-
|
|
38
|
-
| Android / Compose | `ai-android-
|
|
39
|
-
| Generic | `ai-ios-
|
|
37
|
+
| iOS / SwiftUI | `ai-ios-toolkit:figma-component-wiki` | Reads `Configuration` + `+Modifiers` from the source, captures Figma screenshots via MCP / REST, runs Snapshot tests for iOS dark/light/RTL. |
|
|
38
|
+
| Android / Compose | `ai-android-toolkit:figma-component-wiki` | Reads Compose composables + previews, runs Paparazzi screenshots if available, otherwise generates from layout inspector. |
|
|
39
|
+
| Generic | `ai-ios-toolkit:figma-component-wiki` (fallback) | Used when `state.platform` is unknown (fallback path - typically a flag). |
|
|
40
40
|
|
|
41
41
|
Full per-skill contract: `$HOME/.claude/multi-agent-refs/wiki-capture.md`.
|
|
42
42
|
|
|
@@ -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
|
|
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.
|