Contributor Workflow Conventions

On this page

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.

NOTE

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:

  1. Exactly one type:: label

  2. Exactly one priority:: label

  3. At least one program:: or service:: label

  4. A workflow:: label reflecting the current state

  5. A planning:: label once triaged (does the work need an implementation plan?)

Scope Labels Purpose

type::

feature, bug, chore, spike, compliance, documentation, security

What kind of work

priority::

critical, high, medium, low

Urgency and scheduling

program::

snap, tanf, medicaid, chip, caps, wic, cross-program, infrastructure

Which benefit program(s)

service::

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)

workflow::

ready, in-progress, in-review, blocked, needs-spec

Current status

planning::

needs-plan, has-plan, trivial

Implementation-plan readiness (orthogonal to workflow::)

compliance::

pub-1075, irs-pub-1075-audit, hipaa, cma, ievs, wcag-21-aa

Governing compliance framework

federal-partner::

cms, fns, acf

Which federal partner is implicated

  • compliance:: and federal-partner:: are additive (not mutually exclusive within scope) — an issue can carry both compliance::pub-1075 and compliance::hipaa.

  • program::infrastructure is for work with no program affinity (CI, devstack, shared crates); program::cross-program for work spanning programs (orchestrator, reporting).

  • workflow::needs-spec is for issues identified but not yet defined enough to begin.

  • planning:: is orthogonal to workflow::: workflow:: tracks execution state, planning:: tracks whether an implementation plan must be authored first. A workflow::ready — or even a workflow::blocked — item can still be planning::needs-plan; being well-defined (or blocked) is independent of needing a plan. Definitions: planning::needs-plan = substantial work, so author
    iterate a plan under docs/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.skip to skip the branch pipeline, then merge immediately. (-o ci.skip does not skip the local pre-push gate, and does not skip the separate merge_request_event pipeline.)

  • If glab mr merge refuses 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=false is 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_TOKEN lives in .env.local and is not auto-sourced by fish; export it (export GITLAB_TOKEN=$(grep -E '^GITLAB_TOKEN=' .env.local | cut -d= -f2-)) before any glab call.

NOTE

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 #NNN in the commit/MR (not Closes #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":

  1. 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.

  2. The Antora per-service pagesdocs/modules/ROOT/pages/api/canopy-.adoc and data-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.

  3. Antora nav (docs/modules/ROOT/nav.adoc) — plan-archive moves and new pages can leave dangling or missing links.

  4. 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-verify unless the user explicitly authorizes a specific case. The pre-push cargo xtask validate battery 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 log stays 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 provisional flag / 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.

Edit this page · default