Contributor Workflow Conventions
On this page
- GitLab labels (scoped taxonomy)
- Merging when CI is broken / blocking
- Phased issues (ship narrow, keep open)
- Work-list ordering — don’t re-triage
- The board snapshot (
cargo xtask board, #1327) - Documentation drift — the surfaces to sweep
- Bundle doc flips with the implementing MR
- Agent delivery working-agreements
Project-specific GitLab / MR and delivery workflow rules for Canopy. These are not universal git/MR/delivery rules — they encode how this project’s GitLab instance and CI are operated, and the working agreements every agent (and human contributor) is expected to follow.
The universal git / MR / delivery rules live in the synced standards
(Git Workflow, GitLab Workflow,
and Delivery Protocol) and the .claude/rules
agent digests. This page is Canopy’s project-specific overlay — only the
conventions that are unique to this project and that are not covered by, or that
deliberately override, those universal standards. Project setup and commands
live in Local Development and the
Developer Guide.
GitLab labels (scoped taxonomy)
Canopy uses GitLab scoped labels at the group level
(gadhs/application/eligibility) for automatic mutual-exclusivity enforcement
within each scope. These supersede the generic flat labels in the universal
GitLab Workflow standard.
When creating an issue or MR, always apply:
-
Exactly one
type::label -
Exactly one
priority::label -
At least one
program::orservice::label -
A
workflow::label reflecting the current state -
A
planning::label once triaged (does the work need an implementation plan?)
| Scope | Labels | Purpose |
|---|---|---|
|
feature, bug, chore, spike, compliance, documentation, security |
What kind of work |
|
critical, high, medium, low |
Urgency and scheduling |
|
snap, tanf, medicaid, chip, caps, wic, cross-program, infrastructure |
Which benefit program(s) |
|
rules, persons, applications, eligibility, verification, enrollment, renewals, notices, exchange, appeals, reporting, security, web, portal, snap, tanf, medicaid, caps, wic, shared-crates, devstack, ci, seed, xtask |
Which service(s) |
|
ready, in-progress, in-review, blocked, needs-spec |
Current status |
|
needs-plan, has-plan, trivial |
Implementation-plan readiness (orthogonal to |
|
pub-1075, irs-pub-1075-audit, hipaa, cma, ievs, wcag-21-aa |
Governing compliance framework |
|
cms, fns, acf |
Which federal partner is implicated |
-
compliance::andfederal-partner::are additive (not mutually exclusive within scope) — an issue can carry bothcompliance::pub-1075andcompliance::hipaa. -
program::infrastructureis for work with no program affinity (CI, devstack, shared crates);program::cross-programfor work spanning programs (orchestrator, reporting). -
workflow::needs-specis for issues identified but not yet defined enough to begin. -
planning::is orthogonal toworkflow:::workflow::tracks execution state,planning::tracks whether an implementation plan must be authored first. Aworkflow::ready— or even aworkflow::blocked— item can still beplanning::needs-plan; being well-defined (or blocked) is independent of needing a plan. Definitions:planning::needs-plan= substantial work, so author
iterate a plan underdocs/modules/ROOT/pages/plans/(nav-linked) before coding;planning::has-plan= an active plan already governs it, implement against that plan;planning::trivial= safe to code directly (single/few files, no design decision, no new wire/endpoint/table, no cross-service ripple).
Merging when CI is broken / blocking
Canopy’s GitLab CI infrastructure is intermittently broken upstream (runner
registry access, docker-socket contention) and the project sets
only_allow_merge_if_pipeline_succeeds = false. The trusted gate is the local
pre-push cargo xtask validate battery, not the GitLab pipeline — if pre-push
passes you may merge regardless of the remote pipeline state.
Because CI is force-merged past, cargo xtask validate deliberately subsumes
every CI gate that can run locally and deterministically, so a green pre-push is
a real guarantee. As of #896 the battery additionally runs the formerly-CI-only
secrets-yaml-lint, compliance audit-data-tenancy, policy audit (+ --source
federal) / drift / action-coverage / input-coverage, and scenarios audit.
Only gates that genuinely cannot run in a fast local pass
stay CI-only: the GitLab-native SAST / secret-detection / dependency-scanning
runner features, the DinD validate-in-network egress check, and the slow
per-phase coverage baseline.
After a push, the failing stage (and its captured output) is in one file —
test-results/validate-report.json (schema, #1253) — not the multi-thousand-line battery log. That report is validate-only; git ls-remote remains the only proof a push actually landed.
-
Push with
-o ci.skipto skip the branch pipeline, then merge immediately. (-o ci.skipdoes not skip the local pre-push gate, and does not skip the separatemerge_request_eventpipeline.) -
If
glab mr mergerefuses because a merge-request pipeline is "still running", force-merge via the API — cancel the auto-merge first, then merge:glab api projects/<PROJECT>/merge_requests/<N>/cancel_merge_when_pipeline_succeeds -X POST glab api projects/<PROJECT>/merge_requests/<N>/merge -X PUT \ -F squash=false -F should_remove_source_branch=true -
squash=falseis mandatory — a regular merge commit preserves human authorship and the GPG/EdDSA signature (squash rewrites both). See Git Workflow. -
Do not add
[skip ci]to commit subjects to suppress the MR pipeline — it pollutes the commit log. Use the cancel-MWPS + force-merge path instead. -
GITLAB_TOKENlives in.env.localand is not auto-sourced by fish; export it (export GITLAB_TOKEN=$(grep -E '^GITLAB_TOKEN=' .env.local | cut -d= -f2-)) before anyglabcall.
This force-merge path overrides the universal "never force-merge" rule in the
standards. The override applies only when the local pre-push gate is green —
the local cargo xtask validate battery must still pass first. Never
--no-verify, never squash.
Phased issues (ship narrow, keep open)
When review uncovers scope growth mid-flight, do not silently expand the MR or silently close the issue:
-
Ship Phase 1 narrow with
Refs #NNNin the commit/MR (notCloses #NNN). -
Post a comment on the issue spelling out the phased acceptance criteria (what Phase 1 delivered, what remains).
-
Leave the issue open until the remaining phases land.
Work-list ordering — don’t re-triage
For work-list / backlog / "what next" requests, use the existing priority::
labels as-is. Order the work by dependency chains (what unblocks what), not by
re-evaluating or re-assigning priorities — those were set deliberately and
re-triaging them silently discards that judgment.
The board snapshot (cargo xtask board, #1327)
cargo xtask board is THE tier-burndown snapshot — for humans and for agent
session preflight alike (never hand-assemble the state from ad-hoc listings).
It renders the per-tier (T1–T5 milestone) open/closed counts with a delta
against a machine-local baseline (--set-baseline stores one under
.devstack/), the open-T1 roster, the T4 parked list (with each issue’s
would-be tier when a classification note records one — would-be T<n> in a
note), and the open MRs with their referenced issues. --json emits the
machine form. Reads GITLAB_TOKEN from the environment (read-only); a
network failure is one clear error, never a partial table. Counting is
staleness-proof by construction: tiers are fetched UNFILTERED and counted
from each issue’s own state field, because milestone listings have been
observed to return stale results for a state= filter (the 2026-08-26
burn-order trap).
Documentation drift — the surfaces to sweep
After a session that lands multiple MRs back-to-back, the broader docs surface
reliably drifts even when per-MR docs (CHANGELOG entries, plan-archive moves,
regenerated OpenAPI snapshots, CLAUDE.md route counts, citations.toml) were
kept in sync each commit. When the user asks "have you kept up on docs?" — or at
session-end after a rapid MR sequence — do an honest audit against these surfaces
before answering "yes":
-
roadmap.adoc— the Remaining Work Tracker / Tier list. Closed issues and epics don’t tick themselves, and newly-merged scope doesn’t announce itself here. -
The Antora per-service pages —
docs/modules/ROOT/pages/api/canopy-.adocanddata-models/canopy-.adoc, plus the consolidated Service Catalog. Endpoint and table tables here are typically not refreshed per-MR. When a new shared crate ships, its new Antora page is easy to forget in the per-MR rush. -
Antora nav (
docs/modules/ROOT/nav.adoc) — plan-archive moves and new pages can leave dangling or missing links. -
The Service Catalog — per-service status, route counts, and plan refs (the canonical home; CLAUDE.md only points here).
Principle: the Antora docs site is the canonical home for service / endpoint /
table / API knowledge — feature MRs update Antora (the per-service api/
data-models/ pages and the Service Catalog), not the thin
.claude/ agent context. .claude/ carries only the synced rules/ digests
the project context in CLAUDE.md, which point at Antora. Bundle the doc update
into the implementing MR (see next section) rather than letting it pool into an
end-of-session cleanup pass.
Bundle doc flips with the implementing MR
When an implementation MR lands a plan deliverable, flip the plan’s ADR-013
Status cell from In progress (or Not started) → Done (YYYY-MM-DD) — !MR
in the same MR — not a follow-up chore/plan-status-* MR. The same applies to
any other doc the implementation directly necessitates: the
Service Catalog entry (status / route count / tables), a
roadmap.adoc phase tick, the CHANGELOG entry.
Why: a separate Status-flip / doc MR adds churn — a second pre-push
cargo xtask validate, a second CI cycle, a second merge — for what is
essentially a documentation projection of the work the implementation MR already
did. The diff is a line or two in the .adoc Status table and belongs adjacent
to the change that justifies it.
Exception: roadmap-level milestones that span many plans (e.g. a Phase A →
Phase B transition) are worth a standalone docs: MR, because the
implementation surface is too dispersed to anchor against any single MR.
Anti-pattern: opening chore/plan-status-mrN-done after merging the
implementation MR.
Agent delivery working-agreements
Standing working agreements for how Canopy work gets delivered — distinct from the GitLab mechanics above. Each was learned the hard way; treat it as a default, not a suggestion.
Never bypass the pre-push gate
-
Never
git push --no-verify/git commit --no-verifyunless the user explicitly authorizes a specific case. The pre-pushcargo xtask validatebattery is the trusted gate (see [merging-when-ci-is-broken-blocking]). -
A push that SIGPIPEs (gate passed, exit 141, branch didn’t transfer) is a transport failure, not a gate failure — re-run the gated push (delete the partial remote branch first so the hook re-fires). Do not "finish the transfer" with
--no-verify.
Precommit Q1–Q8 are not lip service
-
The two-stage precommit prints a
PRECOMMIT_TOKEN+ the Q1–Q8 / D1–D8 checklist. Answer each item separately with concrete content — file the issue, update the plan, fix the doc — before re-running the commit with the token. Dispatch one detection subagent for the D-items; don’t hand-wave. -
Never paste Q1–Q8 / D1–D8 findings into commit messages or MR descriptions. They are for the user inline;
git logstays clean.
Plan before implementing from an issue
-
Issues are routinely subtly wrong (wrong prescription, wrong scope, or already done). Don’t implement straight from one. Do the code-grounded research, fold it into an ADR-013 plan (grouping related small issues), then implement — it catches the wrong assumption in the plan, not mid-build.
-
Plan quality bar (verbatim): "is the plan fully in accordance with project convention, and can a contextless agent or human implement it?" Always dispatch a reviewer against the draft; don’t defend it.
Fix as encountered — don’t defer, don’t scope down
-
Implement the improvement inside the current MR rather than filing a follow-up that defers it. The project does not leave things in a broken state.
-
If a deferral is genuinely unavoidable, file the GitLab issue now (not "later") with an honest reason ("scope-trim under context pressure"), never dressed up as a "separable concern."
-
Don’t scope down a hard plan under pressure. Real tradeoffs become CHANGELOG follow-ups, not silently-skipped work.
-
Resolve plan placeholders (SME-pending / TBD) before coding — or ship the placeholder with an active output signal (a
provisionalflag / UI badge / citation marker) that propagates to every consumer. Never a silent comment standing in for a value.
Surface surprises; fix pre-existing breakage in its own commit
-
When a change cascades into pre-existing breakage, stop at the first surprise and surface it. Fix the pre-existing problem in its own commit, and never ship broken code even when it isn’t yours.
Verify visible UI changes with a screenshot
-
Markup/render tests assert structure, not styling — an unstyled-but-present element passes every
.contains(…)assertion yet looks broken. Screenshot a visible UI change (light + dark) before calling it done. Reproduce server-side error states by actually stopping the upstream service (recipe in Known Issues › Testing).
Prioritization & diagnosis
-
Named, line-numbered correctness/security bugs get fixed before structural/epic work. In a backlog, in-house security and eligibility-math bugs outrank refactors and new ADRs.
-
Don’t default to "flake / contention / environment" for an intermittent failure. Intermittent ≠ no-bug. Dispatch several independent investigators across disjoint hypotheses (code-path race / shared-state / test-framework race) — without feeding them your theory — before bumping a timeout; convergent findings are the root cause.