@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +1 -1
  3. package/README.tr.md +1 -1
  4. package/docs/facts.json +2 -2
  5. package/install/_common.mjs +1 -1
  6. package/install/_plugin-skills.mjs +13 -9
  7. package/install/catalog-history.json +1 -1
  8. package/install/claude.mjs +9 -0
  9. package/install/codex.mjs +12 -12
  10. package/manifest.json +74 -67
  11. package/package.json +5 -5
  12. package/pipeline/agents/code-reviewer.md +1 -1
  13. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  14. package/pipeline/commands/multi-agent/analysis/SKILL.md +7 -3
  15. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +57 -7
  16. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -0
  18. package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +1 -0
  19. package/pipeline/commands/multi-agent/doctor/SKILL.md +6 -0
  20. package/pipeline/commands/multi-agent/feedback/SKILL.md +2 -2
  21. package/pipeline/commands/multi-agent/forget/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/model/SKILL.md +7 -0
  25. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  26. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +1 -0
  27. package/pipeline/commands/multi-agent/review/SKILL.md +5 -0
  28. package/pipeline/commands/multi-agent/review-analysis/SKILL.md +7 -2
  29. package/pipeline/commands/multi-agent/sync/SKILL.md +5 -6
  30. package/pipeline/lib/agent-model-sync.mjs +95 -0
  31. package/pipeline/lib/analysis-jira-write.sh +81 -27
  32. package/pipeline/lib/analysis-quality.mjs +261 -0
  33. package/pipeline/lib/analysis-sections.mjs +105 -0
  34. package/pipeline/lib/jira-epic-link.sh +54 -0
  35. package/pipeline/lib/model-rung.sh +7 -0
  36. package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
  37. package/pipeline/multi-agent-refs/analysis/redesign.md +3 -1
  38. package/pipeline/multi-agent-refs/analysis/render.md +20 -21
  39. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -1
  40. package/pipeline/multi-agent-refs/analysis/review.md +16 -3
  41. package/pipeline/multi-agent-refs/analysis/synthesis.md +10 -2
  42. package/pipeline/multi-agent-refs/analysis-template-corporate.md +44 -9
  43. package/pipeline/multi-agent-refs/analysis-template.md +21 -14
  44. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -0
  45. package/pipeline/multi-agent-refs/features/analysis-jira.md +69 -19
  46. package/pipeline/multi-agent-refs/features/doctor.md +10 -0
  47. package/pipeline/multi-agent-refs/features/model-fallback.md +10 -0
  48. package/pipeline/multi-agent-refs/features/phone-api.md +1 -1
  49. package/pipeline/multi-agent-refs/features/unattended-security.md +2 -2
  50. package/pipeline/multi-agent-refs/features/usage-reporting.md +13 -0
  51. package/pipeline/schemas/analysis-spec.schema.json +22 -0
  52. package/pipeline/schemas/prefs.schema.json +22 -1
  53. package/pipeline/scripts/analysis-story-body.mjs +210 -0
  54. package/pipeline/scripts/analysis-story-tree.mjs +73 -12
  55. package/pipeline/scripts/analysis-tickets-writeback.mjs +101 -0
  56. package/pipeline/scripts/build-references.mjs +14 -1
  57. package/pipeline/scripts/confluence-readback.mjs +148 -0
  58. package/pipeline/scripts/doctor.mjs +48 -0
  59. package/pipeline/scripts/jira-wiki-escape.mjs +40 -0
  60. package/pipeline/scripts/migrate-prefs.mjs +14 -0
  61. package/pipeline/scripts/unattended_policy.py +12 -5
  62. package/pipeline/scripts/usage-report.mjs +84 -9
  63. package/pipeline/scripts/validate-analysis-doc.mjs +120 -70
  64. package/pipeline/skills/.skill-manifest.json +8 -8
  65. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -1
  66. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +56 -6
  67. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +6 -0
  68. package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +6 -0
  69. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -0
  70. package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +5 -0
  71. 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 (numbering may have re-flowed per the omission rule - match by title, not by the literal number 20).
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 re-flow |
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
- - **Localized picker descriptions are an installed-tree artifact.** When
138
- `outputLanguage=tr`, the installed `description:` values are Turkish and a
139
- `description-en:` sidecar holds the English original (see
140
- `localize-commands.mjs`). The repo must ALWAYS receive the English form:
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. The only creation path in this repo was a curl
14
- # hand-written inside a markdown instruction, which is the thing this replaces.
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 repeat, at tree scale, the defect that made
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
- *) echo "usage: analysis-jira-write.sh --plan FILE --project KEY [--dry-run] [--ledger FILE]" >&2; exit 64 ;;
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. 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.
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. This file is the only ISSUE-CREATION path in the tree, and
130
- # a summary plus a description is outbound text like any other - it was the one
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="Sources: ${s_src:-none (derived from the section heading)}"
208
- if ! create_issue "$s_label" "$s_title" "$s_type" "" "$s_desc"; then
209
- # A failed story does not get its sub-tasks written anyway. `create_issue`
210
- # omits the parent field when the parent key is empty, so without this the
211
- # sub-tasks of a story Jira refused were still POSTed - and landed as live,
212
- # parentless issues nobody asked for, from a run that had already reported
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
- create_issue "$t_label" "${s_title} - ${t_role}" "$t_type" "$skey" "$s_desc" || RC=5
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