@mmerterden/multi-agent-pipeline 20.25.1 → 20.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/docs/facts.json +3 -3
- package/install/catalog-history.json +1 -1
- package/manifest.json +50 -39
- package/package.json +5 -5
- package/pipeline/commands/multi-agent/channels/SKILL.md +23 -19
- package/pipeline/lib/_jira-auth.sh +3 -1
- package/pipeline/lib/issue-fetcher.sh +40 -2
- package/pipeline/lib/jira-family-write.sh +339 -0
- package/pipeline/lib/jira-publish.sh +28 -1
- package/pipeline/lib/jira-remote-links.sh +99 -0
- package/pipeline/multi-agent-refs/analysis/intake.md +2 -0
- package/pipeline/multi-agent-refs/analysis/render.md +4 -2
- package/pipeline/multi-agent-refs/channels/jira.md +12 -8
- package/pipeline/multi-agent-refs/channels/pr.md +11 -8
- package/pipeline/multi-agent-refs/channels/test-run.md +52 -0
- package/pipeline/multi-agent-refs/conventions-defaults.md +2 -0
- package/pipeline/multi-agent-refs/features/analysis-issue-links.md +42 -0
- package/pipeline/multi-agent-refs/features/jira-family.md +175 -0
- package/pipeline/multi-agent-refs/features/localization-reuse.md +25 -0
- package/pipeline/multi-agent-refs/features/url-enrichment.md +11 -3
- package/pipeline/multi-agent-refs/features/visual-evidence.md +35 -9
- package/pipeline/multi-agent-refs/generate-issue.md +9 -11
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +13 -7
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +2 -1
- package/pipeline/multi-agent-refs/phases.md +1 -1
- package/pipeline/multi-agent-refs/platform-parity.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +10 -6
- package/pipeline/schemas/agent-state.schema.json +86 -0
- package/pipeline/schemas/prefs.schema.json +34 -0
- package/pipeline/schemas/unattended-policy.json +1 -0
- package/pipeline/scripts/_sibling-root.mjs +38 -0
- package/pipeline/scripts/gen-facts.mjs +5 -3
- package/pipeline/scripts/jira-family.mjs +947 -0
- package/pipeline/scripts/jira-wiki-escape.mjs +3 -3
- package/pipeline/scripts/plan-remote-links.mjs +339 -0
- package/pipeline/scripts/render-flow-run.mjs +583 -0
- package/pipeline/scripts/render-jira-evidence.mjs +213 -0
- package/pipeline/skills/.skill-manifest.json +5 -5
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +5 -0
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +15 -2
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +5 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# jira-family-write.sh - fetch the issue family of a Jira-started run, and
|
|
4
|
+
# apply the writes scripts/jira-family.mjs planned for it.
|
|
5
|
+
#
|
|
6
|
+
# The planning half is offline and testable; this half is the one that talks
|
|
7
|
+
# to Jira, so it stays small. Every write goes through jira-publish.sh, which
|
|
8
|
+
# escapes the body, runs the outbound gate and backs a description up before it
|
|
9
|
+
# replaces one. A write the plan marks `raw` (the story reformat: the author's
|
|
10
|
+
# lines plus escaped macros) is sent with --raw, so the author's lines reach
|
|
11
|
+
# Jira unchanged. Reads go through jira_api in _jira-auth.sh.
|
|
12
|
+
#
|
|
13
|
+
# A write is checked against the site right before it is sent:
|
|
14
|
+
# comment skipped when a comment by the authenticated user on the issue
|
|
15
|
+
# already carries its marker
|
|
16
|
+
# description skipped when the story already conforms, or when it changed
|
|
17
|
+
# since the plan read it (jira-publish.sh --expect-file)
|
|
18
|
+
# so running the same plan twice writes nothing the second time.
|
|
19
|
+
#
|
|
20
|
+
# Usage, from a run (the state file is agent-state.json; the family's files go
|
|
21
|
+
# to jira-family/ beside it):
|
|
22
|
+
# jira-family-write.sh resolve --state FILE
|
|
23
|
+
# fetch and resolve the family, record state.jiraFamily; no Jira write
|
|
24
|
+
# jira-family-write.sh plan --state FILE --dev-body FILE [--skip id,id]
|
|
25
|
+
# [--dev-task KEY] [--test-task KEY] [--build TEXT]
|
|
26
|
+
# re-resolve and plan; run id, PR URL and mode come from the state
|
|
27
|
+
# jira-family-write.sh apply --state FILE [--dry-run]
|
|
28
|
+
# apply jira-family/plan.json, write jira-family/writes.json, merge it
|
|
29
|
+
# into state.jiraFamily.writes by write id
|
|
30
|
+
#
|
|
31
|
+
# Lower level:
|
|
32
|
+
# jira-family-write.sh fetch --issue KEY --out DIR
|
|
33
|
+
# DIR/input.json, DIR/story.json (the parent when KEY is a sub-task, else
|
|
34
|
+
# the same issue), DIR/site-types.txt (empty when unreadable)
|
|
35
|
+
# jira-family-write.sh apply --plan FILE [--state FILE] [--result FILE] [--dry-run]
|
|
36
|
+
# --state the plan's autopilot decisions are recorded there
|
|
37
|
+
# (decisions-log.mjs) once their write has gone out
|
|
38
|
+
# --result the per-write outcome as JSON, written on every exit
|
|
39
|
+
#
|
|
40
|
+
# Exit: 0 done, 3 the input does not parse or the plan still has questions,
|
|
41
|
+
# 4 auth (also when a write failed on auth), 5 a write failed (the others
|
|
42
|
+
# went out; --result lists which, and a re-run sends only what is missing),
|
|
43
|
+
# 6 unattended (nothing read or written; the runner owns Jira),
|
|
44
|
+
# 64 usage or a malformed issue key.
|
|
45
|
+
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
49
|
+
# shellcheck source=/dev/null
|
|
50
|
+
. "$SELF_DIR/_jira-auth.sh"
|
|
51
|
+
# shellcheck source=/dev/null
|
|
52
|
+
. "$SELF_DIR/unattended.sh"
|
|
53
|
+
|
|
54
|
+
USAGE="usage:
|
|
55
|
+
jira-family-write.sh resolve --state FILE
|
|
56
|
+
jira-family-write.sh plan --state FILE --dev-body FILE [--skip id,id] [--dev-task KEY] [--test-task KEY] [--build TEXT]
|
|
57
|
+
jira-family-write.sh apply --state FILE [--plan FILE] [--result FILE] [--dry-run]
|
|
58
|
+
jira-family-write.sh fetch --issue KEY --out DIR
|
|
59
|
+
jira-family-write.sh apply --plan FILE [--state FILE] [--result FILE] [--dry-run]"
|
|
60
|
+
usage() { echo "$USAGE" >&2; exit 64; }
|
|
61
|
+
|
|
62
|
+
CMD="${1:-}"; [ -n "$CMD" ] || usage; shift
|
|
63
|
+
case "$CMD" in -h|--help) sed -n '2,45p' "$0"; exit 0 ;; esac
|
|
64
|
+
ISSUE=""; OUT=""; PLAN=""; STATE=""; RESULT=""; DRY=0
|
|
65
|
+
DEV_BODY=""; SKIP=""; DEV_TASK=""; TEST_TASK=""; BUILD=""
|
|
66
|
+
while [ $# -gt 0 ]; do
|
|
67
|
+
case "$1" in
|
|
68
|
+
-h|--help) sed -n '2,45p' "$0"; exit 0 ;;
|
|
69
|
+
--dev-body) DEV_BODY="${2:-}"; shift 2 ;;
|
|
70
|
+
--skip) SKIP="${2:-}"; shift 2 ;;
|
|
71
|
+
--dev-task) DEV_TASK="${2:-}"; shift 2 ;;
|
|
72
|
+
--test-task) TEST_TASK="${2:-}"; shift 2 ;;
|
|
73
|
+
--build) BUILD="${2:-}"; shift 2 ;;
|
|
74
|
+
--issue) ISSUE="${2:-}"; shift 2 ;;
|
|
75
|
+
--out) OUT="${2:-}"; shift 2 ;;
|
|
76
|
+
--plan) PLAN="${2:-}"; shift 2 ;;
|
|
77
|
+
--state) STATE="${2:-}"; shift 2 ;;
|
|
78
|
+
--result) RESULT="${2:-}"; shift 2 ;;
|
|
79
|
+
--dry-run) DRY=1; shift ;;
|
|
80
|
+
*) usage ;;
|
|
81
|
+
esac
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required" >&2; exit 3; }
|
|
85
|
+
|
|
86
|
+
FAMILY_MJS="$SELF_DIR/../scripts/jira-family.mjs"
|
|
87
|
+
DECISIONS_MJS="$SELF_DIR/../scripts/decisions-log.mjs"
|
|
88
|
+
PUBLISH="$SELF_DIR/jira-publish.sh"
|
|
89
|
+
FIELDS="summary,issuetype,parent,subtasks,description,project,status"
|
|
90
|
+
|
|
91
|
+
# A key goes into a REST path, so anything but a key is refused before use.
|
|
92
|
+
valid_key() { printf '%s' "$1" | grep -Eq '^[A-Z][A-Z0-9_]+-[0-9]+$'; }
|
|
93
|
+
|
|
94
|
+
# What a jira-publish.sh exit code means, for the result line.
|
|
95
|
+
publish_reason() {
|
|
96
|
+
case "$1" in
|
|
97
|
+
4) echo "Jira host or token could not be resolved (exit 4)" ;;
|
|
98
|
+
5) echo "the Jira call failed (exit 5)" ;;
|
|
99
|
+
64) echo "refused before sending, e.g. by the outbound gate (exit 64): $2" ;;
|
|
100
|
+
*) echo "jira-publish.sh exit $1: $2" ;;
|
|
101
|
+
esac
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fetch_issue() { # fetch_issue <key> <file>
|
|
105
|
+
jira_api GET "/rest/api/2/issue/$1?fields=$FIELDS" > "$2" 2>/dev/null || return 5
|
|
106
|
+
jq -e '.key' "$2" >/dev/null 2>&1 || return 5
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
do_fetch() { # do_fetch <key> <dir>
|
|
110
|
+
local issue="$1" out="$2" parent project
|
|
111
|
+
valid_key "$issue" || { echo "ERR: not an issue key: $issue" >&2; return 64; }
|
|
112
|
+
if ma_unattended; then
|
|
113
|
+
echo "unattended run: the family is not read here; the runner owns Jira" >&2
|
|
114
|
+
return 6
|
|
115
|
+
fi
|
|
116
|
+
jira_auth_resolve || return 4
|
|
117
|
+
mkdir -p "$out"
|
|
118
|
+
fetch_issue "$issue" "$out/input.json" || { echo "ERR: could not read $issue" >&2; return 5; }
|
|
119
|
+
parent="$(jq -r 'if .fields.issuetype.subtask == true then .fields.parent.key // empty else empty end' "$out/input.json")"
|
|
120
|
+
if [ -n "$parent" ] && ! valid_key "$parent"; then
|
|
121
|
+
echo "WARN: the parent key does not look like a key: $parent" >&2; parent=""
|
|
122
|
+
fi
|
|
123
|
+
if [ -n "$parent" ]; then
|
|
124
|
+
fetch_issue "$parent" "$out/story.json" || { echo "WARN: could not read the parent $parent" >&2; rm -f "$out/story.json"; }
|
|
125
|
+
else
|
|
126
|
+
cp "$out/input.json" "$out/story.json"
|
|
127
|
+
fi
|
|
128
|
+
project="$(jq -r '.fields.project.key // empty' "$out/input.json")"
|
|
129
|
+
[ -n "$project" ] || project="${issue%%-*}"
|
|
130
|
+
bash "$SELF_DIR/analysis-jira-write.sh" --subtask-types --project "$project" \
|
|
131
|
+
> "$out/site-types.txt" 2>/dev/null || : > "$out/site-types.txt"
|
|
132
|
+
echo "fetched $issue${parent:+ (story $parent)} into $out"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# The run's own files: the issue key and the family directory beside agent-state.json.
|
|
136
|
+
state_key() { jq -r '.jiraId // .taskId // empty' "$STATE"; }
|
|
137
|
+
family_dir() { printf '%s/jira-family' "$(cd "$(dirname "$STATE")" && pwd)"; }
|
|
138
|
+
|
|
139
|
+
do_resolve() { # fetch, resolve, and record state.jiraFamily; asks and writes nothing in Jira
|
|
140
|
+
local key dir story=""
|
|
141
|
+
key="$(state_key)"; dir="$(family_dir)"
|
|
142
|
+
do_fetch "$key" "$dir" >/dev/null || return $?
|
|
143
|
+
[ -f "$dir/story.json" ] && story="--story $dir/story.json"
|
|
144
|
+
# shellcheck disable=SC2086
|
|
145
|
+
node "$FAMILY_MJS" resolve --input "$dir/input.json" $story \
|
|
146
|
+
--site-types "$dir/site-types.txt" > "$dir/family.json" || return 3
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if [ "$CMD" = "fetch" ]; then
|
|
150
|
+
[ -n "$ISSUE" ] && [ -n "$OUT" ] || usage
|
|
151
|
+
do_fetch "$ISSUE" "$OUT"
|
|
152
|
+
exit $?
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
WRITE_STATE="$SELF_DIR/../scripts/write-state.mjs"
|
|
156
|
+
|
|
157
|
+
if [ "$CMD" = "resolve" ]; then
|
|
158
|
+
[ -n "$STATE" ] && [ -f "$STATE" ] || usage
|
|
159
|
+
do_resolve || exit $?
|
|
160
|
+
DIR="$(family_dir)"
|
|
161
|
+
jq -n --slurpfile f "$DIR/family.json" --slurpfile s "$STATE" \
|
|
162
|
+
'{jiraFamily: ($f[0].stateEntry + {writes: ($s[0].jiraFamily.writes // [])})}' \
|
|
163
|
+
| node "$WRITE_STATE" "$STATE" || exit 3
|
|
164
|
+
jq -r '"family: story \(.stateEntry.storyKey), dev \(.stateEntry.devTaskKey // "-"), test \(.stateEntry.testTaskKey // "-"), story format \(.stateEntry.storyFormat.status)"' "$DIR/family.json"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
if [ "$CMD" = "plan" ]; then
|
|
169
|
+
[ -n "$STATE" ] && [ -f "$STATE" ] && [ -n "$DEV_BODY" ] && [ -f "$DEV_BODY" ] || usage
|
|
170
|
+
do_resolve || exit $?
|
|
171
|
+
DIR="$(family_dir)"
|
|
172
|
+
RUN_ID="$(jq -r '"\(.taskId)-\(.shortId // 0)"' "$STATE")"
|
|
173
|
+
PR_URL="$(jq -r '.pr.url // .projects[0].pr.url // empty' "$STATE")"
|
|
174
|
+
MODE="$(jq -r 'if .autopilot == true then "autopilot" else "interactive" end' "$STATE")"
|
|
175
|
+
set -- --family "$DIR/family.json" --dev-body "$DEV_BODY" --run-id "$RUN_ID" --mode "$MODE"
|
|
176
|
+
[ -z "$PR_URL" ] || set -- "$@" --pr-url "$PR_URL"
|
|
177
|
+
[ -z "$BUILD" ] || set -- "$@" --build "$BUILD"
|
|
178
|
+
[ -z "$SKIP" ] || set -- "$@" --skip "$SKIP"
|
|
179
|
+
[ -z "$DEV_TASK" ] || set -- "$@" --dev-task "$DEV_TASK"
|
|
180
|
+
[ -z "$TEST_TASK" ] || set -- "$@" --test-task "$TEST_TASK"
|
|
181
|
+
node "$FAMILY_MJS" plan "$@" > "$DIR/plan.json" || exit 3
|
|
182
|
+
jq -r '.confirm, (.skipped[] | " not written: \(.id) (\(.reason))"), (.questions[] | " question \(.id): \(.question) [\(.options | join(", "))]")' "$DIR/plan.json"
|
|
183
|
+
echo "plan: $DIR/plan.json"
|
|
184
|
+
exit 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
[ "$CMD" = "apply" ] || usage
|
|
188
|
+
MERGE=0
|
|
189
|
+
if [ -z "$PLAN" ] && [ -n "$STATE" ] && [ -f "$STATE" ]; then
|
|
190
|
+
PLAN="$(family_dir)/plan.json"
|
|
191
|
+
[ -n "$RESULT" ] || RESULT="$(family_dir)/writes.json"
|
|
192
|
+
MERGE=1
|
|
193
|
+
fi
|
|
194
|
+
[ -n "$PLAN" ] && [ -f "$PLAN" ] || usage
|
|
195
|
+
|
|
196
|
+
# Every exit leaves a result file, so the caller's follow-up never reads nothing.
|
|
197
|
+
early_exit() { # early_exit <code> <status> <reason>
|
|
198
|
+
if [ -n "$RESULT" ]; then
|
|
199
|
+
jq -c --arg s "$2" --arg r "$3" '[.writes[]? | {id, issue, status: $s, reason: $r}]' "$PLAN" \
|
|
200
|
+
> "$RESULT" 2>/dev/null || echo '[]' > "$RESULT"
|
|
201
|
+
fi
|
|
202
|
+
exit "$1"
|
|
203
|
+
}
|
|
204
|
+
if ma_unattended; then
|
|
205
|
+
echo "unattended run: the family writes are left to the runner" >&2
|
|
206
|
+
early_exit 6 skipped "unattended run: left to the runner"
|
|
207
|
+
fi
|
|
208
|
+
jq -e . "$PLAN" >/dev/null 2>&1 || { echo "ERR: the plan does not parse: $PLAN" >&2; early_exit 3 failed "the plan does not parse"; }
|
|
209
|
+
if [ "$(jq -r '.ready' "$PLAN")" != "true" ]; then
|
|
210
|
+
echo "ERR: the plan still has open questions ($(jq -r '[.questions[].id] | join(", ")' "$PLAN")); answer them and re-plan" >&2
|
|
211
|
+
early_exit 3 skipped "the plan still has open questions"
|
|
212
|
+
fi
|
|
213
|
+
[ "$DRY" -eq 1 ] || jira_auth_resolve || exit 4
|
|
214
|
+
|
|
215
|
+
WORK="$(mktemp -d)"
|
|
216
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
217
|
+
RESULTS="$WORK/results.jsonl"; : > "$RESULTS"
|
|
218
|
+
ME=""
|
|
219
|
+
RC=0
|
|
220
|
+
|
|
221
|
+
record() { # record <id> <issue> <status> <reason>
|
|
222
|
+
jq -cn --arg i "$1" --arg k "$2" --arg s "$3" --arg r "$4" \
|
|
223
|
+
'{id: $i, issue: $k, status: $s, reason: $r}' >> "$RESULTS"
|
|
224
|
+
printf ' %-8s %-12s %s%s\n' "$3" "$1" "$2" "${4:+ ($4)}"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fail_rc() { # an auth failure (4) outranks any other failure (5) in the exit code
|
|
228
|
+
if [ "$1" -eq 4 ]; then RC=4; elif [ "$RC" -ne 4 ]; then RC=5; fi
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
record_decisions() { # record_decisions <writeId>, or "" for the decisions that made no write
|
|
232
|
+
[ -n "$STATE" ] || return 0
|
|
233
|
+
jq -c --arg w "$1" '.decisions[]? | select(if $w == "" then .writeId == null else .writeId == $w end)' "$PLAN" | while IFS= read -r d; do
|
|
234
|
+
node "$DECISIONS_MJS" add --state "$STATE" \
|
|
235
|
+
--what "$(printf '%s' "$d" | jq -r .what)" \
|
|
236
|
+
--why "$(printf '%s' "$d" | jq -r .why)" \
|
|
237
|
+
--cost-if-wrong "$(printf '%s' "$d" | jq -r .costIfWrong)" \
|
|
238
|
+
--phase "$(printf '%s' "$d" | jq -r .phase)" >/dev/null 2>&1 \
|
|
239
|
+
|| echo " WARN: could not record the decision for $1" >&2
|
|
240
|
+
done
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
echo "jira-family: $(jq -r '.confirm' "$PLAN")$([ "$DRY" -eq 1 ] && echo " (dry run)")"
|
|
244
|
+
N="$(jq '.writes | length' "$PLAN")"
|
|
245
|
+
i=0
|
|
246
|
+
while [ "$i" -lt "$N" ]; do
|
|
247
|
+
W="$(jq -c ".writes[$i]" "$PLAN")"
|
|
248
|
+
i=$((i + 1))
|
|
249
|
+
id="$(printf '%s' "$W" | jq -r .id)"
|
|
250
|
+
kind="$(printf '%s' "$W" | jq -r .kind)"
|
|
251
|
+
issue="$(printf '%s' "$W" | jq -r .issue)"
|
|
252
|
+
printf '%s' "$W" | jq -j .body > "$WORK/body.txt"
|
|
253
|
+
if ! valid_key "$issue"; then
|
|
254
|
+
record "$id" "$issue" "failed" "not an issue key"; RC=5; continue
|
|
255
|
+
fi
|
|
256
|
+
if [ "$DRY" -eq 1 ]; then
|
|
257
|
+
record "$id" "$issue" "dry-run" "$kind"
|
|
258
|
+
continue
|
|
259
|
+
fi
|
|
260
|
+
if [ "$kind" = "comment" ]; then
|
|
261
|
+
mark="$(printf '%s' "$W" | jq -r .marker)"
|
|
262
|
+
if ! jira_api GET "/rest/api/2/issue/$issue/comment?maxResults=1000" > "$WORK/comments.json" 2>/dev/null \
|
|
263
|
+
|| ! jq -e '.comments' "$WORK/comments.json" >/dev/null 2>&1; then
|
|
264
|
+
record "$id" "$issue" "failed" "could not read the comments"; RC=5; continue
|
|
265
|
+
fi
|
|
266
|
+
if [ -z "$ME" ]; then
|
|
267
|
+
jira_api GET "/rest/api/2/myself" > "$WORK/me.json" 2>/dev/null || :
|
|
268
|
+
ME="$(jq -c '{name, key, accountId} | with_entries(select(.value != null))' "$WORK/me.json" 2>/dev/null || echo "")"
|
|
269
|
+
fi
|
|
270
|
+
if [ -z "$ME" ] || [ "$ME" = "{}" ]; then
|
|
271
|
+
ME=""; record "$id" "$issue" "failed" "could not read the authenticated user"; RC=5; continue
|
|
272
|
+
fi
|
|
273
|
+
# Only our own comments count: another user can paste a marker, and it
|
|
274
|
+
# must not be able to stop this one from being posted.
|
|
275
|
+
seen="$(jq --arg m "$mark" --argjson me "$ME" '[.comments[]?
|
|
276
|
+
| select((.author // {}) as $a | any($me | to_entries[]; $a[.key] == .value))
|
|
277
|
+
| .body // "" | select(contains($m))] | length' "$WORK/comments.json")"
|
|
278
|
+
if [ "${seen:-0}" -gt 0 ]; then
|
|
279
|
+
record "$id" "$issue" "exists" "already posted by this run"; continue
|
|
280
|
+
fi
|
|
281
|
+
bash "$PUBLISH" --issue "$issue" --body-file "$WORK/body.txt" --target comment >/dev/null 2>"$WORK/err.txt"
|
|
282
|
+
prc=$?
|
|
283
|
+
if [ "$prc" -eq 0 ]; then
|
|
284
|
+
record "$id" "$issue" "written" ""
|
|
285
|
+
record_decisions "$id"
|
|
286
|
+
else
|
|
287
|
+
record "$id" "$issue" "failed" "$(publish_reason "$prc" "$(head -1 "$WORK/err.txt")")"
|
|
288
|
+
fail_rc "$prc"
|
|
289
|
+
fi
|
|
290
|
+
continue
|
|
291
|
+
fi
|
|
292
|
+
# description
|
|
293
|
+
if ! jira_api GET "/rest/api/2/issue/$issue?fields=description" > "$WORK/current.json" 2>/dev/null \
|
|
294
|
+
|| ! jq -e '.fields' "$WORK/current.json" >/dev/null 2>&1; then
|
|
295
|
+
record "$id" "$issue" "failed" "could not read the description"; RC=5; continue
|
|
296
|
+
fi
|
|
297
|
+
jq -j '.fields.description // ""' "$WORK/current.json" > "$WORK/current.txt"
|
|
298
|
+
printf '%s' "$W" | jq -j '.original // ""' > "$WORK/original.txt"
|
|
299
|
+
jq '{global: {issueTree: (if .storyPanels == null then {} else {storyPanels: .storyPanels} end)}}' \
|
|
300
|
+
"$PLAN" > "$WORK/plan-prefs.json"
|
|
301
|
+
if node "$FAMILY_MJS" check --description "$WORK/current.txt" --prefs "$WORK/plan-prefs.json" >/dev/null 2>&1; then
|
|
302
|
+
record "$id" "$issue" "exists" "already in the story format"; continue
|
|
303
|
+
fi
|
|
304
|
+
if ! cmp -s "$WORK/current.txt" "$WORK/original.txt"; then
|
|
305
|
+
record "$id" "$issue" "skipped" "the description changed since the preview"; continue
|
|
306
|
+
fi
|
|
307
|
+
raw=""
|
|
308
|
+
[ "$(printf '%s' "$W" | jq -r '.raw == true')" = "true" ] && raw="--raw"
|
|
309
|
+
bash "$PUBLISH" --issue "$issue" --body-file "$WORK/body.txt" --target description \
|
|
310
|
+
--mode replace --confirm-overwrite --expect-file "$WORK/original.txt" $raw >/dev/null 2>"$WORK/err.txt"
|
|
311
|
+
prc=$?
|
|
312
|
+
if [ "$prc" -eq 0 ]; then
|
|
313
|
+
record "$id" "$issue" "written" "previous text backed up"
|
|
314
|
+
record_decisions "$id"
|
|
315
|
+
elif [ "$prc" -eq 6 ]; then
|
|
316
|
+
record "$id" "$issue" "skipped" "the description changed since the preview"
|
|
317
|
+
else
|
|
318
|
+
record "$id" "$issue" "failed" "$(publish_reason "$prc" "$(head -1 "$WORK/err.txt")")"
|
|
319
|
+
fail_rc "$prc"
|
|
320
|
+
fi
|
|
321
|
+
done
|
|
322
|
+
|
|
323
|
+
[ "$DRY" -eq 1 ] || record_decisions ""
|
|
324
|
+
[ -z "$RESULT" ] || jq -s '.' "$RESULTS" > "$RESULT"
|
|
325
|
+
# Merged by write id: a re-run that finds a comment already there ("exists")
|
|
326
|
+
# keeps the earlier "written" record instead of replacing it.
|
|
327
|
+
if [ "$MERGE" -eq 1 ] && [ "$DRY" -eq 0 ]; then
|
|
328
|
+
jq -n --slurpfile s "$STATE" --slurpfile n "$RESULT" '
|
|
329
|
+
($s[0].jiraFamily // {}) as $f
|
|
330
|
+
| ($f.writes // []) as $old
|
|
331
|
+
| ($n[0] | map({(.id): .}) | add // {}) as $new
|
|
332
|
+
| ($old | map(.id)) as $oldIds
|
|
333
|
+
| {jiraFamily: ($f + {writes: (
|
|
334
|
+
[$old[] | . as $o | ($new[$o.id] // null) as $x
|
|
335
|
+
| if $x == null or ($o.status == "written" and $x.status == "exists") then $o else $x end]
|
|
336
|
+
+ [$n[0][] | select(.id as $i | $oldIds | index($i) | not)])})}' \
|
|
337
|
+
| node "$WRITE_STATE" "$STATE" >/dev/null || echo " WARN: could not record the writes in $STATE" >&2
|
|
338
|
+
fi
|
|
339
|
+
exit "$RC"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
# [--mode append|replace] (description only, default: append)
|
|
20
20
|
# [--confirm-overwrite] (required for mode=replace on a non-empty field)
|
|
21
21
|
# [--backup-dir DIR]
|
|
22
|
+
# [--raw] (body is already Jira wiki markup; no emoticon escape)
|
|
23
|
+
# [--expect-file FILE] (description only: write only if it still equals FILE)
|
|
22
24
|
# [--dry-run]
|
|
23
25
|
#
|
|
24
26
|
# Resolution:
|
|
@@ -29,6 +31,9 @@
|
|
|
29
31
|
#
|
|
30
32
|
# Every body is run through scripts/jira-wiki-escape.mjs before it is sent, so
|
|
31
33
|
# Jira cannot manufacture an emoticon out of a Swift selector or a table cell.
|
|
34
|
+
# --raw skips that one step for a body built only from text the author already
|
|
35
|
+
# wrote in Jira markup plus macros the caller escaped itself (the story reformat
|
|
36
|
+
# of lib/jira-family-write.sh); the outbound gate and the backup still apply.
|
|
32
37
|
#
|
|
33
38
|
# Exit codes:
|
|
34
39
|
# 0 published (or, with --dry-run, previewed)
|
|
@@ -36,6 +41,8 @@
|
|
|
36
41
|
# --confirm-overwrite was given. The backup is already on disk.
|
|
37
42
|
# 4 host or token could not be resolved
|
|
38
43
|
# 5 a Jira API call failed
|
|
44
|
+
# 6 refused: --expect-file no longer matches the description (someone
|
|
45
|
+
# edited it after the caller read it). Nothing was written.
|
|
39
46
|
# 64 usage error
|
|
40
47
|
|
|
41
48
|
set -euo pipefail
|
|
@@ -91,6 +98,8 @@ TARGET="comment"
|
|
|
91
98
|
MODE="append"
|
|
92
99
|
CONFIRM=0
|
|
93
100
|
DRY_RUN=0
|
|
101
|
+
RAW=0
|
|
102
|
+
EXPECT=""
|
|
94
103
|
BACKUP_DIR="${JIRA_BACKUP_DIR:-$HOME/.claude/logs/multi-agent/jira-backups}"
|
|
95
104
|
|
|
96
105
|
die() { echo "$1" >&2; exit "${2:-64}"; }
|
|
@@ -106,6 +115,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
106
115
|
--backup-dir) need_value "$@"; BACKUP_DIR="$2"; shift 2 ;;
|
|
107
116
|
--confirm-overwrite) CONFIRM=1; shift ;;
|
|
108
117
|
--dry-run) DRY_RUN=1; shift ;;
|
|
118
|
+
--raw) RAW=1; shift ;;
|
|
119
|
+
--expect-file) need_value "$@"; EXPECT="$2"; shift 2 ;;
|
|
109
120
|
-h|--help) sed -n '2,40p' "$0"; exit 0 ;;
|
|
110
121
|
*) die "unknown argument: $1" ;;
|
|
111
122
|
esac
|
|
@@ -148,7 +159,16 @@ api() { "$CURL" -sS -m 30 -K <(auth_cfg "$TOKEN") -H "Content-Type: application/
|
|
|
148
159
|
WORK="$(mktemp -d)"
|
|
149
160
|
trap 'rm -rf "$WORK"' EXIT
|
|
150
161
|
|
|
151
|
-
|
|
162
|
+
if [ "$RAW" = "1" ]; then
|
|
163
|
+
cp "$BODY_FILE" "$WORK/new.txt"
|
|
164
|
+
else
|
|
165
|
+
node "$ESCAPER" "$BODY_FILE" > "$WORK/new.txt"
|
|
166
|
+
fi
|
|
167
|
+
# An escaper that printed nothing for a body that has text would post an empty
|
|
168
|
+
# comment or blank a description; that is refused, never sent.
|
|
169
|
+
if [ -s "$BODY_FILE" ] && [ ! -s "$WORK/new.txt" ]; then
|
|
170
|
+
die "the escaped body came out empty; nothing was posted" 5
|
|
171
|
+
fi
|
|
152
172
|
|
|
153
173
|
if [ "$TARGET" = "comment" ]; then
|
|
154
174
|
if [ "$DRY_RUN" = "1" ]; then
|
|
@@ -175,6 +195,13 @@ case "$http" in
|
|
|
175
195
|
*) echo "could not read the current description of $ISSUE (HTTP $http)" >&2; exit 5 ;;
|
|
176
196
|
esac
|
|
177
197
|
jq -r '.fields.description // ""' "$WORK/issue.json" > "$WORK/existing.txt"
|
|
198
|
+
if [ -n "$EXPECT" ]; then
|
|
199
|
+
jq -j '.fields.description // ""' "$WORK/issue.json" > "$WORK/existing-exact.txt"
|
|
200
|
+
if ! cmp -s "$WORK/existing-exact.txt" "$EXPECT"; then
|
|
201
|
+
echo "REFUSED: the description of $ISSUE changed since it was read; nothing was written." >&2
|
|
202
|
+
exit 6
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
178
205
|
|
|
179
206
|
existing_bytes=$(wc -c < "$WORK/existing.txt" | tr -d ' ')
|
|
180
207
|
existing_lines=$(grep -c . "$WORK/existing.txt" || true)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# jira-remote-links.sh - read and write a Jira issue's Issue Links (remote links).
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# jira-remote-links.sh list <ISSUE> # current remote links, JSON array
|
|
7
|
+
# jira-remote-links.sh apply <ISSUE> <plan.json> [--dry-run]
|
|
8
|
+
#
|
|
9
|
+
# `plan.json` is what `scripts/plan-remote-links.mjs` prints: {"write": [...], "skip": [...]}.
|
|
10
|
+
# `apply` POSTs each `write` entry to /rest/api/2/issue/<ISSUE>/remotelink. Jira treats a
|
|
11
|
+
# POST whose globalId already exists on the issue as an update, so a re-run rewrites the
|
|
12
|
+
# same link instead of adding a second one. This file never deletes a link: the planner
|
|
13
|
+
# decides what is written, and a link nobody planned stays as it is.
|
|
14
|
+
#
|
|
15
|
+
# Output of apply: one line per entry, `written <globalId>` or `failed <globalId> <http status>`,
|
|
16
|
+
# then `skipped <globalId> <reason>` for each skip. Exit 0 when every write succeeded, 1 when
|
|
17
|
+
# any failed (the rest are still attempted), 2 on a usage error, 3 when jq is missing, 4 when
|
|
18
|
+
# host or token did not resolve.
|
|
19
|
+
#
|
|
20
|
+
# Host and token resolve through `_jira-auth.sh`; the token reaches curl only through a -K
|
|
21
|
+
# config on process substitution, never argv or a log.
|
|
22
|
+
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
usage() {
|
|
26
|
+
echo "usage: jira-remote-links.sh list <ISSUE> | apply <ISSUE> <plan.json> [--dry-run]" >&2
|
|
27
|
+
exit 2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[ $# -ge 2 ] || usage
|
|
31
|
+
CMD="$1"
|
|
32
|
+
ISSUE="$2"
|
|
33
|
+
[[ "$ISSUE" =~ ^[A-Z][A-Z0-9]*-[0-9]+$ ]] || { echo "jira-remote-links: not an issue key: $ISSUE" >&2; exit 2; }
|
|
34
|
+
command -v jq >/dev/null 2>&1 || { echo "jira-remote-links: jq not found" >&2; exit 3; }
|
|
35
|
+
|
|
36
|
+
# shellcheck source=_jira-auth.sh
|
|
37
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_jira-auth.sh"
|
|
38
|
+
|
|
39
|
+
case "$CMD" in
|
|
40
|
+
list)
|
|
41
|
+
jira_auth_resolve || exit $?
|
|
42
|
+
body="$(jira_api GET "/rest/api/2/issue/${ISSUE}/remotelink" -w '\n%{http_code}')"
|
|
43
|
+
code="${body##*$'\n'}"
|
|
44
|
+
body="${body%$'\n'*}"
|
|
45
|
+
if [ "$code" != "200" ]; then
|
|
46
|
+
echo "jira-remote-links: list failed for $ISSUE (HTTP $code)" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
printf '%s' "$body" | jq -c 'if type == "array" then . else [] end' 2>/dev/null || {
|
|
50
|
+
echo "jira-remote-links: list for $ISSUE did not return JSON" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
}
|
|
53
|
+
;;
|
|
54
|
+
apply)
|
|
55
|
+
PLAN="${3:-}"
|
|
56
|
+
[ -n "$PLAN" ] && [ -f "$PLAN" ] || usage
|
|
57
|
+
DRY=0
|
|
58
|
+
# A misspelled safety flag must not turn into a real write.
|
|
59
|
+
case "$#:${4:-}" in
|
|
60
|
+
3:) ;;
|
|
61
|
+
4:--dry-run) DRY=1 ;;
|
|
62
|
+
*) usage ;;
|
|
63
|
+
esac
|
|
64
|
+
jq -e '(.write | type) == "array"' "$PLAN" >/dev/null 2>&1 || { echo "jira-remote-links: $PLAN has no write list" >&2; exit 2; }
|
|
65
|
+
# An unattended run posts nothing outside the machine; this write is no exception.
|
|
66
|
+
if [ "$DRY" -eq 0 ] && [ "${MULTI_AGENT_UNATTENDED:-0}" = "1" ]; then
|
|
67
|
+
jq -r '.write[]? | "skipped \(.globalId) unattended"' "$PLAN"
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
if [ "$DRY" -eq 0 ]; then
|
|
71
|
+
jira_auth_resolve || exit $?
|
|
72
|
+
fi
|
|
73
|
+
RC=0
|
|
74
|
+
count="$(jq '.write | length' "$PLAN")"
|
|
75
|
+
i=0
|
|
76
|
+
while [ "$i" -lt "$count" ]; do
|
|
77
|
+
entry="$(jq -c ".write[$i]" "$PLAN")"
|
|
78
|
+
gid="$(printf '%s' "$entry" | jq -r '.globalId // empty')"
|
|
79
|
+
i=$((i + 1))
|
|
80
|
+
if [ -z "$gid" ] || [ -z "$(printf '%s' "$entry" | jq -r '.object.url // empty')" ]; then
|
|
81
|
+
echo "failed ${gid:-<no-globalId>} invalid-entry"
|
|
82
|
+
RC=1
|
|
83
|
+
continue
|
|
84
|
+
fi
|
|
85
|
+
if [ "$DRY" -eq 1 ]; then
|
|
86
|
+
echo "would-write $gid"
|
|
87
|
+
continue
|
|
88
|
+
fi
|
|
89
|
+
code="$(jira_api POST "/rest/api/2/issue/${ISSUE}/remotelink" --data-binary @<(printf '%s' "$entry") -o /dev/null -w '%{http_code}')"
|
|
90
|
+
case "$code" in
|
|
91
|
+
200 | 201) echo "written $gid" ;;
|
|
92
|
+
*) echo "failed $gid $code"; RC=1 ;;
|
|
93
|
+
esac
|
|
94
|
+
done
|
|
95
|
+
jq -r '.skip[]? | "skipped \(.globalId) \(.reason)"' "$PLAN"
|
|
96
|
+
exit "$RC"
|
|
97
|
+
;;
|
|
98
|
+
*) usage ;;
|
|
99
|
+
esac
|
|
@@ -212,6 +212,8 @@ Q7: header="Firebase"
|
|
|
212
212
|
(Other: comma-separated mix of event names like 'profile.view,profile.opened', a /path/to/events.json, or a console.firebase.google.com URL)
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
**Issue Links of a Q4 issue are sources:** `features/analysis-issue-links.md`, "Reading an issue's links at intake".
|
|
216
|
+
|
|
215
217
|
After each batch is submitted, pipe every Other-provided string into
|
|
216
218
|
`~/.claude/lib/context-link-extractor.sh` on **stdin** (`printf '%s' "$answer" | ~/.claude/lib/context-link-extractor.sh`) - it takes no argv, and an
|
|
217
219
|
argv call silently returns `[]` rather than erroring. Results are typed and written to `state.analysisSpec.contextLinks[]`. Standards-question entries are additionally tagged `binding: true` so Phase 2 Section 7 treats them as hard constraints (see the Phase 1 type table and the Phase 2 Section 7 row).
|
|
@@ -137,9 +137,11 @@ Iterate `state.analysisSpec.outputs.requested`. For each target:
|
|
|
137
137
|
|
|
138
138
|
| Target | Action |
|
|
139
139
|
|--------|--------|
|
|
140
|
-
| Local | For each per-platform draft, `cp /tmp/analysis-<feature-slug>-<ts>/<feature>-<platform>.md` into `analysis/<feature>-<platform>.md` in the matching repo's working tree. When multiple repos exist for the same platform, the file is duplicated into each and the dispatch report lists every destination. **When no repo was selected** (Locked 34) there is no working tree to be relative to, so files land in `~/Desktop/multiAgentAnalysis/<feature-name>/`, one per derived channel (`<feature>-mobile.md`, `<feature>-web.md`) or a single `<feature>.md` when none could be derived. The current working directory is never written to: for a repo-less run it is arbitrary
|
|
140
|
+
| Local | For each per-platform draft, `cp /tmp/analysis-<feature-slug>-<ts>/<feature>-<platform>.md` into `analysis/<feature>-<platform>.md` in the matching repo's working tree. When multiple repos exist for the same platform, the file is duplicated into each and the dispatch report lists every destination. **When no repo was selected** (Locked 34) there is no working tree to be relative to, so files land in `~/Desktop/multiAgentAnalysis/<feature-name>/`, one per derived channel (`<feature>-mobile.md`, `<feature>-web.md`) or a single `<feature>.md` when none could be derived. The current working directory is never written to: for a repo-less run it is arbitrary. The user can override the path through the picker's Other input. **No commit.** |
|
|
141
141
|
| Confluence | Re-humanize each draft with `formal-stakeholder` tone. One page per draft under the chosen parent, titled `<Feature> - <Platform>` - including a repo-less run, whose drafts are the derived channels (`<Feature> - Mobile` / `<Feature> - Web`, Locked 34). A single channel-agnostic draft becomes one page titled `<Feature>`, with no suffix implying a split that was not made. Cross-link siblings inside each page via `<ac:link><ri:page ri:content-title="<Feature> - <OtherPlatform>"/></ac:link>`; a lone page has no sibling block. Markdown -> storage XML via `$HOME/.claude/multi-agent-refs/channels/confluence.md`. Re-emit on existing pages uses PUT with version bump. |
|
|
142
|
-
| Jira | Re-humanize the combined body with `informal-technical` tone. Concatenate the drafts under `h2. Platform: <X>` separators in production order - `iOS`, `Android`, `Web`, `Backend` repo-backed, `Mobile` / `Web` for channels derived on a repo-less run (Locked 34). A single channel-agnostic draft gets no separator: a heading announcing a split of one is noise. then run the whole body through the markdown → Jira wiki conversion table in `$HOME/.claude/multi-agent-refs/channels/jira.md` - both the comment body and the `description` field render wiki markup, so raw `##`/`**`/backticks arrive as literal text. Write the converted body to a file
|
|
142
|
+
| Jira | Re-humanize the combined body with `informal-technical` tone. Concatenate the drafts under `h2. Platform: <X>` separators in production order - `iOS`, `Android`, `Web`, `Backend` repo-backed, `Mobile` / `Web` for channels derived on a repo-less run (Locked 34). A single channel-agnostic draft gets no separator: a heading announcing a split of one is noise. then run the whole body through the markdown → Jira wiki conversion table in `$HOME/.claude/multi-agent-refs/channels/jira.md` - both the comment body and the `description` field render wiki markup, so raw `##`/`**`/backticks arrive as literal text. Write the converted body to a file; it is published by step 3 there, with `$HOME/.claude/lib/jira-publish.sh`, never with a hand-rolled `curl`: <br><br>`bash "$HOME/.claude/lib/jira-publish.sh" --issue "$KEY" --body-file "$F" --target comment` <br>`bash "$HOME/.claude/lib/jira-publish.sh" --issue "$KEY" --body-file "$F" --target description --mode append` <br><br>The script runs `jira-wiki-escape.mjs`, keeps host and token out of argv, and on the description path backs the current value up under `~/.claude/logs/multi-agent/jira-backups/` (path reported), appends below a `----` rule by default, and **refuses with exit 3** when `--mode replace` would discard a non-empty description unless `--confirm-overwrite` is passed. Exit 3 is reported with the backup path, never retried with the flag added: only the user's explicit "Description - replace" answer from Phase 3.5 supplies it. `--dry-run` previews the exact final body without writing. |
|
|
143
|
+
|
|
144
|
+
**Jira write and Issue Links:** `$HOME/.claude/multi-agent-refs/features/analysis-issue-links.md`. Nothing reaches Jira before approval.
|
|
143
145
|
|
|
144
146
|
**Output capture**: fill `state.analysisSpec.outputs.localPaths[]` (one entry per per-platform-per-repo write, or one per derived channel on a repo-less run), `outputs.confluencePageUrls[]` (one entry per emitted document), `outputs.confluencePages[]` (the same pages by IDENTITY - `pageId`, `title`, `space`, `channel` - because a write-back needs the id and a `/display/SPACE/Title` URL cannot be parsed for one), `outputs.jiraIssueKey` (single string).
|
|
145
147
|
|
|
@@ -22,8 +22,9 @@ Every Jira comment posted by this adapter follows the same section order. Sectio
|
|
|
22
22
|
|---|---|---|---|---|
|
|
23
23
|
| 1 | `summary` | `## Geliştirme Özeti` | `## Development Summary` | always |
|
|
24
24
|
| 2 | `test_scenarios` | `## Test Senaryoları` | `## Test Scenarios` | always (use " - " placeholder line if truly N/A) |
|
|
25
|
-
| 3 | `
|
|
26
|
-
| 4 | `
|
|
25
|
+
| 3 | `test_run` | `## Test Koşumu` | `## Test Run` | when "Test Koşumu (Xray)" content is selected |
|
|
26
|
+
| 4 | `impact` | `## Etki Analizi` | `## Impact Analysis` | always |
|
|
27
|
+
| 5 | `context_refs` | `## Bağlantılar` | `## References` | when any link exists |
|
|
27
28
|
|
|
28
29
|
> Heading levels in the source markdown are `##`. The wiki-markup converter (next section) rewrites them to `h2.` for Jira rendering.
|
|
29
30
|
|
|
@@ -44,10 +45,9 @@ it is the PR body (`channels/pr.md`).
|
|
|
44
45
|
|
|
45
46
|
**Visual evidence inside these sections.** When `state.visualEvidence` carries artefacts, they render INSIDE `summary` and `test_scenarios` - never as a section of their own, which the fixed section order forbids. Phase 4 Step 2.9 has already uploaded them and written the returned name to `visualEvidence.*[].jiraFilename`: reference that name, and call `jira-attach.sh <issue> <file>...` only for an artefact whose `jiraFilename` is absent. Uploading unconditionally here attaches every file a second time whenever the comment is re-rendered - which is exactly what a post-hoc `/multi-agent:channels` run does.
|
|
46
47
|
|
|
47
|
-
- `
|
|
48
|
-
- `test_scenarios`, under the scenario the recording demonstrates: `!<file>-flow.mp4!` plus one line stating the tier used.
|
|
48
|
+
- The lines come from `scripts/render-jira-evidence.mjs` (`features/visual-evidence.md` section 6): in `summary` the before/after line, under each scenario in `test_scenarios` its evidence line (`Kanıt: Ekran [<file>](<url>) · Kayıt [<file>](<url>)`), plus the flow recording line. Links are the default; `prefs.global.visualEvidence.jiraMedia: thumbnails` embeds images as `|thumbnail` instead. A full-size `!<file>!` embed is never written, and recordings are always links.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Each `gaps[]` line the renderer prints (`Önce: yok - <reason>`) goes where the artefact would have been. Never an empty thumbnail, never a silent omission. After `jira-attach.sh` uploads a missing artefact, its printed name and URL are written back to that artefact's `jiraFilename` and `url` before rendering. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.
|
|
51
51
|
|
|
52
52
|
**`test_scenarios`** - one titled scenario per acceptance criterion, each a
|
|
53
53
|
numbered list of steps ending in the expected result. Not Given/When/Then: it
|
|
@@ -104,6 +104,8 @@ written in the same plain register as the rest of the comment:
|
|
|
104
104
|
Part 4 is answered "none" far more often than it is answered at all, and "none" is
|
|
105
105
|
a real answer worth writing: it is what lets the reader stop looking.
|
|
106
106
|
|
|
107
|
+
**`test_run`** - the block `render-flow-run.mjs --heading-level 2` prints, unedited: the Xray Test Execution per domain, the Test Set / Plan, the run, the result counts and one row per scenario. It sits right after `test_scenarios` because it is the evidence for them. The renderer is the only source: a key that is not in the run history's delivery line is not written, and an undelivered run says so (`channels/test-run.md`). This section is the one exception to the no-identifiers rule above: execution, test and run ids, and the run's commit and branch, are what QA clicks through, not code.
|
|
108
|
+
|
|
107
109
|
**`context_refs`** - flat bullet list of external links the work depends on or produced. PR URL is **not** repeated here; it lives on the first line (see *Cross-link injection*). Common entries (only when present in the task):
|
|
108
110
|
|
|
109
111
|
```markdown
|
|
@@ -122,8 +124,8 @@ The links are pulled from `agent-state.json.contextLinks[]` (see Phase 0 link ex
|
|
|
122
124
|
|
|
123
125
|
```
|
|
124
126
|
1. Read agent-state.json (taskId, contextLinks, prUrls, language).
|
|
125
|
-
2. Build section bodies in markdown, in table order: summary, test_scenarios, impact, context_refs.
|
|
126
|
-
3. Run the assembled body through the `humanizer` skill (see Hard rules below).
|
|
127
|
+
2. Build section bodies in markdown, in table order: summary, test_scenarios, test_run, impact, context_refs.
|
|
128
|
+
3. Run the assembled body through the `humanizer` skill (see Hard rules below). The `test_run` block and the evidence lines pass through verbatim, like code: they are rendered by scripts, and a reworded key or link is a wrong one.
|
|
127
129
|
4. Apply Cross-link injection (PR URL on line 1).
|
|
128
130
|
5. Run Wiki markup conversion.
|
|
129
131
|
6. Run the emoticon escape program on the converted body (see *Emoticon escaping*).
|
|
@@ -232,6 +234,8 @@ Jira's wiki renderer turned it into an emoticon, which is what section
|
|
|
232
234
|
*Emoticon escaping* below is about. A defence that has to be invoked by hand is
|
|
233
235
|
a defence that is eventually not invoked.
|
|
234
236
|
|
|
237
|
+
A run with `state.jiraFamily` posts this body on the development sub-task, plus a test comment on the test sub-task: `features/jira-family.md`.
|
|
238
|
+
|
|
235
239
|
The dispatch summary line includes the comment URL with `?focusedCommentId=...` so the user can paste it into Slack/Teams.
|
|
236
240
|
|
|
237
241
|
### Writing the issue description (not a comment)
|
|
@@ -253,7 +257,7 @@ When the **Wiki** adapter writes pages on the same run AND `prefs.global.wikiToJ
|
|
|
253
257
|
|
|
254
258
|
- Real newlines, no HTML entities - heredoc + `jq --rawfile` + `curl --data-binary @file`.
|
|
255
259
|
- UTF-8 in, UTF-8 out - the body file is UTF-8 and `--data-binary` ships its bytes verbatim. Never round-trip the body through `unicode_escape`, `latin-1`, or any re-encode step, and never hand-roll a Python/curl helper that re-decodes it: that mangles Turkish chars (ç ş ı ö ü ğ) into mojibake (`Çözüm` → `Ãözüm`). Use the `jq --rawfile` + `--data-binary @file` path above as-is. Same rule for the PR / Confluence / Wiki adapters.
|
|
256
|
-
- Section order is fixed: `summary` → `test_scenarios` → `context_refs`. Never insert sections between them; never reorder.
|
|
260
|
+
- Section order is fixed: `summary` → `test_scenarios` → `test_run` → `impact` → `context_refs`. Never insert other sections between them; never reorder.
|
|
257
261
|
- Humanizer pass runs **after** body assembly and **before** wiki-markup conversion. Tone target: informal but technical. No marketing voice, no "we are excited", no "I have...".
|
|
258
262
|
- **No decorative glyphs anywhere in the comment body** - neither emotive ( ) nor status glyphs ([done] [pending] failed skipped active ). The comment is plain technical prose. Status is written in words: `[done]` / `[pending]`, and `done · active · failed · skipped · pending` in the phase strip. This applies to every channel, not only Jira - see `channels/README` note in `phase-5-report.md`.
|
|
259
263
|
- Body content language follows `prefs.global.outputLanguage`. Code identifiers, file paths, type names, branch names, and the wiki-markup syntax stay verbatim. The `promptLanguage="en"` lock means any LLM prompt that produces the body is in English; the body itself is then rendered in the user's language.
|