@mmerterden/multi-agent-pipeline 14.2.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.
- package/CHANGELOG.md +42 -0
- package/README.md +10 -6
- package/README.tr.md +143 -0
- package/docs/architecture.md +23 -8
- package/docs/ecosystem.md +237 -0
- package/install/_plugin-skills.mjs +16 -2
- package/install/codex.mjs +9 -4
- package/install/templates/copilot-instructions.md +12 -9
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -4
- 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/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/ship/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- 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 +76 -9
- package/pipeline/lib/md2confluence-v3.py +16 -2
- 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 +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +2 -2
- 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 +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/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/audit-log-rotate.sh +10 -0
- package/pipeline/scripts/build-stack-plugins.mjs +8 -1
- package/pipeline/scripts/check-derived-drift.mjs +13 -1
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +21 -8
- 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 +38 -14
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +39 -4
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skills-index.json +11 -2
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -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-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-ship/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +144 -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 +175 -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 +3 -2
|
@@ -140,9 +140,31 @@ run_find() {
|
|
|
140
140
|
if [ "${#roots[@]}" -eq 0 ]; then
|
|
141
141
|
return 0
|
|
142
142
|
fi
|
|
143
|
+
# No explicit -print anywhere in this expression means find falls back to
|
|
144
|
+
# its own implicit print on the WHOLE expression - which fires for every
|
|
145
|
+
# true branch of the -prune OR-chain too, including the pruned directories
|
|
146
|
+
# themselves (their contents are still correctly skipped; the dir path
|
|
147
|
+
# itself was not). An explicit -print, implicit-AND'd onto the caller's own
|
|
148
|
+
# criteria as the OR-chain's last branch, only fires on that branch.
|
|
143
149
|
find "${roots[@]}" \
|
|
144
150
|
"${SKIP_PRUNE_ARGS[@]}" \
|
|
145
|
-
"$@" 2>/dev/null || true
|
|
151
|
+
"$@" -print 2>/dev/null || true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Count how many of the newline-separated paths in $1 contain a line matching
|
|
155
|
+
# grep pattern $2. `xargs grep -l` here used to word-split on whitespace by
|
|
156
|
+
# default, so any path with a space in it was silently mis-scanned as several
|
|
157
|
+
# nonexistent partial paths (a false-negative on real matches, not an error -
|
|
158
|
+
# stderr was already redirected to /dev/null). Reading line-by-line handles
|
|
159
|
+
# that correctly since each line is one whole path regardless of embedded
|
|
160
|
+
# spaces.
|
|
161
|
+
count_grep_hits() {
|
|
162
|
+
local list="$1" pattern="$2" n=0 f
|
|
163
|
+
while IFS= read -r f; do
|
|
164
|
+
[ -z "$f" ] && continue
|
|
165
|
+
grep -lq "$pattern" "$f" 2>/dev/null && n=$((n + 1))
|
|
166
|
+
done <<< "$list"
|
|
167
|
+
printf '%s' "$n"
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
# ---- confidence classifier ----------------------------------------------------
|
|
@@ -849,8 +871,15 @@ bucket_localization_key() {
|
|
|
849
871
|
case "$PLATFORM" in
|
|
850
872
|
ios)
|
|
851
873
|
local strings_files xcstrings_files
|
|
852
|
-
|
|
853
|
-
|
|
874
|
+
# An earlier version ANDed -name onto the FRONT of the prune chain
|
|
875
|
+
# (`-name X SKIP_PRUNE_ARGS...`), which requires every -prune branch to
|
|
876
|
+
# ALSO be named "Localizable.strings" - a directory prune can never
|
|
877
|
+
# satisfy that, so no prune ever fired and the search walked into
|
|
878
|
+
# Pods/.build/node_modules looking for vendored copies. Prunes must
|
|
879
|
+
# stay their own independent OR-chain, ahead of (not ANDed with) the
|
|
880
|
+
# real search criteria. The xcstrings search had no prune chain at all.
|
|
881
|
+
strings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.strings' -type f -print 2>/dev/null | head -5 || true)
|
|
882
|
+
xcstrings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.xcstrings' -type f -print 2>/dev/null | head -3 || true)
|
|
854
883
|
if [ -n "$strings_files" ]; then
|
|
855
884
|
keys=$(printf '%s\n' "$strings_files" | while IFS= read -r f; do
|
|
856
885
|
[ -n "$f" ] && grep -hEo '^"[^"]+"' "$f" 2>/dev/null | tr -d '"'
|
|
@@ -918,9 +947,9 @@ bucket_di_registration() {
|
|
|
918
947
|
fi
|
|
919
948
|
files=$(run_find -type f -name '*.swift' 2>/dev/null || true)
|
|
920
949
|
if [ -n "$files" ]; then
|
|
921
|
-
resolver_count=$(
|
|
922
|
-
factory_count=$(
|
|
923
|
-
swinject_count=$(
|
|
950
|
+
resolver_count=$(count_grep_hits "$files" 'Resolver.register')
|
|
951
|
+
factory_count=$(count_grep_hits "$files" '@Injected\|Container.shared')
|
|
952
|
+
swinject_count=$(count_grep_hits "$files" 'Swinject\|Container()')
|
|
924
953
|
fi
|
|
925
954
|
manual_count=${manual_count:-0}; resolver_count=${resolver_count:-0}; factory_count=${factory_count:-0}; swinject_count=${swinject_count:-0}
|
|
926
955
|
|
|
@@ -941,9 +970,9 @@ bucket_di_registration() {
|
|
|
941
970
|
files=$(run_find -type f -name '*.kt' 2>/dev/null || true)
|
|
942
971
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
943
972
|
local hilt_count koin_count dagger_count
|
|
944
|
-
hilt_count=$(
|
|
945
|
-
koin_count=$(
|
|
946
|
-
dagger_count=$(
|
|
973
|
+
hilt_count=$(count_grep_hits "$files" '@Module\|@InstallIn\|@HiltViewModel')
|
|
974
|
+
koin_count=$(count_grep_hits "$files" 'koinViewModel\|module {')
|
|
975
|
+
dagger_count=$(count_grep_hits "$files" '@Component\|@Subcomponent')
|
|
947
976
|
hilt_count=${hilt_count:-0}; koin_count=${koin_count:-0}; dagger_count=${dagger_count:-0}
|
|
948
977
|
if [ "$hilt_count" -ge "$koin_count" ] && [ "$hilt_count" -ge "$dagger_count" ] && [ "$hilt_count" -gt 0 ]; then
|
|
949
978
|
emit_bucket "Hilt" "@Module @InstallIn(SingletonComponent::class)" "$(confidence_for "$hilt_count")" "[]" '["Koin","Dagger"]'
|
|
@@ -959,9 +988,9 @@ bucket_di_registration() {
|
|
|
959
988
|
files=$(run_find -type f -name '*.py' 2>/dev/null || true)
|
|
960
989
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
961
990
|
local depends_count dishka_count di_count
|
|
962
|
-
depends_count=$(
|
|
963
|
-
dishka_count=$(
|
|
964
|
-
di_count=$(
|
|
991
|
+
depends_count=$(count_grep_hits "$files" 'Depends(')
|
|
992
|
+
dishka_count=$(count_grep_hits "$files" 'dishka')
|
|
993
|
+
di_count=$(count_grep_hits "$files" 'dependency_injector')
|
|
965
994
|
depends_count=${depends_count:-0}; dishka_count=${dishka_count:-0}; di_count=${di_count:-0}
|
|
966
995
|
if [ "$depends_count" -ge "$dishka_count" ] && [ "$depends_count" -ge "$di_count" ] && [ "$depends_count" -gt 0 ]; then
|
|
967
996
|
emit_bucket "FastAPI Depends" "def handler(svc: FooService = Depends(get_service))" "$(confidence_for "$depends_count")" "[]" '["dishka","dependency_injector"]'
|
|
@@ -977,9 +1006,9 @@ bucket_di_registration() {
|
|
|
977
1006
|
files=$(run_find -type f \( -name '*.tsx' -o -name '*.ts' \) 2>/dev/null || true)
|
|
978
1007
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
979
1008
|
local context_count zustand_count hook_count
|
|
980
|
-
context_count=$(
|
|
981
|
-
zustand_count=$(
|
|
982
|
-
hook_count=$(
|
|
1009
|
+
context_count=$(count_grep_hits "$files" 'createContext\|\.Provider')
|
|
1010
|
+
zustand_count=$(count_grep_hits "$files" "from 'zustand'\\|from \"zustand\"")
|
|
1011
|
+
hook_count=$(count_grep_hits "$files" 'export function use[A-Z]')
|
|
983
1012
|
context_count=${context_count:-0}; zustand_count=${zustand_count:-0}; hook_count=${hook_count:-0}
|
|
984
1013
|
if [ "$context_count" -ge "$zustand_count" ] && [ "$context_count" -ge "$hook_count" ] && [ "$context_count" -gt 0 ]; then
|
|
985
1014
|
emit_bucket "React Context Provider" "const FooContext = createContext<Foo>(...)" "$(confidence_for "$context_count")" "[]" '["zustand","custom-hook-factory"]'
|
|
@@ -124,10 +124,17 @@ trap 'rm -rf "$TMPDIR_A"' EXIT INT TERM
|
|
|
124
124
|
# curl one /nodes batch (token via -K config so it never lands in argv).
|
|
125
125
|
curl_nodes() {
|
|
126
126
|
local out="$1" ids="$2" code
|
|
127
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
128
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
129
|
+
# catch too - so `code` became the literal string "000000", never
|
|
130
|
+
# matching a plain "000" comparison anywhere downstream). `|| true` keeps
|
|
131
|
+
# a failing curl from tripping any caller's `set -e` without re-printing
|
|
132
|
+
# into $code; `:-000` only fills in when curl fails before -w ever runs.
|
|
127
133
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
128
134
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
129
135
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
130
|
-
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids" ||
|
|
136
|
+
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids") || true
|
|
137
|
+
code="${code:-000}"
|
|
131
138
|
printf '%s' "$code"
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -202,18 +202,33 @@ fortify_get() {
|
|
|
202
202
|
return 1
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
case
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
# Check fortify_get's actual exit status, not a string pattern over its
|
|
206
|
+
# output - the old `case ... HTTP_AUTH_FAILED|*HTTP_4*)` only recognized
|
|
207
|
+
# auth failures and 4xx. A dropped connection, timeout, or 5xx made
|
|
208
|
+
# fortify_get return "...\nHTTP_000"/"...\nHTTP_5xx", which matched neither
|
|
209
|
+
# arm, so the case fell through, VERSION_JSON stayed unparseable garbage,
|
|
210
|
+
# and the Python block below silently treated that as "0 issues found" -
|
|
211
|
+
# the security gate passing precisely when it couldn't check anything.
|
|
212
|
+
if ! VERSION_JSON=$(fortify_get "projectVersions/$VERSION_ID"); then
|
|
213
|
+
echo "ERR: Fortify fetch failed for projectVersions/$VERSION_ID: ${VERSION_JSON:-no response}" >&2
|
|
214
|
+
exit 3
|
|
215
|
+
fi
|
|
211
216
|
|
|
212
217
|
ISSUES_JSON=""
|
|
213
218
|
if [ -n "$ISSUE_ID" ]; then
|
|
214
|
-
ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"
|
|
219
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"); then
|
|
220
|
+
echo "ERR: Fortify issues fetch failed for issue $ISSUE_ID: ${ISSUES_JSON:-no response}" >&2
|
|
221
|
+
exit 3
|
|
222
|
+
fi
|
|
215
223
|
else
|
|
216
|
-
|
|
224
|
+
# filter=ANALYSIS!=NotAnIssue,ANALYSIS!=FalsePositive (%21%3D is `!=`) is
|
|
225
|
+
# meant to EXCLUDE suppressed findings. It was encoded as %3D%3D (`==`),
|
|
226
|
+
# which instead selected only suppressed findings - the opposite of the
|
|
227
|
+
# gate's intent whenever SSC honors the filter.
|
|
228
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?limit=200&filter=ANALYSIS%21%3DNotAnIssue,ANALYSIS%21%3DFalsePositive&filterset=Critical%2CHigh"); then
|
|
229
|
+
echo "ERR: Fortify issues fetch failed for version $VERSION_ID: ${ISSUES_JSON:-no response}" >&2
|
|
230
|
+
exit 3
|
|
231
|
+
fi
|
|
217
232
|
fi
|
|
218
233
|
|
|
219
234
|
# Issue details are fetched per-issue lazily - only for the first 10 to keep
|
|
@@ -180,10 +180,20 @@ curl_figma() {
|
|
|
180
180
|
for attempt in 1 2 3; do
|
|
181
181
|
# Token header goes through a curl config via process substitution so it
|
|
182
182
|
# never appears in argv (argv is visible to ps).
|
|
183
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
184
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
185
|
+
# catch too - so `code` became the literal string "000000", matching
|
|
186
|
+
# neither the `000)` retry branch below nor handle_http_code's "000)
|
|
187
|
+
# network timeout" case, just the generic catch-all). `|| true` on its
|
|
188
|
+
# own line keeps `set -e` from aborting on curl's exit status without
|
|
189
|
+
# re-printing anything into $code; the `:-000` fallback only fires for
|
|
190
|
+
# the rarer case where curl fails before -w ever runs and $code is
|
|
191
|
+
# genuinely empty.
|
|
183
192
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
184
193
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
185
194
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
186
|
-
"$url" ||
|
|
195
|
+
"$url") || true
|
|
196
|
+
code="${code:-000}"
|
|
187
197
|
case "$code" in
|
|
188
198
|
2*) printf '%s' "$code"; return 0 ;;
|
|
189
199
|
401|403) printf '%s' "$code"; return 0 ;;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|