@mmerterden/multi-agent-pipeline 16.28.0 → 16.30.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +119 -2
  2. package/README.md +4 -4
  3. package/README.tr.md +3 -3
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/features.md +14 -0
  7. package/install/claude.mjs +17 -0
  8. package/package.json +1 -1
  9. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +93 -0
  10. package/pipeline/commands/multi-agent/design-check/SKILL.md +6 -5
  11. package/pipeline/commands/multi-agent/doctor/SKILL.md +78 -0
  12. package/pipeline/commands/multi-agent/help/SKILL.md +15 -12
  13. package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/setup/SKILL.md +14 -1
  15. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -9
  16. package/pipeline/commands/multi-agent/update/SKILL.md +12 -0
  17. package/pipeline/lib/_jira-auth.sh +99 -0
  18. package/pipeline/lib/analysis-jira-write.sh +203 -0
  19. package/pipeline/lib/issue-fetcher.sh +4 -4
  20. package/pipeline/multi-agent-refs/analysis/render.md +1 -1
  21. package/pipeline/multi-agent-refs/channels/pr.md +37 -1
  22. package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
  23. package/pipeline/multi-agent-refs/features/analysis-jira.md +128 -0
  24. package/pipeline/multi-agent-refs/features/doctor.md +197 -0
  25. package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
  26. package/pipeline/multi-agent-refs/features/visual-evidence.md +103 -20
  27. package/pipeline/multi-agent-refs/phases/phase-0-init.md +38 -7
  28. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +13 -1
  29. package/pipeline/multi-agent-refs/phases/phase-5-test.md +11 -1
  30. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +23 -0
  31. package/pipeline/multi-agent-refs/picker-contract.md +35 -0
  32. package/pipeline/multi-agent-refs/tracker-contract.md +5 -1
  33. package/pipeline/preferences-template.json +1 -1
  34. package/pipeline/schemas/agent-state.schema.json +84 -1
  35. package/pipeline/schemas/analysis-spec.schema.json +336 -95
  36. package/pipeline/schemas/prefs.schema.json +80 -3
  37. package/pipeline/schemas/token-budget.json +10 -10
  38. package/pipeline/scripts/analysis-story-tree.mjs +441 -0
  39. package/pipeline/scripts/capture-evidence.sh +170 -5
  40. package/pipeline/scripts/doctor.mjs +758 -0
  41. package/pipeline/scripts/evidence-gate.mjs +31 -2
  42. package/pipeline/scripts/phase-tracker.sh +97 -17
  43. package/pipeline/scripts/probe-evidence-capability.sh +250 -0
  44. package/pipeline/scripts/run-ui-tests.sh +380 -0
  45. package/pipeline/scripts/scan-agent-config.sh +48 -10
  46. package/pipeline/scripts/skill-siblings.mjs +1 -1
  47. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +94 -0
  48. package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +79 -0
  49. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +10 -1
  50. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +13 -0
  51. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +9 -6
  52. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +18 -0
@@ -4,6 +4,19 @@ description-tr: "İlk kurulum sihirbazı: keychain token keşfi, Git Identity ka
4
4
  allowed-tools: Bash, Read, Write, AskUserQuestion, WebFetch
5
5
  ---
6
6
 
7
+ ## Step 0 - What is actually wrong
8
+
9
+ ```bash
10
+ node "$HOME/.claude/scripts/doctor.mjs"
11
+ ```
12
+
13
+ Run it before the first question and again at the end. The first run turns setup
14
+ from a fixed script into a targeted one: there is no point asking for a token
15
+ that is already mapped and answering. The second run is the only honest way to
16
+ end - "setup complete" is a claim, and the doctor's exit code is the evidence for
17
+ or against it. Report both in `outputLanguage`, and if the second run still shows
18
+ a BLOCK, say so plainly rather than closing on the word "complete".
19
+
7
20
  ## Setup (Credential Store + Git Identity Onboarding)
8
21
 
9
22
  Cross-platform setup - uses `$HOME/.claude/lib/credential-store.sh` to read/write secrets in the platform-native credential store:
@@ -789,7 +802,7 @@ All tokens are optional in the sense that every service can be answered with Ski
789
802
 
790
803
  ### Step 8 - Enforcement hooks (optional, Claude Code)
791
804
 
792
- Offer to merge `install/templates/claude-hooks.json`: three `PreToolUse` gates that block on a non-zero exit (secret scan, agent-guard, read-size) plus two capture hooks that block nothing (`SessionEnd`, `SessionStart`). What each does: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
805
+ Offer to merge `$HOME/.claude/templates/claude-hooks.json`: three `PreToolUse` gates that block on a non-zero exit (secret scan, agent-guard, read-size) plus two capture hooks that block nothing (`SessionEnd`, `SessionStart`). What each does: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
793
806
 
794
807
  - Ask (picker): "Install the pipeline's hooks into `~/.claude/settings.json`?" Default Yes.
795
808
  - On Yes, deep-merge EVERY event in the template's `hooks` object, not `PreToolUse` alone - merging one event silently drops the capture hooks, and a run killed before Phase 7 then loses its findings exactly as it did before they existed. Preserve existing hooks; never duplicate a matcher already calling the same script.
@@ -57,10 +57,11 @@ The sync skill must run on all three platforms. Commands go through the platform
57
57
  Run every step automatically:
58
58
 
59
59
  ```
60
+ Step 0: DOCTOR doctor.mjs - exit 2 or 4 stops the sync
60
61
  Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
61
62
  Step 1.5: DETECT Compare timestamps, find stale targets
62
- Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 51 sub-command skills)
63
- Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 51 specs as refs + 8 agent TOML)
63
+ Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 53 sub-command skills)
64
+ Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 53 specs as refs + 8 agent TOML)
64
65
  Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
65
66
  Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
66
67
  bump changed plugins' patch version, commit + push the plugins repo)
@@ -72,6 +73,8 @@ Step 6: Report Summary: synced targets, platform, changed files, deploy sta
72
73
 
73
74
  If nothing is stale → report "All targets up to date" and stop.
74
75
 
76
+ Step 0 gate rules and why: `features/doctor.md`.
77
+
75
78
  ---
76
79
 
77
80
  ## Sync rules
@@ -122,8 +125,8 @@ If nothing is stale → report "All targets up to date" and stop.
122
125
  # backstop: no local-only wrapper may exist in the synced target
123
126
  grep -rl "^local-only: true" ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
124
127
  && { echo "ABORT: local-only wrapper leaked into pipeline/commands"; exit 1; } || true
125
- # backstop: no corporate marketplace/skill reference in the synced tree
126
- grep -rniE "ai-ios(-engineering)?-toolkit:(create-ui-component|evolve-ui-component|fix-bug|branch-and-pr|backlog|code-connect|figma-utility|resource-utility|component-wiki|component-docs|figma-setup)" \
128
+ # backstop: corporate refs only (generic ai-ios-toolkit is fine)
129
+ grep -rniE "ai-(ios-engineering|mobile)-toolkit:(create-ui-component|evolve-ui-component|fix-bug|branch-and-pr|backlog|code-connect|figma-utility|resource-utility|component-wiki|component-docs|figma-setup)" \
127
130
  ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
128
131
  && { echo "ABORT: corporate reference leaked into pipeline/commands"; exit 1; } || true
129
132
  ```
@@ -166,7 +169,7 @@ If nothing is stale → report "All targets up to date" and stop.
166
169
  Unlike the Copilot step, this one does **not** hand-copy files. The Codex tree is a
167
170
  *transform* of the Claude tree, not a mirror of it, and the transform is real work:
168
171
 
169
- - the 51 sub-command specs become reference files, because Codex silently truncates
172
+ - the 53 sub-command specs become reference files, because Codex silently truncates
170
173
  its skills block (see `cross-cli-contract.md` 2.6 for the measurement)
171
174
  - every `$HOME/.claude/...` reference to a CLI-owned tree is retargeted, with
172
175
  `agents/<persona>.md` becoming `.toml` and the dispatcher becoming the router skill
@@ -480,18 +483,18 @@ When invoked with the `release` argument:
480
483
  ## Sub-Command Sync (Claude Code <-> Copilot CLI Skills)
481
484
 
482
485
  This runs on the Claude <-> Copilot axis. Codex is NOT synced here: it receives the
483
- same 51 specs as reference files rather than as peer skills, via Step 2b - see
486
+ same 53 specs as reference files rather than as peer skills, via Step 2b - see
484
487
  `cross-cli-contract.md` 2.6 for why the parity axis differs per host.
485
488
 
486
489
  | Claude Code | Copilot CLI |
487
490
  |-------------|-------------|
488
491
  | `~/.claude/commands/multi-agent/{cmd}/SKILL.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
489
492
 
490
- **51 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
493
+ **53 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
491
494
 
492
495
  ```
493
- analysis, analysis-resolve, autopilot, build-optimize, channels,
494
- complaint-analysis, create-jira, design-check, diff-explain, feedback,
496
+ analysis, analysis-jira, analysis-resolve, autopilot, build-optimize, channels,
497
+ complaint-analysis, create-jira, design-check, doctor, diff-explain, feedback,
495
498
  forget, garbage-collect, graph, help, ios-coding-standard, issue, jira,
496
499
  kill, language, local, local-autopilot, log, manual-test, prune-logs,
497
500
  prune-prompts, purge, refactor, resume, resume-local, review,
@@ -4,6 +4,18 @@ description-tr: "Pipeline'ı npm'deki son yayına günceller: registry kontrolü
4
4
  allowed-tools: Bash, Read, Write, AskUserQuestion
5
5
  ---
6
6
 
7
+ ## After the install, check it
8
+
9
+ ```bash
10
+ node "$HOME/.claude/scripts/doctor.mjs"
11
+ ```
12
+
13
+ An update is the moment the layout changes, so it is the moment a layout check is
14
+ worth most: a script that moved, a host tree that did not get the copy, a version
15
+ stamp that disagrees with the package. Report the result in `outputLanguage`.
16
+ Exit 2 means the update left the install in a state a run will fail from - say
17
+ that, do not report "updated" and stop.
18
+
7
19
  # multi-agent update
8
20
 
9
21
  Update the pipeline in one command. The npm registry is the single update channel: the latest published release is downloaded and installed. Existing preferences are preserved; only skill / script / schema files are refreshed.
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # _jira-auth.sh - one resolution of host + token, and one way to call Jira.
4
+ #
5
+ # WHY THIS EXISTS
6
+ #
7
+ # The same twelve lines were written twice (`jira-publish.sh`, `issue-fetcher.sh`)
8
+ # and a third writer was about to make it three. Duplicated auth does not stay
9
+ # duplicated: it drifts, and the copy that drifts is the one nobody is looking at.
10
+ #
11
+ # Today only `analysis-jira-write.sh` sources this. The two older callers still
12
+ # carry their own resolution - `issue-fetcher.sh` resolves per-account token keys
13
+ # this helper does not model yet - so this is where new callers go, not a
14
+ # consolidation that has already happened.
15
+ # Worse, the part most worth getting right is the part most easily retyped badly -
16
+ # the token goes to curl through a `-K` config on process substitution so it never
17
+ # reaches argv, a log, or `ps`. A second-hand copy of that idiom is a leak waiting
18
+ # for the first person who simplifies it.
19
+ #
20
+ # Sourced, never executed. The leading underscore marks it: it is a library for
21
+ # the scripts beside it, not a command.
22
+ #
23
+ # . "$(dirname "$0")/_jira-auth.sh"
24
+ # jira_auth_resolve || exit 4 # sets JIRA_API_HOST and JIRA_API_TOKEN
25
+ # jira_api GET /rest/api/2/myself
26
+ #
27
+ # Resolution, in order, for each of the two values:
28
+ # host $JIRA_HOST, $ACCOUNT_JIRA_HOST, prefs .global.hosts.jira
29
+ # token $JIRA_TOKEN, else credential-store.sh get <key>, where the key is
30
+ # $JIRA_TOKEN_KEY, $ACCOUNT_JIRA_TOKEN_KEY, or
31
+ # prefs .global.keychainMapping.jira
32
+ #
33
+ # Exit contract: `jira_auth_resolve` returns 0 when both resolved, 4 when either
34
+ # did not, having already said which on stderr. 4 is the same code both callers
35
+ # already used for "could not resolve", so nothing downstream changes meaning.
36
+
37
+ # shellcheck shell=bash
38
+
39
+ JIRA_AUTH_PREFS="${JIRA_AUTH_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
40
+
41
+ _jira_auth_pref() { # _jira_auth_pref <jq path> -> value or empty
42
+ [ -f "$JIRA_AUTH_PREFS" ] || { printf ''; return 0; }
43
+ jq -r "$1 // empty" "$JIRA_AUTH_PREFS" 2>/dev/null || printf ''
44
+ }
45
+
46
+ _jira_auth_store() { # locate credential-store.sh from lib/ or the install
47
+ local here
48
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
49
+ for cand in "$here/credential-store.sh" "$HOME/.claude/lib/credential-store.sh"; do
50
+ [ -f "$cand" ] && { printf '%s' "$cand"; return 0; }
51
+ done
52
+ return 1
53
+ }
54
+
55
+ jira_auth_resolve() {
56
+ JIRA_API_HOST="${JIRA_HOST:-${ACCOUNT_JIRA_HOST:-}}"
57
+ [ -n "$JIRA_API_HOST" ] || JIRA_API_HOST="$(_jira_auth_pref '.global.hosts.jira')"
58
+ if [ -z "$JIRA_API_HOST" ]; then
59
+ echo "ERR: no Jira host: set JIRA_HOST or prefs .global.hosts.jira" >&2
60
+ return 4
61
+ fi
62
+ # A host is a host, whatever the caller pasted.
63
+ JIRA_API_HOST="${JIRA_API_HOST#https://}"
64
+ JIRA_API_HOST="${JIRA_API_HOST#http://}"
65
+ JIRA_API_HOST="${JIRA_API_HOST%/}"
66
+
67
+ JIRA_API_TOKEN="${JIRA_TOKEN:-}"
68
+ if [ -z "$JIRA_API_TOKEN" ]; then
69
+ local key store
70
+ key="${JIRA_TOKEN_KEY:-${ACCOUNT_JIRA_TOKEN_KEY:-}}"
71
+ [ -n "$key" ] || key="$(_jira_auth_pref '.global.keychainMapping.jira')"
72
+ if [ -z "$key" ]; then
73
+ echo "ERR: no Jira token: set JIRA_TOKEN or map prefs .global.keychainMapping.jira" >&2
74
+ return 4
75
+ fi
76
+ store="$(_jira_auth_store)" || {
77
+ echo "ERR: credential-store.sh not found next to lib/ or in ~/.claude/lib" >&2
78
+ return 4
79
+ }
80
+ JIRA_API_TOKEN="$(bash "$store" get "$key" 2>/dev/null || printf '')"
81
+ if [ -z "$JIRA_API_TOKEN" ]; then
82
+ echo "ERR: Jira token not in the credential store under: $key" >&2
83
+ return 4
84
+ fi
85
+ fi
86
+ return 0
87
+ }
88
+
89
+ # The token reaches curl only through a -K config on process substitution: never
90
+ # argv, never a log, never `ps`. Every caller goes through here so that stays
91
+ # true in one place instead of three.
92
+ _jira_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$1"; }
93
+
94
+ jira_api() { # jira_api <METHOD> <path> [curl args...]
95
+ local method="$1" path="$2"; shift 2
96
+ curl -sS -m 30 -K <(_jira_auth_cfg "$JIRA_API_TOKEN") \
97
+ -H "Content-Type: application/json" \
98
+ -X "$method" "https://$JIRA_API_HOST$path" "$@"
99
+ }
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # analysis-jira-write.sh - create the tree that analysis-story-tree.mjs planned.
4
+ #
5
+ # WHY THIS IS A SEPARATE FILE
6
+ #
7
+ # Planning is deterministic, testable offline, and safe to run a hundred times.
8
+ # Creating issues is none of those. Keeping them apart means the file that can
9
+ # write to a tracker is small enough to read in one sitting, and the file that
10
+ # decides WHAT to write can be tested without a network.
11
+ #
12
+ # `jira-publish.sh` cannot do this: it writes a comment or a description on an
13
+ # issue that already exists. The only creation path in this repo was a curl
14
+ # hand-written inside a markdown instruction, which is the thing this replaces.
15
+ #
16
+ # THE WRITE IS LEDGERED, AND THE LEDGER IS THE POINT
17
+ #
18
+ # Before each POST an `intent` line is appended to the run ledger; after the
19
+ # response, the key. Crash in between and the ledger holds an intent with no key.
20
+ # The next run sees that and does a label search BEFORE sending anything, so a
21
+ # half-written tree cannot twin itself. Without the ledger, the failure mode is
22
+ # not "the run stopped" - it is "the run stopped and the retry made a second
23
+ # tree", which is the expensive one.
24
+ #
25
+ # IDENTITY IS SERVER-SIDE
26
+ #
27
+ # Each node carries a label from the plan. Finding the tree back is a JQL search
28
+ # on that label, not a lookup in a local file: a local index does not survive a
29
+ # new machine, a deleted ~/.claude, or a SECOND ANALYST - and the second analyst
30
+ # is exactly the person who would otherwise open a duplicate tree.
31
+ #
32
+ # AN EXISTING NODE IS SKIPPED, NEVER UPDATED
33
+ #
34
+ # Jira has no backup path for fields other than description. Rewriting a body an
35
+ # engineer has since edited would repeat, at tree scale, the defect that made
36
+ # jira-publish.sh take backups in the first place.
37
+ #
38
+ # Usage:
39
+ # analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE]
40
+ #
41
+ # Exit: 0 written (or previewed), 3 the plan does not parse, 4 auth, 5 a Jira
42
+ # call failed, 64 usage.
43
+
44
+ set -uo pipefail
45
+
46
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
+ # shellcheck source=/dev/null
48
+ . "$SELF_DIR/_jira-auth.sh"
49
+
50
+ PLAN=""; PROJECT=""; DRY=0; LEDGER=""
51
+ while [ $# -gt 0 ]; do
52
+ case "$1" in
53
+ --plan) PLAN="${2:-}"; shift 2 ;;
54
+ --project) PROJECT="${2:-}"; shift 2 ;;
55
+ --ledger) LEDGER="${2:-}"; shift 2 ;;
56
+ --dry-run) DRY=1; shift ;;
57
+ *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE]" >&2; exit 64 ;;
58
+ esac
59
+ done
60
+ [ -n "$PLAN" ] && [ -f "$PLAN" ] || { echo "ERR: --plan FILE is required and must exist" >&2; exit 64; }
61
+ [ -n "$PROJECT" ] || { echo "ERR: --project KEY is required" >&2; exit 64; }
62
+
63
+ command -v jq >/dev/null 2>&1 || { echo "ERR: jq is required" >&2; exit 3; }
64
+ jq -e . "$PLAN" >/dev/null 2>&1 || { echo "ERR: the plan does not parse as JSON: $PLAN" >&2; exit 3; }
65
+
66
+ DOC_ID="$(jq -r '.document.id // "unidentified"' "$PLAN")"
67
+ VERDICT="$(jq -r '.coverage.verdict // "unverifiable"' "$PLAN")"
68
+ if [ -z "$LEDGER" ]; then
69
+ LEDGER="$HOME/.claude/logs/multi-agent/_analysis-jira/${DOC_ID}.jsonl"
70
+ fi
71
+ mkdir -p "$(dirname "$LEDGER")" 2>/dev/null || true
72
+
73
+ # A run that could not be verified is allowed to write; it is not allowed to look
74
+ # verified afterwards. The ledger records the verdict beside every key, so the
75
+ # question "was this tree checked?" is answerable later from the tree's own trail.
76
+ ledger() { # ledger <event> <label> [key]
77
+ [ "$DRY" -eq 1 ] && return 0
78
+ printf '{"ts":"%s","event":"%s","label":"%s","key":%s,"coverage":"%s"}\n' \
79
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" "$2" \
80
+ "$([ -n "${3:-}" ] && printf '"%s"' "$3" || printf 'null')" \
81
+ "$VERDICT" >> "$LEDGER"
82
+ }
83
+
84
+ if [ "$DRY" -eq 0 ]; then
85
+ jira_auth_resolve || exit 4
86
+ fi
87
+
88
+ # An unfinished write from a previous run: an intent whose key never arrived.
89
+ # Its existence is what forces the search below to run before anything is sent.
90
+ ORPHANS=0
91
+ if [ -f "$LEDGER" ]; then
92
+ ORPHANS=$(jq -rs '[.[] | select(.event=="intent")] as $i
93
+ | [.[] | select(.event=="created") | .label] as $c
94
+ | [$i[] | select(.label as $l | ($c | index($l)) | not)] | length' \
95
+ "$LEDGER" 2>/dev/null || echo 0)
96
+ if [ "${ORPHANS:-0}" -gt 0 ]; then
97
+ echo "NOTE: $ORPHANS write(s) from an earlier run have no recorded key." >&2
98
+ echo " Searching Jira by label before sending anything." >&2
99
+ fi
100
+ fi
101
+
102
+ # What already exists, by label. One search, whatever the tree's size.
103
+ declare -a EXISTING=()
104
+ existing_for() { # existing_for <label> -> prints the key, or empty
105
+ local label="$1" i
106
+ for i in "${EXISTING[@]:-}"; do
107
+ case "$i" in "$label="*) printf '%s' "${i#*=}"; return 0 ;; esac
108
+ done
109
+ printf ''
110
+ }
111
+
112
+ LABELS="$(jq -r '[.nodes[] | .label, (.subtasks[]?.label)] | join(",")' "$PLAN")"
113
+ if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
114
+ JQL="project=${PROJECT} AND labels in (${LABELS})"
115
+ ENC="$(printf '%s' "$JQL" | jq -sRr @uri)"
116
+ SEARCH="$(jira_api GET "/rest/api/2/search?jql=${ENC}&fields=labels&maxResults=200" || echo "")"
117
+ if [ -n "$SEARCH" ]; then
118
+ while IFS= read -r pair; do
119
+ [ -n "$pair" ] && EXISTING+=("$pair")
120
+ done < <(printf '%s' "$SEARCH" | jq -r '.issues[]? | .key as $k | .fields.labels[]? | "\(.)=\($k)"' 2>/dev/null || true)
121
+ fi
122
+ fi
123
+
124
+ # Two channels on purpose. The human line goes to stdout; the created key comes
125
+ # back in CREATED_KEY. Returning the key on stdout too meant a caller using
126
+ # command substitution swallowed the report - the first dry run printed a header
127
+ # and nothing else, and the tree looked empty.
128
+ CREATED_KEY=""
129
+ create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description>
130
+ local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" body key existing
131
+ CREATED_KEY=""
132
+ existing="$(existing_for "$label")"
133
+ if [ -n "$existing" ]; then
134
+ # Skipped, never updated: Jira has no backup path for fields other than
135
+ # description, and an engineer may have edited this body since.
136
+ echo " exists $existing $summary"
137
+ CREATED_KEY="$existing"
138
+ return 0
139
+ fi
140
+ body="$(jq -n --arg p "$PROJECT" --arg s "$summary" --arg t "$itype" \
141
+ --arg l "$label" --arg d "$desc" --arg par "$parent" '
142
+ {fields: ({project: {key: $p}, summary: $s, issuetype: {name: $t},
143
+ labels: [$l], description: $d}
144
+ + (if $par == "" then {} else {parent: {key: $par}} end))}')"
145
+ if [ "$DRY" -eq 1 ]; then
146
+ echo " create $itype $summary [$label]"
147
+ return 0
148
+ fi
149
+ ledger intent "$label"
150
+ local resp
151
+ resp="$(printf '%s' "$body" | jira_api POST "/rest/api/2/issue" --data @- || echo "")"
152
+ key="$(printf '%s' "$resp" | jq -r '.key // empty' 2>/dev/null || echo "")"
153
+ if [ -z "$key" ]; then
154
+ echo "ERR: create failed for '$summary'" >&2
155
+ printf '%s\n' "$resp" | head -3 >&2
156
+ return 5
157
+ fi
158
+ ledger created "$label" "$key"
159
+ echo " created $key $summary"
160
+ CREATED_KEY="$key"
161
+ }
162
+
163
+ echo "analysis-jira-write: ${PROJECT}, coverage ${VERDICT}$([ "$DRY" -eq 1 ] && echo " (dry run)")"
164
+ [ "$VERDICT" = "unverifiable" ] && \
165
+ echo " NOTE: coverage could not be checked for this document; the tree is unverified."
166
+
167
+ RC=0
168
+ NODES="$(jq -c '.nodes[]' "$PLAN")"
169
+ while IFS= read -r node; do
170
+ [ -n "$node" ] || continue
171
+ s_label="$(printf '%s' "$node" | jq -r '.label')"
172
+ s_title="$(printf '%s' "$node" | jq -r '.title')"
173
+ s_src="$(printf '%s' "$node" | jq -r '.sourceIds | join(", ")')"
174
+ s_type="$(jq -r '.prefsUsed.storyIssueType // "Story"' "$PLAN")"
175
+ s_desc="Sources: ${s_src:-none (derived from the section heading)}"
176
+ if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc"; then
177
+ # A failed story does not get its sub-tasks written anyway. `create_issue`
178
+ # omits the parent field when the parent key is empty, so without this the
179
+ # sub-tasks of a story Jira refused were still POSTed - and landed as live,
180
+ # parentless issues nobody asked for, from a run that had already reported
181
+ # an error.
182
+ RC=5
183
+ echo " skipped this story's sub-tasks: the story itself was not created" >&2
184
+ continue
185
+ fi
186
+ skey="$CREATED_KEY"
187
+ while IFS= read -r sub; do
188
+ [ -n "$sub" ] || continue
189
+ t_label="$(printf '%s' "$sub" | jq -r '.label')"
190
+ t_role="$(printf '%s' "$sub" | jq -r '.role')"
191
+ t_type="$(jq -r '.prefsUsed.subtaskIssueType // empty' "$PLAN")"
192
+ if [ -z "$t_type" ] && [ "$DRY" -eq 0 ]; then
193
+ # Discover rather than assume: whichever type this site marks subtask:true,
194
+ # under whatever name the site gave it.
195
+ t_type="$(jira_api GET "/rest/api/2/issue/createmeta?projectKeys=${PROJECT}&expand=projects.issuetypes" \
196
+ | jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
197
+ fi
198
+ [ -n "$t_type" ] || t_type="Sub-task"
199
+ create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$s_desc" || RC=5
200
+ done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
201
+ done <<< "$NODES"
202
+
203
+ exit "$RC"
@@ -314,8 +314,8 @@ labels_tr = {
314
314
  "description_empty_sibling_available":"Açıklama boş, parent da boş, ama kardeş maddede ({}) içerik var - oradan devam edilsin mi?".format(sibling_key or " - "),
315
315
  "short_description": "Açıklama çok kısa (<80 karakter)",
316
316
  "short_title": "Başlık çok kısa (<10 karakter)",
317
- "no_repro_steps": "Bug için repro adımları yok",
318
- "no_acceptance_criteria":"Kabul kriteri / AC görünmüyor",
317
+ "no_repro_steps": "Hatanın nasıl tekrarlanacağı yazmamış: ne yapıldı, ne bekleniyordu, ne oldu",
318
+ "no_acceptance_criteria":"Kabul kriteri yok: işin bittiğini neye bakarak anlayacağımız yazmamış",
319
319
  }
320
320
  labels_en = {
321
321
  "status_closed": "Issue is closed/cancelled",
@@ -325,8 +325,8 @@ labels_en = {
325
325
  "description_empty_sibling_available":"Description is empty and so is the parent, but a sibling issue ({}) has content - continue from there?".format(sibling_key or " - "),
326
326
  "short_description": "Description too short (<80 chars)",
327
327
  "short_title": "Title too short (<10 chars)",
328
- "no_repro_steps": "Bug missing reproduction steps",
329
- "no_acceptance_criteria":"No acceptance criteria detected",
328
+ "no_repro_steps": "No reproduction steps: what was done, what was expected, what happened",
329
+ "no_acceptance_criteria":"No acceptance criteria: nothing says what \"done\" looks like",
330
330
  }
331
331
  labels = labels_tr if lang == "tr" else labels_en
332
332
  lines = []
@@ -137,7 +137,7 @@ Iterate `state.analysisSpec.outputs.requested`. For each target:
137
137
  | 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 35). 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. |
138
138
  | 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 35). 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 and publish it 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 owns the parts that are easy to get wrong: it runs `jira-wiki-escape.mjs` on the body, resolves host + token without putting either in argv, and on the description path it GETs the current value, writes it to a backup under `~/.claude/logs/multi-agent/jira-backups/` and reports the path, 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 to the user with the backup path, never retried with the flag added automatically - only the user's explicit "Description - replace" answer from Phase 3.5 supplies it. `--dry-run` previews the exact final body without writing. |
139
139
 
140
- **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.jiraIssueKey` (single string).
140
+ **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).
141
141
 
142
142
  ### Phase 5 - Report & stop
143
143
 
@@ -63,7 +63,9 @@ Multi-repo PRs (one PR per repo) emit verification commands for that repo's stac
63
63
  - Rollback: feature flag <name> | git revert <sha> | none, and why
64
64
  ```
65
65
 
66
- **`visuals`** - only when `state.visualEvidence.required`. The images live on the Jira issue as attachments; this section exists so a reviewer opening the PR knows they are there and what each one shows. Filenames, not raw URLs - a Jira attachment URL is auth-gated and renders as a broken image for anyone reading the PR outside a Jira session.
66
+ **`visuals`** - only when `state.visualEvidence.required`. What this section can show depends on where the artefacts are hosted, which Phase 6 resolves into `state.visualEvidence.host`. Render the form for that host and no other.
67
+
68
+ **`host: jira`.** Filenames, never URLs. A Jira attachment URL is auth-gated and renders as a broken image for anyone reading the PR outside a Jira session, and a broken image is worse than a filename because it looks like the evidence is missing.
67
69
 
68
70
  ```markdown
69
71
  ## Visual Evidence
@@ -73,6 +75,40 @@ Multi-repo PRs (one PR per repo) emit verification commands for that repo's stac
73
75
  - Flow video: `<flow-filename>`, tier <N> (attached to PROJ-XXXXX)
74
76
  ```
75
77
 
78
+ **`host: github-public`.** The stills are on the `evidence/<task-id>` branch, so they embed and the reviewer sees them without leaving the PR:
79
+
80
+ ```markdown
81
+ ## Visual Evidence
82
+
83
+ **Before**
84
+
85
+ ![before](https://raw.githubusercontent.com/<owner>/<repo>/evidence/<task-id>/<before-filename>)
86
+
87
+ **After**
88
+
89
+ ![after](https://raw.githubusercontent.com/<owner>/<repo>/evidence/<task-id>/<after-filename>)
90
+ ```
91
+
92
+ **`host: github-private`.** Same branch, but a link rather than an embed. GitHub renders markdown images through its own proxy, which has no credentials for a private repo, so an embedded raw URL renders broken for every reader including the author. A blob link opens the image for anyone who can already see the repo:
93
+
94
+ ```markdown
95
+ ## Visual Evidence
96
+
97
+ - Before: [<before-filename>](https://github.com/<owner>/<repo>/blob/evidence/<task-id>/<before-filename>)
98
+ - After: [<after-filename>](https://github.com/<owner>/<repo>/blob/evidence/<task-id>/<after-filename>)
99
+ ```
100
+
101
+ **`host: none`.** Filenames plus the artefact directory, and the reason there is no host:
102
+
103
+ ```markdown
104
+ ## Visual Evidence
105
+
106
+ - After: `<after-filename>` (run artefacts: `<artifactsPath>`)
107
+ - Not published: <hostReason>
108
+ ```
109
+
110
+ **Video is Jira-only.** On a GitHub-hosted run no recording is made and none is published: an mp4 behind a blob link is a download, not something a reviewer opens mid-review, and paying for a recording nobody watches is worse than saying plainly that there is none. The gap line carries that reason.
111
+
76
112
  Every `state.visualEvidence.gaps[]` entry becomes its own line with the reason instead of a filename (`- Before: none - the ticket carries no image attachment`). Phase 6 Step 3 blocks on a required artefact that is neither listed nor explained. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.
77
113
 
78
114
  **`dependencies`** - only when `Package.swift` / `Podfile` / `build.gradle` / `package.json` changed. Each entry: `package@old → new - reason`.
@@ -6,11 +6,11 @@
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Command Inventory (51 commands)
9
+ ## 1. Command Inventory (53 commands)
10
10
 
11
11
  ```
12
- analysis, analysis-resolve, autopilot, build-optimize, channels,
13
- complaint-analysis, create-jira, design-check, diff-explain, feedback,
12
+ analysis, analysis-jira, analysis-resolve, autopilot, build-optimize, channels,
13
+ complaint-analysis, create-jira, design-check, doctor, diff-explain, feedback,
14
14
  forget, garbage-collect, graph, help, ios-coding-standard, issue, jira,
15
15
  kill, language, local, local-autopilot, log, manual-test, prune-logs,
16
16
  prune-prompts, purge, refactor, resume, resume-local, review,
@@ -0,0 +1,128 @@
1
+ # analysis-jira - an analysis document, read as work
2
+
3
+ `/multi-agent:analysis-jira` turns a rendered analysis document into a Jira tree.
4
+ Two files do it, and the split is the design:
5
+
6
+ | File | Does | Network |
7
+ |---|---|---|
8
+ | `scripts/analysis-story-tree.mjs` | decides WHAT to create, checks coverage, assigns identity | none |
9
+ | `lib/analysis-jira-write.sh` | creates it | yes |
10
+
11
+ Planning is deterministic and testable offline; creating issues is neither. Kept
12
+ apart, the file that can write to a tracker is small enough to read in one
13
+ sitting.
14
+
15
+ `jira-publish.sh` could not be extended to do this: it writes a comment or a
16
+ description on an issue that already exists. The only creation path in the repo
17
+ was a curl hand-written inside a markdown instruction, which is what this
18
+ replaces.
19
+
20
+ ## The marker gate runs first, before any network call
21
+
22
+ Zero `EKLENECEK`, zero `TBD`, no open Section 20 row. A document with an open
23
+ placeholder is not a plan, and turning it into a tree publishes the gap as work
24
+ somebody is now assigned. Refusal is exit 4 and it names
25
+ `/multi-agent:analysis-resolve` as the step.
26
+
27
+ This is the same closure contract `status: final` enforces in
28
+ `validate-analysis-doc.mjs`, applied at the point the document leaves the
29
+ analysis world.
30
+
31
+ ## Coverage is two-way, and the second direction is the useful one
32
+
33
+ | Direction | Catches |
34
+ |---|---|
35
+ | every defined id appears in some story | a dropped requirement |
36
+ | every cited id is defined in the document | an **invented story** - a node with no requirement behind it |
37
+
38
+ No forward check can see the second one, and it is the failure mode of building
39
+ a tree from a model's reading rather than from the document's own ids.
40
+
41
+ The atom is `BR-<slug>-NN` in the global profile and `FG-NN` in the corporate
42
+ one; the group is the `BR-<slug>` prefix, or `UC-NN`. Locked 31 guarantees both
43
+ exist, which is why the tree can be derived rather than invented.
44
+
45
+ `coverageOf()` is exported and tested directly. The planner cannot emit an
46
+ invented id - it derives every `sourceIds` from the defined set - so a test that
47
+ fabricates one and re-checks it with its own logic proves nothing about the
48
+ shipped code. The backward direction guards the boundary where a plan arrives
49
+ from somewhere else: hand-edited, resumed from an older format, or produced by
50
+ something that read the prose instead of the ids.
51
+
52
+ ## An unverifiable run is allowed; looking verified is not
53
+
54
+ A lite document may carry no ids at all. Coverage then cannot run, and the
55
+ verdict is `unverifiable`, never `ok`. It appears:
56
+
57
+ - on its own line in the preview, where a reader looks for `ok`
58
+ - as a separate fourth approval option, not folded into `Approve`
59
+ - in the writer's own output, and beside every key in the ledger
60
+
61
+ Such a document still plans work, from its user-story sub-sections. That is not
62
+ a convenience: without it the no-atom document produced no stories, exited
63
+ "nothing to plan", and the `unverifiable` branch was unreachable - a case the
64
+ code claimed to handle and never could.
65
+
66
+ ## Identity is a label, not a title
67
+
68
+ Each node carries `<labelPrefix>-<10 hex>`, hashed from the document id
69
+ (`evidence_digest`) plus the node's own source ids. A second run finds its tree
70
+ back with one JQL search on those labels.
71
+
72
+ Titles were the obvious key and are the wrong one: they get edited, and matching
73
+ on them breaks exactly when someone has improved the wording. The label lives
74
+ server-side, so it survives a new machine, a deleted `~/.claude`, and a **second
75
+ analyst** - who is precisely the person positioned to open a duplicate tree.
76
+
77
+ ## The write is ledgered
78
+
79
+ `~/.claude/logs/multi-agent/_analysis-jira/<docId>.jsonl`. An `intent` line
80
+ before each POST, the key after the response. A crash between them leaves an
81
+ intent with no key; the next run sees that and searches by label before sending
82
+ anything. Without the ledger the failure mode is not "the run stopped" but "the
83
+ run stopped and the retry made a second tree", which is the expensive one.
84
+
85
+ Every line also carries the coverage verdict, so "was this tree checked?" stays
86
+ answerable from the tree's own trail.
87
+
88
+ ## An existing node is skipped, never updated
89
+
90
+ Jira has no backup path for fields other than description. Rewriting a body an
91
+ engineer has since edited would repeat, at tree scale, the defect that made
92
+ `jira-publish.sh` take backups in the first place.
93
+
94
+ ## Every site-specific name is a VALUE, never a schema key
95
+
96
+ `prefs.global.issueTree` holds the vocabulary. A key is a published literal and
97
+ this schema ships to everyone, so a site's component, team and issue-type names
98
+ are values the site fills in:
99
+
100
+ - `channelComponents` / `channelTeams` are free-form maps: the key is the
101
+ channel, the value is the site's own name for it
102
+ - `subtaskRoles` ships **empty**. An empty list is an instruction to look at how
103
+ this board actually splits work; a ready-made list would be the guess most
104
+ worth avoiding
105
+ - `subtaskIssueType: null` means discover it - `createmeta` returns whichever
106
+ type carries `subtask: true`, under whatever name the site gave it. Same rule
107
+ as `features/jira-context.md`: the type travels as it comes from Jira and is
108
+ never a matching criterion in code
109
+
110
+ The preview prints every field beside the pref key it came from, so a wrong
111
+ setting is visible before the writes rather than in Jira afterwards.
112
+
113
+ ## Auth
114
+
115
+ `lib/_jira-auth.sh` holds one host-and-token resolution and one curl idiom: the
116
+ token reaches curl through a `-K` config on process substitution and never
117
+ touches argv, a log, or `ps`. That idiom is the part most easily retyped badly,
118
+ which is why the third writer got a shared copy instead of a third hand-written
119
+ one.
120
+
121
+ **Only this writer consumes it today.** `jira-publish.sh` and `issue-fetcher.sh`
122
+ still carry their own resolution, and retrofitting them is real work rather than
123
+ a rename - `issue-fetcher.sh` resolves per-account token keys that this helper
124
+ does not model yet. So the file is the shared copy going forward, not a
125
+ consolidation that has already happened, and saying otherwise would describe a
126
+ cleanup nobody did. The leak property itself is asserted on all three callers
127
+ independently in `smoke-analysis-jira.sh`, which is the part that must hold
128
+ whether or not they ever share code.