Skip to main content

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, see docs/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 to Status = Backlog AND label = needs-triage. Walk the queue top-to-bottom.

For each issue

  1. Read it. If it’s a duplicate or wontfix → label and close with a kind message.
  2. 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.
  3. Otherwise, apply labels:
    • Exactly one type:* (type:bug, type:enhancement, …).
    • Exactly one area:*.
    • One priority:* and one effort:*.
    • Pathway decision (see §2 below).
    • good first issue if a newcomer can finish it in under 2 hours without reading internal docs.
  4. Add to a milestone if the issue is on the near-term roadmap.
  5. 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 apply path: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).
Coco raises a ⚠️ warning in the plan only for a security-sensitive surface (auth, crypto, uploads, deserialization) or a destructive or irreversible migration. A warning is informational: approve with /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 in docs/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-triage must be removed once triage is done. This is the signal to the rest of the world that the issue is actionable.
  • good first issue is 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-specific path:ai-automation-{claude,codex,copilot}) is added explicitly by you, or automatically when you assign HAI-Coco to the issue. Default is traditional (absence of any path:* label means traditional). The bare label is the Copilot alias (default engine; requires a seat).
  • stage:* labels are managed automatically by coco-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 to main are 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

On path:ai-automation PRs, the reviewer iterates with Coco using the /coco slash command (gh-aw has no @mention trigger):
Coco picks up the command, treats the entire PR thread + AGENTS.md as context, and pushes new commits to the PR branch (which re-runs CI). These iteration pushes do not trigger a review; when you are satisfied, mark the PR Ready for review to run Coco’s automated review once (or comment /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

  1. 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.
  2. From develop, push the next dev tag:
  3. The tag push triggers ci.yml, which builds and pushes the :dev + :X.Y.Z images (stamped with that version), runs the dev deploy job, and asserts that the running stack reports that version.
  4. Optionally, publish the draft release (its tag-template matches 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:
  1. 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 --write it prepends that skeleton to the JSON instead of printing it:
  2. 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 #123 issue refs, no @handles: the text is rendered outside GitHub. An optional href must be an absolute https:// link, typically a page on haico.gr. Set scope: "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 titled Co-Construction Studio vX.Y.Z: <title>. npm test in frontend/ runs the schema check (whats-new.test.ts) that CI also runs.
  3. 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.
The entry is an ordinary change on 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

  1. The prod draft release accumulates as develop is fast-forwarded into main. Review it. Its proposed version is derived from the dev-vX.Y.Z tag on the promoted commit, so it already carries the number you validated on dev.
  2. Promote develop → main and push the prod tag. Version rule: the prod tag carries the same number as the validated dev tag (v0.3.1 and dev-v0.3.1 point at the same commit):
  3. The tag push triggers ci.yml, which builds and pushes the :latest + :X.Y.Z images, 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.
  4. 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).
  5. The Discord announcement posts itself. Once the prod deploy job has succeeded, the announce-release job in ci.yml reads the entry for this version from frontend/src/data/whats-new.json (as committed at the tag) and posts it to #announcements on the “Co-Construction Studio” Discord server through a channel webhook, linking to https://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 the PATCH URL 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-release job failed (Discord outage, an expired webhook) and you do not want to re-run the whole release workflow: run it with dry_run off. 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 force on, 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 point notes_ref elsewhere; to change the words, edit the message in place with the PATCH URL 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.json had no entry for it: commit the entry on develop, then run with force on and notes_ref set to develop, 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 with dry_run on shows the prod embed in the run summary without posting; a dev-v* tag with dry_run off posts the short dev line to the dev channel.

First-time setup

  1. In Discord, open #announcements → Channel settings → Integrations → Webhooks → New Webhook, name it “Co-Construction Studio” and copy the webhook URL.
  2. 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 the dev/prod Environments, where an Environment secret resolves to empty without any error. Optionally repeat for a private dev channel (#dev-builds) as DISCORD_DEV_WEBHOOK_URL; without it, dev tags simply skip the post.
  3. Before the first prod tag relies on it, run the re-send workflow once with dry_run on (any existing tag) to check the payload, then once for real against a dev-v* tag (needs DISCORD_DEV_WEBHOOK_URL) to watch a message land in the dev channel.
Until the secrets exist, the job exits green with a note in the run summary saying that no webhook is configured.

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 on pull_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 (and https://dev.haico.gr/api/version) returns {name, version, channel, commit, built_at, api_contract}.
  • Every backend response carries X-App-Version and X-App-Commit headers.
  • 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 shows dev build instead of a number.
  • On the dev environment the navbar shows a DEV chip 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-new lists the same history and is public, so a reporter without an account can still say which release they mean.
When someone reports a bug, ask for the version line from the account menu (or the footer chip) and which host they were on; together with the short commit that pins the report to one image and one tag. 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 is path: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.
Don’t ADR cosmetic choices, reversible-in-a-day decisions, or things the code adequately documents. To override an existing ADR: open a new one, link forward, and change the old ADR’s 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 in docs/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.
Visitors look at this page once the repo is public; an unticked box is a signal that the project is not yet serious. The page is publicly visible.

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.yml and done by hand (see Dependency updates). The non-blocking Dependency Audit CI 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-release in ci.yml posts to #announcements after every successful prod deploy, from frontend/src/data/whats-new.json. If nothing appeared, read that job’s run summary (no webhook configured, already announced, or a Discord error).
The human-driven ones:
  • Weekly triage (§1).
  • Monthly: glance at the Discussions tab for unanswered Q&A.
  • Quarterly: re-read docs/repository-foundation.md and tune the issue forms based on which fields contributors skip.
  • On release: add the whats-new.json entry when there is a Studio update or research milestone worth announcing, cut tags, optionally publish the drafts. The Discord announcement is automatic.