@mmerterden/multi-agent-pipeline 20.11.1 → 20.13.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 +51 -0
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/docs/facts.json +2 -2
- package/install/_common.mjs +1 -1
- package/install/_plugin-skills.mjs +13 -9
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +9 -0
- package/install/codex.mjs +12 -12
- package/manifest.json +74 -67
- package/package.json +5 -5
- package/pipeline/agents/code-reviewer.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
- package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/model/SKILL.md +7 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
- package/pipeline/commands/multi-agent/sync/SKILL.md +5 -6
- package/pipeline/lib/agent-model-sync.mjs +95 -0
- package/pipeline/lib/analysis-jira-write.sh +81 -27
- package/pipeline/lib/analysis-quality.mjs +261 -0
- package/pipeline/lib/analysis-sections.mjs +105 -0
- package/pipeline/lib/jira-epic-link.sh +54 -0
- package/pipeline/lib/model-rung.sh +7 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
- package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
- package/pipeline/multi-agent-refs/analysis/render.md +20 -21
- package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
- package/pipeline/multi-agent-refs/analysis/review.md +16 -3
- package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
- package/pipeline/multi-agent-refs/analysis-template.md +21 -14
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
- package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
- package/pipeline/multi-agent-refs/features/doctor.md +10 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +10 -0
- package/pipeline/multi-agent-refs/features/phone-api.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
- package/pipeline/multi-agent-refs/features/usage-reporting.md +13 -0
- package/pipeline/schemas/analysis-spec.schema.json +22 -0
- package/pipeline/schemas/prefs.schema.json +22 -1
- package/pipeline/scripts/analysis-story-body.mjs +210 -0
- package/pipeline/scripts/analysis-story-tree.mjs +73 -12
- package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
- package/pipeline/scripts/build-references.mjs +14 -1
- package/pipeline/scripts/confluence-readback.mjs +148 -0
- package/pipeline/scripts/doctor.mjs +48 -0
- package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
- package/pipeline/scripts/migrate-prefs.mjs +14 -0
- package/pipeline/scripts/unattended_policy.py +12 -5
- package/pipeline/scripts/usage-report.mjs +84 -9
- package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
- package/pipeline/skills/.skill-manifest.json +8 -8
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
- package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
description: "Turn a rendered analysis document into a Jira story tree: derive stories from the document's own rule ids, check coverage both ways, preview every byte, then create only what does not already exist. Use when an analysis is final and the work needs tickets."
|
|
3
3
|
description-tr: "Yazılmış bir analiz dokümanını Jira story ağacına çevirir: story'leri dokümanın kendi kural kimliklerinden türetir, kapsamı iki yönlü kontrol eder, yazılacak her baytı önizler, sonra yalnız var olmayanı açar."
|
|
4
4
|
allowed-tools: Bash, Read, AskUserQuestion
|
|
5
|
-
argument-hint: "[analysis.md] [--project KEY] - optional; with no argument, pick from the documents this run emitted"
|
|
6
|
-
parameters: [{"name":"document","kind":"path"},{"name":"--project","kind":"text"}]
|
|
5
|
+
argument-hint: "[analysis.md] [--project KEY] [--epic KEY] - optional; with no argument, pick from the documents this run emitted"
|
|
6
|
+
parameters: [{"name":"document","kind":"path"},{"name":"--project","kind":"text"},{"name":"--epic","kind":"text"}]
|
|
7
7
|
gui: form
|
|
8
8
|
destructive: false
|
|
9
9
|
confirm: none
|
|
@@ -22,13 +22,26 @@ invents a story: every node comes from an id the document defines.
|
|
|
22
22
|
Contract, severities and reasoning:
|
|
23
23
|
`$HOME/.claude/multi-agent-refs/features/analysis-jira.md`.
|
|
24
24
|
|
|
25
|
+
## Gotchas
|
|
26
|
+
|
|
27
|
+
- An issue whose label already exists is skipped, never updated. Editing the document and re-running creates only what is missing; existing tickets keep their old text.
|
|
28
|
+
- A document with no `BR-`/`FG-` ids still plans, from its `4.N` sub-sections, but its coverage verdict is `unverifiable`: there is nothing to check the tree against.
|
|
29
|
+
- An open `EKLENECEK`/`TBD` marker or an open Section 20 row stops planning with exit 4. Resolve them with `/multi-agent:analysis-resolve` first.
|
|
30
|
+
- The epic must already exist: stories are linked to it, never an epic created. On Server/Data Center the Epic Link field is discovered from the site; when none is found the stories are created unlinked and the writer says so.
|
|
31
|
+
- Channel clones (`issueTree.cloneByChannel`) carry their own labels and are searched like stories, so a re-run never doubles them or their links.
|
|
32
|
+
- Story bodies hold only document text. A body warning (no user story, fewer than 2 criteria, no negative criterion) is a gap in the document; fix the document, not the ticket.
|
|
33
|
+
|
|
25
34
|
## Phase 1 - Plan, offline
|
|
26
35
|
|
|
27
36
|
```bash
|
|
28
|
-
node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" --json > /tmp/ma-plan.json
|
|
29
|
-
node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>"
|
|
37
|
+
node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" --json $PAGE > /tmp/ma-plan.json
|
|
38
|
+
node "$HOME/.claude/scripts/analysis-story-tree.mjs" "<analysis.md>" $PAGE
|
|
30
39
|
```
|
|
31
40
|
|
|
41
|
+
`$PAGE` is `--page-url <url>` when the document was published (the run's
|
|
42
|
+
`outputs.confluencePages[]`); without it the front-matter `confluence_url` is
|
|
43
|
+
used, and with neither the story bodies carry no analysis link.
|
|
44
|
+
|
|
32
45
|
Exit 4 means the document still carries an open placeholder. Stop and report it:
|
|
33
46
|
the step is `/multi-agent:analysis-resolve`, and a tree built from an open
|
|
34
47
|
question publishes the gap as work somebody is now assigned.
|
|
@@ -52,12 +65,19 @@ what makes a preview meaningful:
|
|
|
52
65
|
- every field beside the pref key it came from, so a wrong setting shows here
|
|
53
66
|
rather than in Jira afterwards
|
|
54
67
|
- the write count
|
|
68
|
+
- every story's full description, as it will be written, with its body
|
|
69
|
+
warnings; `--brief` gives the one-line-per-node view
|
|
70
|
+
|
|
71
|
+
Then ask for the epic, once. `AskUserQuestion`:
|
|
72
|
+
|
|
73
|
+
- **Link to an existing epic** - the user types its key (`--epic <KEY>`)
|
|
74
|
+
- **No epic** - the stories are created unlinked
|
|
55
75
|
|
|
56
76
|
Then the dry run, which is what proves the writer agrees with the plan:
|
|
57
77
|
|
|
58
78
|
```bash
|
|
59
79
|
bash "$HOME/.claude/lib/analysis-jira-write.sh" \
|
|
60
|
-
--plan /tmp/ma-plan.json --project "<KEY>" --dry-run
|
|
80
|
+
--plan /tmp/ma-plan.json --project "<KEY>" [--epic "<EPIC>"] --dry-run
|
|
61
81
|
```
|
|
62
82
|
|
|
63
83
|
## Phase 3 - Approve
|
|
@@ -83,11 +103,14 @@ be reachable.
|
|
|
83
103
|
invented id means the plan cites something the document does not define - treat
|
|
84
104
|
that as a defect in the plan, not a warning to click past.
|
|
85
105
|
|
|
106
|
+
Body warnings are printed before the question too. They do not block: the
|
|
107
|
+
ticket says only what the document says, and the warning names the gap.
|
|
108
|
+
|
|
86
109
|
## Phase 4 - Write
|
|
87
110
|
|
|
88
111
|
```bash
|
|
89
112
|
bash "$HOME/.claude/lib/analysis-jira-write.sh" \
|
|
90
|
-
--plan /tmp/ma-plan.json --project "<KEY>"
|
|
113
|
+
--plan /tmp/ma-plan.json --project "<KEY>" [--epic "<EPIC>"]
|
|
91
114
|
```
|
|
92
115
|
|
|
93
116
|
It searches by label first and skips what exists. Re-running is safe and is the
|
|
@@ -97,9 +120,36 @@ findable rather than duplicable.
|
|
|
97
120
|
**Never pass `--force`-like flags, and never update an existing node.** Jira has
|
|
98
121
|
no backup path for fields other than description.
|
|
99
122
|
|
|
123
|
+
### Write the tickets back
|
|
124
|
+
|
|
125
|
+
The document lists what it produced. Local and idempotent: the table sits
|
|
126
|
+
between `<!-- tickets:start -->` and `<!-- tickets:end -->`, a re-run replaces
|
|
127
|
+
it, and only nodes with a key appear.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
LEDGER="$HOME/.claude/logs/multi-agent/_analysis-jira/$(jq -r '.document.id // "unidentified"' /tmp/ma-plan.json).jsonl"
|
|
131
|
+
node "$HOME/.claude/scripts/analysis-tickets-writeback.mjs" \
|
|
132
|
+
--doc "<analysis.md>" --ledger "$LEDGER" --plan /tmp/ma-plan.json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When the document has a Confluence page, updating it is a **separate**
|
|
136
|
+
`AskUserQuestion`, never part of the tree approval:
|
|
137
|
+
|
|
138
|
+
- **Update the Confluence page** - publish the document with its new table
|
|
139
|
+
- **Keep it local** - the page stays as it is
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3 "$HOME/.claude/lib/md2confluence-v3.py" update \
|
|
143
|
+
--page-id "<pageId>" --title "<page title>" \
|
|
144
|
+
--markdown "<analysis.md>" --attachments-dir "$(dirname "<analysis.md>")"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Read its JSON envelope as the analysis render step does, and surface any
|
|
148
|
+
missing-attachment warning.
|
|
149
|
+
|
|
100
150
|
## Phase 5 - Report
|
|
101
151
|
|
|
102
|
-
Keys created, keys skipped, the coverage verdict, and the ledger path. If the
|
|
152
|
+
Keys created, keys skipped, the epic linked (or none), the coverage verdict, and the ledger path. If the
|
|
103
153
|
verdict was `unverifiable`, say that in the report too - not only at approval
|
|
104
154
|
time, because the report is what gets pasted elsewhere.
|
|
105
155
|
|
|
@@ -45,7 +45,7 @@ Inherited decisions: `$HOME/.claude/multi-agent-refs/analysis/locked.md`.
|
|
|
45
45
|
- 1 match: use it; echo the path, no question.
|
|
46
46
|
- 0 or 2+ matches: AskUserQuestion, `header: "Doc"`, question `<localized: "Which analysis document should we resolve?">`, options = matches (max 3) + Other for a free-typed path.
|
|
47
47
|
|
|
48
|
-
**Validate**: file exists; front-matter parses with `template_version: v3` (a `v2` doc gets `WARN: v2 doc has no resolvable Section 20 contract; re-run /multi-agent:analysis` and the run halts); a `## <n>. Riskler ve Acik Sorular` / `## <n>. Risks and Open Questions` heading exists (
|
|
48
|
+
**Validate**: file exists; front-matter parses with `template_version: v3` (a `v2` doc gets `WARN: v2 doc has no resolvable Section 20 contract; re-run /multi-agent:analysis` and the run halts); a `## <n>. Riskler ve Acik Sorular` / `## <n>. Risks and Open Questions` heading exists (match by title, not by the literal number 20: the profiles number the section differently).
|
|
49
49
|
|
|
50
50
|
**Step 2 - Repo root for "from repo" candidates.** Default order: front-matter platform's repo from `state.analysisSpec.repos[]` if the session holds it, else the repo containing the doc, else cwd. Confirm via AskUserQuestion only when no default resolves (`header: "Repo"`, options `Use current working directory` + Other).
|
|
51
51
|
|
|
@@ -60,7 +60,7 @@ Inherited decisions: `$HOME/.claude/multi-agent-refs/analysis/locked.md`.
|
|
|
60
60
|
|
|
61
61
|
| Path | Reason |
|
|
62
62
|
|------|--------|
|
|
63
|
-
| `$HOME/.claude/multi-agent-refs/analysis-template.md` | Section 20 / Section 23 table contracts, front-matter shape, omission
|
|
63
|
+
| `$HOME/.claude/multi-agent-refs/analysis-template.md` | Section 20 / Section 23 table contracts, front-matter shape, section omission |
|
|
64
64
|
| `$HOME/.claude/multi-agent-refs/conventions-defaults.md` | `convention-fallback` default values per platform |
|
|
65
65
|
| `~/.claude/lib/extract-conventions.sh` | fresh single-field extraction for convention-fallback candidates |
|
|
66
66
|
| `ai-common-toolkit:humanizer` | tone reference for longer merged fragments (short fragments only need the punctuation gate) |
|
|
@@ -18,6 +18,12 @@ confirm: none
|
|
|
18
18
|
|
|
19
19
|
Run the task end-to-end with no confirmations.
|
|
20
20
|
|
|
21
|
+
## Gotchas
|
|
22
|
+
|
|
23
|
+
- The guard decides within a 5-second internal deadline and fails closed when unattended. On a heavily loaded machine a correct command can be refused for time, not for policy.
|
|
24
|
+
- An unattended run never pushes or opens a PR itself: it records the request in `pr-request.json` and the runner publishes it.
|
|
25
|
+
- An unattended run cannot run `/multi-agent:stack` or write `.claude/` in the repo; a missing stack toolkit is read from a local marketplace clone instead.
|
|
26
|
+
|
|
21
27
|
## What changes
|
|
22
28
|
|
|
23
29
|
| Phase | Normal | Autopilot |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Turn off continuous mode on this machine: the schedule is removed, work already running finishes, the repo selection is kept. Use when the machine should stop picking work up."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "Bu makinede sürekli modu kapatır: zamanlama kaldırılır, koşan iş biter, repo seçimi saklanır."
|
|
4
5
|
argument-hint: "[--now] - --now also stops the item currently running"
|
|
5
6
|
parameters: [{"name":"--now","kind":"flag"}]
|
|
@@ -22,6 +22,12 @@ same way - late, mid-run, after the pickers had been answered. A missing script
|
|
|
22
22
|
fails at the call. Malformed preferences fail after Phase 0 has asked five
|
|
23
23
|
questions. A token in a remote URL does not fail at all; it leaks.
|
|
24
24
|
|
|
25
|
+
## Gotchas
|
|
26
|
+
|
|
27
|
+
- Exit 4 means the layout did not resolve and nothing was checked. It is not a pass.
|
|
28
|
+
- Without `--probe` no credential is sent anywhere, so a revoked token still reads as mapped.
|
|
29
|
+
- `usage-report` SKIP means nothing has been sent from this machine yet, not that reporting works.
|
|
30
|
+
|
|
25
31
|
## Run it
|
|
26
32
|
|
|
27
33
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Send one message to the maintainer: a bug, an idea or a question. Only the text you type is sent - no logs, no repo names, no paths. Shows the payload and asks before sending."
|
|
3
|
-
description-tr: "Bakımcıya tek mesaj gönderir: hata, fikir ya da soru. Yalnızca yazdığın metin gider - log yok, repo adı yok, yol yok. Göndermeden önce içeriği gösterip onay ister."
|
|
2
|
+
description: "Send one message to the maintainer: a bug, an idea or a question. Only the text you type is sent - no logs, no repo names, no paths. Shows the payload and asks before sending. Use when a run went wrong and the maintainer should know."
|
|
3
|
+
description-tr: "Bakımcıya tek mesaj gönderir: hata, fikir ya da soru. Yalnızca yazdığın metin gider - log yok, repo adı yok, yol yok. Göndermeden önce içeriği gösterip onay ister. Bir koşu ters gittiğinde ve bakımcının bilmesi gerektiğinde kullanılır."
|
|
4
4
|
argument-hint: "[\"<message>\"] [bug | idea | question] - with no argument, you are asked for the text"
|
|
5
5
|
parameters: [{"name":"message","kind":"text"},{"name":"category","kind":"enum","enum":["bug","idea","question"]}]
|
|
6
6
|
gui: form
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Remove a saved /multi-agent routine (created by /multi-agent:save): deletes its local-only command and its registry entry. Asks which one and confirms. Use when a saved routine is no longer wanted and should be removed."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "Kaydedilmiş bir /multi-agent rutinini (/multi-agent:save ile oluşturulan) siler: local-only komutunu ve registry kaydını kaldırır. Hangisini soracak ve onay alacak."
|
|
4
5
|
argument-hint: "[name] - routine to remove; asked if omitted"
|
|
5
6
|
parameters: [{"name":"name","kind":"text"}]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Sweep leftover /tmp scratch (picker state, review diffs, channel payloads, analysis drafts), worktree residue (orphan .worktrees dirs, gitlink index entries) and offloaded tool payloads under .multi-agent/refs/ from past runs. With --abandoned, also reaps runs that stopped and were never picked back up, stashing any uncommitted work. Dry-run first; confirms before deleting. Use when scratch files from earlier runs need clearing out, or when stopped runs are holding worktrees."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "Geçmiş çalışmalardan kalan /tmp scratch'ini (picker durumu, review diff'leri, kanal payload'ları, analiz taslakları), worktree kalıntılarını (sahipsiz .worktrees klasörleri, gitlink index kayıtları) ve .multi-agent/refs/ altındaki offload edilmiş araç çıktılarını süpürür. --abandoned ile durup bir daha dönülmemiş koşuları da toplar, commit edilmemiş işi stash'ler. Önce dry-run; silmeden önce onay alır."
|
|
4
5
|
argument-hint: "[--older-than=<minutes>] [--abandoned [--days=N]] [--yes] - dry-run unless confirmed"
|
|
5
6
|
parameters: [{"name":"--older-than","kind":"text"},{"name":"--abandoned","kind":"flag"},{"name":"--days","kind":"text"},{"name":"--yes","kind":"flag"}]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Stop the given task, then remove its worktree and branch. Asks for confirmation. Use when a running or stuck task should be stopped and its worktree removed."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "Verilen görevi durdurur, ardından worktree'sini ve branch'ini kaldırır. Onay ister."
|
|
4
5
|
argument-hint: "#id [--yes] - task ID to delete (e.g. #2); --yes answers the confirmation"
|
|
5
6
|
parameters: [{"name":"task","kind":"run-id","required":true},{"name":"--yes","kind":"flag"}]
|
|
@@ -38,6 +38,13 @@ This command sets both, together:
|
|
|
38
38
|
Flipping one without the other is the defect this closes, so the pair moves as
|
|
39
39
|
one write or not at all.
|
|
40
40
|
|
|
41
|
+
The same step re-applies the installed agents (`lib/agent-model-sync.mjs`): every
|
|
42
|
+
agent under `~/.claude/agents` whose `preferredModel` is `fable` gets
|
|
43
|
+
`model: fable` while the switch is on and `model: opus` while it is off. An agent
|
|
44
|
+
spawned without an explicit model runs on that line, so without this the switch
|
|
45
|
+
would cover routed dispatches only. The installer applies the same rule on every
|
|
46
|
+
install and update, so a sync never puts the agents back on a rung that is off.
|
|
47
|
+
|
|
41
48
|
## What it reports with no argument
|
|
42
49
|
|
|
43
50
|
The current rung, the pricing model, and **what the switch means on this host** -
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Delete per-task project logs under ~/.claude/logs/multi-agent (filter by age/project/task). Audit trail + metrics are preserved. Dry-run first; confirms before deleting. Use when task logs have built up and need trimming by age, project or task."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "~/.claude/logs/multi-agent altındaki görev bazlı proje loglarını siler (yaş/proje/görev filtresi). Denetim izi + metrikler korunur. Önce dry-run; silmeden önce onay alır."
|
|
4
5
|
argument-hint: "[--older-than=<days>] [--project=<name>] [--task=<id>] [--yes]"
|
|
5
6
|
parameters: [{"name":"--older-than","kind":"text"},{"name":"--project","kind":"text"},{"name":"--task","kind":"run-id"},{"name":"--yes","kind":"flag"}]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Zero-base prompt review: measure the always-on instruction footprint, classify every rule block, propose keep/trial-removal/delete; applies only approved rows. Use when a new model ships or instructions have accumulated."
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
description-tr: "Sıfır-tabanlı prompt incelemesi: sürekli yüklü talimat yükünü ölçer, her kural bloğunu sınıflar, tut/dene/sil önerir; yalnızca onaylananı uygular."
|
|
4
5
|
argument-hint: "[--report-only]"
|
|
5
6
|
parameters: [{"name":"--report-only","kind":"flag"}]
|
|
@@ -26,6 +26,11 @@ Skip Phase 0-3 and review a diff only. Five input shapes, two providers:
|
|
|
26
26
|
|
|
27
27
|
Branch-mode runs print to chat only. PR-mode runs print to chat AND post the verdict to the PR per `$HOME/.claude/multi-agent-refs/channels/pr-review-actions.md`: per-finding inline comments + an explicit approve / needs-work state. **No monolithic advisory comment** is ever posted.
|
|
28
28
|
|
|
29
|
+
## Gotchas
|
|
30
|
+
|
|
31
|
+
- The counterpart-repo comparison needs exactly one resolved candidate. With several, an unattended run skips it silently, so an autopilot review can have no cross-platform section.
|
|
32
|
+
- The counterpart's stack comes from marker files, never from the repo name.
|
|
33
|
+
|
|
29
34
|
## Steps
|
|
30
35
|
|
|
31
36
|
### 0. Session bootstrap - TASK_ID + tmp cleanup
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Review a written analysis document instead of a diff: resolve it from a path, a Confluence page or a Jira issue, run the deterministic gates first, then a parallel model review. Findings cite the Locked rule they break. Never edits the document."
|
|
3
|
-
description-tr: "Diff yerine yazılmış analiz dokümanını review eder: yol, Confluence sayfası veya Jira issue'sundan getirir, önce deterministik geçitleri koşar, sonra paralel model review. Bulgular ihlal edilen Locked kuralını gösterir. Dokümanı düzenlemez."
|
|
2
|
+
description: "Review a written analysis document instead of a diff: resolve it from a path, a Confluence page or a Jira issue, run the deterministic gates first, then a parallel model review. Findings cite the Locked rule they break. Never edits the document. Use when an analysis needs judging before development starts."
|
|
3
|
+
description-tr: "Diff yerine yazılmış analiz dokümanını review eder: yol, Confluence sayfası veya Jira issue'sundan getirir, önce deterministik geçitleri koşar, sonra paralel model review. Bulgular ihlal edilen Locked kuralını gösterir. Dokümanı düzenlemez. Bir analizin geliştirme başlamadan önce değerlendirilmesi gerektiğinde kullanılır."
|
|
4
4
|
argument-hint: "[path | Confluence URL | pageId | JIRA-KEY] [--state <state.json>] - optional; with no argument, pick from recent analyses"
|
|
5
5
|
parameters: [{"name":"target","kind":"text"},{"name":"--state","kind":"path"}]
|
|
6
6
|
gui: form
|
|
@@ -14,6 +14,11 @@ confirm: none
|
|
|
14
14
|
|
|
15
15
|
`/multi-agent:review` judges a diff. This judges the document that diff was supposed to come from, before anyone writes the code it describes. No worktree, no branch, no commit, and the reviewed document is never edited in place.
|
|
16
16
|
|
|
17
|
+
## Gotchas
|
|
18
|
+
|
|
19
|
+
- Validator ERRORs are Blockers and WARNs are Important, unless the document states why the warned-about item is acceptable.
|
|
20
|
+
- Every rubric class is printed, with `none` when empty. A class missing from the verdict did not run; it is not a clean result.
|
|
21
|
+
|
|
17
22
|
## Flow
|
|
18
23
|
|
|
19
24
|
Read `$HOME/.claude/multi-agent-refs/analysis/review.md` and execute it:
|
|
@@ -134,12 +134,10 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
134
134
|
~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
135
135
|
&& { echo "ABORT: corporate reference leaked into pipeline/commands"; exit 1; } || true
|
|
136
136
|
```
|
|
137
|
-
- **
|
|
138
|
-
`
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
after copying commands into `pipeline/commands/`, normalize the copy and
|
|
142
|
-
verify no sidecar or Turkish description leaked:
|
|
137
|
+
- **Installed-tree artifacts.** Picker descriptions are Turkish with a
|
|
138
|
+
`description-en:` sidecar when `outputLanguage=tr`, and fable-tier agents
|
|
139
|
+
carry `model: opus` while `fableEnabled` is off. The repo always gets the
|
|
140
|
+
English form and `model: fable`, so normalize after copying:
|
|
143
141
|
```bash
|
|
144
142
|
# normalize: restore English descriptions, strip description-en sidecars
|
|
145
143
|
node ~/multi-agent-pipeline/pipeline/scripts/localize-commands.mjs restore \
|
|
@@ -149,6 +147,7 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
149
147
|
# backstop: repo description lines must be English (Turkish-diacritics proxy)
|
|
150
148
|
grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
151
149
|
&& { echo "ABORT: localized description leaked into pipeline/commands"; exit 1; } || true
|
|
150
|
+
node ~/multi-agent-pipeline/pipeline/lib/agent-model-sync.mjs --dir ~/multi-agent-pipeline/pipeline/agents --fable-on
|
|
152
151
|
```
|
|
153
152
|
|
|
154
153
|
6. **Cross-platform smoke gate** (final step of the REPO sync, before push):
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-model-sync.mjs - installed agents follow the fable switch.
|
|
3
|
+
*
|
|
4
|
+
* prefs.global.modelFallback.fableEnabled is the one switch for the fable rung.
|
|
5
|
+
* The router honours it for every dispatch it resolves, but an agent spawned
|
|
6
|
+
* without an explicit model runs on its frontmatter `model:`. So every installed
|
|
7
|
+
* agent whose `preferredModel` is fable has that line written from the switch:
|
|
8
|
+
* fable while it is on, opus while it is off. Sources keep `model: fable`; only
|
|
9
|
+
* installed copies change. Called by model-rung.sh on every flip and by the
|
|
10
|
+
* installer after it lays the agents down.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node agent-model-sync.mjs [--dir <agents dir>] [--prefs <prefs file>] [--fable-on]
|
|
14
|
+
*
|
|
15
|
+
* --fable-on ignores the switch and writes fable: the sync step uses it to put
|
|
16
|
+
* the repo copy back on its source form.
|
|
17
|
+
*
|
|
18
|
+
* @module pipeline/lib/agent-model-sync
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
|
|
26
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} dir - an installed agents directory
|
|
30
|
+
* @param {boolean} fableOn
|
|
31
|
+
* @returns {string[]} the file names whose model line changed
|
|
32
|
+
*/
|
|
33
|
+
export function applyFableRung(dir, fableOn) {
|
|
34
|
+
if (!existsSync(dir)) return [];
|
|
35
|
+
// A linked agents directory is the source tree itself (a maintainer's
|
|
36
|
+
// --symlink install); writing there would edit the shipped personas.
|
|
37
|
+
if (lstatSync(dir).isSymbolicLink()) return [];
|
|
38
|
+
const want = fableOn ? "fable" : "opus";
|
|
39
|
+
const changed = [];
|
|
40
|
+
for (const name of readdirSync(dir).sort()) {
|
|
41
|
+
if (!name.endsWith(".md")) continue;
|
|
42
|
+
const path = join(dir, name);
|
|
43
|
+
let text;
|
|
44
|
+
try {
|
|
45
|
+
text = readFileSync(path, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const front = FRONTMATTER.exec(text);
|
|
50
|
+
if (!front || !/^preferredModel:\s*fable\s*$/m.test(front[1])) continue;
|
|
51
|
+
const nextFront = front[1].replace(/^model:.*$/m, `model: ${want}`);
|
|
52
|
+
if (nextFront === front[1]) continue;
|
|
53
|
+
const next = text.replace(front[1], nextFront);
|
|
54
|
+
if (lstatSync(path).isSymbolicLink()) rmSync(path);
|
|
55
|
+
writeFileSync(path, next);
|
|
56
|
+
changed.push(name);
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} prefsPath
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function fableEnabled(prefsPath) {
|
|
66
|
+
try {
|
|
67
|
+
const prefs = JSON.parse(readFileSync(prefsPath, "utf8"));
|
|
68
|
+
return prefs?.global?.modelFallback?.fableEnabled === true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function arg(argv, flag) {
|
|
75
|
+
const i = argv.indexOf(flag);
|
|
76
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
80
|
+
if (invokedDirectly) {
|
|
81
|
+
const argv = process.argv.slice(2);
|
|
82
|
+
const claude = join(homedir(), ".claude");
|
|
83
|
+
const dir = arg(argv, "--dir") ?? join(claude, "agents");
|
|
84
|
+
const prefs =
|
|
85
|
+
arg(argv, "--prefs") ??
|
|
86
|
+
(process.env.MULTI_AGENT_PREFS || undefined) ??
|
|
87
|
+
join(claude, "multi-agent-preferences.json");
|
|
88
|
+
const on = argv.includes("--fable-on") || fableEnabled(prefs);
|
|
89
|
+
const changed = applyFableRung(dir, on);
|
|
90
|
+
if (changed.length) {
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
`agents on ${on ? "fable" : "opus"}: ${changed.map((n) => n.replace(/\.md$/, "")).join(", ")}\n`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
# decides WHAT to write can be tested without a network.
|
|
11
11
|
#
|
|
12
12
|
# `jira-publish.sh` cannot do this: it writes a comment or a description on an
|
|
13
|
-
# issue that already exists.
|
|
14
|
-
#
|
|
13
|
+
# issue that already exists. This file is the one place that creates issues, so
|
|
14
|
+
# the gate, the ledger and the label search apply to every creation.
|
|
15
15
|
#
|
|
16
16
|
# THE WRITE IS LEDGERED, AND THE LEDGER IS THE POINT
|
|
17
17
|
#
|
|
@@ -32,11 +32,10 @@
|
|
|
32
32
|
# AN EXISTING NODE IS SKIPPED, NEVER UPDATED
|
|
33
33
|
#
|
|
34
34
|
# Jira has no backup path for fields other than description. Rewriting a body an
|
|
35
|
-
# engineer has since edited would
|
|
36
|
-
# jira-publish.sh take backups in the first place.
|
|
35
|
+
# engineer has since edited would lose that edit with no way back.
|
|
37
36
|
#
|
|
38
37
|
# Usage:
|
|
39
|
-
# analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE]
|
|
38
|
+
# analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE] [--epic KEY]
|
|
40
39
|
#
|
|
41
40
|
# Exit: 0 written (or previewed), 3 the plan does not parse, 4 auth, 5 a Jira
|
|
42
41
|
# call failed, 64 usage.
|
|
@@ -47,14 +46,15 @@ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
47
46
|
# shellcheck source=/dev/null
|
|
48
47
|
. "$SELF_DIR/_jira-auth.sh"
|
|
49
48
|
|
|
50
|
-
PLAN=""; PROJECT=""; DRY=0; LEDGER=""
|
|
49
|
+
PLAN=""; PROJECT=""; DRY=0; LEDGER=""; EPIC=""
|
|
51
50
|
while [ $# -gt 0 ]; do
|
|
52
51
|
case "$1" in
|
|
53
52
|
--plan) PLAN="${2:-}"; shift 2 ;;
|
|
54
53
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
55
54
|
--ledger) LEDGER="${2:-}"; shift 2 ;;
|
|
56
55
|
--dry-run) DRY=1; shift ;;
|
|
57
|
-
|
|
56
|
+
--epic) EPIC="${2:-}"; shift 2 ;;
|
|
57
|
+
*) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE] [--epic KEY]" >&2; exit 64 ;;
|
|
58
58
|
esac
|
|
59
59
|
done
|
|
60
60
|
[ -n "$PLAN" ] && [ -f "$PLAN" ] || { echo "ERR: --plan FILE is required and must exist" >&2; exit 64; }
|
|
@@ -90,7 +90,7 @@ fi
|
|
|
90
90
|
ORPHANS=0
|
|
91
91
|
if [ -f "$LEDGER" ]; then
|
|
92
92
|
ORPHANS=$(jq -rs '[.[] | select(.event=="intent")] as $i
|
|
93
|
-
| [.[] | select(.event=="created") | .label] as $c
|
|
93
|
+
| [.[] | select(.event=="created" or .event=="exists") | .label] as $c
|
|
94
94
|
| [$i[] | select(.label as $l | ($c | index($l)) | not)] | length' \
|
|
95
95
|
"$LEDGER" 2>/dev/null || echo 0)
|
|
96
96
|
if [ "${ORPHANS:-0}" -gt 0 ]; then
|
|
@@ -109,7 +109,7 @@ existing_for() { # existing_for <label> -> prints the key, or empty
|
|
|
109
109
|
printf ''
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
LABELS="$(jq -r '[.nodes[] | .label, (.subtasks[]?.label)] | join(",")' "$PLAN")"
|
|
112
|
+
LABELS="$(jq -r '[.nodes[] | .label, (.clones[]?.label), (.subtasks[]?.label)] | join(",")' "$PLAN")"
|
|
113
113
|
if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
|
|
114
114
|
JQL="project=${PROJECT} AND labels in (${LABELS})"
|
|
115
115
|
ENC="$(printf '%s' "$JQL" | jq -sRr @uri)"
|
|
@@ -122,13 +122,11 @@ if [ "$DRY" -eq 0 ] && [ -n "$LABELS" ]; then
|
|
|
122
122
|
fi
|
|
123
123
|
|
|
124
124
|
# Two channels on purpose. The human line goes to stdout; the created key comes
|
|
125
|
-
# back in CREATED_KEY.
|
|
126
|
-
#
|
|
127
|
-
# and nothing else, and the tree looked empty.
|
|
125
|
+
# back in CREATED_KEY. Keeping the key off stdout lets a caller capture the
|
|
126
|
+
# report with command substitution without losing any of it.
|
|
128
127
|
# Same gate, same five candidate paths, same refusal as jira-publish.sh and
|
|
129
|
-
# post-pr-review.sh.
|
|
130
|
-
#
|
|
131
|
-
# writer the gate did not cover.
|
|
128
|
+
# post-pr-review.sh. A summary plus a description is outbound text like any
|
|
129
|
+
# other, so every issue this file creates passes the gate first.
|
|
132
130
|
ma_outbound_gate_text() {
|
|
133
131
|
local text="$1" og="" tmp rc
|
|
134
132
|
for c in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/outbound-gate.mjs" \
|
|
@@ -149,25 +147,52 @@ ma_outbound_gate_text() {
|
|
|
149
147
|
return $rc
|
|
150
148
|
}
|
|
151
149
|
|
|
150
|
+
link_issues() { # link_issues <type> <fromKey> <toKey> <cloneLabel>
|
|
151
|
+
local ltype="$1" from="$2" to="$3" label="$4" linked
|
|
152
|
+
if [ "$DRY" -eq 1 ]; then
|
|
153
|
+
echo " link $ltype ${from:-<story>} <-> ${to:-<clone>}"
|
|
154
|
+
return 0
|
|
155
|
+
fi
|
|
156
|
+
# A link already there is left alone, so a re-run never doubles it.
|
|
157
|
+
linked="$(jira_api GET "/rest/api/2/issue/${from}?fields=issuelinks" 2>/dev/null \
|
|
158
|
+
| jq -r --arg t "$to" '[.fields.issuelinks[]? | (.outwardIssue.key // .inwardIssue.key)] | index($t) != null' 2>/dev/null || echo false)"
|
|
159
|
+
if [ "$linked" = "true" ]; then
|
|
160
|
+
echo " linked $from <-> $to"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
if ! printf '%s' "$(jq -cn --arg t "$ltype" --arg a "$from" --arg b "$to" \
|
|
164
|
+
'{type: {name: $t}, inwardIssue: {key: $a}, outwardIssue: {key: $b}}')" \
|
|
165
|
+
| jira_api POST "/rest/api/2/issueLink" --data @- >/dev/null; then
|
|
166
|
+
echo "ERR: could not link $from to $to" >&2
|
|
167
|
+
return 5
|
|
168
|
+
fi
|
|
169
|
+
ledger link "$label" "$to"
|
|
170
|
+
echo " linked $from <-> $to"
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
CREATED_KEY=""
|
|
153
|
-
create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description>
|
|
154
|
-
local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" body key existing
|
|
174
|
+
create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description> [extraFieldsJson]
|
|
175
|
+
local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" extra="${6:-}" body key existing
|
|
176
|
+
[ -n "$extra" ] || extra="{}"
|
|
155
177
|
CREATED_KEY=""
|
|
156
178
|
existing="$(existing_for "$label")"
|
|
157
179
|
if [ -n "$existing" ]; then
|
|
158
180
|
# Skipped, never updated: Jira has no backup path for fields other than
|
|
159
181
|
# description, and an engineer may have edited this body since.
|
|
160
182
|
echo " exists $existing $summary"
|
|
183
|
+
ledger exists "$label" "$existing"
|
|
161
184
|
CREATED_KEY="$existing"
|
|
162
185
|
return 0
|
|
163
186
|
fi
|
|
164
187
|
body="$(jq -n --arg p "$PROJECT" --arg s "$summary" --arg t "$itype" \
|
|
165
|
-
--arg l "$label" --arg d "$desc" --arg par "$parent" '
|
|
188
|
+
--arg l "$label" --arg d "$desc" --arg par "$parent" --argjson x "$extra" '
|
|
166
189
|
{fields: ({project: {key: $p}, summary: $s, issuetype: {name: $t},
|
|
167
190
|
labels: [$l], description: $d}
|
|
168
|
-
+ (if $par == "" then {} else {parent: {key: $par}} end)
|
|
191
|
+
+ (if $par == "" then {} else {parent: {key: $par}} end)
|
|
192
|
+
+ $x)}')"
|
|
169
193
|
if [ "$DRY" -eq 1 ]; then
|
|
170
194
|
echo " create $itype $summary [$label]"
|
|
195
|
+
[ "$parent" = "" ] && printf '%s\n' "$desc" | sed 's/^/ /'
|
|
171
196
|
return 0
|
|
172
197
|
fi
|
|
173
198
|
# After the dry-run branch, because a dry run publishes nothing, and before
|
|
@@ -196,6 +221,24 @@ echo "analysis-jira-write: ${PROJECT}, coverage ${VERDICT}$([ "$DRY" -eq 1 ] &&
|
|
|
196
221
|
[ "$VERDICT" = "unverifiable" ] && \
|
|
197
222
|
echo " NOTE: coverage could not be checked for this document; the tree is unverified."
|
|
198
223
|
|
|
224
|
+
# The epic the stories join, if one was named. The link field depends on the
|
|
225
|
+
# deployment, so it is resolved once, against the live site, before any write.
|
|
226
|
+
STORY_EXTRA="{}"
|
|
227
|
+
if [ -n "${EPIC:-}" ]; then
|
|
228
|
+
echo " epic $EPIC"
|
|
229
|
+
if [ "$DRY" -eq 0 ]; then
|
|
230
|
+
. "$(dirname "${BASH_SOURCE[0]}")/jira-epic-link.sh"
|
|
231
|
+
E_MODE="$(jq -r '.prefsUsed.epicLinkMode // "auto"' "$PLAN")"
|
|
232
|
+
E_FIELD="$(jq -r '.prefsUsed.epicLinkFieldId // empty' "$PLAN")"
|
|
233
|
+
E_SERVER="" E_FIELDS=""
|
|
234
|
+
if [ "$E_MODE" = "auto" ]; then
|
|
235
|
+
E_SERVER="$(jira_api GET "/rest/api/2/serverInfo" 2>/dev/null || echo "")"
|
|
236
|
+
[ -n "$E_FIELD" ] || E_FIELDS="$(jira_api GET "/rest/api/2/field" 2>/dev/null || echo "")"
|
|
237
|
+
fi
|
|
238
|
+
STORY_EXTRA="$(epic_link_fields "$E_MODE" "$EPIC" "$E_FIELD" "$E_SERVER" "$E_FIELDS")"
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
|
|
199
242
|
RC=0
|
|
200
243
|
NODES="$(jq -c '.nodes[]' "$PLAN")"
|
|
201
244
|
while IFS= read -r node; do
|
|
@@ -204,18 +247,27 @@ while IFS= read -r node; do
|
|
|
204
247
|
s_title="$(printf '%s' "$node" | jq -r '.title')"
|
|
205
248
|
s_src="$(printf '%s' "$node" | jq -r '.sourceIds | join(", ")')"
|
|
206
249
|
s_type="$(jq -r '.prefsUsed.storyIssueType // "Story"' "$PLAN")"
|
|
207
|
-
s_desc="
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
# parentless issues
|
|
213
|
-
# an error.
|
|
250
|
+
s_desc="$(printf '%s' "$node" | jq -r '.description // empty')"
|
|
251
|
+
[ -n "$s_desc" ] || s_desc="Sources: ${s_src:-none (derived from the section heading)}"
|
|
252
|
+
if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc" "$STORY_EXTRA"; then
|
|
253
|
+
# A failed story does not get its sub-tasks or clones written. `create_issue`
|
|
254
|
+
# omits the parent field when the parent key is empty, so they would land as
|
|
255
|
+
# live, parentless issues from a run that has already reported an error.
|
|
214
256
|
RC=5
|
|
215
257
|
echo " skipped this story's sub-tasks: the story itself was not created" >&2
|
|
216
258
|
continue
|
|
217
259
|
fi
|
|
218
260
|
skey="$CREATED_KEY"
|
|
261
|
+
LINK_TYPE="$(jq -r '.prefsUsed.cloneLinkType // "Relates"' "$PLAN")"
|
|
262
|
+
while IFS= read -r clone; do
|
|
263
|
+
[ -n "$clone" ] || continue
|
|
264
|
+
c_label="$(printf '%s' "$clone" | jq -r '.label')"
|
|
265
|
+
c_title="$(printf '%s' "$clone" | jq -r '.title')"
|
|
266
|
+
c_extra="$(printf '%s' "$clone" | jq -c --argjson e "$STORY_EXTRA" \
|
|
267
|
+
'$e + {components: [.components[] | {name: .}]}')"
|
|
268
|
+
create_issue "$c_label" "$c_title" "$s_type" "" "$s_desc" "$c_extra" || { RC=5; continue; }
|
|
269
|
+
link_issues "$LINK_TYPE" "$skey" "$CREATED_KEY" "$c_label" || RC=5
|
|
270
|
+
done < <(printf '%s' "$node" | jq -c '.clones[]?')
|
|
219
271
|
while IFS= read -r sub; do
|
|
220
272
|
[ -n "$sub" ] || continue
|
|
221
273
|
t_label="$(printf '%s' "$sub" | jq -r '.label')"
|
|
@@ -228,7 +280,9 @@ while IFS= read -r node; do
|
|
|
228
280
|
| jq -r '[.projects[]?.issuetypes[]? | select(.subtask==true) | .name] | first // empty' 2>/dev/null || echo "")"
|
|
229
281
|
fi
|
|
230
282
|
[ -n "$t_type" ] || t_type="Sub-task"
|
|
231
|
-
|
|
283
|
+
t_desc="$(printf '%s' "$sub" | jq -r '.description // empty')"
|
|
284
|
+
[ -n "$t_desc" ] || t_desc="Sources: ${s_src:-none (derived from the section heading)}"
|
|
285
|
+
create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$t_desc" || RC=5
|
|
232
286
|
done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
|
|
233
287
|
done <<< "$NODES"
|
|
234
288
|
|