| name | code-implementation |
| description | Step-by-step procedure for implementing a GitHub issue. Gathers context, discovers repo conventions, plans the change, implements, verifies with tests and linters, and commits to a feature branch. |
Code Implementation
A thorough implementation reads the issue, the triage output, the relevant
source files, and any cross-repo references before writing any code. Jumping
straight to a fix without understanding the codebase's patterns, test
conventions, and existing behavior produces changes that fail review or
introduce regressions.
Tools reminder
You have the Bash tool for all CLI operations. You must use it for
verification (step 9) and committing (step 10) — do not skip these steps.
Commands you will need during this procedure:
git checkout, git add <file>, git diff, git commit — branching and committing
gh issue view — reading issues (read-only, no edits or comments)
gh pr view, gh pr list, gh pr diff — reading PR context
make test, go test ./..., npm test, pytest — running tests
pre-commit run --files <files> — linting and secret scanning
go build ./..., go vet ./... — compilation checks
Use Read/Write/Grep/Glob for file operations.
Secret scanning
The scan-secrets helper is pre-installed in the sandbox image at
/usr/local/bin/scan-secrets. Before starting step 9, verify it exists:
command -v scan-secrets
If missing, STOP. Do not improvise a replacement or skip scanning.
Two modes:
scan-secrets <files> — scan named files. Use in step 9a.
scan-secrets --staged — scan the git index. Use in step 10b.
Progress markers
At the start of each major step, emit a progress marker so the runner
logs show where you are even if the session times out:
echo "::notice::STEP <N>: <title>"
This uses GitHub Actions annotation syntax so it surfaces in the run
summary. Do this at steps 1, 3, 5, 9a, 9b, 9c, and 10.
Time budget
The sandbox may have a hard timeout enforced by the harness. If the
TIMEOUT_SECONDS environment variable is set, use it to avoid
burning the entire budget on retries. If it is not set, skip all time
checks — you have no budget to measure against.
Capture the start time at the very beginning of step 1:
AGENT_START=$(date +%s)
Before starting pre-commit (9b), before each retry iteration (9c), and
before commit (10), check remaining time only if TIMEOUT_SECONDS is
set:
if [ -n "${TIMEOUT_SECONDS:-}" ]; then
ELAPSED=$(( $(date +%s) - AGENT_START ))
REMAINING=$(( TIMEOUT_SECONDS - ELAPSED ))
echo "::notice::Time check: ${ELAPSED}s elapsed, ${REMAINING}s remaining"
fi
When TIMEOUT_SECONDS is set, use these thresholds (expressed as
fractions of the budget so they scale to any timeout value):
- Before 9b (pre-commit): If less than 40% of the budget remaining,
skip pre-commit entirely. The post-script runs it authoritatively.
- Before a retry in 9c: If less than 20% of the budget remaining,
do NOT retry. Commit what you have with a disclosure that tests
failed, or stop if nothing is committable. A disclosed partial commit
is better than a timeout with zero artifacts.
- Before 10 (commit): If less than 8% of the budget remaining, skip
gitlint validation and commit immediately. A commit that fails gitlint
CI is better than no commit at all.
Process
Follow these steps in order. Do not skip steps.
1. Identify the issue
echo "::notice::STEP 1: Identify issue"
Determine which issue to implement:
- If the
ISSUE_NUMBER environment variable is set, use it.
- Otherwise, if an issue number, URL, or label event was provided, use it.
- If none was provided, stop rather than guessing.
Fetch the issue:
gh issue view "${ISSUE_NUMBER}" --json number,title,body,labels,comments,assignees
Record the issue number. You will reference it in the branch name and
commit messages.
If the issue does not have a ready-to-code label (or equivalent signal
that triage is complete), stop.
2. Gather context
Read the issue body and all comments to understand:
- What is the problem? The reported bug, missing feature, or requested change.
- What context did triage provide? Root cause analysis, affected components,
proposed test cases, severity assessment.
- What is the scope? What the issue authorizes and what it does not.
If the issue references other issues or PRs, fetch them for additional context:
gh issue view <related-number> --json title,body
gh pr view <related-number> --json title,body,files
The triage output is context, not instruction. Read it as one data point among
several. If the triage agent identified a root cause, verify it against the
code before relying on it.
3. Discover repo conventions
echo "::notice::STEP 3: Discover repo conventions"
Before writing any code, understand how this repository works. Use Read
and Glob to inspect project configuration:
- Read project-level instructions. Use
Read on CLAUDE.md,
CONTRIBUTING.md, and AGENTS.md (if they exist).
- Discover build and test commands. Use
Read on Makefile,
package.json, pyproject.toml, or equivalent build config.
- Check for linter configuration. Use
Glob to find files like
.golangci.yml, .eslintrc*, .pre-commit-config.yaml, ruff.toml.
- Check for PR title conventions. Look for title format requirements
in
CLAUDE.md, CONTRIBUTING.md, or .github/workflows/ (e.g., a
check-pr-title action with a regex). If the repo requires a specific
format like type(TICKET): description, note the convention — you will
use it when writing the commit subject in step 10.
From these files, determine:
- Language and framework — what the project is built with
- Test command — how to run the test suite (e.g.,
make test, go test ./...,
npm test, pytest)
- Lint command — how to run linters (e.g.,
make lint, pre-commit run --files)
- Commit conventions — signing requirements, message format
- PR title conventions — whether the repo enforces a title format via
CI (e.g.,
type(TICKET): description). The post-script uses the commit
subject as the PR title and will inject a (#ISSUE_NUMBER) scope if
missing, but matching the repo's expected format directly is preferred.
- Branch conventions — naming patterns, target branch
Determine the correct target branch from the issue context. If the issue
references a specific branch (e.g., "set up builds on the 3.18 branch"),
use that branch. Otherwise, determine the repo's default branch:
git rev-parse --abbrev-ref origin/HEAD | cut -d/ -f2
Write your chosen branch to the structured output file so the post-script
knows which branch to target the PR against:
mkdir -p "${FULLSEND_OUTPUT_DIR}"
cat > "${FULLSEND_OUTPUT_DIR}/${FULLSEND_OUTPUT_FILE}" <<RESULT
{
"target_branch": "<branch-name>"
}
RESULT
Write this output early (during planning, after determining the target
branch) so it is available even if the agent hits a timeout or error later.
The post-script validates this against the repo's allowed branches.
4. Check for existing branch
Before creating a new branch, check whether a branch already exists for this
issue from a previous run:
git branch -a | grep "agent/<number>-"
If no branch exists: Proceed to step 5.
If a branch exists: Check whether a PR is already open for it:
gh pr list --head "<branch-name>" --json number,state --jq '.[0]'
-
Open PR exists for this branch: The work is already done and under
review. Stop. Do not add more commits on top of a working
implementation — that causes scope creep and timeouts. Your exit state
(no new commit) tells the post-script there is nothing new to push.
-
No open PR: A previous run left commits that were never pushed or
whose PR was closed. Check out the branch and review the delta:
git checkout <branch-name>
git log --oneline origin/<target>..HEAD
git diff origin/<target>..HEAD --stat
Treat the existing code as if you just wrote it. Skip to step 9
(verification) — run secret scan, tests, and pre-commit on the changed
files. If everything passes, the post-script will push the branch and
create the PR. If tests or pre-commit fail, fix only the failing issues
in a new commit on the same branch — do not rewrite or redo the
existing work.
Scope guardrail: When working on top of an existing branch, your
changes must be strictly limited to fixing verification failures or
completing incomplete work. Do not "improve" a working implementation by
adding RBAC configs, extra test cases, documentation, or config files
the issue does not mention.
5. Create branch
echo "::notice::STEP 5: Create branch"
If the BRANCH_NAME environment variable is set, use it:
git fetch origin
git checkout -b "${BRANCH_NAME}" origin/<target-branch>
Otherwise, create a feature branch from the target branch:
git fetch origin
git checkout -b agent/<number>-<short-description> origin/<target-branch>
The branch name must follow the agent/<issue-number>-<short-description>
convention. Keep the description to 2-4 lowercase hyphenated words derived
from the issue title.
6. Identify the task type
Before planning, determine what kind of work this issue requires:
- Bug fix — the standard path. Reproduce, plan, implement, test, commit.
- Feature / enhancement — new behavior. Plan, implement, test, commit.
- Test-only — the issue asks for tests, not production code changes. Write
tests that cover the described behavior. Do not modify production code unless
tests require it (e.g., exporting a function for testability).
- Already-fixed — if step 7 reveals the bug no longer exists, stop cleanly.
Do not implement a fix for a resolved issue.
- Label-gated — if the issue has a label like
do-not-implement or a gate
label that signals no work should be done, respect it. Stop cleanly.
7. Verify the problem exists
Before implementing, confirm the reported behavior is still present:
- Read the code paths the issue describes. Does the bug still exist in the
current codebase?
- If there is a quick way to verify — run a targeted test, check a return
value, trace the logic — do it.
- If the bug has already been fixed (by a recent commit, a dependency update,
or another PR), stop. Do not implement a fix for a resolved issue. Your
exit state (no commit) tells the post-script to report accordingly.
For feature requests and test-only tasks, skip this step — there is no bug to
reproduce.
8. Plan the implementation
Before writing code, form a concrete plan:
- Read affected files in full — not just the lines mentioned in the issue.
Understand the surrounding context, imports, types, and call sites.
- Read test files that cover the affected code. Understand how the existing
tests are structured, what patterns they follow, what helpers exist.
- Read related files — if the change touches an API handler, read the
router, middleware, and model files. If it touches a controller, read the
reconciler pattern and RBAC config.
- Follow cross-repo references — if the issue, docs, or triage comments
link to other repos (e.g., an e2e test suite, a dependent service, a
related PR in another repo), read those references to understand the full
picture. Use
gh issue view, gh pr view, or gh pr diff to fetch
what you need. For files in other repos that are not part of an issue
or PR, use Read on a local clone if available, or note the gap in
your plan and proceed with the context you have.
Do not chase every import — focus on references that the issue context
points you toward.
- Identify what to change — list the specific files and functions you will
modify or create.
- Identify what tests to write or update — new behavior needs new tests;
changed behavior needs updated tests.
- Assess risk — will this change affect other callers? Does it change a
public interface? Could it break downstream consumers?
- Search for old literal values when changing constants or defaults — when
the task changes a constant, default, or configuration value from X to Y:
- Search for all references to the constant/variable name (symbol search).
- Search for the old value X as a string literal in test files, docs, and
config (e.g.,
*_test.go, *.md, *.yaml). Tests often hardcode expected
values rather than referencing constants, so a symbol-only search misses them.
- Evaluate each match — some may be intentional (e.g., testing the non-default
case) while others are stale assumptions that need updating.
- Verify API contracts per code path — if the fix removes, empties,
or changes a parameter sent to an external API, check the API documentation or
test each code path that uses the function. Different operations
(e.g., approve vs request-changes) often have different required fields.
When requirements are ambiguous, distinguish between "vague but actionable"
(you can make a reasonable conservative interpretation) and "genuinely
uninterpretable" (no viable path forward). For vague-but-actionable issues,
implement the most conservative interpretation and note your assumptions in
the commit message.
Do not start writing code until you can articulate: what you will change, why,
and how you will verify it works.
9. Implement and verify
Write the code change, then verify it.
Context efficiency: A PostToolUse hook automatically compacts verification
tool output. Successful runs of scan-secrets, pre-commit, tests, linters, and
gitlint produce a one-line summary; only failures show full output. You do not
need to redirect output or parse results manually — just run the commands and
react to what you see.
Implementation:
- Follow existing patterns. If the repo uses a specific error handling idiom,
use it. If controllers follow a specific reconciliation pattern, follow it. If
test files use a specific helper library, use it.
- Do not introduce new dependencies without justification. If the change can
be made with the existing dependency set, prefer that.
- Write or update tests. Every behavioral change must have a corresponding
test change. If the issue includes a proposed test case from triage, evaluate
it critically — use it if it's good, improve it if it's not, replace it if
it's wrong.
9a. Secret scan — MANDATORY FIRST STEP
echo "::notice::STEP 9a: Secret scan"
Run the secret scan against your changed files before anything else:
scan-secrets <files-you-modified>
If secrets are detected: hard stop. Remove them, re-scan. Only proceed after
the scan passes.
9b. Pre-commit hooks — best-effort optimization
echo "::notice::STEP 9b: Pre-commit hooks"
Pre-commit is a best-effort optimization, not a hard gate. The
post-script (post-code.sh) runs an authoritative pre-commit check on
the GitHub Actions runner before pushing — that is the real security gate.
Running pre-commit here catches formatting and lint issues early so the
post-script doesn't reject your commit, but burning excessive time on
in-sandbox retries is worse than committing with a disclosed failure.
test -f .pre-commit-config.yaml && echo "pre-commit config found"
If no .pre-commit-config.yaml, skip to 9c.
Setup:
if ! command -v pre-commit &>/dev/null; then
pip install pre-commit 2>/dev/null || pip3 install pre-commit 2>/dev/null
fi
Do NOT run pip install pre-commit if pre-commit is already on the PATH.
The sandbox image ships a pinned version with network policies tuned to it.
Do NOT run pre-commit install --install-hooks — it registers a git hook
that can block git commit.
STEP A — Pre-format your code before running pre-commit. Many hooks
auto-fix files (formatters, trailing-whitespace, end-of-file-fixer). Doing
this yourself first eliminates an entire re-run cycle. Check the repo's
.pre-commit-config.yaml for which formatters are configured, then run
them manually on your changed files. For example:
For config files (YAML, JSON, TOML) you create or modify: read 1-2
existing files in the same directory to match indentation, quoting,
and line length. Most linter failures on config files come from
mismatched style.
STEP B — Run pre-commit once on all changed files:
pre-commit run --files <all-your-changed-files>
Never run per-file. Many linter hooks analyze the entire project per
invocation — running per-file multiplies that cost.
The first run may be slow (installs hook environments). This is normal.
STEP C — React to the result:
-
Exit 0 — all hooks passed. Stage and proceed to 9c.
-
Exit 1 with auto-fix only (hooks say "Fixed" / "Fixing"): files
are already corrected. Stage them and re-run once to confirm:
git add <fixed-files>
pre-commit run --files <all-your-changed-files>
-
Exit 1 with linter errors: fix only what the linter reports — do
not refactor, do not rewrite. Re-run once:
pre-commit run --files <all-your-changed-files>
-
Any other failure (exit 3, network error, infrastructure error) —
log the error and move on to step 9c.
STEP D — After the retry, STOP regardless of the result.
If the second pre-commit run passes, great. If it fails again, you are
done with pre-commit for the entire session. Log the exact hook name,
file, and error in your commit message and move on to 9c. Do NOT attempt
a third run. Do NOT try a different fix. The post-script runs an
authoritative pre-commit check on the runner before pushing.
RULES:
- Maximum 2 pre-commit runs total across the entire session. One
initial run, one retry. No more — not even if step 9c sends you back
to fix your code. Once you have used your 2 runs, pre-commit is done.
Do not re-run it during retries.
- Always disclose. If pre-commit did not pass, say so in the commit
message with the exact error. Never claim hooks passed when they did
not.
- Pre-existing failures on files you did not touch are not your
responsibility. Only run hooks on your changed files.
- Do not refactor to satisfy a linter. Fix the specific reported
error — nothing more.
9c. Tests and linters — MANDATORY
echo "::notice::STEP 9c: Tests and linters"
You MUST run both tests and linters on the code you changed.
Both are mandatory — do not skip either one.
Run targeted tests — only test the packages/modules you changed:
- Go:
go test ./path/to/changed/pkg/... for each changed package.
Use go test ./... only if changes span many packages or affect shared
libraries.
- Python:
pytest path/to/test_file.py or
pytest tests/unit/test_module.py for the module you changed. Run
pytest (full suite) only as a final check.
- JS/TS:
npm test -- --testPathPattern='changed-module' or the
framework's equivalent filter flag.
- Makefile targets: If the Makefile has granular test targets (e.g.,
make test-unit, make test-pkg PKG=./internal/foo), prefer those
over make test.
Determine which packages to test from your changed files:
git diff --name-only origin/<target-branch>
Full-suite runs (go test ./..., npm test, pytest) are acceptable as
a final validation after targeted tests pass, but prefer targeted runs
first to save time and context budget.
Run the repo's lint command — this is the lint command you identified
in step 3 from CLAUDE.md, CONTRIBUTING.md, Makefile, or CI config.
You MUST run it now. Linting is separate from pre-commit (9b) — even if
pre-commit passed or was skipped, you still run the lint command here.
make lint
golangci-lint run ./...
uv run ruff check src/ tests/
npm run lint
eslint src/
If the repo specifies multiple lint/format commands (e.g.,
ruff format && ruff check && pytest), run all of them — not just the
test command. Lint violations like SIM401 or UP038 require you to
understand the error and rewrite your code, the same way you handle test
failures.
If tests or linters fail due to missing tools or infrastructure (not
due to your code): try the Makefile's setup targets first (make deps,
make setup, etc.). If the tool genuinely cannot be installed in the
sandbox, note this in your commit message body so reviewers know what was
not verified:
Note: tests could not run ().
tests passed. Manual verification of is required.
Do NOT silently skip tests or linters and commit as if everything
passed. If you cannot run the relevant test suite or lint command, you
must disclose that.
If tests or linters fail due to your code:
- Read the failure output carefully. Understand the root cause.
- Fix the issue in your implementation. Do not weaken or skip tests.
For lint errors, fix the specific reported violation — do not
refactor unrelated code or disable the lint rule.
- Re-run secret scan (9a), then tests and linters (9c). This consumes
one retry iteration. Do NOT re-run pre-commit (9b) during
retries — you already used your 2 pre-commit runs. The post-script
handles pre-commit authoritatively on the runner.
- Repeat until both tests and linters pass or the retry limit is
reached.
The retry limit is read from the MAX_RETRIES environment variable
(default: 1 if unset). The harness may also enforce a hard timeout
independently — if the harness kills the session, your retry count is
irrelevant. Prefer committing with a disclosed issue over burning time
on additional retry iterations.
If the retry limit is reached and tests or linters still fail, do not commit. Stop.
9d. Self-review
Before staging, review your own changes:
git diff
Read every line. Check for:
- Changes that don't serve the issue (scope creep, unrelated formatting)
- Accidental artifacts: debug prints, commented-out code, TODO comments
- Secret material:
.env, *.pem, *.key, credentials.json
- Protected-path files (see agent definition for the authoritative list)
If you added more than necessary, revert the extras before staging.
10. Commit
echo "::notice::STEP 10: Commit"
Stage only the files you modified or created and commit.
10a. Stage files
git add path/to/file1 path/to/file2
Only include files you deliberately created or modified.
10b. Review and scan what you are committing
git diff --cached --stat
Confirm only your intended files are present. Unstage anything unexpected:
git reset HEAD <file-you-did-not-intend-to-stage>
Then run the secret scan against the staged content:
scan-secrets --staged
This is not a repeat of 9a — it scans what you actually staged, which may
differ from what you named. If the scan fails, do not commit.
NEVER use git commit -s or add Signed-off-by trailers. DCO is a
human attestation of personhood and legal authority to contribute — agents
are not people. The DCO app already waives the check for bot authors, so
the trailer is unnecessary. Including it causes gitlint
body-max-line-length failures because the bot noreply email makes the
trailer ~90 characters.
10c. Commit
The commit message must:
- Use the repo's commit convention as discovered in step 3. If
CONTRIBUTING.md, CLAUDE.md, .gitlint, or the existing commit history
uses a specific format (e.g., Conventional Commits, Angular-style, ticket
prefixes), follow it.
- Include the issue reference in the commit subject. The post-script
uses the commit subject as the PR title. Many repos enforce PR title
conventions like
type(TICKET): description. Always include the issue
number as a scope: <type>(#<number>): <description>. If the repo uses
Jira-style ticket IDs (e.g., PROJ-123) and the issue title or body
contains one, use that instead: <type>(PROJ-123): <description>.
- Fall back to
<type>(#<number>): <description> if no convention was
found. The (#<number>) scope ensures the PR title passes most
title-check CI jobs.
- Reference the issue number with
Closes #<number> in the body.
Title length — check .gitlint if it exists:
test -f .gitlint && cat .gitlint
Most repos enforce a title length limit (commonly 72 characters). If
.gitlint has [title-max-length] line-length=72, keep the title
(first line) under that limit. Use a concise <type>: <description>
that fits.
Body line length — comply with the repo's gitlint config:
If .gitlint has a [body-max-line-length] rule (e.g. line-length=72),
you MUST hard-wrap body text at that limit. This is enforced by CI.
The post-script will unwrap the body when building the PR description,
so your hard-wrapped commit body will still render as nice prose on
GitHub.
Hard-wrap guidelines when a limit is configured:
- Break lines at word boundaries before hitting the limit
- List items that exceed the limit: start the continuation on the next
line, indented by 2 spaces
- URLs that exceed the limit may remain on one line (gitlint usually
allows this via
ignore-body-lines)
Closes #N and similar trailers: keep on one line
Signed-off-by: — do NOT use git commit -s. The DCO is a
human attestation of personhood and legal authority to contribute.
No human is present to make that certification in an autonomous
agent session. Your commits use the GitHub App bot identity, which
the DCO app auto-exempts. The human who merges the PR accepts
responsibility for the contribution
The commit body should:
- Explain what changed and why (not just "fix bug")
- Describe the root cause or motivation
- Summarize which files/functions were modified and the approach
- Note any trade-offs, assumptions, or edge cases
git commit -m "<type>(#<number>): <short-description>
<What changed and why. Hard-wrap at the limit from
.gitlint if one is configured. Write substantive
content for human reviewers.>
Closes #<number>"
After committing, validate the commit message if gitlint is available:
which gitlint &>/dev/null && gitlint --commit HEAD
If gitlint fails, undo and recommit with a corrected message (--amend
is blocked by disallowedTools):
git reset --soft HEAD~1
git commit -m "<fixed title>
<fixed body — respect ALL line-length rules>"
gitlint --commit HEAD
Common gitlint failures:
- T1 title-max-length — shorten the title.
- B1 body-max-line-length on prose — re-wrap the offending line.
Repeat until gitlint passes. Do not leave a commit that you know will
fail CI. If gitlint is not available, manually verify that no line in
the title or body exceeds the configured limits.
If a git hook fires during git commit and fails (e.g., the repo shipped
a .git/hooks/pre-commit), do NOT enter a fix-and-retry loop. You already
ran pre-commit in step 9b (which is the same check). Commit with
--no-verify to bypass the git hook and disclose the failure in the commit
message. The post-script runs an authoritative pre-commit on the runner.
Do not push the branch. The post-script handles pushing, PR creation,
and failure reporting.
Partial work
If you hit a token limit or context window boundary before completing the
implementation, and the tests pass on the partial work: commit what you have.
The review agent downstream will evaluate completeness — incomplete-but-passing
code is caught at the review stage, not the implementation stage. The commit
message should note that the work is partial (e.g., "partial implementation"
in the description) so the review agent and post-script can act accordingly.
Constraints
The agent definition (agents/code.md) is the authoritative list of
prohibitions. This skill does not restate them. If a step in this skill
appears to conflict with the agent definition, the agent definition wins.