@mmerterden/multi-agent-pipeline 12.10.0 → 13.0.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 +179 -0
- package/README.md +24 -7
- package/index.js +5 -2
- package/install/_codex-agents.mjs +211 -0
- package/install/_codex-instructions.mjs +33 -0
- package/install/_managed-block.mjs +99 -0
- package/install/codex.mjs +478 -0
- package/install/copilot.mjs +34 -80
- package/install/index.mjs +25 -9
- package/install/templates/codex-instructions.md +45 -0
- package/package.json +5 -3
- package/pipeline/claude-md-template.md +1 -0
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev/SKILL.md +31 -6
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/language/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +69 -2
- package/pipeline/commands/multi-agent/sync/SKILL.md +128 -5
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +219 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +7 -4
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +51 -17
- package/pipeline/multi-agent-refs/features/model-fallback.md +29 -0
- package/pipeline/multi-agent-refs/features/review-multi-repo.md +1 -1
- package/pipeline/multi-agent-refs/phases/log-format.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +14 -2
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +36 -5
- package/pipeline/multi-agent-refs/progress-contract.md +1 -1
- package/pipeline/multi-agent-refs/tracker-contract.md +17 -1
- package/pipeline/schemas/prefs.schema.json +296 -62
- package/pipeline/schemas/reviewer-output.schema.json +1 -1
- package/pipeline/schemas/triage-output.schema.json +1 -1
- package/pipeline/scripts/cost-table.json +15 -1
- package/pipeline/scripts/output-quality-check.sh +34 -0
- package/pipeline/scripts/phase-tracker.sh +79 -1
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +25 -10
- package/pipeline/scripts/uninstall.mjs +105 -9
- package/pipeline/scripts/update-check.sh +2 -1
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +48 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +87 -6
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +120 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/reviewer-output.schema.json",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"title": "Multi-Agent Pipeline - Phase 4 reviewer output",
|
|
6
|
-
"description": "Contract for a single code-reviewer subagent's JSON output in Phase 4 Step 2. Reviewer set is CLI-aware: Claude Code dispatches 2 parallel reviewers (
|
|
6
|
+
"description": "Contract for a single code-reviewer subagent's JSON output in Phase 4 Step 2. Reviewer set is CLI-aware: Claude Code dispatches 2 parallel reviewers (Fable, Sonnet); Copilot CLI dispatches 3 (Opus, GPT-5.4, Sonnet); Codex CLI dispatches 3 (gpt-5.6 at xhigh, gpt-5.4, gpt-5.6 at medium). Every reviewer must return an object matching this shape before Opus triage merges them.",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"required": ["findings", "approved"],
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"reviewerCount": {
|
|
87
87
|
"type": "integer",
|
|
88
88
|
"minimum": 1,
|
|
89
|
-
"description": "Reviewers dispatched this iteration (Claude Code = 2, Copilot CLI = 3)."
|
|
89
|
+
"description": "Reviewers dispatched this iteration (Claude Code = 2, Copilot CLI = 3, Codex CLI = 3)."
|
|
90
90
|
},
|
|
91
91
|
"verdict": {
|
|
92
92
|
"type": "string",
|
|
@@ -32,7 +32,21 @@
|
|
|
32
32
|
"outPerMtok": 30.0,
|
|
33
33
|
"cacheReadPerMtok": 1.0,
|
|
34
34
|
"modelId": "gpt-5.4",
|
|
35
|
-
"note": "Copilot CLI Reviewer 2 - approximate; verify against OpenAI pricing page before relying on totals."
|
|
35
|
+
"note": "Copilot CLI Reviewer 2 and Codex CLI Reviewer 2 - approximate; verify against OpenAI pricing page before relying on totals."
|
|
36
|
+
},
|
|
37
|
+
"gpt-5.6": {
|
|
38
|
+
"inPerMtok": 10.0,
|
|
39
|
+
"outPerMtok": 30.0,
|
|
40
|
+
"cacheReadPerMtok": 1.0,
|
|
41
|
+
"modelId": "gpt-5.6",
|
|
42
|
+
"note": "Codex CLI top tier - Reviewer 1 at xhigh effort, Reviewer 3 at medium, triage at max, and the fable/opus rungs of the Codex persona tier map. Reasoning effort changes output volume, not the per-token rate, so one entry covers every effort level. Approximate; verify against OpenAI pricing before relying on totals."
|
|
43
|
+
},
|
|
44
|
+
"gpt-5.6-terra": {
|
|
45
|
+
"inPerMtok": 1.0,
|
|
46
|
+
"outPerMtok": 5.0,
|
|
47
|
+
"cacheReadPerMtok": 0.1,
|
|
48
|
+
"modelId": "gpt-5.6-terra",
|
|
49
|
+
"note": "Codex CLI floor tier - speed-optimised, maps the haiku rung of the Codex persona tier map (task-clarifier). Approximate; verify against OpenAI pricing before relying on totals."
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
}
|
|
@@ -89,6 +89,40 @@ else
|
|
|
89
89
|
warn "no .pr-body.md in task dir (skipped PR-body check)"
|
|
90
90
|
fi
|
|
91
91
|
|
|
92
|
+
# 2b. Tracker tile titles
|
|
93
|
+
#
|
|
94
|
+
# `rules.md` forbids HTML entities in "titles, commit messages, task subjects, or
|
|
95
|
+
# body text", but this check only ever looked at the PR body and the Jira comment.
|
|
96
|
+
# Tile titles are task subjects, and that is where the rule actually broke: a real
|
|
97
|
+
# run rendered `Phase 1: Build & Launch` in the TaskList widget. The rule was
|
|
98
|
+
# right and its enforcement was scoped to the wrong two surfaces.
|
|
99
|
+
#
|
|
100
|
+
# `phase-tracker.sh add` now decodes at the funnel, so this is the backstop that
|
|
101
|
+
# says so: an entity reaching the stored state means the funnel was bypassed.
|
|
102
|
+
TRACKER_STATE="$TASK_DIR/tracker-state.json"
|
|
103
|
+
if [ -f "$TRACKER_STATE" ]; then
|
|
104
|
+
BAD_TITLES=$(python3 -c "
|
|
105
|
+
import json, re, sys
|
|
106
|
+
try:
|
|
107
|
+
d = json.load(open('$TRACKER_STATE'))
|
|
108
|
+
except Exception:
|
|
109
|
+
sys.exit(0)
|
|
110
|
+
# named, decimal and hex - a pattern covering only the named forms would miss
|
|
111
|
+
# exactly the two spellings the first version of the decoder missed.
|
|
112
|
+
pat = re.compile(r'&(amp|lt|gt|quot|apos|nbsp|mdash|ndash|hellip|#[0-9]+|#[xX][0-9a-fA-F]+);')
|
|
113
|
+
for p in d.get('phases', []):
|
|
114
|
+
n = p.get('name', '')
|
|
115
|
+
if pat.search(n):
|
|
116
|
+
print('%s: %s' % (p.get('id'), n))
|
|
117
|
+
" 2>/dev/null)
|
|
118
|
+
if [ -n "$BAD_TITLES" ]; then
|
|
119
|
+
bad "tracker tile title(s) carry HTML entities (they render literally):"
|
|
120
|
+
printf '%s\n' "$BAD_TITLES" | sed 's/^/ /'
|
|
121
|
+
else
|
|
122
|
+
ok "tracker tile titles free of HTML entities"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
92
126
|
# 3. Jira comment
|
|
93
127
|
JIRA_COMMENT="$TASK_DIR/.jira-comment.txt"
|
|
94
128
|
if [ -f "$JIRA_COMMENT" ]; then
|
|
@@ -115,6 +115,84 @@ need_jq() {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
# Decode HTML entities in a tile title.
|
|
119
|
+
#
|
|
120
|
+
# `rules.md` forbids entities in "titles, commit messages, task subjects, or body
|
|
121
|
+
# text" because nothing downstream decodes them - they render literally. A tracker
|
|
122
|
+
# tile title IS a task subject, and it rendered exactly that way:
|
|
123
|
+
#
|
|
124
|
+
# Phase 1: Build & Launch
|
|
125
|
+
# Phase 3: Drive & Compare
|
|
126
|
+
#
|
|
127
|
+
# The rule existed and `output-quality-check.sh` enforced it, but only over the PR
|
|
128
|
+
# body and the Jira comment, never over a tile title. The one surface where the
|
|
129
|
+
# rule actually broke was the one surface nothing inspected.
|
|
130
|
+
#
|
|
131
|
+
# `add` is the single funnel for every tile title, so fixing it here fixes every
|
|
132
|
+
# caller. Decode rather than reject: the intent behind `&` is unambiguous, and
|
|
133
|
+
# a display path should render correctly now rather than abort a run over
|
|
134
|
+
# punctuation. The stderr warning keeps the caller mistake visible instead of
|
|
135
|
+
# silently papering over it.
|
|
136
|
+
#
|
|
137
|
+
# Handles all three encodings, because a title can arrive in any of them:
|
|
138
|
+
# named & < " —
|
|
139
|
+
# decimal & < "   —
|
|
140
|
+
# hex & < "   —
|
|
141
|
+
# A first version handled only the named forms, so the numeric spellings still
|
|
142
|
+
# rendered literally: the same bug fixed for one spelling out of three.
|
|
143
|
+
#
|
|
144
|
+
# ONE left-to-right pass, which also gets nesting right for free: a global replace
|
|
145
|
+
# never rescans its own output, so `&lt;` yields the literal text `<` rather
|
|
146
|
+
# than `<`. An ordered-sed version needed `&` decoded last for the same effect,
|
|
147
|
+
# and that ordering was a latent trap.
|
|
148
|
+
#
|
|
149
|
+
# Fancy punctuation degrades to ASCII instead of decoding faithfully. `—`
|
|
150
|
+
# becomes `-`, not an em-dash: this repo bans em/en-dash and ellipsis in shipped
|
|
151
|
+
# text and gates it in the scorecard, so decoding them would make this function
|
|
152
|
+
# INJECT what another gate rejects. Same for any numeric entity resolving to one.
|
|
153
|
+
#
|
|
154
|
+
# The program is fed through a QUOTED heredoc, not `node -e '...'`. Inside a
|
|
155
|
+
# single-quoted shell string every apostrophe in the JS (and in its comments)
|
|
156
|
+
# terminates the string early; shellcheck flags it as SC2140 and the first version
|
|
157
|
+
# here worked only by accident of where the quotes happened to fall. A quoted
|
|
158
|
+
# heredoc passes the body through verbatim, so the JS can use any quote it likes.
|
|
159
|
+
decode_entities() {
|
|
160
|
+
local raw="$1" out
|
|
161
|
+
out=$(MA_RAW="$raw" node - <<'MA_DECODE_JS'
|
|
162
|
+
const NAMED = {
|
|
163
|
+
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'",
|
|
164
|
+
nbsp: " ",
|
|
165
|
+
// Deliberately ASCII: see the note above this function.
|
|
166
|
+
ndash: "-", mdash: "-", hellip: "...", laquo: "<<", raquo: ">>",
|
|
167
|
+
};
|
|
168
|
+
// Codepoints that must degrade to ASCII rather than decode faithfully.
|
|
169
|
+
const ASCII_FOR = new Map([
|
|
170
|
+
[0x00a0, " "], [0x2013, "-"], [0x2014, "-"], [0x2026, "..."],
|
|
171
|
+
[0x2018, "'"], [0x2019, "'"], [0x201c, '"'], [0x201d, '"'],
|
|
172
|
+
[0x00a7, ""],
|
|
173
|
+
]);
|
|
174
|
+
const src = process.env.MA_RAW || "";
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
src.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => {
|
|
177
|
+
if (body[0] === "#") {
|
|
178
|
+
const hex = body[1] === "x" || body[1] === "X";
|
|
179
|
+
const cp = parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
180
|
+
if (!Number.isFinite(cp) || cp < 0 || cp > 0x10ffff) return m;
|
|
181
|
+
if (ASCII_FOR.has(cp)) return ASCII_FOR.get(cp);
|
|
182
|
+
try { return String.fromCodePoint(cp); } catch { return m; }
|
|
183
|
+
}
|
|
184
|
+
const key = body.toLowerCase();
|
|
185
|
+
// An unknown named entity is left alone: guessing would corrupt a title that
|
|
186
|
+
// legitimately contains "&foo;".
|
|
187
|
+
return Object.prototype.hasOwnProperty.call(NAMED, key) ? NAMED[key] : m;
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
MA_DECODE_JS
|
|
191
|
+
) || out="$raw"
|
|
192
|
+
[ "$out" != "$raw" ] && echo "phase-tracker: decoded HTML entities in title (rules.md forbids them): '$raw' -> '$out'" >&2
|
|
193
|
+
printf '%s' "$out"
|
|
194
|
+
}
|
|
195
|
+
|
|
118
196
|
# OTel-compatible span emission - v6.1.0+, opt-in via $MULTI_AGENT_OTEL_SPANS.
|
|
119
197
|
# Appends one JSON line to otel-spans.jsonl. Never fatal on failure.
|
|
120
198
|
#
|
|
@@ -575,7 +653,7 @@ case "$ACTION" in
|
|
|
575
653
|
add)
|
|
576
654
|
need_jq
|
|
577
655
|
[ "$#" -ge 2 ] || { echo "add needs <phase_id> <name>" >&2; exit 64; }
|
|
578
|
-
PID="$1"; PNAME="$2"
|
|
656
|
+
PID="$1"; PNAME="$(decode_entities "$2")"
|
|
579
657
|
acquire_state_lock
|
|
580
658
|
state=$(load_state)
|
|
581
659
|
# Idempotent: a phase id that already exists is a no-op (name, status, and
|
|
@@ -185,11 +185,12 @@ else
|
|
|
185
185
|
fi
|
|
186
186
|
|
|
187
187
|
# ──────────────────────────────────────────────────────────────────────────
|
|
188
|
-
# CLI-aware reviewer-count contract: Claude Code = 2
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# drift across the phase doc, the schema,
|
|
192
|
-
|
|
188
|
+
# CLI-aware reviewer-count contract: Claude Code = 2 (Fable + Sonnet), Copilot CLI
|
|
189
|
+
# = 3 (Opus + GPT-5.4 + Sonnet), Codex CLI = 3 (gpt-5.6 xhigh + gpt-5.4 + gpt-5.6
|
|
190
|
+
# medium). Nothing in code enforces the count (the orchestrator dispatches per the
|
|
191
|
+
# doc), so lock the CONTRACT here against drift across the phase doc, the schema,
|
|
192
|
+
# and the consensus block.
|
|
193
|
+
echo "→ reviewer-count contract (Claude=2, Copilot=3, Codex=3)"
|
|
193
194
|
# `/multi-agent:update` runs this smoke on the user's machine, where the tree is
|
|
194
195
|
# ~/.claude/{schemas,multi-agent-refs} and NOT <root>/pipeline/*. Resolving the
|
|
195
196
|
# root by hand here reported three phantom failures in an install, which reads as
|
|
@@ -202,16 +203,30 @@ TRSCHEMA="${MA_SCHEMAS:+$MA_SCHEMAS/triage-output.schema.json}"
|
|
|
202
203
|
|
|
203
204
|
if [ -z "$P4" ] || [ ! -f "$P4" ]; then
|
|
204
205
|
echo " ↷ SKIP: phase-4-review.md not present in this $MA_LAYOUT layout"
|
|
205
|
-
elif grep -qiE "Claude Code (dispatches|=|:) ?2|2-model" "$P4"
|
|
206
|
-
|
|
206
|
+
elif grep -qiE "Claude Code (dispatches|=|:) ?2|2-model" "$P4" \
|
|
207
|
+
&& grep -qiE "Copilot CLI (dispatches|=|:) ?3|3-model" "$P4" \
|
|
208
|
+
&& grep -qiE "Codex CLI (dispatches|=|:) ?3|Claude Code 2, Copilot CLI 3, Codex CLI 3" "$P4"; then
|
|
209
|
+
pass "phase-4-review declares Claude=2 / Copilot=3 / Codex=3 reviewers"
|
|
207
210
|
else
|
|
208
|
-
fail "phase-4-review does not declare the CLI-aware reviewer count"
|
|
211
|
+
fail "phase-4-review does not declare the CLI-aware reviewer count for all three hosts"
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# The two Codex constraints are silent-failure shaped, so the contract has to name
|
|
215
|
+
# them: a spawn without fork_turns collapses the panel onto one model, and the
|
|
216
|
+
# 4-slot ceiling (orchestrator included) is why the count is 3 and not more.
|
|
217
|
+
if [ -n "$P4" ] && [ -f "$P4" ]; then
|
|
218
|
+
if grep -q 'fork_turns' "$P4" && grep -qiE "concurrency|slots" "$P4"; then
|
|
219
|
+
pass "phase-4-review documents the fork_turns override rule + the concurrency ceiling"
|
|
220
|
+
else
|
|
221
|
+
fail "phase-4-review must document fork_turns and the Codex concurrency ceiling"
|
|
222
|
+
fi
|
|
209
223
|
fi
|
|
210
224
|
|
|
211
225
|
if [ -z "$REVSCHEMA" ] || [ ! -f "$REVSCHEMA" ]; then
|
|
212
226
|
echo " ↷ SKIP: reviewer-output.schema.json not present in this $MA_LAYOUT layout"
|
|
213
|
-
elif grep -qi "Copilot CLI" "$REVSCHEMA" && grep -qi "Claude" "$REVSCHEMA"
|
|
214
|
-
|
|
227
|
+
elif grep -qi "Copilot CLI" "$REVSCHEMA" && grep -qi "Claude" "$REVSCHEMA" \
|
|
228
|
+
&& grep -qi "Codex" "$REVSCHEMA"; then
|
|
229
|
+
pass "reviewer-output schema notes the CLI-aware reviewer set (all three hosts)"
|
|
215
230
|
else
|
|
216
231
|
fail "reviewer-output schema missing the CLI-aware note"
|
|
217
232
|
fi
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
39
|
+
import { execFileSync } from "child_process";
|
|
39
40
|
import { join } from "path";
|
|
40
41
|
import { pathToFileURL } from "url";
|
|
41
42
|
import { createInterface } from "readline";
|
|
@@ -123,6 +124,54 @@ function rmIfExists(path) {
|
|
|
123
124
|
return true;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Remove generated Codex agent TOML files, identified by the header the
|
|
129
|
+
* installer stamps on them. A user-authored `.toml` sharing a persona name is
|
|
130
|
+
* left alone - `~/.codex/agents/` is a co-owned directory.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} agentsDir
|
|
133
|
+
* @returns {number} files removed
|
|
134
|
+
*/
|
|
135
|
+
function removeGeneratedCodexAgents(agentsDir) {
|
|
136
|
+
if (!existsSync(agentsDir)) return 0;
|
|
137
|
+
const MARKER = "# Generated by multi-agent-pipeline install --codex.";
|
|
138
|
+
let removed = 0;
|
|
139
|
+
for (const name of readdirSync(agentsDir)) {
|
|
140
|
+
if (!name.endsWith(".toml")) continue;
|
|
141
|
+
const path = join(agentsDir, name);
|
|
142
|
+
try {
|
|
143
|
+
if (!readFileSync(path, "utf-8").startsWith(MARKER)) continue;
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (rmIfExists(path)) removed++;
|
|
148
|
+
}
|
|
149
|
+
if (removed > 0) console.log(` removed ${removed} generated agent file(s) under ${agentsDir}`);
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Hand the dev-toolkit MCP registration back to Codex.
|
|
155
|
+
*
|
|
156
|
+
* Best-effort: a missing `codex` binary or an already-absent entry is not an
|
|
157
|
+
* uninstall failure. Never edits `config.toml` directly - Codex keeps
|
|
158
|
+
* marketplace and plugin state in the same file.
|
|
159
|
+
*/
|
|
160
|
+
function deregisterCodexMcpServer() {
|
|
161
|
+
if (dryRun) {
|
|
162
|
+
report("would run", "codex mcp remove dev-toolkit");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
execFileSync("codex", ["mcp", "remove", "dev-toolkit"], { stdio: "pipe", timeout: 20_000 });
|
|
167
|
+
console.log(" removed: dev-toolkit MCP registration");
|
|
168
|
+
} catch {
|
|
169
|
+
console.log(
|
|
170
|
+
" note: could not run `codex mcp remove dev-toolkit` (codex not on PATH, or not registered)",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
/**
|
|
127
176
|
* Remove every directory under `parent` whose name matches a predicate.
|
|
128
177
|
* @param {string} parent
|
|
@@ -160,6 +209,32 @@ function rmMatchingFiles(parent, predicate) {
|
|
|
160
209
|
// (v11.4.1+). Must stay in sync with INSTRUCTIONS_END_MARKER there.
|
|
161
210
|
const COPILOT_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
|
|
162
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Every end marker the installers write. Both host instruction files share the
|
|
214
|
+
* same START marker but terminate with their own comment, so a single-marker
|
|
215
|
+
* lookup silently falls through to the legacy heading-bounded path - and since
|
|
216
|
+
* the managed body has no top-level heading after the first, that path treats
|
|
217
|
+
* ALL trailing user content as part of the block and deletes it.
|
|
218
|
+
*/
|
|
219
|
+
const MANAGED_END_MARKERS = [
|
|
220
|
+
COPILOT_END_MARKER,
|
|
221
|
+
"<!-- multi-agent-pipeline:codex-instructions:end -->",
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Locate whichever managed end marker terminates this block.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} fromStart - content from the start marker onward
|
|
228
|
+
* @returns {{index: number, length: number}} index -1 when none is present
|
|
229
|
+
*/
|
|
230
|
+
function findManagedEndMarker(fromStart) {
|
|
231
|
+
for (const marker of MANAGED_END_MARKERS) {
|
|
232
|
+
const index = fromStart.indexOf(marker);
|
|
233
|
+
if (index >= 0) return { index, length: marker.length };
|
|
234
|
+
}
|
|
235
|
+
return { index: -1, length: 0 };
|
|
236
|
+
}
|
|
237
|
+
|
|
163
238
|
/**
|
|
164
239
|
* Legacy copilot-instructions files (written before the end marker existed)
|
|
165
240
|
* have no explicit terminator. Bound the pipeline span at the next top-level
|
|
@@ -205,10 +280,10 @@ function stripManagedBlock(filePath) {
|
|
|
205
280
|
if (idx >= 0) {
|
|
206
281
|
const before = content.slice(0, idx).trimEnd();
|
|
207
282
|
const fromStart = content.slice(idx);
|
|
208
|
-
const
|
|
283
|
+
const end = findManagedEndMarker(fromStart);
|
|
209
284
|
const rawTrailing =
|
|
210
|
-
|
|
211
|
-
? fromStart.slice(
|
|
285
|
+
end.index >= 0
|
|
286
|
+
? fromStart.slice(end.index + end.length)
|
|
212
287
|
: legacyTrailingContent(fromStart);
|
|
213
288
|
const trailing = rawTrailing.replace(/^[\r\n]+/, "").trimEnd();
|
|
214
289
|
let remaining = before;
|
|
@@ -332,7 +407,7 @@ export async function main() {
|
|
|
332
407
|
);
|
|
333
408
|
if (forCodex)
|
|
334
409
|
console.log(
|
|
335
|
-
" -
|
|
410
|
+
" - Codex CLI (~/.codex: skills/multi-agent, multi-agent-refs, agents/*.toml, prompts/multi-agent.md, scripts, lib, schemas, rules, AGENTS.md block, dev-toolkit MCP entry)",
|
|
336
411
|
);
|
|
337
412
|
console.log("");
|
|
338
413
|
if (allData) {
|
|
@@ -478,14 +553,35 @@ export async function main() {
|
|
|
478
553
|
|
|
479
554
|
if (forCodex && HOME) {
|
|
480
555
|
console.log("");
|
|
481
|
-
console.log(" [
|
|
556
|
+
console.log(" [Codex CLI] Removing from ~/.codex...");
|
|
482
557
|
const CODEX = join(HOME, ".codex");
|
|
558
|
+
|
|
559
|
+
// Wholly pipeline-owned trees.
|
|
560
|
+
rmIfExists(join(CODEX, "multi-agent-refs"));
|
|
561
|
+
rmIfExists(join(CODEX, "scripts"));
|
|
562
|
+
rmIfExists(join(CODEX, "lib"));
|
|
563
|
+
rmIfExists(join(CODEX, "schemas"));
|
|
564
|
+
rmIfExists(join(CODEX, "rules"));
|
|
565
|
+
rmIfExists(join(CODEX, ".pipeline-version"));
|
|
566
|
+
|
|
567
|
+
// Co-owned dirs: remove only what the pipeline wrote. `skills/` holds
|
|
568
|
+
// Codex's own `.system` set plus user skills; `prompts/` and `agents/` hold
|
|
569
|
+
// user-authored files.
|
|
570
|
+
const skills = join(CODEX, "skills");
|
|
571
|
+
const n = rmMatchingDirs(
|
|
572
|
+
skills,
|
|
573
|
+
(name) => name === "multi-agent" || name.startsWith("multi-agent-"),
|
|
574
|
+
);
|
|
575
|
+
if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
|
|
483
576
|
rmIfExists(join(CODEX, "prompts", "multi-agent.md"));
|
|
577
|
+
removeGeneratedCodexAgents(join(CODEX, "agents"));
|
|
578
|
+
|
|
484
579
|
stripManagedBlock(join(CODEX, "AGENTS.md"));
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
580
|
+
|
|
581
|
+
// The MCP entry was written by `codex mcp add`, so hand it back the same
|
|
582
|
+
// way rather than editing config.toml - Codex also stores marketplace and
|
|
583
|
+
// plugin state in that file.
|
|
584
|
+
deregisterCodexMcpServer();
|
|
489
585
|
}
|
|
490
586
|
|
|
491
587
|
console.log("");
|
|
@@ -48,7 +48,8 @@ if [ -z "$LOCAL_VERSION" ]; then
|
|
|
48
48
|
fi
|
|
49
49
|
# npx-only installs have no repo clone; the installer stamps the version here.
|
|
50
50
|
if [ -z "$LOCAL_VERSION" ]; then
|
|
51
|
-
for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version"
|
|
51
|
+
for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
|
|
52
|
+
"$HOME/.codex/.pipeline-version"; do
|
|
52
53
|
if [ -f "$marker" ]; then
|
|
53
54
|
LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
|
|
54
55
|
[ -n "$LOCAL_VERSION" ] && break
|
|
@@ -17,7 +17,7 @@ Two-axis language preference for the pipeline:
|
|
|
17
17
|
| `promptLanguage` | Interactive prompts during a pipeline run (account picker, project picker, dev-context picker, base-branch picker, branch-name picker, maturity ack, channels picker, Phase 5 test prompt, Phase 6 local-checkout prompt) | `prefs.global.promptLanguage` | **Fixed to `en`** - never toggled by this skill |
|
|
18
18
|
| `outputLanguage` | Assistant's explanations, status updates, error messages, and pipeline-generated reports rendered to the user (NOT external payloads) | `prefs.global.outputLanguage` | Toggled by this skill |
|
|
19
19
|
|
|
20
|
-
**Why promptLanguage is fixed:**
|
|
20
|
+
**Why promptLanguage is fixed:** it governs the picker's structural chrome only - `AskUserQuestion` `label` (button text) and `header` (chip), host error UI, and internal contract identifiers. Those stay English so tooling reads the same across CLIs. Everything a user actually reads follows `outputLanguage`, including the picker `question` and each option's `description`, per the canonical per-field matrix in `multi-agent-refs/rules.md`. A picker whose question is English on a Turkish run is a bug, not the contract.
|
|
21
21
|
|
|
22
22
|
**Always English regardless of either field**: commit messages, PR titles/bodies, Jira comments, wiki pages, reviewer/triage system prompts, agent-log.md payloads, skill-picker / confirmation / error UI exposed by the CLI host.
|
|
23
23
|
|
|
@@ -83,5 +83,5 @@ Render in the new outputLanguage:
|
|
|
83
83
|
|
|
84
84
|
- `multi-agent-setup` - first-run language picker (asks only outputLanguage; promptLanguage seeded as `en`)
|
|
85
85
|
- `prefs.schema.json` - `global.promptLanguage` (fixed `"en"`) and `global.outputLanguage`
|
|
86
|
-
- Phase 5 / Phase 6 -
|
|
86
|
+
- Phase 5 / Phase 6 - interactive prompts follow the per-field matrix: `question` + `description` in `outputLanguage`, `label` + `header` English
|
|
87
87
|
- `multi-agent-help` - reads outputLanguage for its own copy
|
|
@@ -15,7 +15,7 @@ Ask BEFORE anything else. The pipeline has two language fields:
|
|
|
15
15
|
|
|
16
16
|
| Field | Controls | Configurable? |
|
|
17
17
|
|---|---|---|
|
|
18
|
-
| `promptLanguage` |
|
|
18
|
+
| `promptLanguage` | The picker's structural UI chrome only: `AskUserQuestion` `label` + `header`, host error UI, internal contract identifiers | **No - fixed to `en`.** Button and chip text stays English. |
|
|
19
19
|
| `outputLanguage` | The assistant's non-interactive explanations, status updates, error messages, and pipeline-generated reports | Yes - set here, change later via `/multi-agent-language <en\|tr>` |
|
|
20
20
|
|
|
21
21
|
`promptLanguage` is seeded as `"en"` and never offered to the user. External payloads (commits, PR/Jira/wiki, reviewer/triage prompts, agent-log.md) always stay English regardless.
|
|
@@ -278,6 +278,53 @@ pbcopy < /dev/null
|
|
|
278
278
|
# ~/.claude/settings.json -> mcpServers -> figma
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
+
### App Store Connect Credentials (optional, iOS only)
|
|
282
|
+
|
|
283
|
+
Unlocks Gate 2 of `multi-agent-testflight-validation` - Apple's own
|
|
284
|
+
`altool --validate-app`, the only check that sees an unregistered bundle ID, a
|
|
285
|
+
profile that does not match the App Store Connect app record, or a version+build
|
|
286
|
+
pair already used. Skipping is a first-class answer: the validation command still
|
|
287
|
+
runs its static audit and guideline review and reports Gate 2 as `SKIPPED` with
|
|
288
|
+
the reason, never as a pass.
|
|
289
|
+
|
|
290
|
+
Ask (picker): **API key** / **Apple ID + app-specific password** / **Skip**.
|
|
291
|
+
|
|
292
|
+
**API key** - needs an Admin or App Manager role in App Store Connect. Map the key
|
|
293
|
+
id and issuer id as `appstore_connect_key_id` / `appstore_connect_issuer_id`. The
|
|
294
|
+
private key is a FILE and never enters the credential store; it goes to one of the
|
|
295
|
+
directories altool searches:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
ls ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null \
|
|
299
|
+
|| echo "MISSING: put AuthKey_<keyId>.p8 in ~/.appstoreconnect/private_keys/"
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Apple ID + app-specific password** - usable by **any Apple ID holder, no elevated
|
|
303
|
+
role**, which is the realistic path when API-key creation is not permitted on the
|
|
304
|
+
account. Lead with this option when the user says they cannot create an API key.
|
|
305
|
+
The password goes into Apple's own keychain helper, which is what
|
|
306
|
+
`altool -p @keychain:<item>` reads - never into chat, never into an argument:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# export AC_PASSWORD_ONCE in your own shell first, for this one command
|
|
310
|
+
xcrun altool --store-password-in-keychain-item "<item-name>" \
|
|
311
|
+
-u "<apple-id>" -p @env:AC_PASSWORD_ONCE
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Map only the ITEM NAME as `appstore_connect_password_item`, plus the Apple ID as
|
|
315
|
+
`appstore_connect_apple_id`. The password stays in the keychain and is referenced,
|
|
316
|
+
never read by the pipeline.
|
|
317
|
+
|
|
318
|
+
**Multi-provider accounts.** A corporate Apple ID often belongs to several
|
|
319
|
+
providers and altool fails opaquely without one. Resolve it once with
|
|
320
|
+
`ios_testflight_validate({list_providers: true, ...})` and store the id under
|
|
321
|
+
`prefs.projects[<key>].appStoreConnect.providerPublicId` - per-project, since a
|
|
322
|
+
user can ship for more than one team.
|
|
323
|
+
|
|
324
|
+
A credential that resolves but is rejected (401/403) follows the Expired-token
|
|
325
|
+
decision (Regenerate / Use a different token / Skip and continue) rather than
|
|
326
|
+
being silently dropped.
|
|
327
|
+
|
|
281
328
|
### Security Rules
|
|
282
329
|
|
|
283
330
|
- Never paste tokens into terminal or chat - use `pbpaste` for input
|
|
@@ -20,6 +20,7 @@ When invoked, it synchronizes all targets in order. It detects what changed, upd
|
|
|
20
20
|
|---|-------|-----|-----|
|
|
21
21
|
| 1 | Claude Code (source of truth) | `~/.claude/commands/multi-agent.md` + `~/.claude/commands/multi-agent/` + `~/.claude/agents/` + `~/.claude/scripts/` | source |
|
|
22
22
|
| 2 | Copilot CLI | `~/.copilot/copilot-instructions.md` + `~/.copilot/skills/` | <- from Claude |
|
|
23
|
+
| 2b | Codex CLI | `~/.codex/AGENTS.md` + `~/.codex/skills/multi-agent/` + `~/.codex/multi-agent-refs/` + `~/.codex/agents/*.toml` | <- from Claude (path-rewritten) |
|
|
23
24
|
| 3 | multi-agent-pipeline repo | `~/multi-agent-pipeline/pipeline/` | <- from Claude (genericized) |
|
|
24
25
|
| 4 | Website | `{owner}/{website-host}` | <- version + features |
|
|
25
26
|
| 5 | dev-toolkit MCP server | resolved from `prefs.global.devToolkit` or the `mcpServers` registration | own repo: gate, commit, publish |
|
|
@@ -30,7 +31,8 @@ Run all steps automatically:
|
|
|
30
31
|
|
|
31
32
|
```
|
|
32
33
|
Step 1: DETECT Compare timestamps, find stale targets
|
|
33
|
-
Step 2: COPILOT Claude Code -> Copilot CLI (instructions +
|
|
34
|
+
Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 43 sub-command skills)
|
|
35
|
+
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 43 specs as refs + 8 agent TOML)
|
|
34
36
|
Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub)
|
|
35
37
|
Step 3d: DEV-TOOLKIT Companion MCP server -> detect movement, ship gates, commit + publish
|
|
36
38
|
Step 4: WEBSITE Version + phase/model counts -> {website-host} (i18n + projects.ts)
|
|
@@ -93,6 +95,41 @@ If nothing is stale -> report "All targets up to date" and stop.
|
|
|
93
95
|
- `~/.claude/settings.json`
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
## Codex Sync (Step 2b)
|
|
99
|
+
|
|
100
|
+
This step does **not** hand-copy files. The Codex tree is a *transform* of the Claude
|
|
101
|
+
tree, not a mirror: the 43 sub-command specs become reference files (Codex silently
|
|
102
|
+
truncates its skills block - see `cross-cli-contract.md` 2.6), every reference to a
|
|
103
|
+
CLI-owned tree is retargeted (`agents/<persona>.md` becomes `.toml`, the dispatcher
|
|
104
|
+
becomes the router skill), the 8 personas are regenerated as TOML with a model +
|
|
105
|
+
reasoning-effort map, and shared state (`logs/`, prefs, `knowledge/`) is deliberately
|
|
106
|
+
left under `~/.claude`.
|
|
107
|
+
|
|
108
|
+
That transform lives in `install/codex.mjs` and is gate-locked by
|
|
109
|
+
`smoke-codex-install.sh`. Describing it again in prose would give the pipeline two
|
|
110
|
+
definitions of the same thing, and the prose one would rot. So the step runs the
|
|
111
|
+
installer:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
cd "$HOME/multi-agent-pipeline" && node install.js --codex
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Verify:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ls -1 "$HOME/.codex/skills" | grep -c '^multi-agent$' # want 1
|
|
121
|
+
find "$HOME/.codex/multi-agent-refs/commands" -name SKILL.md | wc -l # want the command count
|
|
122
|
+
grep -rhoE '(\$HOME|~)/\.claude/(agents|scripts|lib|schemas|commands|multi-agent-refs|rules)' \
|
|
123
|
+
"$HOME/.codex/skills" "$HOME/.codex/multi-agent-refs" | sort -u # want empty
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
MCP is registered by the installer via `codex mcp add dev-toolkit` (idempotent,
|
|
127
|
+
skipped with a warning when `codex` is absent). Never hand-edit
|
|
128
|
+
`~/.codex/config.toml` - Codex owns it. Run this AFTER the REPO step, since the
|
|
129
|
+
installer reads the repo tree.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
96
133
|
## Dev-Toolkit Sync (Step 3d)
|
|
97
134
|
|
|
98
135
|
The companion MCP server (`dev-toolkit-mcp`) is its own repo with its own registry. Pipeline skills call its tools and several declare a minimum version, so when it moves it has to ship.
|
|
@@ -114,13 +151,19 @@ When the toolkit ships its own gate script (`npm run gates` / `scripts/gates.sh`
|
|
|
114
151
|
|
|
115
152
|
**Version bump**: patch for fixes and docs, minor for a new tool, major for a removed or renamed tool.
|
|
116
153
|
|
|
117
|
-
**Ship**: commit with that repo's own convention, tag `v<version>`, push with `--tags`, then publish to the registry from `publishConfig` using a throwaway userconfig - never edit `~/.npmrc`, never a bare `npm publish`. The token depends on the registry: `npm.pkg.github.com` needs a GitHub PAT with `write:packages
|
|
154
|
+
**Ship**: commit with that repo's own convention, tag `v<version>`, push with `--tags`, then publish to the registry from `publishConfig` using a throwaway userconfig - never edit `~/.npmrc`, never a bare `npm publish`. The token depends on the registry AND on the package scope: the registry picks the credential type, the scope picks the account. `npm.pkg.github.com` needs a **Classic** GitHub PAT with `write:packages`; `registry.npmjs.org` needs the `npm` key. Resolving on host alone misroutes on any machine with a work and a personal GitHub identity - prefer a scope-specific mapping (`github_<scope>`) and fall back to the generic `github` key only when there is one identity.
|
|
118
155
|
|
|
119
156
|
```bash
|
|
120
157
|
NPMRC=$(mktemp); trap 'rm -f "$NPMRC"' EXIT
|
|
121
158
|
REG=$(node -p "require('./package.json').publishConfig?.registry || 'https://registry.npmjs.org'")
|
|
122
159
|
HOST=${REG#https://}; HOST=${HOST%/}
|
|
123
|
-
|
|
160
|
+
SCOPE=$(node -p "(require('./package.json').name.match(/^@([^/]+)/)||[])[1] || ''")
|
|
161
|
+
case "$HOST" in
|
|
162
|
+
npm.pkg.github.com*) KEY=github; [ -n "$SCOPE" ] && KEY="github_$SCOPE" ;;
|
|
163
|
+
*) KEY=npm ;;
|
|
164
|
+
esac
|
|
165
|
+
# Fall back to the generic key when no scope-specific mapping exists.
|
|
166
|
+
bash "$HOME/.copilot/lib/credential-store.sh" get "$KEY" >/dev/null 2>&1 || KEY=github
|
|
124
167
|
TOKEN=$(bash "$HOME/.claude/lib/credential-store.sh" get "$KEY")
|
|
125
168
|
[ -n "$TOKEN" ] || echo "ABORT: no '$KEY' token - onboard it via /multi-agent:setup before publishing"
|
|
126
169
|
printf '%s\n' "registry=$REG" "//$HOST/:_authToken=$TOKEN" > "$NPMRC"
|
|
@@ -167,24 +210,27 @@ When invoked with the `release` argument:
|
|
|
167
210
|
7. DEV-TOOLKIT Ship the companion MCP server if it moved (Step 3d gates, then publish)
|
|
168
211
|
8. WEBSITE Version + features -> {website-host}
|
|
169
212
|
9. COPILOT Copilot CLI instructions + skills sync
|
|
213
|
+
9b. CODEX Codex CLI router skill + refs + agent TOML (node install.js --codex)
|
|
170
214
|
10. Report Summary: version, touched repos, deploy status
|
|
171
215
|
```
|
|
172
216
|
|
|
173
217
|
|
|
174
218
|
## Sub-Command Sync (Claude Code <-> Copilot CLI Skills)
|
|
175
219
|
|
|
220
|
+
> Codex takes the Step 2b path instead; see that section.
|
|
221
|
+
|
|
176
222
|
| Claude Code | Copilot CLI |
|
|
177
223
|
|-------------|-------------|
|
|
178
224
|
| `~/.claude/commands/multi-agent/{cmd}.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
|
|
179
225
|
|
|
180
|
-
**
|
|
226
|
+
**43 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
|
|
181
227
|
|
|
182
228
|
```
|
|
183
229
|
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, design-check, dev,
|
|
184
230
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, forget, garbage-collect,
|
|
185
231
|
help, issue, jira, kill, language, local,
|
|
186
232
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
187
|
-
routines, save, scan, search, setup, stack, status, sync, test, uninstall, update
|
|
233
|
+
routines, save, scan, search, setup, stack, status, sync, test, testflight-validation, uninstall, update
|
|
188
234
|
```
|
|
189
235
|
|
|
190
236
|
**NOT synced**: `refs/*` - Lazy-load references, Claude Code specific
|
|
@@ -200,8 +246,43 @@ routines, save, scan, search, setup, stack, status, sync, test, uninstall, updat
|
|
|
200
246
|
| `gh` CLI (personal) | `{owner}` gh auth (Keychain) |
|
|
201
247
|
| `gh` CLI (work) | `${USER}_{work-gh-alias}` gh auth (Keychain) |
|
|
202
248
|
| npm publish | `NODE_AUTH_TOKEN` - `GITHUB_TOKEN` in CI, Keychain PAT locally |
|
|
203
|
-
| dev-toolkit publish (Step 3d) | `
|
|
249
|
+
| dev-toolkit publish (Step 3d) | scope-resolved key (`github_<scope>`, else `github`, else `npm`) -> throwaway `--userconfig`, registry from that repo's `publishConfig` |
|
|
204
250
|
|
|
205
251
|
```bash
|
|
206
252
|
gh auth switch --user {owner} # for personal repos
|
|
207
253
|
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## Pre-flight the token scope
|
|
257
|
+
|
|
258
|
+
GitHub reports a token's scopes on any authenticated request, so check before
|
|
259
|
+
uploading instead of reading it out of a 403:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -sI -H "Authorization: token $TOKEN" https://api.github.com/user \
|
|
263
|
+
| grep -i '^x-oauth-scopes:' | grep -q 'write:packages'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Candidate order for `npm.pkg.github.com`, because storage location and scope are
|
|
267
|
+
not correlated: `github_<scope>` from the mapping, then `gh auth token -u <scope>`
|
|
268
|
+
(gh's own OAuth token often carries `write:packages` when a hand-made PAT does
|
|
269
|
+
not), then the generic `github` key only on a single-identity machine. Take the
|
|
270
|
+
first candidate whose scopes include `write:packages` AND whose login matches the
|
|
271
|
+
package scope. If none qualifies, stop before publishing and report each candidate
|
|
272
|
+
with the login it resolved to and the scope it was missing - that is the
|
|
273
|
+
actionable output, not a 403 body.
|
|
274
|
+
|
|
275
|
+
## Publish 403s: two causes, two fixes
|
|
276
|
+
|
|
277
|
+
Neither message says "wrong token" plainly, and they need opposite responses:
|
|
278
|
+
|
|
279
|
+
- `Unauthorized: As an Enterprise Managed User, you cannot access this content` -
|
|
280
|
+
the resolved token belongs to a corporate EMU account, which cannot publish to a
|
|
281
|
+
personal scope at all. No scope grant fixes this; map the personal account's PAT
|
|
282
|
+
under `github_<scope>`.
|
|
283
|
+
- `The token provided does not match expected scopes` - right account, missing
|
|
284
|
+
permission. Regenerate as a **Classic** PAT with `write:packages`
|
|
285
|
+
(plus `repo` for a private package) and re-onboard through setup.
|
|
286
|
+
|
|
287
|
+
Always report which of the two occurred. "Permission denied" alone sends the user
|
|
288
|
+
to regenerate a token that was never the problem.
|