fix(release): name the running model in the catalog commit via --model

The catalog commit hardcoded "Co-Authored-By: Claude Opus 4.8 (1M context)"
whatever model actually ran the release. A script cannot know its driver,
so the running session now states it with --model "<name>"; without the
flag the trailer reads "Claude" and names no model. A value with a newline
or angle brackets is refused before any tag or write. Operator decision
2026-11-04. Tests first (7 red on assertions), suite 238/238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-04 09:32:09 +01:00
commit e52805e317
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 71 additions and 4 deletions

View file

@ -25,6 +25,8 @@
// node scripts/release-plugin.mjs <name> --write # write the bumped catalog ref (no commit)
// node scripts/release-plugin.mjs <name> --write --commit # + git commit the catalog
// node scripts/release-plugin.mjs <name> --write --commit --push # + push
// ... --commit --model "Opus 5.5" # name the model running the release
// # in the commit trailer (else: "Claude" only)
//
// PUSHING NEEDS THE SAME ONE-SHOT TOKEN AS pre-push-gate.sh. That hook matches `git
// push` in command text and cannot see a push this script issues via execFileSync
@ -665,8 +667,24 @@ function observePlugin(catalogDir, name) {
return { repoDir, pluginVersion, readmeBadge, tags: gitTags(repoDir) };
}
function parseArgs(argv) {
const out = { name: null, version: null, write: false, commit: false, push: false, createTag: false };
// The catalog commit's attribution trailer (operator decision 2026-11-04). It used to
// hardcode `Claude Opus 4.8 (1M context)` whatever model actually ran the release — a
// claim nobody made, carried by every release commit since. A script cannot know which
// model drives it, so the running session states it with `--model "<name>"`; without
// it the trailer names Claude and no model: incomplete, never wrong. A value that would
// break the one-line trailer (a newline smuggling in a second trailer, or angle
// brackets that would forge the address part) is refused rather than written.
export function coAuthorTrailer(model) {
const raw = typeof model === 'string' ? model.trim() : '';
if (/[\r\n<>]/.test(raw)) {
throw new Error(`--model must be a plain model name on one line (e.g. "Opus 5.5"), got ${JSON.stringify(model)}`);
}
const name = raw.replace(/^claude\s+/i, '').trim();
return name ? `Co-Authored-By: Claude ${name} <noreply@anthropic.com>` : 'Co-Authored-By: Claude <noreply@anthropic.com>';
}
export function parseArgs(argv) {
const out = { name: null, version: null, write: false, commit: false, push: false, createTag: false, model: null };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--version') out.version = argv[++i];
@ -674,6 +692,7 @@ function parseArgs(argv) {
else if (a === '--commit') out.commit = true;
else if (a === '--push') out.push = true;
else if (a === '--create-tag') out.createTag = true;
else if (a === '--model') out.model = argv[++i] ?? null;
else if (!a.startsWith('--') && out.name === null) out.name = a;
}
return out;
@ -687,6 +706,12 @@ export function runRelease({ args, catalogDir, mktPath, marketplace, pushGate, r
const checkVersionsRunner = runCheckVersions
|| (() => execFileSync('node', [join(catalogDir, 'scripts', 'check-versions.mjs')], { cwd: catalogDir, encoding: 'utf8' }));
// Validate the attribution BEFORE anything touches a remote: a bad --model found only
// at commit time would arrive after --create-tag had already pushed the plugin tag.
let trailer;
try { trailer = coAuthorTrailer(args.model); }
catch (e) { console.error(`release-plugin: ${e.message}`); return 1; }
let obs = observePlugin(catalogDir, args.name);
const target = normalizeVersion(args.version ?? obs.pluginVersion ?? '');
const readmePath = join(catalogDir, 'README.md');
@ -757,7 +782,8 @@ export function runRelease({ args, catalogDir, mktPath, marketplace, pushGate, r
if (args.commit) {
const subject = plan.commitSubject ?? `chore(catalog): release ${plan.name} ${plan.newRef}`;
const body = `${plan.name} ${plan.newRef} — release. Catalog ref now pins the ${plan.newRef} tag so \`claude plugin update\` resolves the release.`;
const msg = `${subject}\n\n${body}\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n`;
const msg = `${subject}\n\n${body}\n\n${trailer}\n`;
if (!args.model) console.log(' (no --model given: the commit credits Claude without naming a model)');
execFileSync('git', ['-C', catalogDir, 'add', '.claude-plugin/marketplace.json', 'README.md'], { stdio: 'inherit' });
execFileSync('git', ['-C', catalogDir, 'commit', '-m', msg], { stdio: 'inherit' });
console.log(' ✓ committed the catalog');