@mmerterden/multi-agent-pipeline 20.23.2 → 20.25.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 +46 -0
- package/README.md +8 -6
- package/README.tr.md +7 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +7 -6
- package/docs/features.md +9 -4
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/install/templates/codex-hooks.json +18 -0
- package/manifest.json +126 -98
- package/package.json +1 -1
- package/pipeline/agents/task-clarifier.md +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
- package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/lib/analysis-jira-write.sh +70 -0
- package/pipeline/lib/credential-inventory.sh +5 -5
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/fetch-crashlytics.sh +6 -8
- package/pipeline/lib/model-rung.sh +1 -1
- package/pipeline/lib/plan-todos.sh +177 -37
- package/pipeline/lib/post-pr-review.sh +4 -6
- package/pipeline/lib/route-state.sh +1 -1
- package/pipeline/lib/shadow-git.sh +4 -5
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
- package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
- package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/help/en.md +2 -0
- package/pipeline/multi-agent-refs/help/tr.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +36 -1
- package/pipeline/schemas/phases.json +4 -0
- package/pipeline/schemas/plan-todos.schema.json +104 -3
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +122 -3
- package/pipeline/scripts/_code-graph.mjs +2 -2
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/_transcript-hosts.mjs +406 -0
- package/pipeline/scripts/analysis-story-tree.mjs +153 -8
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/build-stack-plugins.mjs +3 -3
- package/pipeline/scripts/capture-flush.sh +7 -7
- package/pipeline/scripts/code-graph-rules/android.json +1 -1
- package/pipeline/scripts/cost-analyze.mjs +32 -43
- package/pipeline/scripts/cost-table.json +35 -19
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/dep-bump.mjs +1556 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
- package/pipeline/scripts/edit-syntax-gate.sh +32 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/gc-abandoned.sh +2 -2
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
- package/pipeline/scripts/git-path.mjs +1 -2
- package/pipeline/scripts/host-hooks.mjs +265 -0
- package/pipeline/scripts/jira-attach.sh +1 -1
- package/pipeline/scripts/keychain.py +3 -3
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/output-quality-check.sh +1 -1
- package/pipeline/scripts/phase-tracker.sh +5 -6
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/pre-push-check.sh +3 -6
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +1131 -0
- package/pipeline/scripts/uninstall.mjs +90 -4
- package/pipeline/scripts/update-issue-progress.sh +4 -6
- package/pipeline/scripts/usage-report.mjs +2 -2
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +20 -12
- package/pipeline/skills/.skills-index.json +23 -1
- package/pipeline/skills/shared/README.md +5 -3
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/skills-index.md +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.25.0",
|
|
4
4
|
"description": "6-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -116,7 +116,7 @@ pipeline/scripts/log-metric.sh "$TASK_ID" 0 clarify.call \
|
|
|
116
116
|
tokens_in=$TI tokens_out=$TO
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Cost expectation on Haiku (input ~1.5k tokens issue body, output ~400 tokens JSON): **~$0.
|
|
119
|
+
Cost expectation on Haiku (input ~1.5k tokens issue body, output ~400 tokens JSON): **~$0.0004 per call**. Off by default; flip on for teams burned by ambiguity-driven rework.
|
|
120
120
|
|
|
121
121
|
## Pattern citation
|
|
122
122
|
|
|
@@ -29,6 +29,7 @@ Contract, severities and reasoning:
|
|
|
29
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
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
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
|
+
- A requirement row with `Depends on: <ids>` links the stories, prerequisite first, as `issueTree.dependencyLinkType` (default Blocks). A cycle, or a prerequisite no story covers, makes the plan `incomplete`; fix the document, not the links.
|
|
32
33
|
- 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
34
|
|
|
34
35
|
## Phase 1 - Plan, offline
|
|
@@ -64,6 +65,7 @@ what makes a preview meaningful:
|
|
|
64
65
|
- the coverage verdict **on its own line**, and the consistency verdict below it
|
|
65
66
|
- every field beside the pref key it came from, so a wrong setting shows here
|
|
66
67
|
rather than in Jira afterwards
|
|
68
|
+
- the dependency edges between stories, prerequisite first
|
|
67
69
|
- the write count
|
|
68
70
|
- every story's full description, as it will be written, with its body
|
|
69
71
|
warnings; `--brief` gives the one-line-per-node view
|
|
@@ -106,7 +108,7 @@ A run that could not be checked is allowed. One that looks checked when it was
|
|
|
106
108
|
not is the defect, so the approval has to name it and a plain "Approve" must not
|
|
107
109
|
be reachable.
|
|
108
110
|
|
|
109
|
-
`incomplete` prints its uncovered and invented ids before the question. An
|
|
111
|
+
`incomplete` prints its uncovered and invented ids, and any dependency cycle or uncovered prerequisite, before the question. An
|
|
110
112
|
invented id means the plan cites something the document does not define - treat
|
|
111
113
|
that as a defect in the plan, not a warning to click past.
|
|
112
114
|
|
|
@@ -156,7 +158,7 @@ missing-attachment warning.
|
|
|
156
158
|
|
|
157
159
|
## Phase 5 - Report
|
|
158
160
|
|
|
159
|
-
Keys created, keys skipped, the epic linked (or none), the coverage verdict, and the ledger path. If the
|
|
161
|
+
Keys created, keys skipped, the epic linked (or none), the dependency links written, the coverage verdict, and the ledger path. If the
|
|
160
162
|
verdict was `unverifiable`, say that in the report too - not only at approval
|
|
161
163
|
time, because the report is what gets pasted elsewhere.
|
|
162
164
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Bump one dependency, fix only what the bump broke, review, open a PR; rolls back past the fix round cap. SwiftPM, Gradle, npm, pip. Use when a library needs updating."
|
|
3
|
+
description-tr: "Tek bağımlılığı yükseltir, yalnızca yükseltmenin kırdığını düzeltir, review edip PR açar; düzeltme turu sınırında geri alır. SwiftPM, Gradle, npm, pip. Bir kütüphaneyi güncellemek için kullanılır."
|
|
4
|
+
argument-hint: "[package[@version]] [--major] - no package opens a picker over the outdated candidates; --major allows a breaking bump"
|
|
5
|
+
parameters: [{"name":"package","kind":"text"},{"name":"--major","kind":"flag"}]
|
|
6
|
+
gui: form
|
|
7
|
+
destructive: false
|
|
8
|
+
confirm: none
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# multi-agent deps - bump one dependency and fix what it broke
|
|
12
|
+
|
|
13
|
+
> **Pickers follow** `$HOME/.claude/multi-agent-refs/picker-contract.md`: never a one-option call, and branch on the option selected, not on its text.
|
|
14
|
+
|
|
15
|
+
**Input**: $ARGUMENTS
|
|
16
|
+
|
|
17
|
+
> **Language (read FIRST)**: read `prefs.global.outputLanguage` and render every conversational line in it. `AskUserQuestion` renders `question`, option `label`s and `description`s in `outputLanguage`; only `header` stays English (<=12-char chip). Commit messages and branch names stay English. Full contract: `$HOME/.claude/multi-agent-refs/rules.md` "Language Application".
|
|
18
|
+
|
|
19
|
+
The normal pipeline with a fixed task. Read `$HOME/.claude/multi-agent-refs/features/dep-bump.md` first: it is the contract for every step below (run shape, scope rule, rollback). `dep-bump.mjs` does every deterministic step; never edit a version string or a lockfile by hand.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Repo and settings.** Resolve the repo as `/multi-agent` does for a free-text task (`_account-picker.md`, `_repo-picker.md`). Then:
|
|
24
|
+
```bash
|
|
25
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" config # depBump.maxFixRounds, depBump.queryRegistry
|
|
26
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" detect --dir "$REPO"
|
|
27
|
+
```
|
|
28
|
+
No ecosystem detected: say so and stop.
|
|
29
|
+
|
|
30
|
+
2. **Candidates.**
|
|
31
|
+
```bash
|
|
32
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" list --dir "$REPO" [--registry] # --registry when queryRegistry is true
|
|
33
|
+
```
|
|
34
|
+
Show `registry.note` and every per-package `note`: an offline listing is reported as offline, never as "nothing outdated".
|
|
35
|
+
- `package@version` given: use it. `package` alone: its `latestInMajor`, else `latest`. Not in the list: stop with the manifests that were read.
|
|
36
|
+
- No package: a single-select picker over the candidates with a newer version, non-breaking first, each option `name current -> target (risk)`. Two or more options per the picker contract; one candidate is confirmed with a yes/no, none means report "nothing newer found" with the registry note.
|
|
37
|
+
|
|
38
|
+
3. **Major gate.** Risk `major` or `unknown`, or a prerelease target (`prerelease: true`), needs `--major` in the input or an explicit confirmation naming the versions (`Bump anyway` / `Take {latestInMajor} instead` when one exists / `Cancel`). An autopilot run without `--major` takes `latestInMajor` or stops.
|
|
39
|
+
|
|
40
|
+
4. **Run the pipeline** with `state.mode = "deps"`, `state.taskType = "chore"`, the free-text task `chore(deps): bump {name} from {current} to {target}` and the branch `chore/deps-{name}-{target}` (kebab rules from Phase 0 Step 4). Phase 0 intake, worktree and tracker run as in `/multi-agent`. Then:
|
|
41
|
+
- **Phase 1**: the fixed plan from the feature doc, with the changelog excerpt as evidence:
|
|
42
|
+
```bash
|
|
43
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" notes --dir "$WORKTREE" --ecosystem E --name N --from A --to B
|
|
44
|
+
```
|
|
45
|
+
- **Phase 2**: apply first, then the exit gate as written:
|
|
46
|
+
```bash
|
|
47
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" apply --dir "$WORKTREE" --ecosystem E --name N --to B [--allow-major]
|
|
48
|
+
```
|
|
49
|
+
Exit 1: report `error` and `lock.output` (or `dirty`: the worktree had uncommitted changes), stop with nothing to commit. Persist the output to `state.depBump`. Green: no dev agent, on to Phase 3. Red: fix rounds, at most `depBump.maxFixRounds`, each dispatched with the failing log tail, `surface` output, the changelog excerpt and the scope rule (feature doc section 4); after each round:
|
|
50
|
+
```bash
|
|
51
|
+
node "$HOME/.claude/scripts/dep-bump.mjs" scope --dir "$WORKTREE" --log "$GATE_LOG" # the round's build/test log
|
|
52
|
+
```
|
|
53
|
+
Only the surface recorded by `apply` is in scope. A file listed in `newImporters` with `cited: null` started importing the dependency without an error line behind it: revert it before the next round.
|
|
54
|
+
- **Cap reached and still red** (or still out of scope): `dep-bump.mjs rollback --dir "$WORKTREE"` (restores the manifest, lockfile and fix files; anything under `kept` is reported, not discarded), set `state.depBump.rolledBack = true`, and end with the report from feature doc section 5. No commit, no push, no PR.
|
|
55
|
+
- **Phase 3**: standard review. Each `outOfScope` file without a cited error line, and each `newImporters` entry with `cited: null`, is a blocking finding.
|
|
56
|
+
- **Phase 4**: commit with `commitMessage` from `apply` (plus ` [{jiraId}]` when present). PR body per the feature doc; `Ref:` or `Related:`, never `Closes`, `Fixes` or `Resolves`, and never close an issue.
|
|
57
|
+
- **Phase 5**: channels as in every mode.
|
|
58
|
+
|
|
59
|
+
## Report
|
|
60
|
+
|
|
61
|
+
In `outputLanguage`, at most 10 lines: the dependency, from and to, risk, lockfile command and result, rounds taken against the cap, files changed (in scope / justified), review verdict, and the PR URL or the rollback reason.
|
|
@@ -52,8 +52,8 @@ because it does not mean the same thing on all three:
|
|
|
52
52
|
|
|
53
53
|
| Host | What `off` does | Why |
|
|
54
54
|
|---|---|---|
|
|
55
|
-
| Claude Code | every `preferredModel: fable` persona (architects, reviewer 1, triage) starts on `opus` | the only host where the rung is Fable 5 |
|
|
56
|
-
| Copilot CLI | nothing | Fable
|
|
55
|
+
| Claude Code | every `preferredModel: fable` persona (architects, reviewer 1, triage) starts on `opus` | the only host where the rung is Fable 5.1 |
|
|
56
|
+
| Copilot CLI | nothing | Fable is not offered there; its personas never sat on this rung |
|
|
57
57
|
| Codex CLI | nothing, deliberately | the `fable` rung there means `gpt-5.6 @ xhigh`, a different model on a different account - switching it from a knob named after an Anthropic model would surprise a Codex user |
|
|
58
58
|
|
|
59
59
|
So on two of the three hosts this command is a **status report**, not a switch,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Post-mortem of a run from its session transcripts: loops, stalls, root-cause hypotheses on file:line, fixes. Read-only, redacted. Use when a run went wrong or ran far too long."
|
|
3
|
+
description-tr: "Bir koşunun oturum transkriptlerinden post-mortem'i: ne oldu, nerede döngüye girdi veya takıldı, file:line'a dayalı kök-neden hipotezleri, önerilen düzeltmeler. Salt-okunur; gösterilmeden önce redakte edilir."
|
|
4
|
+
argument-hint: "[#id | transcript.jsonl] [--audit] - a run (task id or #N) or a session file; no argument means the most recent run. --audit re-checks the draft against the index"
|
|
5
|
+
parameters: [{"name":"target","kind":"text"},{"name":"--audit","kind":"flag"}]
|
|
6
|
+
gui: button
|
|
7
|
+
destructive: false
|
|
8
|
+
confirm: none
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# multi-agent postmortem - Run Post-Mortem
|
|
12
|
+
|
|
13
|
+
**Input**: $ARGUMENTS
|
|
14
|
+
|
|
15
|
+
Explains what a run actually did, read from the session transcripts it ran in,
|
|
16
|
+
not from what the run log says it decided. The evidence comes from a
|
|
17
|
+
deterministic indexer; the model only interprets it.
|
|
18
|
+
|
|
19
|
+
The indexer reads all three hosts and detects the format per file:
|
|
20
|
+
|
|
21
|
+
| Host | Session files |
|
|
22
|
+
|------|---------------|
|
|
23
|
+
| Claude Code | `~/.claude/projects/<dir>/<session>.jsonl` plus its subagent files |
|
|
24
|
+
| Copilot CLI | `~/.copilot/session-state/<session>/events.jsonl` (cited as `<first 8 of session>/events.jsonl`) |
|
|
25
|
+
| Codex | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` (cited as `rollout-<first 8 of session>.jsonl`) |
|
|
26
|
+
|
|
27
|
+
Copilot CLI reports tokens per session, so its turns carry output tokens only
|
|
28
|
+
and the totals come from the session's shutdown record; Codex reports them per
|
|
29
|
+
request.
|
|
30
|
+
|
|
31
|
+
**Read-only.** Nothing is sent anywhere: no issue comment, no PR, no channel.
|
|
32
|
+
The one write is the report itself, saved under the run's log directory.
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
1. **Resolve the target.**
|
|
37
|
+
- A path ending in `.jsonl` (any of the three hosts): index that file
|
|
38
|
+
(Claude Code subagent files beside it are included). There is no run directory, so nothing is saved unless the user
|
|
39
|
+
names one.
|
|
40
|
+
- A task id or `#N`: locate the run and its transcripts:
|
|
41
|
+
```bash
|
|
42
|
+
node "$HOME/.claude/scripts/transcript-index.mjs" --run "<id>" --locate
|
|
43
|
+
```
|
|
44
|
+
`matchedBy: "session"` means the run recorded its session id and the match
|
|
45
|
+
is exact. `"time-window"` means it did not (attended runs record none), so
|
|
46
|
+
the files are the sessions, on any host, that ran in the run's worktree or
|
|
47
|
+
checkout while it was active; say that this match is a heuristic. Exit 3 with a `reason`:
|
|
48
|
+
report the reason, and offer to take a transcript path instead.
|
|
49
|
+
- No argument: take the most recent run from
|
|
50
|
+
`node "$HOME/.claude/scripts/runs-index.mjs" --json` (latest `startedAt`)
|
|
51
|
+
and continue as above.
|
|
52
|
+
|
|
53
|
+
2. **Index.**
|
|
54
|
+
```bash
|
|
55
|
+
node "$HOME/.claude/scripts/transcript-index.mjs" --run "<id>" # or: <file.jsonl>
|
|
56
|
+
```
|
|
57
|
+
The output lists turns, loops (identical tool calls repeated), stalls (gaps
|
|
58
|
+
while a tool or the model was working), compactions, subagents with their
|
|
59
|
+
results, errors and denials, skill and command invocations, the slowest tool
|
|
60
|
+
calls and a timeline. Every item carries `file:line`, the line of the JSONL
|
|
61
|
+
it came from. Exit 4 means the redaction guard found a credential left in the
|
|
62
|
+
output and printed nothing: report that and stop; never fall back to reading
|
|
63
|
+
the transcript directly.
|
|
64
|
+
|
|
65
|
+
Read only the index. Do not open the transcript files: the index is the
|
|
66
|
+
redacted view, the files are not.
|
|
67
|
+
|
|
68
|
+
3. **Write the post-mortem** in `prefs.global.outputLanguage`, short:
|
|
69
|
+
- **What happened**: the run in five lines or fewer, from the turns and
|
|
70
|
+
timeline.
|
|
71
|
+
- **Where it looped or stalled**: each loop and stall that matters, with its
|
|
72
|
+
citations.
|
|
73
|
+
- **Root-cause hypotheses**: each one a hypothesis, not a verdict, and each
|
|
74
|
+
anchored to at least one `file:line` from the index. A hypothesis with no
|
|
75
|
+
citation is left out.
|
|
76
|
+
- **Suggested fixes**: concrete and tied to a hypothesis (a prompt rule, a
|
|
77
|
+
guard, a timeout, a tool to use instead).
|
|
78
|
+
- **Evidence limits**: the matching method (`session` / `time-window`),
|
|
79
|
+
malformed or truncated lines, and the redaction count the index reported.
|
|
80
|
+
|
|
81
|
+
Quote only excerpts the index already shows; they are redacted.
|
|
82
|
+
|
|
83
|
+
4. **Audit pass** (`--audit`): re-read the draft against the index. Every cited
|
|
84
|
+
`file:line` must exist in the index and say what the draft claims; every
|
|
85
|
+
count must match the index's counts. Correct or drop what does not hold, and
|
|
86
|
+
add one line: `audit: N claims checked, M corrected`.
|
|
87
|
+
|
|
88
|
+
5. **Save.** With a run directory, write the report to
|
|
89
|
+
`<runDir>/postmortem.md` (`runDir` from step 1, `~` expanded) and give the
|
|
90
|
+
path. That file is the only write this command makes.
|
|
@@ -247,14 +247,14 @@ off: Agent 1 is dropped, the panel is Agent 2 + Agent 3, triage runs on
|
|
|
247
247
|
Never dispatch a Fable reviewer or a Fable triage on the model list below alone.
|
|
248
248
|
|
|
249
249
|
**Claude Code (3 in parallel while the fable rung is on, 2 while it is off):**
|
|
250
|
-
- Agent 1: `claude-fable-5` → security + architecture (only when `REVIEW_RUNG` is `fable`)
|
|
251
|
-
- Agent 2: `claude-opus-5` → edge cases, alternate perspective
|
|
252
|
-
- Agent 3: `claude-sonnet-5` → general quality
|
|
250
|
+
- Agent 1: `claude-fable-5-1` → security + architecture (only when `REVIEW_RUNG` is `fable`)
|
|
251
|
+
- Agent 2: `claude-opus-5-5` → edge cases, alternate perspective
|
|
252
|
+
- Agent 3: `claude-sonnet-5-5` → general quality
|
|
253
253
|
|
|
254
254
|
**Copilot CLI (3 in parallel):**
|
|
255
|
-
- Agent 1: `claude-opus-
|
|
255
|
+
- Agent 1: `claude-opus-4.7` → security + architecture (Fable is not offered on Copilot CLI)
|
|
256
256
|
- Agent 2: `gpt-5.4` → edge cases, alternate perspective
|
|
257
|
-
- Agent 3: `claude-sonnet-
|
|
257
|
+
- Agent 3: `claude-sonnet-4.6` → general quality
|
|
258
258
|
|
|
259
259
|
**Codex CLI (3 in parallel, single vendor):**
|
|
260
260
|
- Agent 1: `gpt-5.6` @ `xhigh` → security + architecture
|
|
@@ -30,7 +30,7 @@ honoured only at `bulk-read` and `delegate` and only while `--free` is on.
|
|
|
30
30
|
|
|
31
31
|
**Rung names are the contract; model ids are not.** `opus` is a rung here and a
|
|
32
32
|
model id in `cost-table.json`, and the second can change without anyone editing
|
|
33
|
-
a rule. Writing `claude-opus-5` into a rule pins a decision to a string that will
|
|
33
|
+
a rule. Writing `claude-opus-5-5` into a rule pins a decision to a string that will
|
|
34
34
|
go stale.
|
|
35
35
|
|
|
36
36
|
Set rules with:
|
|
@@ -429,14 +429,16 @@ To set up multi-agent on a new machine:
|
|
|
429
429
|
|
|
430
430
|
All tokens are optional in the sense that every service can be answered with Skip - but the ASKING is not optional: the Step 3 sequential loop still walks every missing service one by one (token → author → host). Phase 0 re-asks at runtime only for tokens the user skipped here.
|
|
431
431
|
|
|
432
|
-
### Step 8 - Enforcement hooks (optional
|
|
432
|
+
### Step 8 - Enforcement hooks (optional)
|
|
433
433
|
|
|
434
|
-
|
|
434
|
+
On Claude Code, offer to merge `$HOME/.claude/templates/claude-hooks.json`: three blocking `PreToolUse` gates (secret scan, agent-guard, read-size) plus four non-blocking hooks (`PreCompact`, `SessionEnd`, `SessionStart`, `PostToolUse`). What each does: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
|
|
435
435
|
|
|
436
436
|
- Ask (picker): "Install the pipeline's hooks into `~/.claude/settings.json`?" Default Yes.
|
|
437
|
-
- On Yes, deep-merge EVERY event in the template's `hooks` object
|
|
438
|
-
- Say
|
|
439
|
-
- Say
|
|
437
|
+
- On Yes, deep-merge EVERY event in the template's `hooks` object (merging `PreToolUse` alone drops the capture hooks); keep existing hooks, never duplicate a matcher calling the same script.
|
|
438
|
+
- Say only the three gates are hookable (no run-specific arguments); the rest are phase-enforced.
|
|
439
|
+
- Say the read-size gate stays inert until `prefs.global.bulkRead.mode` is set; recommend `observe` first (why, and the Phase 3 exemption: `picker-contract.md`).
|
|
440
|
+
|
|
441
|
+
Codex: offer only the syntax gate, then run `node "$HOME/.codex/scripts/host-hooks.mjs" install --host codex`. Copilot CLI: offer no hook (it discards `postToolUse` output as of 1.0.45).
|
|
440
442
|
|
|
441
443
|
### Step 9 - Default stack plugin enablement
|
|
442
444
|
|
|
@@ -73,7 +73,7 @@ Run every step automatically:
|
|
|
73
73
|
```
|
|
74
74
|
Step 0: DOCTOR doctor.mjs - exit 2 or 4 stops the sync
|
|
75
75
|
Step 1.5: DETECT Compare timestamps, find stale targets
|
|
76
|
-
Step 2: COPILOT Claude Code -> Copilot CLI (instructions +
|
|
76
|
+
Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 66 sub-command skills)
|
|
77
77
|
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + sub-command specs as refs + agent TOML)
|
|
78
78
|
Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
|
|
79
79
|
Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
|
|
@@ -241,14 +241,14 @@ same specs as reference files rather than as peer skills, via Step 2b - see
|
|
|
241
241
|
|-------------|-------------|
|
|
242
242
|
| `~/.claude/commands/multi-agent/{cmd}/SKILL.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
|
|
243
243
|
|
|
244
|
-
**
|
|
244
|
+
**66 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
|
|
245
245
|
|
|
246
246
|
```
|
|
247
247
|
analysis, analysis-jira, analysis-resolve, autopilot, autopilot-off, autopilot-on,
|
|
248
248
|
autopilot-status, bug-bash, build-optimize, channels, complaint-analysis, create-jira,
|
|
249
|
-
design-check, diff-explain, doctor, estimate, feedback, forget, garbage-collect, graph, help,
|
|
249
|
+
deps, design-check, diff-explain, doctor, estimate, feedback, forget, garbage-collect, graph, help,
|
|
250
250
|
ios-coding-standard, issue, jira, kill, language, log,
|
|
251
|
-
manual-test, model, prune-logs, prune-prompts, purge, refactor, research, resume, review, review-analysis, review-issue, review-jira, route-off, route-on, route-status,
|
|
251
|
+
manual-test, model, postmortem, prune-logs, prune-prompts, purge, refactor, research, resume, review, review-analysis, review-issue, review-jira, route-off, route-on, route-status,
|
|
252
252
|
routines, save, scaffold, scan, scenario-audit, search, security-review, serve, setup, stack, status, steer, store-ready, sync, test,
|
|
253
253
|
test-accessibility, test-dark-mode, test-dynamic-type, test-screenshots,
|
|
254
254
|
testflight-validation, uninstall, update
|
|
@@ -11,10 +11,9 @@ allowed-tools: Agent, Bash, Read, Write, Edit, Glob, Grep, TaskCreate, TaskUpdat
|
|
|
11
11
|
|
|
12
12
|
# /multi-agent:testflight-validation - iOS alias for store-ready
|
|
13
13
|
|
|
14
|
-
This command is an alias. The implementation
|
|
14
|
+
This command is an alias. The implementation lives in
|
|
15
15
|
`/multi-agent:store-ready`, which runs the same three gates on iOS and adds the
|
|
16
|
-
Android side, so there is one flow to maintain instead of two that
|
|
17
|
-
started to drift.
|
|
16
|
+
Android side, so there is one flow to maintain instead of two that drift.
|
|
18
17
|
|
|
19
18
|
The name is kept because it is the one people reach for when the target is
|
|
20
19
|
TestFlight, and removing a command is a breaking change to the slash-command
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
# Jira has no backup path for fields other than description. Rewriting a body an
|
|
35
35
|
# engineer has since edited would lose that edit with no way back.
|
|
36
36
|
#
|
|
37
|
+
# A STORY DEPENDENCY IS A LINK, WRITTEN LAST
|
|
38
|
+
#
|
|
39
|
+
# A plan node's `dependsOn` names the stories it waits for. Once every story has
|
|
40
|
+
# a key, each edge becomes an issue link of `prefsUsed.dependencyLinkType`
|
|
41
|
+
# (default Blocks) from the prerequisite to the dependent, checked before it is
|
|
42
|
+
# sent so a re-run adds none.
|
|
43
|
+
#
|
|
37
44
|
# Usage:
|
|
38
45
|
# analysis-jira-write.sh --plan plan.json --project KEY [--dry-run] [--ledger FILE] [--epic KEY]
|
|
39
46
|
# analysis-jira-write.sh --subtask-types --project KEY the site's sub-task type names, one per line
|
|
@@ -202,6 +209,40 @@ link_issues() { # link_issues <type> <fromKey> <toKey> <cloneLabel>
|
|
|
202
209
|
echo " linked $from <-> $to"
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
# A dependency link: the prerequisite is the link's source (the POST's
|
|
213
|
+
# inwardIssue), so the site reads "<prerequisite> blocks <dependent>" for the
|
|
214
|
+
# default Blocks type, and lists the link on the prerequisite under outwardIssue.
|
|
215
|
+
# Only a link of this type in this direction counts as already there: a Relates
|
|
216
|
+
# link a person added between the same two stories is not the dependency.
|
|
217
|
+
link_dependency() { # link_dependency <type> <prerequisiteKey> <dependentKey> <dependentLabel>
|
|
218
|
+
local ltype="$1" pre="$2" dep="$3" label="$4" linked
|
|
219
|
+
linked="$(jira_api GET "/rest/api/2/issue/${pre}?fields=issuelinks" 2>/dev/null \
|
|
220
|
+
| jq -r --arg t "$ltype" --arg d "$dep" \
|
|
221
|
+
'[.fields.issuelinks[]? | select(.type.name == $t and .outwardIssue.key == $d)] | length > 0' \
|
|
222
|
+
2>/dev/null || echo false)"
|
|
223
|
+
if [ "$linked" = "true" ]; then
|
|
224
|
+
echo " linked $pre -> $dep ($ltype)"
|
|
225
|
+
return 0
|
|
226
|
+
fi
|
|
227
|
+
if ! printf '%s' "$(jq -cn --arg t "$ltype" --arg a "$pre" --arg b "$dep" \
|
|
228
|
+
'{type: {name: $t}, inwardIssue: {key: $a}, outwardIssue: {key: $b}}')" \
|
|
229
|
+
| jira_api POST "/rest/api/2/issueLink" --data @- >/dev/null; then
|
|
230
|
+
echo "ERR: could not link $pre to $dep" >&2
|
|
231
|
+
return 5
|
|
232
|
+
fi
|
|
233
|
+
ledger dependency "$label" "$pre"
|
|
234
|
+
echo " linked $pre -> $dep ($ltype)"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
declare -a STORY_KEYS=()
|
|
238
|
+
story_key() { # story_key <label> -> prints the key this run created or found, or empty
|
|
239
|
+
local i
|
|
240
|
+
for i in "${STORY_KEYS[@]:-}"; do
|
|
241
|
+
case "$i" in "$1="*) printf '%s' "${i#*=}"; return 0 ;; esac
|
|
242
|
+
done
|
|
243
|
+
printf ''
|
|
244
|
+
}
|
|
245
|
+
|
|
205
246
|
CREATED_KEY=""
|
|
206
247
|
create_issue() { # create_issue <label> <summary> <issuetype> <parentKey|""> <description> [extraFieldsJson]
|
|
207
248
|
local label="$1" summary="$2" itype="$3" parent="$4" desc="$5" extra="${6:-}" body key existing
|
|
@@ -290,6 +331,7 @@ while IFS= read -r node; do
|
|
|
290
331
|
continue
|
|
291
332
|
fi
|
|
292
333
|
skey="$CREATED_KEY"
|
|
334
|
+
[ -n "$skey" ] && STORY_KEYS+=("$s_label=$skey")
|
|
293
335
|
LINK_TYPE="$(jq -r '.prefsUsed.cloneLinkType // "Relates"' "$PLAN")"
|
|
294
336
|
while IFS= read -r clone; do
|
|
295
337
|
[ -n "$clone" ] || continue
|
|
@@ -319,4 +361,32 @@ while IFS= read -r node; do
|
|
|
319
361
|
done < <(printf '%s' "$node" | jq -c '.subtasks[]?')
|
|
320
362
|
done <<< "$NODES"
|
|
321
363
|
|
|
364
|
+
# Dependencies, once every story has a key. An edge inside a strongly connected
|
|
365
|
+
# component (`onCycle`, set by the planner from task-graph.mjs) is not linked:
|
|
366
|
+
# a cycle has no order to record, and the plan already reported it.
|
|
367
|
+
DEP_TYPE="$(jq -r '.prefsUsed.dependencyLinkType // "Blocks"' "$PLAN")"
|
|
368
|
+
EDGES="$(jq -r '(.nodes | map({(.label): .title}) | add // {}) as $t
|
|
369
|
+
| .dependencies.edges[]? | .prerequisite as $p | .dependent as $d
|
|
370
|
+
| [$p, $d, $t[$p], $t[$d], (if .onCycle == true then "cycle" else "" end)]
|
|
371
|
+
| map(. // "" | tostring | gsub("[\n\r\u001f]"; " "))
|
|
372
|
+
| join("\u001f")' "$PLAN")"
|
|
373
|
+
# \x1f, unlike a tab, is not IFS whitespace, so an empty field stays a field.
|
|
374
|
+
while IFS=$'\x1f' read -r p_label d_label p_title d_title on_cycle; do
|
|
375
|
+
[ -n "$p_label" ] || continue
|
|
376
|
+
if [ "$on_cycle" = "cycle" ]; then
|
|
377
|
+
echo " skipped link $p_label ($p_title) -> $d_label ($d_title): the two stories are on a dependency cycle"
|
|
378
|
+
continue
|
|
379
|
+
fi
|
|
380
|
+
if [ "$DRY" -eq 1 ]; then
|
|
381
|
+
echo " link $DEP_TYPE $p_label ($p_title) -> $d_label ($d_title) (prerequisite -> dependent)"
|
|
382
|
+
continue
|
|
383
|
+
fi
|
|
384
|
+
p_key="$(story_key "$p_label")"; d_key="$(story_key "$d_label")"
|
|
385
|
+
if [ -z "$p_key" ] || [ -z "$d_key" ]; then
|
|
386
|
+
echo " skipped link $p_label ($p_title) -> $d_label ($d_title): one of the stories was not created" >&2
|
|
387
|
+
continue
|
|
388
|
+
fi
|
|
389
|
+
link_dependency "$DEP_TYPE" "$p_key" "$d_key" "$d_label" || RC=5
|
|
390
|
+
done <<< "$EDGES"
|
|
391
|
+
|
|
322
392
|
exit "$RC"
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
#
|
|
4
4
|
# WHY THIS EXISTS
|
|
5
5
|
#
|
|
6
|
-
# A run
|
|
7
|
-
# service-account JSON
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# A run that asks the user to paste a Crashlytics stack trace by hand while a valid
|
|
7
|
+
# Firebase service-account JSON sits in the Keychain, mapped as `firebase`, has failed
|
|
8
|
+
# even though nothing is broken. From the user's side that is worse than a failure: they
|
|
9
|
+
# supplied the key precisely so this would not happen. So the pipeline asks itself "do I
|
|
10
|
+
# already hold a credential that answers this?" before it asks the user.
|
|
11
11
|
#
|
|
12
12
|
# One command, so there is no excuse for asking blind. Run it before any question that
|
|
13
13
|
# requests data an external system holds, and let the answer shape the question:
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decisions.mjs - the record of what an autopilot run decided in place of a
|
|
3
|
+
* person, and its rendering.
|
|
4
|
+
*
|
|
5
|
+
* An attended run asks: a picker, a park, a waived finding, a model change are
|
|
6
|
+
* each a question with an answer someone gave. An autopilot run answers them
|
|
7
|
+
* itself. `state.decisions[]` holds one entry per such answer, so the person
|
|
8
|
+
* who reads the PR or the Phase 5 report sees every place the run chose for
|
|
9
|
+
* them, why, and what it costs if the choice was wrong:
|
|
10
|
+
*
|
|
11
|
+
* { what, why, costIfWrong, at, phase }
|
|
12
|
+
*
|
|
13
|
+
* The entries are written from text the run composed (gate reasons, finding
|
|
14
|
+
* locations, picker labels), so every field is passed through redact.mjs
|
|
15
|
+
* before it is capped, and the rendered section again before it reaches any
|
|
16
|
+
* outward text. The caller still runs the
|
|
17
|
+
* outbound gate over the whole body it publishes; redaction here only keeps a
|
|
18
|
+
* decision's wording from being the reason a publish is refused.
|
|
19
|
+
*
|
|
20
|
+
* Pure functions; the writers that hold the state lock are gate-ledger.mjs
|
|
21
|
+
* (park) and scripts/decisions-log.mjs (everything else).
|
|
22
|
+
*
|
|
23
|
+
* @module pipeline/lib/decisions
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { redact } from "./redact.mjs";
|
|
27
|
+
|
|
28
|
+
export const DECISIONS_HEADING = "## Decisions made on your behalf";
|
|
29
|
+
|
|
30
|
+
const FIELD_MAX = { what: 200, why: 500, costIfWrong: 300 };
|
|
31
|
+
const MIN_PHASE = 0;
|
|
32
|
+
const MAX_PHASE = 5;
|
|
33
|
+
// Room for the heading's blank line and one "... N more" line.
|
|
34
|
+
const MIN_ROOM = 48;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `value` on one line, redacted, then capped at `max`. Redaction comes first:
|
|
38
|
+
* a token cut at the boundary is too short for its rule to recognise, and its
|
|
39
|
+
* prefix would survive.
|
|
40
|
+
*/
|
|
41
|
+
function oneLine(value, max) {
|
|
42
|
+
const text = redact(
|
|
43
|
+
String(value ?? "")
|
|
44
|
+
.replace(/\s+/g, " ")
|
|
45
|
+
.trim(),
|
|
46
|
+
);
|
|
47
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A validated entry. Throws when a field is missing: an entry that cannot say
|
|
52
|
+
* what was decided, why, or what being wrong costs is not worth showing.
|
|
53
|
+
*
|
|
54
|
+
* @param {{what: string, why: string, costIfWrong: string, phase: number, at?: string}} input
|
|
55
|
+
* @returns {{what: string, why: string, costIfWrong: string, at: string, phase: number}}
|
|
56
|
+
*/
|
|
57
|
+
export function decisionEntry({ what, why, costIfWrong, phase, at } = {}) {
|
|
58
|
+
const out = {
|
|
59
|
+
what: oneLine(what, FIELD_MAX.what),
|
|
60
|
+
why: oneLine(why, FIELD_MAX.why),
|
|
61
|
+
costIfWrong: oneLine(costIfWrong, FIELD_MAX.costIfWrong),
|
|
62
|
+
};
|
|
63
|
+
for (const [k, v] of Object.entries(out)) {
|
|
64
|
+
if (!v) throw new Error(`a decision needs a non-empty ${k}`);
|
|
65
|
+
}
|
|
66
|
+
const n = Number(phase);
|
|
67
|
+
if (!Number.isInteger(n) || n < MIN_PHASE || n > MAX_PHASE) {
|
|
68
|
+
throw new Error(`a decision needs a phase between ${MIN_PHASE} and ${MAX_PHASE}`);
|
|
69
|
+
}
|
|
70
|
+
const stamp = typeof at === "string" && !Number.isNaN(Date.parse(at)) ? at : null;
|
|
71
|
+
return { ...out, at: stamp || new Date().toISOString(), phase: n };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The state with `entry` appended to `decisions[]`; `state` is not changed. */
|
|
75
|
+
export function withDecision(state, entry) {
|
|
76
|
+
const list = Array.isArray(state?.decisions) ? state.decisions : [];
|
|
77
|
+
return { ...state, decisions: [...list, entry] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function line(d) {
|
|
81
|
+
const phase = Number.isInteger(d?.phase) ? `Phase ${d.phase}` : "Phase ?";
|
|
82
|
+
return redact(
|
|
83
|
+
`- **${phase}** - ${oneLine(d?.what, FIELD_MAX.what)}. Why: ${oneLine(d?.why, FIELD_MAX.why)}. If wrong: ${oneLine(d?.costIfWrong, FIELD_MAX.costIfWrong)}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The markdown section, or "" for an empty list: a run that decided nothing
|
|
89
|
+
* for anyone adds nothing to the report.
|
|
90
|
+
*
|
|
91
|
+
* @param {object[]|undefined} decisions
|
|
92
|
+
* @param {{maxChars?: number}} [opts] trim to this many characters, oldest first kept
|
|
93
|
+
*/
|
|
94
|
+
export function renderDecisions(decisions, { maxChars = Infinity } = {}) {
|
|
95
|
+
const list = Array.isArray(decisions) ? decisions.filter((d) => d && typeof d === "object") : [];
|
|
96
|
+
if (!list.length) return "";
|
|
97
|
+
const head = `${DECISIONS_HEADING}\n\n`;
|
|
98
|
+
const lines = [];
|
|
99
|
+
let size = head.length;
|
|
100
|
+
for (let i = 0; i < list.length; i++) {
|
|
101
|
+
const l = `${line(list[i])}\n`;
|
|
102
|
+
const rest = list.length - i - 1;
|
|
103
|
+
const tail = rest ? `- ... ${rest + 1} more in the run report\n`.length : 0;
|
|
104
|
+
if (size + l.length + tail > maxChars) {
|
|
105
|
+
lines.push(`- ... ${list.length - i} more in the run report\n`);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
lines.push(l);
|
|
109
|
+
size += l.length;
|
|
110
|
+
}
|
|
111
|
+
return head + lines.join("");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `body` with the decisions section appended, once. A body that already holds
|
|
116
|
+
* the heading is returned unchanged; `maxChars` bounds the whole result.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} body
|
|
119
|
+
* @param {object[]|undefined} decisions
|
|
120
|
+
* @param {{maxChars?: number}} [opts]
|
|
121
|
+
*/
|
|
122
|
+
export function appendDecisionsSection(body, decisions, { maxChars = Infinity } = {}) {
|
|
123
|
+
const text = typeof body === "string" ? body : "";
|
|
124
|
+
if (text.includes(DECISIONS_HEADING)) return text;
|
|
125
|
+
const sep = text.endsWith("\n") ? "\n" : "\n\n";
|
|
126
|
+
const room = maxChars - text.length - sep.length;
|
|
127
|
+
const section = renderDecisions(decisions, { maxChars: room });
|
|
128
|
+
if (!section || room < DECISIONS_HEADING.length + MIN_ROOM) return text;
|
|
129
|
+
return `${text}${sep}${section}`;
|
|
130
|
+
}
|
|
@@ -214,15 +214,13 @@ if [ -z "$SA_JSON_RAW" ]; then
|
|
|
214
214
|
fi
|
|
215
215
|
|
|
216
216
|
# The credential store returns the JSON as stored - encoding is its internal
|
|
217
|
-
# business, not this script's
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
# nor JSON, so both branches failed and the failure surfaced three layers later
|
|
221
|
-
# as a token-exchange error.
|
|
217
|
+
# business, not this script's, so nothing here guesses at a decoding. A guess
|
|
218
|
+
# that misfires (bare hex read as base64, say) fails three layers later as a
|
|
219
|
+
# token-exchange error instead of here.
|
|
222
220
|
#
|
|
223
|
-
# With no decode step there is nothing to stage on disk, so
|
|
224
|
-
#
|
|
225
|
-
#
|
|
221
|
+
# With no decode step there is nothing to stage on disk, so there is no temp file
|
|
222
|
+
# and no cleanup trap: the service-account JSON only ever exists in a shell
|
|
223
|
+
# variable, which is one fewer place a GCP private key can be left behind.
|
|
226
224
|
SA_JSON="$SA_JSON_RAW"
|
|
227
225
|
|
|
228
226
|
# Verify project_id from SA JSON matches the URL - protects against pasting a
|