Maintainer’s handbook
This handbook is for HAI-Co² maintainers: the people with write access to the repository who triage issues, review PRs, cut releases, and make the judgement calls that keep the project moving. If you are an external contributor, the document you want is community-guide.md. This is reference material, not a runbook for one-off setup. For setup instructions, seedocs/repository-foundation.md.
1. The triage loop
Cadence
The metric contributors notice is: every issue triaged within 7 days of being opened.The Triage view
Open the Triage view on the community Project board; it filters toStatus = Backlog AND label = needs-triage. Walk the queue top-to-bottom.
For each issue
- Read it. If it’s a duplicate or wontfix → label and close with a kind message.
- Under-specified? Add
needs-info, ask the question, await reply. Set a calendar reminder to follow up if the reporter goes silent for 14 days. - Otherwise, apply labels:
- Exactly one
type:*(type:bug,type:enhancement, …). - Exactly one
area:*. - One
priority:*and oneeffort:*. - Pathway decision (see §2 below).
good first issueif a newcomer can finish it in under 2 hours without reading internal docs.
- Exactly one
- Add to a milestone if the issue is on the near-term roadmap.
- Remove
needs-triage.
Pathway decision
The pathway decision is the most consequential triage call. See the pathway guidance and ADR-0002. The label is the entry signal, not a hard gate: once you applypath:ai-automation, Coco plans the issue, warns in the plan about any higher-risk traits, and you authorize the work by commenting /approve-plan on the PR.
Route an issue to the AI pathway when it is well-scoped and you are comfortable reviewing Coco’s diff. The plan surfaces these so you can weigh them before approving:
- Size and layer span.
- DB schema changes or migrations.
- New runtime dependencies.
- Public-API contract changes.
- Whether an architectural decision is involved (an ADR may be warranted).
/approve-plan to implement, or relabel path:traditional to send it to a human. Prefer traditional when the design is the hard part, or when you would not want an agent authoring the change even with a warning.
Pick the engine with the label. Apply exactly one pathway label: path:ai-automation (or path:ai-automation-copilot) runs Copilot (default; requires a Copilot seat); path:ai-automation-claude runs Claude (seat-free); path:ai-automation-codex runs Codex / OpenAI (seat-free). The chosen engine drives the whole PR through. See ai-pathway.md §2.4 and §2.6.
2. Labels: what each one means
The full taxonomy lives indocs/labels-reference.md and is applied by .github/scripts/init-labels.sh. The short version for triage:
- Every issue gets exactly one
type:label. - Every issue gets exactly one
area:label. (Labeler workflow auto-applies the area on PRs from changed paths.) needs-triagemust be removed once triage is done. This is the signal to the rest of the world that the issue is actionable.good first issueis sacred. Reserve it for issues a newcomer can finish in under 2 hours without reading internal docs. Putting it on harder issues kills trust faster than anything else.path:ai-automation(or an engine-specificpath:ai-automation-{claude,codex,copilot}) is added explicitly by you, or automatically when you assignHAI-Cocoto the issue. Default is traditional (absence of anypath:*label means traditional). The bare label is the Copilot alias (default engine; requires a seat).stage:*labels are managed automatically bycoco-stage-labels.yml; never set them by hand. Each AI-pathway issue and PR shows its lifecycle stage (stage:planning…stage:awaiting-merge) in the Issues and Pull-requests lists, so you can see where everything is at a glance; warm colors mean it is your turn to act. See the state diagram in ai-pathway.md.
3. Reviewing PRs
The same bar for both pathways
A PR’s review effort does not depend on its author. Coco is bounded by human approval (/approve-plan), not by a triviality gate: the review bar stays constant regardless of who authored the change; see ADR-0002.
What to check
- Target branch is
develop. PRs tomainare release promotions only. - PR title is Conventional Commits. Reject anything else, since the title becomes the squash-commit message and the Release Drafter category.
Closes #...in the body. Without this, the issue does not auto-close.- CI is green. Lint, tests, build.
- CODEOWNERS approval. You are usually the CODEOWNER; review accordingly.
- No secrets committed. Push protection catches the obvious ones; you catch the rest.
- ADR for architectural changes. If the diff changes how modules compose, how data flows, or which framework owns what, ask for an ADR in the same PR. See
docs/adr/README.md.
Responding to Coco
Onpath:ai-automation PRs, the reviewer iterates with Coco using the /coco slash command (gh-aw has no @mention trigger):
/review for an extra pass on demand).
If a Coco PR repeatedly misses the mark, the change may be a poor fit for an agent: re-label it path:traditional and assign a human implementer (or do it yourself).
Merging
- Squash merge. Always. The PR title becomes the commit; the body appears below.
- The branch auto-deletes.
- The linked issue auto-closes on merge into
develop.
4. Releases
Two channels, one CI workflow
ci.yml handles both: tag-driven.
The end-to-end release flow, from a merged PR to production:
Cutting a dev release
-
The dev draft release accumulates as PRs merge to
develop. Review the draft at Releases → Draft (dev) for any title cleanups. The version in its title is a suggestion that Release Drafter computes from the merged PRs’type:*labels; the tag you push is what counts. -
From
develop, push the next dev tag: -
The tag push triggers
ci.yml, which builds and pushes the:dev+:X.Y.Zimages (stamped with that version), runs the dev deploy job, and asserts that the running stack reports that version. -
Optionally, publish the draft release (its
tag-templatematches your pushed tag; if you tagged a different number, pick the existing tag in the release dropdown). This is a courtesy for people reading the Releases tab: it is not needed for deployment and never triggers one.
Curated release notes (optional, before the prod tag)
Release Drafter’s notes are written for contributors: PR titles, labels, compare links. What the HAI-Co² community is told lives in a different file,frontend/src/data/whats-new.json:
hand-written, newest first, at most one entry per prod release. It is the
single source for three surfaces that all read the same entries: the Discord
announcement ci.yml posts after a successful prod deploy, the in-app “What’s
new” dialog (which opens itself once per release for signed-in users and is
always available from the notification bell), and the public
/whats-new page the announcement links to (see
ADR-0008). Writing the entry is
therefore what tells the community what shipped, not just Discord readers.
Before you cut a prod release with a Studio change or research milestone worth announcing:
-
Scaffold the entry. The script prints the commit subjects since the last
prod tag, grouped by Conventional-Commit type, as a reminder of what
shipped, then a skeleton entry for the version you name. With
--writeit prepends that skeleton to the JSON instead of printing it: -
Write one to three plain sentences per item, one item per thing worth
announcing (at most five per entry), each tagged with a
kind:added(something new),improved(existing behaviour got better),fixed(a bug users could hit),changed(behaviour differs and users may need to adjust),removed(something is gone). No markdown, no#123issue refs, no@handles: the text is rendered outside GitHub. An optionalhrefmust be an absolutehttps://link, typically a page on haico.gr. Setscope: "research"for researcher or operator tooling that is not in the Studio; it is then visibly labelled Research in the app, public page, and Discord, and its text must say that it is not a Studio workflow. Give the entry a short title; the Discord embed is titledCo-Construction Studio vX.Y.Z: <title>.npm testinfrontend/runs the schema check (whats-new.test.ts) that CI also runs. - Add nothing for a release with nothing worth announcing (dependency bumps, internal refactors). The announcement then says so in one neutral sentence instead of inventing a changelog.
develop, so land it before the
dev-vX.Y.Z tag you intend to promote: the prod tag must point at the
validated dev commit.
Cutting a production release
-
The prod draft release accumulates as
developis fast-forwarded intomain. Review it. Its proposed version is derived from thedev-vX.Y.Ztag on the promoted commit, so it already carries the number you validated on dev. -
Promote
develop → mainand push the prod tag. Version rule: the prod tag carries the same number as the validated dev tag (v0.3.1anddev-v0.3.1point at the same commit): -
The tag push triggers
ci.yml, which builds and pushes the:latest+:X.Y.Zimages, runs the prod deploy job, and asserts that the running backend (/api/version) and frontend (<html data-app-*>) report the tag’s version and commit. - Optionally, publish the prod draft release. This is a courtesy for people reading the Releases tab: it is not needed for deployment and never triggers one. If you ever choose a different number from the one the draft proposes, pick the existing tag in the release dropdown and make the draft’s body follow (its compare link and image refs were rendered with the derived number).
-
The Discord announcement posts itself. Once the prod deploy job has
succeeded, the
announce-releasejob inci.ymlreads the entry for this version fromfrontend/src/data/whats-new.json(as committed at the tag) and posts it to#announcementson the “Co-Construction Studio” Discord server through a channel webhook, linking tohttps://haico.gr/whats-new; a dev tag posts a one-line ops note to the dev channel instead. Each tag is announced once: after a successful post the job records a marker ref (refs/discord-announced/<tag>), so a re-run of the workflow does not post twice. The job’s run summary shows the message id (and thePATCHURL for editing that message later) or, when the version had no entry, a ⚠️ note that the neutral maintenance wording went out. A webhook cannot crosspost to following servers; if you want that, it stays a manual click in Discord. Opening a 📣 Announcement discussion remains an optional extra for people who follow the repository rather than the Discord.
Re-sending an announcement
discord-resend.yml
is the manual escape hatch: Actions → “Re-send a Discord release
announcement” → Run workflow, give it the tag (v1.4.0 or dev-v1.4.0)
and leave dry_run on to see the payload in the run summary without posting
anything. It reads whats-new.json as committed at that tag (the script
itself runs from the branch you dispatch from, so tags older than the script
work too); set notes_ref (for example develop) to read the file from
another ref instead. Use it when:
- the
announce-releasejob failed (Discord outage, an expired webhook) and you do not want to re-run the whole release workflow: run it withdry_runoff. The marker ref is only written after a successful post, so it posts normally. - a message went out wrong and you deleted it in Discord: run it with
forceon, which ignores the marker ref and posts again. It always posts a new message, never edits the old one, and the content is whatever the tag carries unless you pointnotes_refelsewhere; to change the words, edit the message in place with thePATCHURL from the original run summary, or carry the correction into the next release’s entry. - a prod tag went out as the maintenance notice because
whats-new.jsonhad no entry for it: commit the entry ondevelop, then run withforceon andnotes_refset todevelop, so the post uses the entry you just committed rather than the file as of the tag. - you want to see what a payload looks like: a
v*tag withdry_runon shows the prod embed in the run summary without posting; adev-v*tag withdry_runoff posts the short dev line to the dev channel.
First-time setup
- In Discord, open
#announcements→ Channel settings → Integrations → Webhooks → New Webhook, name it “Co-Construction Studio” and copy the webhook URL. - Store it as the repository secret
DISCORD_RELEASE_WEBHOOK_URL(Settings → Secrets and variables → Actions → Repository secrets). It must be a repository secret, not an Environment secret: the announce job runs outside thedev/prodEnvironments, where an Environment secret resolves to empty without any error. Optionally repeat for a private dev channel (#dev-builds) asDISCORD_DEV_WEBHOOK_URL; without it, dev tags simply skip the post. - Before the first prod tag relies on it, run the re-send workflow once with
dry_runon (any existing tag) to check the payload, then once for real against adev-v*tag (needsDISCORD_DEV_WEBHOOK_URL) to watch a message land in the dev channel.
Why two Drafters
Release Drafter only watches one branch. Two configs and two workflows let us have running notes for both channels without one polluting the other. Since release-drafter v7 the autolabeler is its own action, so each workflow gates its steps on the event: the autolabeler runs onpull_request_target and the drafter on push (the prod workflow first derives the version from the dev tag, see above). Keep both configs in sync, especially the autolabeler rules.
Identifying a running build
ci.yml stamps both images with the tag they were built from (the
APP_VERSION, APP_CHANNEL, APP_COMMIT and APP_BUILT_AT build args), and
the stamp surfaces in several places:
GET https://haico.gr/api/version(andhttps://dev.haico.gr/api/version) returns{name, version, channel, commit, built_at, api_contract}.- Every backend response carries
X-App-VersionandX-App-Commitheaders. - The site footer shows a version chip (
vX.Y.Z; hover it for the channel and short commit, click it for/whats-new). An image built outside CI showsdev buildinstead of a number. - On the dev environment the navbar shows a
DEVchip beside the wordmark and the browser tab title is prefixed[DEV]. - Inside the Studio (
/app), which has no footer, the version and short commit sit at the foot of the account menu. - The notification bell (signed in) has a permanent “What’s new” item showing
the same version label; opening it shows the release notes for that build.
https://haico.gr/whats-newlists the same history and is public, so a reporter without an account can still say which release they mean.
curl -s https://haico.gr/api/version
gives you the same answer from the server side.
5. ADRs: the bright line
If a change needs an Architecture Decision Record, the issue ispath:traditional by construction. Coco does not propose architecture.
Write an ADR when:
- The decision shapes the structure of more than one module or service.
- The decision picks one of several reasonable approaches, and a future contributor would reasonably ask “why this?”.
- The decision constrains future work (framework, database, wire format, topology).
- The decision touches the dual-pathway model, the release workflow, or any governance question.
status: to superseded by NNNN-.... Do not edit the old decision, since that is the historical record.
6. Reading the Project board
The community Project board has at minimum these views (defined indocs/repository-foundation.md §6.10):
Add the richer views (
Ready for AI, On Dev, Approved) only when you turn on the AI-automation pathway. See docs/ai-pathway.md.
7. The community standards page
GitHub maintains an Insights → Community Standards page listing the files it considers community-essential. Keep every box green:- Description.
- README.
- Code of Conduct.
- Contributing.
- License.
- Security policy.
- Issue templates.
- PR template.
8. Recurring chores
The CI-runs-it-for-you ones:- Dependabot: security-update PRs arrive as soon as a fix exists (grouped per ecosystem): squash-merge them the week they appear once CI is green. Weekly grouped minor/patch PRs are routine; majors for the framework and toolchain are ignored by
dependabot.ymland done by hand (see Dependency updates). The non-blockingDependency AuditCI job flags new advisories on every PR. - Release Drafter (dev/prod): runs on every merge; keep an eye on the draft, fix titles before publishing.
- Labeler: runs on every PR open; auto-applies
area:*. - Stale bot: Monday 06:00 UTC weekly; conservative (180-day idle, 21-day warning). Watch the first run after enabling and confirm nothing important was swept.
- CodeQL + Autofix: runs on PRs; surface findings in Security tab.
- Discord announcement:
announce-releaseinci.ymlposts to#announcementsafter every successful prod deploy, fromfrontend/src/data/whats-new.json. If nothing appeared, read that job’s run summary (no webhook configured, already announced, or a Discord error).
- Weekly triage (§1).
- Monthly: glance at the Discussions tab for unanswered Q&A.
- Quarterly: re-read
docs/repository-foundation.mdand tune the issue forms based on which fields contributors skip. - On release: add the
whats-new.jsonentry when there is a Studio update or research milestone worth announcing, cut tags, optionally publish the drafts. The Discord announcement is automatic.
9. Sources and related docs
docs/repository-foundation.md: the long-form setup guide; rationale for every convention here.docs/community-guide.md: the contributor-facing view of the same conventions.docs/labels-reference.md: full taxonomy.docs/adr/README.md: how ADRs work.docs/going-public.md: public-launch checklist.docs/ai-pathway.md: the as-built AI-pathway automation (Coco, gh-aw).docs/github-projects-copilot-automation.mdis the original Copilot-based design, kept as background.MAINTAINERS.md: who maintains what and how to add a maintainer.