Fix a policy violation
A commit is blocked, a push is refused, or CI is red. Find which gate fired and what to do about it.
Construct enforces policy in three places. If something's blocking, the gate's output tells you what fired and what to do. This walks the four most common failures end-to-end.
For the full enforcement model, see Concepts → Gates and enforcement. For an inventory of every gate, see construct gates:audit.
"Edit blocked: comment policy"
You see: the agent tries to write a file, the write fails, output mentions comment-lint and a banned pattern (narrative voice, point-in-time note, noise sentinel, missing required header).
What fired: Layer 1, the real-time comment-lint PostToolUse hook.
Fix:
- Remove the banned phrase from the offending line. The hook output names the line and the pattern category.
- Common patterns:
// We do X(narrative voice),// Previously this was Y(point-in-time),// best effort(noise sentinel). - For new files in scoped paths (
bin/,lib/**/*.mjs,lib/hooks/,tests/, etc.), add the file-header/** */block first.
When the policy is wrong: there is no CONSTRUCT_SKIP_* for this gate. If the lint fires on a legitimate comment, fix lib/comment-lint.mjs (the rule source) and rules/common/comments.md (the documented policy) — do not re-introduce a skip env var. The principle: gates fire unconditionally; a skip env var on a quality gate is an admission the policy is wrong, the gate is over-scoped, or the team will route around it.
"Commit blocked: code changed but docs unchanged"
You see: git commit refuses with Code changed but CHANGELOG.md / docs / .construct/context not updated.
What fired: Layer 2 .beads/hooks/pre-commit: specifically the construct docs:verify check.
Fix:
- Add an entry to
CHANGELOG.mddescribing what changed. - If the change is genuinely operational (a build script tweak, a config file update) and doesn't warrant a CHANGELOG entry, update
.construct/context.mdor relevantdocs/**to reflect the work. - The gate scope is
lib/,bin/,src/,app/. Tweaks totests/,scripts/, andagents/don't trip the gate.
When the gate is wrong: there is no CONSTRUCT_SKIP_* for this gate. If the doc-coupling check fires on a change that legitimately doesn't need docs, fix the scope in lib/docs-verify.mjs instead of bypassing.
"Push blocked: last CI run failed"
You see: git push refuses with [pre-push-gate] Last CI run on <branch> FAILED. Investigate before pushing more.
What fired: Layer 2 pre-push-gate.mjs: the remote CI status check.
Fix:
gh run view --branch=$(git branch --show-current) --log-failed | head -50
Read the failure. Usually one of:
- A test flake (run
npm testlocally: does it reproduce?) - A linter newly tripping (run the lint the failing job named)
- A dependency advisory (
npm audit --omit=dev --audit-level=high) - A real regression you missed
Fix the underlying cause, then push. The gate clears as soon as a new CI run goes green.
There is no CONSTRUCT_SKIP_* for the pre-push gate. The legitimate "I already fixed the CI failure in this push" case is handled by the SHA-aware check: the gate only blocks when HEAD equals the SHA CI rejected. Add a fix commit (or amend), and the gate clears automatically once HEAD advances past the failed SHA.
"PR can't merge: required status checks pending or failed"
You see: GitHub UI shows the PR as not mergeable. One or more required-status-checks are pending, failed, or missing entirely.
What fired: Layer 3 branch protection.
Fix paths:
| Symptom | Action |
|---|---|
| Checks pending | Wait. CI is still running. |
| Checks failed | Open the failed job's log: gh pr checks <pr> --watch then gh run view <run-id> --log-failed. |
| Check missing entirely | The required-status-check name in branch protection doesn't match an actual CI job name. Check .github/workflows/ci.yml for the job's name: and reconcile with the GitHub branch protection settings. |
| Required check exists but never reports | The CI job didn't run on this PR. Check the workflow's on: triggers: does it run on pull_request? Path filters may be excluding your changes. |
For unblocking on a non-required job that's intermittently failing (e.g., a CDN timeout): re-run just that job from the GitHub Actions UI, or gh run rerun <run-id> --failed. Do this only after you've ruled out a real bug.
Emergency bypass: if you're a repo admin, you can dismiss required-status-checks for a specific merge. Don't make a habit of it: that defeats the whole layer.
"Session won't end"
You see: Trying to end the session, policy-engine reports a blocking condition: red CI, open beads issues, or unmet drive criteria.
What fired: Layer 3 policy-engine.mjs Stop handler.
Three causes, three fixes:
- Red CI on the current branch: fix the failure or
CONSTRUCT_STOP_OK_RED_CI=1to acknowledge. - Open
in_progressbeads issues: close them withbd close <id>(orbd update <id> --status opento defer back to ready), orCONSTRUCT_STOP_OK_OPEN_BD=1to acknowledge. - Drive mode active with unmet criteria: set the criterion's
met: trueevidence in.construct/drive-state.json, orCONSTRUCT_STOP_OK_OPEN_BD=1for an explicit override.
When the gates themselves are wrong
If you've been hitting a gate that's a false-positive (the lint flags content that shouldn't be flagged, the doc-coupling gate fires on a change that legitimately doesn't need docs) fix the gate, not the bypass:
- Comment patterns:
rules/common/comments.mdis the source of truth for what's banned. - Doc-coupling scope:
lib/docs-verify.mjsdefines which paths trigger the gate. - Pre-push job set:
lib/hooks/pre-push-gate.mjsdeclares the local pre-push jobs. - Branch protection required-status-checks:
gh api branches/main/protection/required_status_checksis the source.
construct gates:audit is the audit tool: it walks all four enforcement surfaces and reports gaps. Run it when you're uncertain which gate fired or why.
Reference
- Concepts → Gates and enforcement: the full enforcement architecture.
construct gates:audit: surface gaps.construct doctor: health checks including the wired-hooks-path check.- Comment policy:
rules/common/comments.md