From 85315488c93d543657aa18c48af1cd8d1b5fcd47 Mon Sep 17 00:00:00 2001 From: serkanalgur Date: Tue, 6 Oct 2026 17:58:01 +0400 Subject: [PATCH] fix: prohibit Co-Authored-By trailers unless explicitly requested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models hallucinate attribution trailers such as 'Co-Authored-By: Claude' even when the actual model is not Claude, which pollutes commit history and misattributes authorship. The rule is added in two layers: - Per-prompt: a '## Commits' section in every bundled subagent whose frontmatter allows shell (architect, coder, documenter, reviewer, explorer, tester) and in the orchestrator's Git Workflow section, which opens PRs. - Convention section: buildGitFlowConventionSection emits an unconditional 'No attribution trailers' clause appended to the seven bundled subagents inside a work tree when gitFlow is active. No gitFlow key gates it — authorship is not a knob. Tests pin both layers: agent-definitions asserts the rule in each shell-capable prompt and adds an invariant that any agent not explicitly shell-denied carries it; git-flow asserts the clause survives every combination of gitFlow sub-keys being off. CONTRIBUTING.md notes the rule for human contributors. --- CONTRIBUTING.md | 2 ++ assets/agents/nexus-architect.md | 4 +++ assets/agents/nexus-coder.md | 4 +++ assets/agents/nexus-documenter.md | 4 +++ assets/agents/nexus-explorer.md | 4 +++ assets/agents/nexus-orchestrator.md | 1 + assets/agents/nexus-reviewer.md | 4 +++ assets/agents/nexus-tester.md | 4 +++ src/index.ts | 1 + test/agent-definitions.test.ts | 42 ++++++++++++++++++++++++++++- test/git-flow.test.ts | 26 ++++++++++++++++++ 11 files changed, 95 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6d6ad6..02dd7d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,6 +148,8 @@ refactor: simplify DAG executor test: add unit tests for cost router ``` +Do not add `Co-Authored-By` or any attribution trailer to commits or PRs unless a maintainer explicitly asks for it. Do not infer authorship from the model you are using. + ### Code Style - TypeScript strict mode — `tsconfig.json` sets `"strict": true` diff --git a/assets/agents/nexus-architect.md b/assets/agents/nexus-architect.md index ab6f7e3..7d63ec5 100644 --- a/assets/agents/nexus-architect.md +++ b/assets/agents/nexus-architect.md @@ -43,6 +43,10 @@ You are a senior software architect. You design systems that are scalable, resil - Synchronous chains that create tight coupling - Over-engineering simple problems (YAGNI) +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract Return a written architecture direction, not code. It must contain: diff --git a/assets/agents/nexus-coder.md b/assets/agents/nexus-coder.md index 30cb190..06abd63 100644 --- a/assets/agents/nexus-coder.md +++ b/assets/agents/nexus-coder.md @@ -48,6 +48,10 @@ You are a senior software engineer who writes clean, maintainable, production-re - Tests covering happy path, edge cases, and error paths - Brief inline comments for complex logic (why, not what) +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract Return what you changed and how it was verified. It must contain: diff --git a/assets/agents/nexus-documenter.md b/assets/agents/nexus-documenter.md index bb867dc..3c1aabf 100644 --- a/assets/agents/nexus-documenter.md +++ b/assets/agents/nexus-documenter.md @@ -49,6 +49,10 @@ You are a technical writer who creates documentation that developers actually wa - TODO/FIXME/HACK comments are tracked and explained - Changelog follows semantic versioning with clear descriptions +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract Documentation that is true of the code as it is now. It must contain: diff --git a/assets/agents/nexus-explorer.md b/assets/agents/nexus-explorer.md index ad3128d..7049ae2 100644 --- a/assets/agents/nexus-explorer.md +++ b/assets/agents/nexus-explorer.md @@ -39,6 +39,10 @@ You are a code archaeologist. You navigate unknown codebases efficiently and bui - Report uncertainty explicitly — don't guess about unexamined code - Cite specific file paths and line numbers for all findings +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract A map, with evidence. It must contain: diff --git a/assets/agents/nexus-orchestrator.md b/assets/agents/nexus-orchestrator.md index 640ffcf..554cf1a 100644 --- a/assets/agents/nexus-orchestrator.md +++ b/assets/agents/nexus-orchestrator.md @@ -206,6 +206,7 @@ When code changes are needed, follow this workflow: - One logical change per commit - Imperative mood in commit message - Reference issues if applicable +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to commits or PR descriptions unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. ### 4. Pull Request - Create PR with descriptive title and body diff --git a/assets/agents/nexus-reviewer.md b/assets/agents/nexus-reviewer.md index 3697b8d..92254bc 100644 --- a/assets/agents/nexus-reviewer.md +++ b/assets/agents/nexus-reviewer.md @@ -52,6 +52,10 @@ For each finding: - Be thorough — check for issues the author might have missed - Prioritize — Critical/High issues first, then Medium/Low +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract Findings, ordered by severity, and nothing else. Each finding must carry: diff --git a/assets/agents/nexus-tester.md b/assets/agents/nexus-tester.md index fd10123..c24f181 100644 --- a/assets/agents/nexus-tester.md +++ b/assets/agents/nexus-tester.md @@ -47,6 +47,10 @@ You are a QA engineer who writes tests that catch real bugs, not just increase c - Edge case coverage alongside happy path - Mock/stub strategy that doesn't hide real bugs +## Commits + +- NEVER add `Co-Authored-By` or any attribution trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use. + ## Output Contract Tests plus evidence they run. It must contain: diff --git a/src/index.ts b/src/index.ts index 3093ba6..c53b4d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -896,6 +896,7 @@ export function buildGitFlowConventionSection( lines.push( '- **Never commit, push, or merge on your own initiative.** Report the change you made and let the user decide. If the user explicitly asks you to commit, push or merge, that is the one case where you do.', + '- **No attribution trailers.** Never add `Co-Authored-By` or any similar trailer (e.g. `Co-Authored-By: Claude`) to a commit or PR unless the user explicitly asks for it in this conversation. Do not infer authorship from the model in use.', '', 'You can check where things stand with `nexus.git.check`. It only reports — it never changes anything and never refuses.', '', diff --git a/test/agent-definitions.test.ts b/test/agent-definitions.test.ts index 157f389..aa90464 100644 --- a/test/agent-definitions.test.ts +++ b/test/agent-definitions.test.ts @@ -154,4 +154,44 @@ describe('each role states an output contract and when to escalate', () => { expect(content).toMatch(/^## When to Escalate$/m) } ) -}) \ No newline at end of file +}) + +describe('commit-capable prompts prohibit attribution trailers', () => { + // Roles whose frontmatter grants `shell` can run `git commit`; the + // orchestrator opens PRs. Models hallucinate trailers such as + // "Co-Authored-By: Claude" even when the model is not Claude, so the rule is + // pinned per prompt. Only designer denies `shell`; reviewer and explorer + // deny only `edit`, and OpenCode's permission resolution falls through to a + // catch-all allow for them — they CAN run `git commit`, so they carry the + // rule too. Designer's protection is the appended git-convention section + // (`buildGitFlowConventionSection`), covered in `test/git-flow.test.ts`. + const COMMIT_CAPABLE = ['nexus-coder.md', 'nexus-tester.md', 'nexus-documenter.md', 'nexus-architect.md', 'nexus-reviewer.md', 'nexus-explorer.md', ORCHESTRATOR] + + it.each(COMMIT_CAPABLE)( + '%s forbids Co-Authored-By and similar trailers unless the user asks', + (name) => { + // One anchored regex, not three loose substrings: the substring version + // passes for an INVERTED rule ("never add ... unless ... Do not + // infer ..."), because each fragment is still present somewhere in the + // file. The anchor ties the fragments together in the order the rule + // states them, so an inversion fails here. + const content = readAgent(name) + expect(content).toMatch( + /NEVER add `Co-Authored-By`[\s\S]{0,120}unless the user explicitly asks for it in this conversation\. Do not infer authorship from the model in use\./ + ) + } + ) + + it('every agent that can shell carries the attribution rule', () => { + // OpenCode's permission resolution falls through to a catch-all allow, so + // an agent that never names `shell` can still run `git commit`. Stated + // that way round on purpose: unless the frontmatter explicitly denies + // `shell`, the prompt must carry the rule. This is the invariant that + // would have caught reviewer and explorer missing it. + for (const name of [...SPAWNABLE_ROLES.map((r) => `nexus-${r}.md`), ORCHESTRATOR]) { + const frontmatter = frontmatterOf(readAgent(name)) + const shellDenied = /action:\s*shell[\s\S]*?effect:\s*deny/.test(frontmatter) + if (!shellDenied) expect(readAgent(name)).toMatch(/Co-Authored-By/) + } + }) +}) diff --git a/test/git-flow.test.ts b/test/git-flow.test.ts index 4ba6741..d3f0202 100644 --- a/test/git-flow.test.ts +++ b/test/git-flow.test.ts @@ -757,6 +757,32 @@ describe('the agent-markdown convention', () => { expect(section).toContain('Never commit, push, or merge on your own initiative') }) + it('never emits an attribution-trailer clause with any sub-key off', () => { + // The no-Co-Authored-By rule is unconditional for the same reason the + // unprompted-commit rule is: models hallucinate trailers such as + // "Co-Authored-By: Claude" even when the model is not Claude, and no + // gitFlow key is a knob for authorship. Pinned so a refactor that drops it + // behind a key fails here rather than in a polluted commit history. + // + // `gitFlow.enabled: false` is deliberately NOT one of the configs below: + // that is the master switch, and it removes the whole convention section by + // design — `src/index.ts` gates `buildGitFlowConventionSection` on + // `gitFlowResolved.active`. What this pins is that no SUB-key can disable + // the clause. + for (const config of [ + ALL_ON, + { ...ALL_ON, requireBranch: false }, + { ...ALL_ON, conventionalCommits: false }, + { ...ALL_ON, prBeforeMerge: false }, + { ...ALL_ON, requireBranch: false, conventionalCommits: false, prBeforeMerge: false }, + ]) { + const section = buildGitFlowConventionSection(config, { isRepo: true, repoRoot: '/x', repoId: '/x/.git', branch: 'feat/y', detached: false, hasRemote: true, remote: 'origin', known: true }) + expect(section).toContain('No attribution trailers') + expect(section).toContain('Co-Authored-By') + expect(section).toContain('unless the user explicitly asks') + } + }) + it('tells the agent the section can be turned off, and names the check as read-only', () => { const section = buildGitFlowConventionSection(ALL_ON, { isRepo: true, repoRoot: '/x', repoId: '/x/.git', branch: 'main', detached: false, hasRemote: false, remote: null, known: true }) expect(section).toContain('gitFlow.enabled')