ci: cut PR CI to the fast required set; welcome contributor PRs [skip ci]

Founder direction (2026-11-07): remove CI/CD steps that cost time without
need, use CI conservatively, and make contributor rules standard and
welcoming instead of closing or blocking PRs over CI. A read-only audit of
two weeks of run history (Sonnet workflow with an adversarial release-
engineer critique) drove the list; nothing release-critical is touched.

Removed (enforced nothing, duplicated other checks, or gated contributors):
- dco.yml (always exited 0), pr-issue-link.yml (red X on ~8% of contributor
  PRs), auto-close-harvested.yml, pr-gate.yml / issue-gate.yml /
  approve-contributor.yml and .github/APPROVED_CONTRIBUTORS (dry-run
  allowlist gate; drops a pull_request_target surface), agent-task-labels.yml
  (duplicates triage.yml).

Moved to manual (workflow_dispatch) only:
- codewhale-review.yml (review bot; AGENTS.md says review bots stay off),
  main-landing.yml (push trigger; run scripts/preflight.sh --check locally),
  ci.yml test (Linux suite, 54-94 min per PR), safety-gate (18-29 min), msrv,
  mobile-smoke, npm-wrapper-smoke (macOS leg dropped from the dispatch matrix;
  the release artifacts cover macOS).
- The PR feature release-note receipt step is advisory (continue-on-error).
- spam-lockdown keeps only unambiguous spam patterns.

Unchanged: Lint and the compile-only Test (windows-latest) stay the
required PR checks; release-candidate, release-artifacts, release-parity,
release, the mirrors, OHOS and the Windows import guard are untouched. The
branch ruleset drops "npm wrapper smoke (ubuntu-latest)" next (founder
approved "Lint + 1 compile").

Rules: AGENTS.md, CONTRIBUTING.md, the PR template, AGENT_ETHOS.md and the
cw-land skill now say contributor PRs are never closed or blocked because
CI is slow, flaky or red for a reason they did not cause; nothing closes a
PR automatically; harvesting is a rare, asked-first fallback with the
contributor as author; the PR issue line is optional; maintainers reply
generously. web/data/agent-docs.generated.json is regenerated from a clean
export of main plus this change only.

Verified: actionlint with CI's flags (-ignore SC2129/SC2221/SC2222) over all
workflows passes; web/scripts/check-facts.mjs passes on the isolated export;
only the contribution entry of agent-docs.generated.json changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-11-07 13:33:36 -08:00
1 parent 92c268959e
commit 99cb5a0f4f
18 files changed
+96 -1054

No files matched your search

-77
View File
@@ -1,77 +0,0 @@
# Scoped contribution-gate allowlist.
#
# Maintainers and collaborators bypass the gate automatically. Use this file
# for external contributors who are allowed through the automated front door.
# Seed active contributors here before switching the gate workflows to enforce mode.
#
# Supported entries:
# pr:username
# issue:username
# all:username
all:hmbown
all:reidliu41
all:ousamabenyounes
all:ljm3790865
all:HUQIANTAO
all:xyuai
all:merchloubna70-dot
all:h3c-hexin
all:axobase001
all:donglovejava
all:Oliver-ZPLiu
all:idling11
all:angziii
all:aboimpinto
all:encyc
all:Duducoco
all:cyq1017
all:zlh124
all:THINKER-ONLY
all:nightt5879
all:Liu-Vince
all:JiarenWang
all:wdw8276
all:pengyou200902
all:linzhiqin2003
all:LING71671
all:JasonOA888
all:Inference1
all:hongqitai
all:gordonlu
all:gaord
all:zhuangbiaowei
all:yuanchenglu
all:Vishnu1837
all:sximelon
all:Sskift
all:New2Niu
all:shenjackyuanjie
all:AdityaVG13
all:mvanhorn
all:MengZ-super
all:membphis
all:LeoAlex0
all:Lee-take
all:lbcheng888
all:Implementist
all:jrcjrcc
all:yusufgurdogan
all:kunpeng-ai-lab
all:elowen53
all:CrepuscularIRIS
all:chnjames
all:ChaceLyee2101
all:AresNing
pr:dmitri-0
pr:sparkofspike
pr:angel-hair
pr:wuisabel-gif
all:whp233
all:musichen
all:M-Maciej
all:serephus
all:Pinvou
all:SparkofSpike
all:goransh-walia
all:xiechimon
all:yrk111222
+6 -11
View File
@@ -4,22 +4,17 @@
<!-- What does this change, and why? A few sentences is enough. -->
## Issue
## Related issue (optional)
<!-- CI needs one line here. Replace N with a number:
- Closes #N this PR finishes the issue (Fixes / Resolves also work)
- Refs #N related or partial work; the issue stays open
- No-Issue: <one-line reason> for typos, chores, dependency bumps
Only write a closing word when you mean it. GitHub closes the issue even in
"this does not close ..." and CI rejects that wording.
Put issue numbers here rather than in `feat:` commit messages: CI requires a
`feat:` commit that mentions #N to also add #N to CHANGELOG.md. -->
<!-- Closes #N if this finishes it, Refs #N if related. Skip it if there is none.
Please do not write a closing word inside a negation ("this does not close #N"):
GitHub closes the issue anyway. -->
## How I tested it
<!-- Commands you ran and what they showed. Test what you changed, for example
`scripts/dev-test.sh tui <filter>`; the PR checks run fmt, clippy, the Linux
suite and a Windows build for you (CONTRIBUTING.md lists them).
`scripts/dev-test.sh tui <filter>`. A few quick checks run on your PR; if one
fails for a reason unrelated to your change, a maintainer will deal with it.
For visible UI changes, add a screenshot or recording. -->
## Checklist
-67
View File
@@ -1,67 +0,0 @@
name: Agent task labels
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
labels:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Add area hints to agent tasks
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
if (!issue) return;
const labels = new Set((issue.labels || []).map(label => label.name));
if (!labels.has('agent-ready')) {
core.info(`#${issue.number} is not an agent task; leaving it unchanged.`);
return;
}
// Milestones are the canonical release roadmap and are assigned
// explicitly during triage. Never infer one from a title or label:
// old version labels must remain harmless historical metadata.
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title}\n${body}`;
if (/\bworkflow\b|whaleflow|workflow-runtime/.test(text)) labels.add('workflow-runtime');
// `tui` means "touches the terminal UI layer", not "mentions the TUI
// in passing". A bare \btui\b over-fired on every issue that merely
// referenced the UI — the v0.9.2 localization issues all describe TUI
// locale packs in prose and were labelled on that alone.
//
// Anchor on signals that survive the single-binary refactor (#4747):
// the crate path and the rendering framework stay put when
// codewhale-tui is lib-ized, whereas the `codewhale-tui` *binary*
// name goes away. Do not reintroduce a bare word match here.
if (/crates\/tui\b|ratatui|\bcodewhale-tui\b/.test(text)) labels.add('tui');
if (/fleet|subagent|sub-agent/.test(text)) labels.add('subagents');
if (/catalog|openrouter|model.?picker|provider.?lake/.test(text)) labels.add('reliability');
const existing = await github.paginate(github.rest.issues.listLabelsForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const existingNames = new Set(existing.map(label => label.name));
const currentNames = new Set((issue.labels || []).map(label => label.name));
const toAdd = [...labels].filter(
name => existingNames.has(name) && !currentNames.has(name)
);
if (toAdd.length === 0) return;
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: toAdd,
});
-216
View File
@@ -1,216 +0,0 @@
name: Approve gated contributor
on:
issue_comment:
types: [created]
permissions: {}
jobs:
approve:
# Cheap pre-filter before any token is minted: this workflow fires on
# every issue comment, but only a maintainer's lgtm/lgtmi command does
# anything. Expression string comparisons are case-insensitive; the
# script below still performs the exact trimmed-command match.
if: >-
contains(github.event.comment.body, 'lgtm')
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
issues: write
pull-requests: write
# Job-level so skipped runs never enter the group: a workflow-level group
# let any unrelated comment queue and cancel a pending approval run.
concurrency:
group: contribution-gate-approval
cancel-in-progress: false
steps:
- name: Open allowlist update PR
uses: actions/github-script@v9
with:
script: |
const comment = context.payload.comment;
const issue = context.payload.issue;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
const command = (comment.body || '').trim().toLowerCase();
const scopeByCommand = new Map([
['/lgtm', 'pr'],
['lgtm', 'pr'],
['/lgtmi', 'issue'],
['lgtmi', 'issue'],
]);
const scope = scopeByCommand.get(command);
// Answer the maintainer's command with a reaction, never a comment
// (founder, 2026-09-22). The run log carries the detail, and the
// allowlist PR body links back here, so the thread still shows it.
async function react(content, message) {
core.notice(message);
await github.rest.reactions.createForIssueComment({
owner,
repo,
comment_id: comment.id,
content,
});
}
if (!scope) return;
if (!privileged.has(comment.author_association)) return;
if (scope === 'pr' && !issue.pull_request) {
await react('confused', '`/lgtm` grants PR access and must be used on a pull request. Use `/lgtmi` to grant issue access.');
return;
}
if (scope === 'issue' && issue.pull_request) {
await react('confused', '`/lgtmi` grants issue access and must be used on an issue. Use `/lgtm` to grant PR access.');
return;
}
const path = '.github/APPROVED_CONTRIBUTORS';
const targetLogin = issue.user.login;
const normalizedLogin = targetLogin.toLowerCase();
const entry = `${scope}:${normalizedLogin}`;
const branchSlug = normalizedLogin.replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'contributor';
const defaultContent = [
'# Scoped contribution-gate allowlist.',
'#',
'# Maintainers and collaborators bypass the gate automatically. Use this file',
'# for external contributors who are allowed through the automated front door.',
'# Seed active contributors here before switching the gate workflows to enforce mode.',
'#',
'# Supported entries:',
'# pr:username',
'# issue:username',
'# all:username',
'',
].join('\n');
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
const { data: repoData } = await github.rest.repos.get({ owner, repo });
const defaultBranch = repoData.default_branch;
const { data: baseRef } = await github.rest.git.getRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
});
const baseSha = baseRef.object.sha;
const { data: baseCommit } = await github.rest.git.getCommit({
owner,
repo,
commit_sha: baseSha,
});
let content = defaultContent;
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path,
ref: defaultBranch,
});
if (!Array.isArray(data) && data.type === 'file') {
content = Buffer.from(data.content, data.encoding || 'base64').toString('utf8');
}
} catch (error) {
if (error.status !== 404) throw error;
}
const existing = parseAllowlist(content);
if (existing.has(entry) || existing.has(`all:${normalizedLogin}`)) {
await react('eyes', `@${targetLogin} is already approved for ${scope} contributions in \`${path}\`.`);
return;
}
const openPrs = [];
for (let page = 1; ; page++) {
const { data: pagePrs } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
per_page: 100,
page,
});
openPrs.push(...pagePrs);
if (pagePrs.length < 100) break;
}
const repoFullName = `${owner}/${repo}`.toLowerCase();
const pendingPr = openPrs.find(openPr => {
const sameRepo = (openPr.head?.repo?.full_name || '').toLowerCase() === repoFullName;
const body = openPr.body || '';
return sameRepo && body.includes(`Adds \`${entry}\` to \`${path}\`.`);
});
if (pendingPr) {
await react('eyes', `@${targetLogin} already has a pending allowlist update PR for ${scope} contributions: ${pendingPr.html_url}`);
return;
}
const nextContent = `${content.trimEnd()}\n${entry}\n`;
const { data: blob } = await github.rest.git.createBlob({
owner,
repo,
content: nextContent,
encoding: 'utf-8',
});
const { data: tree } = await github.rest.git.createTree({
owner,
repo,
base_tree: baseCommit.tree.sha,
tree: [
{
path,
mode: '100644',
type: 'blob',
sha: blob.sha,
},
],
});
const branchName = `contribution-gate/${scope}-${branchSlug}-${Date.now()}`;
await github.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: baseSha,
});
const { data: commit } = await github.rest.git.createCommit({
owner,
repo,
message: `chore: approve @${targetLogin} for ${scope} contributions`,
tree: tree.sha,
parents: [baseSha],
});
await github.rest.git.updateRef({
owner,
repo,
ref: `heads/${branchName}`,
sha: commit.sha,
});
const { data: pr } = await github.rest.pulls.create({
owner,
repo,
title: `chore: approve @${targetLogin} for ${scope} contributions`,
head: branchName,
base: defaultBranch,
body: [
`Adds \`${entry}\` to \`${path}\`.`,
'',
`Requested by @${comment.user.login} in #${issue.number}.`,
].join('\n'),
});
await react('rocket', `Created allowlist update PR: ${pr.html_url}`);
-154
View File
@@ -1,154 +0,0 @@
name: Auto-close harvested PRs
# When a commit on main contains a "Harvested from PR #N" line in its
# message, close PR #N with a templated thank-you that links back to
# the merged commit. Solves the long-standing problem where contributor
# PRs whose code lands via maintainer cherry-pick stay open and
# `CONFLICTING` forever, even though their fix is credited in the
# CHANGELOG.
#
# The expected commit-message convention is documented in
# CONTRIBUTING.md. Two patterns are recognised:
#
# * `Harvested from PR #1234 by @username` (preferred)
# * `harvested from #1234` (case-insensitive fallback)
#
# The first match's PR number is closed; multiple PRs can be closed
# per commit by repeating the line. The match runs on the commit
# body only, not on the subject line, so the subject can describe
# the change naturally without baking a number into it.
on:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
issues: write
# Only one auto-close run at a time so two near-simultaneous main
# pushes can't both try to close the same PR (the second would just
# fail with "Pull request is already closed", harmless but noisy).
concurrency:
group: auto-close-harvested
cancel-in-progress: false
jobs:
close:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
# We need at least the commits that this push introduced.
# fetch-depth: 0 is the simplest correct option; the
# alternative (fetching just `before..after`) is fragile
# when force-pushes happen.
fetch-depth: 0
- name: Close PRs referenced by harvested-from lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.event.after }}
shell: bash
run: |
set -euo pipefail
# The first push to a fresh branch has BEFORE_SHA = 0000…0000.
# In that case fall back to the latest commit only — we don't
# want to scan the entire history.
if [[ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" || -z "${BEFORE_SHA:-}" ]]; then
RANGE="${AFTER_SHA}"
RANGE_ARGS=("-1" "${AFTER_SHA}")
else
RANGE="${BEFORE_SHA}..${AFTER_SHA}"
RANGE_ARGS=("${RANGE}")
fi
echo "Scanning commit range: ${RANGE}"
# `git log --format=%H%n%B%n--END--` separates commits with a
# sentinel so multi-line bodies don't get mangled.
mapfile -t commits < <(git log "${RANGE_ARGS[@]}" --format="%H")
if [[ ${#commits[@]} -eq 0 ]]; then
echo "No commits in range; nothing to do."
exit 0
fi
declare -A processed_prs=()
for sha in "${commits[@]}"; do
body="$(git log -1 --format=%B "${sha}")"
# Two patterns, both case-insensitive on the keyword:
# "Harvested from PR #1234 by @username" (preferred form)
# "harvested from #1234" (short form)
mapfile -t pr_numbers < <(
printf '%s\n' "${body}" \
| grep -oiE 'harvested from (pr )?#[0-9]+' \
| grep -oE '#[0-9]+' \
| tr -d '#' \
| sort -u || true
)
if [[ ${#pr_numbers[@]} -eq 0 ]]; then
continue
fi
short_sha="${sha:0:12}"
subject="$(git log -1 --format=%s "${sha}")"
for pr in "${pr_numbers[@]}"; do
key="${pr}-${sha}"
if [[ -n "${processed_prs[${key}]:-}" ]]; then
continue
fi
processed_prs[${key}]=1
# Idempotency: skip if the PR is already closed.
state="$(gh pr view "${pr}" --json state --jq .state 2>/dev/null || echo "MISSING")"
if [[ "${state}" == "CLOSED" || "${state}" == "MERGED" ]]; then
echo "PR #${pr} is already ${state}; skipping."
continue
fi
if [[ "${state}" == "MISSING" ]]; then
echo "::warning::PR #${pr} not found or inaccessible; skipping."
continue
fi
author="$(gh pr view "${pr}" --json author --jq '.author.login' 2>/dev/null || echo "")"
greeting="Hi"
if [[ -n "${author}" ]]; then
greeting="Thanks @${author}"
fi
# NOTE: this block intentionally avoids `<<EOF` heredocs.
# YAML's `|` block scalar requires consistent indentation,
# but heredoc bodies have to start at column 0 — those two
# constraints can't coexist in the same file. We assemble
# the body with `printf` + `\n` so every line of the
# message lives at the same indent as the surrounding
# shell code.
commit_url="https://github.com/${GITHUB_REPOSITORY}/commit/${sha}"
contributing_url="https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md"
body_text="$(printf '%s\n' \
"${greeting} — your contribution landed in [\`${short_sha}\`](${commit_url}) on \`main\`:" \
"" \
"> ${subject}" \
"" \
"Closing this PR now that the code is on \`main\`. Credit lives in the commit message and (where applicable) the \`CHANGELOG.md\` entry for the next release. Apologies for not closing this at the time of the merge — the auto-close workflow is new in v0.8.31." \
"" \
"If you want to land more work and would prefer your future PRs merge cleanly without a harvest step, the [\`CONTRIBUTING.md\`](${contributing_url}) doc has a short note on what makes a contribution mergeable as-is." \
)"
echo "Closing PR #${pr} (harvested in ${short_sha})"
if ! gh pr close "${pr}" \
--repo "${GITHUB_REPOSITORY}" \
--comment "${body_text}"; then
echo "::warning::Failed to close PR #${pr}; continuing"
fi
done
done
echo "Auto-close pass complete."
+10 -15
View File
@@ -215,12 +215,11 @@ jobs:
# missing receipt actually matters.
run: ./scripts/release/check-versions.sh --range-audit-advisory
- name: Check this PR's feature release-note receipts
# The range audit above is advisory because previous-tag..HEAD blames
# every open PR for receipts other merges forgot. This is the same
# check scoped to the PR's own commits, so it blocks: a `feat:` commit
# that references #N must add #N to CHANGELOG.md in the same PR.
# Advisory, like the range audit above: a missing CHANGELOG receipt
# shows as a warning and maintainers add it while landing.
# Locally: scripts/preflight.sh.
if: github.event_name == 'pull_request'
continue-on-error: true
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: ./scripts/release/check-feature-release-notes.sh "${PR_BASE_SHA}" HEAD
@@ -422,7 +421,7 @@ jobs:
safety-gate:
name: Safety gate
needs: changes
if: needs.changes.outputs.heavy == 'true'
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
@@ -726,7 +725,7 @@ jobs:
# fail the build there while stable stays green (#6543).
name: MSRV check (1.89)
needs: changes
if: needs.changes.outputs.heavy == 'true'
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
@@ -775,6 +774,7 @@ jobs:
test:
name: Test
needs: changes
if: github.event_name == 'workflow_dispatch'
# Contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
# "Test (windows-latest)" derive from job name + matrix.os and are
# independent of runs-on. For light changes the macOS/Windows legs only
@@ -1282,7 +1282,7 @@ jobs:
npm-wrapper-smoke:
name: npm wrapper smoke
needs: changes
if: github.event_name != 'schedule'
if: github.event_name == 'workflow_dispatch'
# Same ternary rationale as the Test job: light legs only echo, so keep
# them off macOS/Windows runners. On pull_request the matrix is
# ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
@@ -1298,7 +1298,7 @@ jobs:
runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || github.event_name == 'workflow_dispatch' && '["ubuntu-latest","windows-latest"]' || '["ubuntu-latest","windows-latest"]') }}
steps:
- name: Skip npm wrapper smoke for light change
if: needs.changes.outputs.heavy != 'true'
@@ -1358,13 +1358,8 @@ jobs:
mobile-smoke:
name: Mobile runtime smoke
needs: changes
# Not a required PR context. Pull requests run it only when the mobile
# runtime surface changed (see the `mobile` filter above); every push to
# main runs it unconditionally as the pre-release safety net.
if: >-
github.event_name != 'schedule' &&
needs.changes.outputs.heavy == 'true' &&
(github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
# Manual (workflow_dispatch) only; the release candidate builds android-arm64.
if: github.event_name == 'workflow_dispatch' && needs.changes.outputs.heavy == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
-6
View File
@@ -1,11 +1,5 @@
name: Codewhale PR Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
workflow_dispatch:
inputs:
pr-number:
-57
View File
@@ -1,57 +0,0 @@
name: DCO
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
jobs:
dco:
name: Check Signed-off-by
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Report missing Signed-off-by (dry-run advisory)
shell: bash
run: |
set -euo pipefail
base_rev=$(git merge-base origin/${{ github.base_ref }} HEAD 2>/dev/null || echo "HEAD~1")
echo "✅ merge-base: $base_rev"
echo ""
bad=()
while IFS= read -r commit; do
msg=$(git log --format=%B -n1 "$commit")
if ! echo "$msg" | grep -qi "^Signed-off-by:"; then
bad+=("$commit")
fi
done < <(git rev-list "${base_rev}..HEAD")
if [ ${#bad[@]} -eq 0 ]; then
echo "✅ All commits have Signed-off-by."
exit 0
fi
echo "⚠️ Advisory — the following commit(s) are missing Signed-off-by:"
for c in "${bad[@]}"; do
echo " • $c $(git log --format=%s -n1 "$c")"
done
echo ""
echo "This check is advisory and does not block merging."
echo "Please amend commits with:"
echo " git commit --amend -s"
echo "See CONTRIBUTING.md for details."
echo ""
echo "::warning title=DCO: missing Signed-off-by::Some commits are missing Signed-off-by — amend with git commit --amend -s"
# Dry-run: always pass, but surface the warning
exit 0
-90
View File
@@ -1,90 +0,0 @@
name: Contribution intake - issues
on:
issues:
types: [opened, reopened]
permissions:
contents: read
issues: write
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
# Labels only, never comments (founder, 2026-09-22): the intake note used
# to thank other bots, and a comment is noise a label does not make.
# Maintainers still see who needs triage; `/lgtmi` still skips it.
- name: Label new external issues for triage
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
async function label(name, color, description) {
try {
await github.rest.issues.createLabel({ owner, repo, name, color, description });
} catch (error) {
if (error.status !== 422) throw error; // 422: label already exists
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: [name],
});
}
if (privileged.has(issue.author_association)) return;
// `user.type` is set by GitHub for app and bot accounts and cannot
// be spoofed by a login that merely ends in "[bot]".
if (issue.user.type === 'Bot') {
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
return;
}
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
async function readAllowlist() {
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: '.github/APPROVED_CONTRIBUTORS',
ref: context.payload.repository.default_branch,
});
if (Array.isArray(data) || data.type !== 'file') return new Set();
return parseAllowlist(
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
);
} catch (error) {
if (error.status === 404) return new Set();
throw error;
}
}
const allowlist = await readAllowlist();
const login = issue.user.login.toLowerCase();
if (
allowlist.has(`all:${login}`) ||
allowlist.has(`issue:${login}`)
) {
return;
}
await label(
'needs-triage',
'fbca04',
'New external report awaiting maintainer triage; repro, logs and version output help'
);
+2 -5
View File
@@ -22,11 +22,8 @@ name: Main landing gate
# catch a red landing in minutes, not to re-run the release gate on every push.
on:
push:
branches: [main]
# Also manually runnable: GitHub does not evaluate a workflow on the push that
# introduces it, so the first run of this file would otherwise wait for the
# next landing. A maintainer can also re-run the gate after an infra outage.
# Manual only: run it after a landing you want checked or after an infra
# outage. Before pushing, run scripts/preflight.sh --check locally.
workflow_dispatch:
permissions:
-110
View File
@@ -1,110 +0,0 @@
name: Contribution gate - pull requests
on:
pull_request_target:
types: [opened, reopened]
permissions:
contents: read
issues: write
pull-requests: write
env:
# Keep new gates observable first. Switch to "enforce" only after maintainers
# have seeded active contributors and reviewed the dry-run signal.
CONTRIBUTION_GATE_MODE: dry-run
jobs:
gate:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Gate unapproved external pull requests
uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const privileged = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
const gateMode = (process.env.CONTRIBUTION_GATE_MODE || 'dry-run').trim().toLowerCase();
const enforceGate = gateMode === 'enforce';
if (!['dry-run', 'enforce'].includes(gateMode)) {
core.warning(`Unknown CONTRIBUTION_GATE_MODE "${gateMode}"; defaulting to dry-run.`);
}
async function label(name, color, description) {
try {
await github.rest.issues.createLabel({ owner, repo, name, color, description });
} catch (error) {
if (error.status !== 422) throw error; // 422: label already exists
}
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: [name],
});
}
if (privileged.has(pr.author_association)) return;
// `user.type` is set by GitHub for app and bot accounts and cannot
// be spoofed by a login that merely ends in "[bot]".
if (pr.user.type === 'Bot') {
await label('bot-authored', 'ededed', 'Opened by a bot or app account');
return;
}
function parseAllowlist(content) {
return new Set(
content
.split(/\r?\n/)
.map(line => line.replace(/#.*/, '').trim().toLowerCase())
.filter(Boolean)
);
}
async function readAllowlist() {
try {
const { data } = await github.rest.repos.getContent({
owner,
repo,
path: '.github/APPROVED_CONTRIBUTORS',
ref: context.payload.repository.default_branch,
});
if (Array.isArray(data) || data.type !== 'file') return new Set();
return parseAllowlist(
Buffer.from(data.content, data.encoding || 'base64').toString('utf8')
);
} catch (error) {
if (error.status === 404) return new Set();
throw error;
}
}
const allowlist = await readAllowlist();
const login = pr.user.login.toLowerCase();
if (
allowlist.has(`all:${login}`) ||
allowlist.has(`pr:${login}`)
) {
return;
}
// Labels only (founder, 2026-09-22). The label description carries
// the contributor-facing explanation a comment used to.
await label(
'contribution-gate',
'c5def5',
'Author not yet in .github/APPROVED_CONTRIBUTORS; a maintainer grants access with /lgtm'
);
if (!enforceGate) return;
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: 'closed',
});
-109
View File
@@ -1,109 +0,0 @@
name: PR closes an issue
# 342 open issues, 329 of them touched within the month: nothing here is rotting,
# the drain is just clogged. Only 8 of 35 open PRs carried a closing keyword, so
# work ships and its issue stays open, and nobody can tell which of the 342 are
# already done. That is how 121 issues end up on one milestone.
#
# This check asks every PR to close an issue, reference one with `Refs #N`, or
# say why it has none (`No-Issue:`). The opt-out is one line, so this is a
# prompt, not a wall. It also fails a negated closing keyword ("does not close
# #N"), which GitHub would otherwise act on and close the issue.
on:
pull_request:
types: [opened, edited, reopened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
link:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Automated dependency bumps (dependabot and any other GitHub-verified
# bot account) are machine-generated and can never carry a closing
# keyword; failing them here would require hand-editing every bot body,
# which defeats the automation. The gate stays strict for every human
# PR. `user.type` is set by GitHub for verified bot accounts, so a PR
# author cannot spoof it to dodge the check.
- name: Require an issue link or an explicit opt-out
if: github.event.pull_request.user.type != 'Bot'
env:
# Fetched live rather than read from the event payload. A rerun
# replays the payload the run started with, so a body-only fix could
# never turn this check green: the obvious operator move — add the
# missing line, rerun the failed check — re-read the old body and
# failed again with no hint why. Reading the current body makes a
# rerun mean what everyone already assumes it means.
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Through a variable, never interpolated into the script body:
# a PR body is attacker-controlled text.
PR_BODY=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json body --jq '.body // ""')
# Body only, deliberately. GitHub resolves closing keywords from the
# PR description; a "Closes #123" in the title auto-closes nothing.
# Accepting the title here would pass PRs that never close an issue,
# which is the exact false-assurance this check exists to prevent.
text="${PR_BODY:-}"
# GitHub resolves a closing keyword wherever it appears, negated or
# not: PR #6371 said "does not close #6184", GitHub put #6184 in
# closingIssuesReferences, and the P1 closed on merge. A keyword a
# few words after a negation is always that mistake, so fail first.
# The reference takes all three forms GitHub closes on: #N,
# owner/repo#N and a full issue URL.
keyword='(close[sd]?|fix(e[sd])?|resolve[sd]?)[[:space:]]*:?[[:space:]]*([[:alnum:]_.-]+/[[:alnum:]_.-]+#|#|https?://github\.com/[[:alnum:]_.-]+/[[:alnum:]_.-]+/issues/)[0-9]+'
negation="(\\b(not|never|no longer|without)|n't)([[:space:]]+[[:alnum:]'-]+){0,3}[[:space:]]+"
# `|| true`, not `| head`: under pipefail a SIGPIPE'd grep would turn a
# hit into a miss.
negated=$(grep -m1 -oiE "${negation}${keyword}" <<<"$text" || true)
if [ -n "$negated" ]; then
cat >&2 <<MSG
Negated closing keyword found: "${negated}"
GitHub closes the issue anyway: it ignores the "not". Reword it as
Refs #1234 (the PR relates to the issue; nothing closes)
and keep Closes / Fixes / Resolves for the issue this PR really finishes.
MSG
exit 1
fi
# GitHub's own closing-keyword set, plus the #N it must attach to.
if grep -qiE "\\b${keyword}" <<<"$text"; then
echo "Closing keyword found — this PR will close its issue on merge."
exit 0
fi
# Opt-outs. `Refs #N` links the issue without closing it; `No-Issue:`
# takes a one-line reason for work that has no issue at all.
if grep -qiE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text"; then
echo "Linked without closing — $(grep -iE '^[[:space:]]*Refs?:?[[:space:]]+#[0-9]+' <<<"$text" | head -1)"
exit 0
fi
if grep -qiE '^[[:space:]]*No-Issue:[[:space:]]*\S' <<<"$text"; then
reason=$(grep -iE '^[[:space:]]*No-Issue:' <<<"$text" | head -1)
echo "Opted out — ${reason}"
exit 0
fi
cat >&2 <<'MSG'
This PR neither links an issue nor says why it doesn't.
Add one of these lines to the PR body:
Closes #1234 (only when this PR finishes the issue; Fixes / Resolves also close)
Refs #1234 (related or partial work; the issue stays open)
No-Issue: <one-line why> (chores, docs typos, revert, dependency bump)
Write a closing keyword only when you mean it: GitHub closes the issue
on merge even inside "does not close #1234".
MSG
exit 1
-7
View File
@@ -27,11 +27,6 @@ jobs:
const blob = `${issue.title || ''}\n${issue.body || ''}`;
const patterns = [
/\bcrypto\b/i,
/\bairdrop\b/i,
/\bnft\b/i,
/\bpresale\b/i,
/\busdt\b/i,
/\btg\s*@/i,
/\btelegram\s+@/i,
/\bt\.me\//i,
@@ -40,8 +35,6 @@ jobs:
/\bguest\s+post/i,
/\bbacklink/i,
/\bbuy\s+followers/i,
/\bjoin\s+our\s+(community|server|group)/i,
/\bpromot[ei]\s+your\b/i,
];
const hit = patterns.find(p => p.test(blob));
if (!hit) return;
+28 -25
View File
@@ -77,11 +77,11 @@ base prompt". Two more corollaries earned here:
- Commit by explicit path; never `git add -A`.
- Work that fails its check and cannot be fixed now is not pushed to `main`.
Record it and report it instead.
- **Maintainers do not open PR branches** (founder, 2026-10-08: more PRs means
more CI and slower work). Verified slices land directly on `main` and are
pushed: run the focused gate, put the real counts in the commit message,
fetch and rebase onto `origin/main` and retry on rejection. Never force-push
or rewrite a shared ref. Contributor PRs still merge as PRs.
- **Maintainers land small verified slices directly on `main`.** Run the
focused local check (`scripts/preflight.sh --check`), put the real counts in
the commit message, fetch and rebase onto `origin/main` and retry on
rejection. Never force-push or rewrite a shared ref. Contributor PRs are
always welcome and merge as themselves (founder, 2026-11-07).
- When the task is local-only, stay fully offline: no browsing, GitHub or remote
Git operations, downloads, dependency installation, provider calls, or
source/diff transmission. Record the missing external receipt and keep working
@@ -102,15 +102,14 @@ base prompt". Two more corollaries earned here:
- **Write down what a design does not do**, beside the behaviour it owns — a
short known-limitations note in the owning module. A stated limit stops the
next reader from assuming a capability that was never built.
- **Substantive contributor replies are authorized for the 0.10.2 release**
(founder, 2026-10-13). Thank people specifically for their reports, diagnosis,
fixes and testing; link the carrying commit or PR and distinguish candidate
work from a shipped release. Status, answers and useful questions are welcome;
repetitive progress posts and bot noise are not. Evidence stays in commits
and PR bodies; ownership stays in Linear. Outside this release, the previous
no-comment rule remains, except a one-sentence reason when closing or
superseding a human contribution. Review workflows remain disabled unless
the founder separately authorizes enabling them.
- **Be generous in public** (founder, 2026-11-07). Thank people specifically
for their reports, diagnosis, fixes and testing; link the carrying commit or
PR and distinguish candidate work from a shipped release. Status, answers
and useful questions are welcome; repetitive progress posts and bot noise
are not. Evidence stays in commits and PR bodies; ownership stays in Linear.
A contributor PR is never closed because CI is slow, flaky or red for a
reason the PR did not cause; when a PR is closed, say why in one specific,
thankful sentence. Review bots stay off unless the founder enables them.
- **A user feature lands with its registry row.** A new command, `[features]`
flag, provider or user-visible feature adds or updates its row in
`docs/features.toml` in the same change;
@@ -123,17 +122,18 @@ base prompt". Two more corollaries earned here:
An external contributor's branch goes stale because *we* land things, not
because they did anything wrong. Treat their time as more expensive than ours.
**The goal is the contributor's PR merging as itself.** Review it, help it
rebase, or fix it on their branch — that is the default path. Closing their PR
and re-landing the work as our own commit (`auto-close-harvested`) is the
fallback for a branch that truly cannot merge in reasonable time; done
casually it reads as taking the work even when credit is preserved.
**The goal is the contributor's PR merging as itself.** Review it, run or
rerun only the checks it needs, fix small problems on their branch, and resolve
conflicts that `main` caused. Landing their commits as ours (harvesting) is the
rare fallback for a branch we cannot push to and that cannot merge in
reasonable time; ask on the PR first, because done casually it reads as taking
the work even when credit is preserved.
- **Never make a contributor rebase around our churn.** If their PR conflicts
only because main moved, a maintainer resolves it.
- Landing mechanics — merge-base diffing, mid-function conflicts, fork-push
refusal, the contribution gate — live in `cw-land`. Follow them instead of
improvising.
refusal, runs awaiting first-contributor approval — live in `cw-land`.
Follow them instead of improvising.
- **Preserve credit in the mechanical sense, not just the polite one.** Commit
authorship and `Co-authored-by` trailers must use the contributor's own
GitHub-linked address. `AUTHOR_MAP` and `.mailmap` are project conventions —
@@ -258,8 +258,11 @@ lands as our commit, the contributor is that commit's **author** (`git commit
list counts commit authors only: about 200 harvested commits authored by
maintainer identities left their contributors off it, which is why
`docs/CONTRIBUTORS.md` carries the full wall (`scripts/contributor-wall.py`).
`auto-close-harvested.yml` closes their PR with credit. Canonical human
identities come from `.github/AUTHOR_MAP`.
A maintainer closes their PR by hand with a thank-you naming the commit;
nothing closes a PR automatically. Canonical human identities come from
`.github/AUTHOR_MAP`.
Leave unrelated work intact and keep new enforcement dry-run unless explicitly
approved.
Leave unrelated work intact. No automation closes, blocks or labels a
contributor as unapproved, and no workflow checks out or runs untrusted PR code
under `pull_request_target`. Keep required PR checks few and fast; exhaustive
validation belongs to the release candidate.
+33 -82
View File
@@ -229,9 +229,9 @@ Co-author a *person* freely; the address must be their GitHub-linked one
When a commit harvests code from a community PR (see "How Your Contribution
Lands" below), include a `Harvested from PR #N by @author` line in the commit
body. An auto-close workflow watches for this pattern and closes the
referenced PR with credit so the contributor gets a clear signal that
their work shipped.
body. A maintainer then closes the referenced PR by hand with a thank-you and
a link to the commit, so the contributor gets a clear signal that their work
shipped.
## What happens after you open a pull request
@@ -242,26 +242,23 @@ maintainer to approve the first run for a brand-new GitHub account.
| Check | What it runs | When it is red |
| --- | --- | --- |
| `Lint` (required) | `cargo fmt --check`, clippy on every target, and the fast drift checks (locales, READMEs, provider registry, dead-code and blocking-call budgets) | Run `cargo fmt --all` and the clippy command in [Pre-push verification](#pre-push-verification); the log names the file and the check |
| `Test (ubuntu-latest)` | The whole workspace suite on Linux with `cargo nextest`, plus doctests | Re-run the named test locally, for example `scripts/dev-test.sh tui <test name>` |
| `Test (windows-latest)` (required) | Compiles every workspace target for Windows with warnings denied, and the installer PATH helper tests | Usually a `cfg(windows)` path or an import only Windows sees; the log has the compiler error |
| `npm wrapper smoke (ubuntu-latest)` (required) | Builds `codewhale` and installs it through the npm wrapper | The log shows the failing install or entrypoint step |
| `Safety gate` | The sandbox, authorization and auto-review tests | Treat it as a real regression in a trust boundary |
| `Version drift` | Version files, the `feat:` release-note rule, release helper tests and contributor credit | The credit step only reports: a maintainer adds your credit when the PR merges |
| `PR closes an issue` | Your description has `Closes #N`, `Refs #N` or `No-Issue: <reason>` | Edit the description, then re-run the check |
| `DCO` | Lists commits without `Signed-off-by` | Advisory; it never blocks |
| `Contribution gate` | Labels PRs from authors not yet in `.github/APPROVED_CONTRIBUTORS` | A label, not a judgement; see [Contribution Gate](#contribution-gate) |
Smaller checks cover MSRV, the VS Code extension, integrations, the extension
host and plugin conversion, plus workflow lint when you change
`.github/workflows`. A documentation-only change skips the Rust builds but
still reports the required checks, so it can merge. The Linux test suite took
45 minutes on a recent pull request; the other checks run alongside it.
Smaller checks cover the VS Code extension, integrations, the extension host
and plugin conversion, plus workflow lint when you change `.github/workflows`.
A documentation-only change skips the Rust builds but still reports the
required checks, so it can merge.
Your pull request does not run the macOS and Windows test suites, the slow
runtime-contract and persistence-backlog measurements, the dependency audits
or the OpenHarmony build. Maintainers run those once per release, on the exact
commit being released. If that run finds a problem your change introduced, we
fix it on `main`; we will not ask you to chase it.
Your pull request does not run the test suites, the safety gate, MSRV, the npm
wrapper smoke, the slow runtime-contract and persistence-backlog measurements,
the dependency audits or the OpenHarmony build. Maintainers run those once per
release, on the exact commit being released. If that run finds a problem your
change introduced, we fix it on `main`; we will not ask you to chase it.
A red check that your change did not cause is not your job to fix. We rerun it
or fix it on `main`, and we never close a PR because CI is slow or flaky. Your
PR may be a draft, and the issue line in its description is optional.
## How Your Contribution Lands
@@ -280,9 +277,10 @@ Before merging, a maintainer may:
### Fallback — harvest
Rarely, a branch cannot merge in reasonable time, for example when it is in a
fork we cannot push to and the conflicts cannot be resolved there. Then a
maintainer may **harvest** the useful commits into `main` instead. This is
**not a rejection**: your code landed, and your credit is kept mechanically.
fork we cannot push to and the conflicts cannot be resolved there. We will ask
on your PR first. If it still cannot merge, a maintainer may **harvest** the
useful commits into `main` instead. This is **not a rejection**: your code
landed, and your credit is kept mechanically.
- You stay the author of the harvested commit, with your GitHub-linked address
from `.github/AUTHOR_MAP` or
@@ -291,8 +289,8 @@ maintainer may **harvest** the useful commits into `main` instead. This is
- The commit body includes `Harvested from PR #N by @your-handle`. That line is
the contract: it is your credit and the signal that your work shipped.
- The `CHANGELOG.md` entry for the next release credits you by handle.
- `auto-close-harvested.yml` closes your PR with a thank-you and a link to the
commit on `main`.
- A maintainer closes your PR by hand with a thank-you and a link to the commit
on `main`.
When a maintainer closes a harvested PR by hand, the closing comment
follows this template (the pattern set on PR #2634):
@@ -402,50 +400,6 @@ them from the title and form answers.
Release targeting uses milestones, not version labels.
## Contribution Gate
Codewhale uses a maintainer-managed contribution gate for the community front
door. Maintainers and collaborators bypass this gate automatically. The gate
workflows run in dry-run mode and only add labels, so maintainers can observe
the signal before changing contributor flow.
The maintainer posture is documented in
[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): automation should reduce load while
keeping good-faith contributors seen, credited, and able to keep helping.
Issues are never auto-closed by the contribution gate. Unapproved external
issues receive a triage label and remain open for maintainers. Codewhale
depends on real edge cases from real users, so issue intake should stay warm
and open.
Pull requests are different because they can touch code, CI, release plumbing,
auth, sandboxing, provider policy, and other trust-boundary surfaces. The PR
gate can be switched from dry-run to enforcement when maintainers decide they
need that safety control, but it should be treated as a review-load control,
not a judgment on contributor quality. Before enabling PR enforcement, seed the
allowlist broadly enough for active external contributors who should not be
interrupted by the rollout.
The allowlist is scoped:
- `pr:username` allows pull requests.
- `issue:username` allows issues.
- `all:username` allows both.
A maintainer can approve someone by commenting `/lgtm` on a pull request for PR
access, or `/lgtmi` on an issue for issue access. The exact bare commands
`lgtm` and `lgtmi` are also accepted for compatibility, but the prefixed forms
are preferred because they are harder to trigger accidentally in ordinary review
discussion.
Approvals do not edit `main` directly. The approval workflow opens a small
allowlist update PR so the new entry is reviewable before it takes effect.
If the PR gate fires on a good contributor incorrectly, use the same approval
flow to restore them: comment `/lgtm`, merge the generated allowlist PR, then
reopen the affected pull request. If GitHub will not allow the closed PR to be
reopened, ask the contributor to resubmit after the allowlist PR is merged.
## Agent-Assisted Improvements
Codewhale is allowed to help improve Codewhale, but the contribution still has
@@ -469,13 +423,12 @@ branding, or global prompts without prior maintainer sign-off.
## How maintainers and agents ship
Maintainers, and the agents working for them, do not open pull requests. They
land verified work directly on `main` with the local checks they ran and their
counts in the commit message. A push to `main` starts no GitHub build or test
workflow: GitHub only mirrors `main` to CNB and GitCode and closes harvested
PRs. CNB then runs its own Linux gates on the mirror and reports them on CNB,
not on GitHub. Do not write `[skip ci]`; nothing needs skipping, and it would
also skip the mirrors and the harvest auto-close.
Maintainers, and the agents working for them, land small verified slices
directly on `main` with the local checks they ran and their counts in the
commit message. A push to `main` starts no GitHub build or test workflow:
GitHub only mirrors `main` to CNB and GitCode. CNB then runs its own Linux
gates on the mirror and reports them on CNB, not on GitHub. Do not write
`[skip ci]`; nothing needs skipping, and it would also skip the mirrors.
The exhaustive gate runs once, at the final release step, from one dispatch on
the frozen candidate commit:
@@ -555,19 +508,17 @@ verification.
- Use the [pull request template](.github/PULL_REQUEST_TEMPLATE.md) when opening
a PR — what and why, the issue line, and how you tested it
- The PR description needs one issue line, checked by CI
(`.github/workflows/pr-issue-link.yml`): `Closes #N` (or `Fixes` /
- An issue line in the PR description is optional: `Closes #N` (or `Fixes` /
`Resolves`) when the PR finishes the issue, `Refs #N` for related or partial
work, or `No-Issue: <one-line reason>`. Never write a negated closing
keyword such as "does not close #N": GitHub closes the issue anyway, so CI
rejects it
work. Never write a closing keyword inside a negation such as "does not
close #N": GitHub closes the issue anyway
- If you add a new layer, module, or abstraction, say which one it replaces
or deletes
- Keep PRs focused on a single change
- Update documentation if needed
- Add tests for new functionality
- Get the pull-request checks green before requesting review, or say in the
PR why one is red
- If a check is red for a reason your change did not cause, a maintainer will
deal with it
## Shape of a Typical PR
+7 -11
View File
@@ -14,17 +14,13 @@ could not cover by ourselves.
- Issues are intake, not a privilege boundary. Do not auto-close good-faith
issues because the reporter is not allowlisted. Ask for missing reproduction
detail and leave room for maintainer triage.
- PR gates exist for code review, CI load, and trust-boundary safety. They are
not a quality judgment on the contributor. Keep dry-run mode unless a
maintainer deliberately enables enforcement, and use warm copy when the gate
comments.
- Be generous with recurring contributors. When someone repeatedly brings
useful reports or patches, use `/lgtmi` for issue access or `/lgtm` for PR
access so the automation gets out of their way.
- Preserve contributor credit. When harvesting work, inspect the PR and linked
issues, keep author/co-author attribution where possible, add
`Harvested from PR #N by @handle`, and credit the contributor in the
changelog or release notes.
- Never close or block a PR because CI is slow, flaky or red for a reason the
PR did not cause; maintainers rerun or fix it. No automation closes, blocks
or labels a contributor as unapproved.
- Preserve contributor credit. If contributor work lands as our commit, keep
them as author, add `Harvested from PR #N by @handle`, credit them in the
changelog or release notes, and close the PR by hand with a specific
thank-you.
- Make credit machine-readable. If a harvested commit cannot preserve the
contributor as the author, add a `Co-authored-by` trailer with the GitHub
numeric noreply address from `.github/AUTHOR_MAP` or
+6 -8
View File
@@ -97,9 +97,8 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
`gh api users/<handle> --jq '"\(.id)+\(.login)@users.noreply.github.com"'`.
A clean cherry-pick keeps the author by itself. This step is the single
owner of credit mechanics; other skills link here.
The `Harvested from` line is what lets `auto-close-harvested.yml` close
their PR with credit. Canonical human identities live in
`.github/AUTHOR_MAP`.
Close their PR by hand with a thank-you naming the commit; nothing closes a
PR automatically. Canonical human identities live in `.github/AUTHOR_MAP`.
Whether a bot or agent also appears in a trailer no longer matters — the CI
check that policed trailer identities was removed because it rejected
@@ -128,11 +127,10 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
`${sha}`, not `$sha:refs`, because `:r` is a history modifier.
`maintainerCanModify` does not guarantee that the push is accepted. When
the fork refuses it, ask on the PR. Harvesting is the last resort.
- **Check the contribution gate before assuming a PR is stalled.** An
unlisted author's workflow runs sit at `action_required` and never start,
so the PR looks abandoned when nobody has actually looked at it. Approve
the runs, then fix the cause: add them to `.github/APPROVED_CONTRIBUTORS`
(`all:username`), or comment `/lgtm` (PR scope) or `/lgtmi` (issue scope).
- **Check for runs awaiting approval before assuming a PR is stalled.** A
first-time contributor's workflow runs can sit at `action_required` until
a maintainer approves them, so the PR looks abandoned when nobody has
actually looked at it. Approve the runs.
5. **Merging under a gate.** `AGENTS.md` ("Merging under a gate") owns this:
the artifact must literally say PASS, the review thread outranks the check
+4 -4
View File
@@ -331,13 +331,13 @@
"sources": [
{
"path": "CONTRIBUTING.md",
"sha256": "c2747cbfb73a335fb77ef217497a5e8f07c70eab0922fcd23dfb1a46e28ced4b",
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
"sha256": "3336ce37f447b947a456e84db91a1abdd63aab2eb98a399bf427e8b81f13003f",
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
},
{
"path": "docs/AGENT_ETHOS.md",
"sha256": "d9cd36fc7b6d9d236c68e62ffc92f41d3f3fab6e2e5eb9bf2bb2ae1d53f82331",
"content": "# Agent Ethos\n\n> 阅读简体中文版:[zh_hans/AGENT_ETHOS.md](zh_hans/AGENT_ETHOS.md)。\n\nCodewhale is maintained with agents, but it is not maintained by automation\nalone. Treat community reports and patches as real collaboration: people are\nbringing us machines, providers, regions, shells, packages, and edge cases we\ncould not cover by ourselves.\n\n## Stewardship\n\n- Verify live truth before acting. Check the current branch, release state,\n registry state, CI, and linked issues instead of trusting a handoff.\n- Issues are intake, not a privilege boundary. Do not auto-close good-faith\n issues because the reporter is not allowlisted. Ask for missing reproduction\n detail and leave room for maintainer triage.\n- PR gates exist for code review, CI load, and trust-boundary safety. They are\n not a quality judgment on the contributor. Keep dry-run mode unless a\n maintainer deliberately enables enforcement, and use warm copy when the gate\n comments.\n- Be generous with recurring contributors. When someone repeatedly brings\n useful reports or patches, use `/lgtmi` for issue access or `/lgtm` for PR\n access so the automation gets out of their way.\n- Preserve contributor credit. When harvesting work, inspect the PR and linked\n issues, keep author/co-author attribution where possible, add\n `Harvested from PR #N by @handle`, and credit the contributor in the\n changelog or release notes.\n- Make credit machine-readable. If a harvested commit cannot preserve the\n contributor as the author, add a `Co-authored-by` trailer with the GitHub\n numeric noreply address from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`.\n Do not use `.local`, placeholder, bot/tool, or raw third-party emails for\n human contributor credit.\n- Deferral is a maintainer action, not a dismissal. If a PR or issue is not\n ready, say what is blocked, what evidence would change the decision, and\n which part of the work remains valuable.\n\n## Agent Workflow\n\n- Use sub-agents for exploration, review, and verification, but keep a human\n maintainer posture in the parent session. Sub-agent output is evidence; the\n parent is responsible for the final decision.\n- Personally review community PRs before merging, harvesting, closing, or\n deferring them. Do not close work based only on title, labels, or an agent's\n summary.\n- Prefer narrow, reversible changes that match the existing codebase. Avoid\n drive-by refactors while harvesting community work.\n- Run the smallest meaningful validation first, then broaden tests when a\n change touches shared behavior, release plumbing, auth, sandboxing,\n providers, or UI workflows.\n- Do not tag, publish, push release artifacts, or create GitHub releases\n without explicit maintainer approval.\n\n## Product Tone\n\nCodewhale should feel like a capable coding harness with a public community,\nnot a closed queue. Automation should reduce maintainer load while making\ncontributors feel seen, credited, and able to keep helping.\n"
"sha256": "f656bf5b39018c295d5b82f406c9b67c658c2646b766821e20bdbf8633cc6667",
"content": "# Agent Ethos\n\n> 阅读简体中文版:[zh_hans/AGENT_ETHOS.md](zh_hans/AGENT_ETHOS.md)。\n\nCodewhale is maintained with agents, but it is not maintained by automation\nalone. Treat community reports and patches as real collaboration: people are\nbringing us machines, providers, regions, shells, packages, and edge cases we\ncould not cover by ourselves.\n\n## Stewardship\n\n- Verify live truth before acting. Check the current branch, release state,\n registry state, CI, and linked issues instead of trusting a handoff.\n- Issues are intake, not a privilege boundary. Do not auto-close good-faith\n issues because the reporter is not allowlisted. Ask for missing reproduction\n detail and leave room for maintainer triage.\n- Never close or block a PR because CI is slow, flaky or red for a reason the\n PR did not cause; maintainers rerun or fix it. No automation closes, blocks\n or labels a contributor as unapproved.\n- Preserve contributor credit. If contributor work lands as our commit, keep\n them as author, add `Harvested from PR #N by @handle`, credit them in the\n changelog or release notes, and close the PR by hand with a specific\n thank-you.\n- Make credit machine-readable. If a harvested commit cannot preserve the\n contributor as the author, add a `Co-authored-by` trailer with the GitHub\n numeric noreply address from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`.\n Do not use `.local`, placeholder, bot/tool, or raw third-party emails for\n human contributor credit.\n- Deferral is a maintainer action, not a dismissal. If a PR or issue is not\n ready, say what is blocked, what evidence would change the decision, and\n which part of the work remains valuable.\n\n## Agent Workflow\n\n- Use sub-agents for exploration, review, and verification, but keep a human\n maintainer posture in the parent session. Sub-agent output is evidence; the\n parent is responsible for the final decision.\n- Personally review community PRs before merging, harvesting, closing, or\n deferring them. Do not close work based only on title, labels, or an agent's\n summary.\n- Prefer narrow, reversible changes that match the existing codebase. Avoid\n drive-by refactors while harvesting community work.\n- Run the smallest meaningful validation first, then broaden tests when a\n change touches shared behavior, release plumbing, auth, sandboxing,\n providers, or UI workflows.\n- Do not tag, publish, push release artifacts, or create GitHub releases\n without explicit maintainer approval.\n\n## Product Tone\n\nCodewhale should feel like a capable coding harness with a public community,\nnot a closed queue. Automation should reduce maintainer load while making\ncontributors feel seen, credited, and able to keep helping.\n"
},
{
"path": "docs/CONTRIBUTORS.md",