@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.
- package/CHANGELOG.md +119 -2
- package/README.md +4 -4
- package/README.tr.md +3 -3
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/features.md +14 -0
- package/install/claude.mjs +17 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +93 -0
- package/pipeline/commands/multi-agent/design-check/SKILL.md +6 -5
- package/pipeline/commands/multi-agent/doctor/SKILL.md +78 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +15 -12
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +14 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -9
- package/pipeline/commands/multi-agent/update/SKILL.md +12 -0
- package/pipeline/lib/_jira-auth.sh +99 -0
- package/pipeline/lib/analysis-jira-write.sh +203 -0
- package/pipeline/lib/issue-fetcher.sh +4 -4
- package/pipeline/multi-agent-refs/analysis/render.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +37 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
- package/pipeline/multi-agent-refs/features/analysis-jira.md +128 -0
- package/pipeline/multi-agent-refs/features/doctor.md +197 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
- package/pipeline/multi-agent-refs/features/visual-evidence.md +103 -20
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +38 -7
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +13 -1
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +11 -1
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +23 -0
- package/pipeline/multi-agent-refs/picker-contract.md +35 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +5 -1
- package/pipeline/preferences-template.json +1 -1
- package/pipeline/schemas/agent-state.schema.json +84 -1
- package/pipeline/schemas/analysis-spec.schema.json +336 -95
- package/pipeline/schemas/prefs.schema.json +80 -3
- package/pipeline/schemas/token-budget.json +10 -10
- package/pipeline/scripts/analysis-story-tree.mjs +441 -0
- package/pipeline/scripts/capture-evidence.sh +170 -5
- package/pipeline/scripts/doctor.mjs +758 -0
- package/pipeline/scripts/evidence-gate.mjs +31 -2
- package/pipeline/scripts/phase-tracker.sh +97 -17
- package/pipeline/scripts/probe-evidence-capability.sh +250 -0
- package/pipeline/scripts/run-ui-tests.sh +380 -0
- package/pipeline/scripts/scan-agent-config.sh +48 -10
- package/pipeline/scripts/skill-siblings.mjs +1 -1
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +94 -0
- package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +79 -0
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +10 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +13 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +9 -6
- 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
|
|
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 +
|
|
63
|
-
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill +
|
|
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:
|
|
126
|
-
grep -rniE "ai-ios
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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": "
|
|
318
|
-
"no_acceptance_criteria":"Kabul kriteri
|
|
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": "
|
|
329
|
-
"no_acceptance_criteria":"No acceptance criteria
|
|
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`.
|
|
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
|
+

|
|
86
|
+
|
|
87
|
+
**After**
|
|
88
|
+
|
|
89
|
+

|
|
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 (
|
|
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.
|