add-a-feature
GitHub管理功能上线流程,包括在代码前声明功能、配置多阶段灰度发布及回滚开关。适用于需逐步 rollout、可逆或涉及不同部署环境的新增用户可见特性。
Trigger Scenarios
Install
npx skills add different-ai/openwork --skill add-a-feature -g -y
SKILL.md
Frontmatter
{
"name": "add-a-feature",
"description": "Add a new feature, put work behind a feature flag, roll something out (per organization, for everyone, cloud or self-hosted), revert or kill a feature, or make a big change to existing behavior such as a new engine. Use BEFORE writing the feature code, and when finishing or removing a rollout."
}
Skill: Add a feature
Every new user-visible feature, and every change existing users would notice, is a feature in the registry, declared before any feature code, off. Nothing big ships ungated.
Registry: packages/features/src/registry.ts (package @openwork/features).
That file is the only one you edit to declare a feature. The rules live in
resolve.ts and are tested in resolve.test.ts
(pnpm --filter @openwork/features test).
The model: one rollout, several dimensions
A feature is rolled out, and reverted, along these dimensions, resolved in
this order by one function (resolveFeature) everywhere it is checked:
- Deployments: products it exists on (
cloud,self_hosted). Fixed in code. - Kill switch: off everywhere at once, outranking everything below. This is the revert; no deploy.
- Operator lock: a self-hosted operator forces it on or off for the whole install (Helm
config.features.<key>). - Organization override: platform admins turn it on or off for one organization in
/admin. - Everyone: on or off for everyone on the deployment, changed in
/adminwithout a deploy.
Steps 2, 4 and 5 are set per deployment in /admin (Features page and each
organization's row) or with the admin MCP tools den_list_features,
den_set_feature_rollout and den_set_org_capability.
Does this need a feature?
Yes, if any of these is true:
- People get a new screen, tab, button, tool, route family, worker, or engine.
- Existing users would notice the change (a flow works differently, something moves or disappears).
- It should roll out gradually, be revertable, or might not belong on self-hosted installs.
No for bug fixes, copy changes, refactors and internal tooling.
Not a feature either: something that only keeps older clients working
(compat shim with a removal condition), something worked out from what is
configured (derive it in code), or a setting that changes how something
works rather than whether people get it (put it in env.ts / Helm config.<area>).
1. Declare it, off
newThing: {
label: "New thing",
description: "What a person gets, in words they see in the product.",
since: "2026-10",
deployments: ["cloud", "self_hosted"],
default: false,
},
- Key: lowerCamelCase, no consecutive capitals. Permanent: also the Helm key,
DEN_FEATURE_NEW_THING, the API field and stored rows. - Deployments: leave one out on purpose (e.g.
["cloud"]for a cloud-only feature). - default: false for new work. A fresh deployment, and a client that cannot reach Den, uses it, so never ship half-done work as
true. - permanent: true only when the switch itself is part of the product (e.g. turning Connect off for one organization). Rollouts are temporary.
Then run pnpm features:sync. It regenerates the Helm files and the Den API
contract/SDK (feature keys are part of API schemas). It prepares a disposable
local database and removes it afterwards; MySQL must be running
(pnpm dev:den:mysql). Commit the Helm files, packages/docs/openapi.json, and
packages/sdk/src/gen/** together. Do not stop after Helm generation if the
contract step fails.
2. Make it safe to turn off at any moment
Before shipping, answer: what happens when the kill switch is used? In-flight work, stored data, and what the person sees must survive a revert. For example, a new engine switches only when nothing is running, falls back on failure, and keeps history readable after a revert.
3. Gate the code
Only through the registry. Never read organization metadata or an environment variable to decide whether a feature is on.
- den-api, one check:
await organizationFeatureEnabled(orgId, "newThing", { userId }) - den-api, several checks:
const features = await getOrganizationFeatures(orgId, { userId }) - den-api route guard:
requireFeature("newThing")afterorgMemberRoute(); answers 404feature_disabled - Inside a transaction that must stay consistent: pass
{ database: tx, lock: "share" } - Den web:
orgFeatureEnabled(orgContext, "newThing")(fromapp/(den)/_lib/den-org.ts), backed byGET /v1/orgfeatures - People without an organization (signed-out desktop): read
GET /v1/features; a missing key means off
Gated UI follows DESIGN.md P4: show a locked entry point with a plain reason rather than silently removing it, when the person could act on it.
4. Roll it out
- Turn it on for our own organization (override in
/admin). - Turn it on for more organizations, one override at a time, checking errors as you go.
- Turn it on for everyone in
/admin› Features. Organizations can still be turned off one by one. - Anything wrong: Turn off everywhere (kill switch). Fix, restore, continue.
- Once it is on everywhere: set
default: true, then delete the entry and every check.pnpm features:checkasks for a decision six months aftersinceunless the entry ispermanent.
Evals turn features on per organization through the admin API:
enableOrganizationCapabilities(seed, admin, { newThing: true }) in
evals/worlds/dashboards.ts.
Never
- Add a
DEN_*_ENABLEDenv var for a product surface (CI rejects new ones). - Read or write
organization.metadata.capabilities(CI rejects it). - Hand-write an
/admincontrol, a/v1/orgfield, or a Helm line for a feature. - Ship something on self-hosted by accident: choose
deploymentson purpose.
In the PR
Name the feature and its deployments under "How is this implemented?", e.g.
"Behind newThing (cloud and self-hosted, off), safe to kill: falls back to
the old flow." Run pnpm features:check before
pushing.
Version History
- b73f6c6 Current 2026-10-19 21:56


