Read by Claude Code, Cursor, Codex (
AGENTS.md) and by the PM dispatch loop, which carries these rules into every developer dispatch. When a skill and this file disagree, this file wins. When this file andDESIGN.mddisagree on architecture,DESIGN.mdwins — fix this file.
始终使用中文与用户沟通。 代码、标识符、提交信息、PR 标题/正文、代码注释保持英文。
An ObjectStack application: contract lifecycle management (intake → review → approval → signing
and execution formalities → obligations and payments → renewal and archive) defined as typed metadata. A sellable
standard product, not a starter: every customer-specific need goes through docs/requirements/
triage (A already supported · B standard enhancement · C customer overlay · D decline) before it
touches src/.
- Entry point:
objectstack.config.ts(defineStack()) - Spec package:
@objectstack/spec^17 (Zod-first); protocol range pinned inmanifest.engines - Architecture authority:
DESIGN.md— objects, permissions, audiences, automation, milestones - Work cards:
docs/backlog/— each file is a dispatch-ready issue body - Sibling app: HotCRM. Commercial terms are the CRM's
(
crm_contract); legal state is ours (clm_contract). Never duplicate a field across the seam (DESIGN.md §01).
pnpm validate # protocol schema + CEL predicates (record.<field> existence) + widget bindings
pnpm lint # data-model conventions: reserved vocabulary, titles, master-detail, select options
pnpm typecheck
pnpm lint:i18n-gate # zh-CN bundle completeness: a key missing there renders as English, not an errorvalidate runs the same gates as pnpm build without emitting dist/. Each exits non-zero with a
located, corrective message. Never report a change as done, and never open a PR, until all of them
pass. Paste one green tail per gate into the PR body.
Green gates prove the metadata parses. They prove nothing about whether a person can use the thing. Every card that changes a surface a human touches — an object a user lists or edits, a view, a page, an app, a flow with a screen, an action with a button — is not done until it has been driven in a real browser and the PR carries the evidence.
pnpm dev --seed-admin # http://localhost:3000/_console/ · admin@objectos.ai / admin123Drive it with Playwright against the pre-installed Chromium
(executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome'; do not run
playwright install). Sign in, reach the surface the card changed, and do the thing a user would do.
The PR body records what you clicked, what you saw, and what the browser console reported — a
screenshot when the finding is visual.
Two rules about what that evidence may claim:
- A boot that logs warnings is not a passing boot. Read the startup banner.
System started with degraded capabilities,no such table, or a plugin that failed to load each mean the app is running as something other than the product; a card that ships on top of that state has not been verified. This is not hypothetical — see the fixture inpnpm-workspace.yaml. - Report what the browser did, not what you expect it to do. "The list should render" is not
evidence.
GET /api/v1/data/clm_contract → 200 {"total":0}is.
| Context | Convention | Example |
|---|---|---|
Object name |
clm_ + snake_case; object name is the table name |
clm_contract |
| Field keys / option values | snake_case / lowercase |
signed_at, in_review |
| Config keys (TS props) | camelCase |
maxLength, defaultValue |
| Metadata type names | singular | 'view', 'flow' |
| Files | {name}.{type}.ts |
contract.object.ts, contract-intake.flow.ts |
| Exports | PascalCase, barrel via Object.values() |
export { Contract } from './contract.object.js' |
- Industry-neutral, always. No vertical vocabulary in any object, field, option value or label. Contract types, approval thresholds, execution formalities, currencies, payment terms and signing entities live in seed data.
- Global by default. English is the default locale and the source of every label;
zh-CNis a full second bundle. Nothing in schema, option values or defaults assumes one country: a region-specific requirement is an execution formality, a seed row or a connector, never a hard-coded path. - Reserved platform words — never as field names:
role,position,permission_set,business_unit(ADR-0090 D3;validaterefuses them assecurity-role-word). Use a domain word. - Never set
namespaceortableNameon an object. Prefix lives inname. - Every object authors
sharingModel—private·public_read·public_read_write·controlled_by_parent. Which one is decided inDESIGN.md§03/§04. - Every object resolves a title. A stored
name/titlefield, or a storeddisplay_namemirror declared asnameField— never a formula (formulas are not searchable). requiredis not a column constraint. A field whoserequired: truemaps to a real column also declaresstorage: { notNull: true }. ADR-0113 split the two axes:requiredis the write-time contract the engine enforces (and what the Console form reads),storage.notNullis the physicalNOT NULL— absent means the column stays nullable even underrequired: true. Nothing mechanical catches the bare spelling: the only signal was the boot's ADR-0087 conversion notice, which retires in protocol 18 and is itself under question (#8, PR #22; upstreamobjectstack-ai/objectstack#16693). Write both, and remember that addingstorage.notNullto a column that already has rows is a destructive migration, not a tidy-up.- Numbers declare their four: decimals, min, max, unit. No platform defaults on amounts or counts.
- Predicates are CEL and reference fields as
record.<field>; a bare<field>is a silentnull.scriptvalidations are inverted: the rule fails when the expression is true. - Uniqueness is an index, not a validation —
indexes: [{ fields: [...], unique: 'organization' }]. - State transitions are enforced in hooks, never only hidden in the UI (DESIGN.md §03 状态机).
- Honest capabilities. No AI output that is a stub; no seeded number that looks computed.
A capability the runtime does not deliver is hidden, not faked (the
templatesrepo lesson).
objectstack.config.ts defineStack() — the single entry point
src/objects/ clm_*.object.ts + *.hook.ts src/profiles/ src/sharing/ permission sets, positions, sharing, FLS
src/views/ src/pages/ *.view.ts / *.page.ts src/flows/ F1–F15 (DESIGN.md §06)
src/apps/ one App, five audience groups src/skills/ S1–S4 (DESIGN.md §07)
src/datasets/ src/dashboards/ analytics src/mappings/ import projections
src/translations/ en (default), zh-CN src/data/ demo-en/ · demo-zh/
docs/backlog/ work cards docs/requirements/ customer requirement triage
| Topic | Rule |
|---|---|
| Default branch | main |
| Branch naming | claude/issue-<n>-<slug>, from origin/main |
| Worktree | One dedicated worktree per task: git worktree add ../hotclm-issue-<n> -b claude/issue-<n>-<slug> origin/main. Never edit a shared checkout. |
| Stash | ⛔ Never git stash — the stash stack is shared across worktrees. Use a wip commit or a patch. |
| Claim | Assign yourself and post Claim: <session> · <branch> before the first edit; an existing claim from another session means taken. |
| Commits | Imperative subject, scope prefix: feat(objects): add clm_contract, fix(security): …. Body says why. |
| PR | Draft PR against main, title = issue title, body: what changed · gate output · Fixes #<n>. One issue per PR. |
| Release notes | None per PR. CHANGELOG.md is written at release time by the maintainer. |
| Files a code PR never touches | LICENSE · CHANGELOG.md · DESIGN.md §01–§04 without a needs-user-decision first |
| Gates | pnpm validate && pnpm lint && pnpm typecheck && pnpm lint:i18n-gate. |
| Merge policy | The loop merges its own green work (maintainer, 2026-09-07, verbatim: 「你自己派发自己合并」 and 「改策略,让循环真的无人值守」). The PM seat squash-merges a PR when all of: CI green on the head · an ACCEPT review recorded on the issue · the diff touches no governed surface · it is not a second REWORK round. Anything else still goes to the maintainer. |
| Governed surface (maintainer merges) | DESIGN.md §01–§04 · AGENTS.md · CLAUDE.md · LICENSE · CHANGELOG.md · docs/design/**. A PR touching any of these is ACCEPTed and left open with a ## 维护者速读 comment. |
| Decisions stay with the maintainer | The merge authorization covers merging, not deciding. Product semantics, DESIGN.md §01–§04 wording, and anything on the escalation ladder still becomes a needs-user-decision card. |
| Capability expansion | Tight. No new runtime dependency, plugin, requires: capability or external service unless the card says so. Propose via needs_decision. |
| Platform gaps | Report, never patch. A platform limitation goes to objectstack-ai/objectstack as an issue (symptom, minimal repro, expected capability, platform version) and is appended to its docs/PLATFORM_GAPS_FROM_TEMPLATES.md. The app may carry an env-gated temporary fixture that names the platform issue. |
| Scope | Deliver the card, whole. Out-of-scope findings become new unassigned issues, not riders. |
The repository is its own backlog. Labels: pm:queue (ready), pm:dispatched (in flight),
needs-user-decision (blocked on the maintainer — never dispatch). Blocked-by: #<n> lines are
honoured at selection time. One issue per agent, in a dedicated worktree, returning the dev-report JSON.
Stop instead of guessing when a card underspecifies a public contract — an object or field name,
an enum value, an OWD, a permission scope — or conflicts with DESIGN.md: return needs_decision
with options, costs and a recommendation.
Install the ObjectStack authoring skills once per machine:
npx skills add objectstack-ai/objectstack/skills --skill '*' --agent claude-code -yNever --all — it writes one copy of the bundle per agent runtime the CLI knows about.
| Skill | Load when |
|---|---|
| objectstack-platform | defineStack(), requires:, drivers, boot, plugins |
| objectstack-data | objects, fields, relationships, validations, indexes, hooks, RLS, seeds |
| objectstack-ui | views, apps, dashboards, actions, pages |
| objectstack-automation | flows, approvals, triggers, jobs, state machines |
| objectstack-formula | every CEL expression |
| objectstack-i18n | translation bundles, locale config |
| objectstack-query / -api / -ai | ObjectQL, REST/auth surface, MCP tools, skills |
Skills give shape and intent; the Zod sources under
node_modules/@objectstack/spec/src/**/*.zod.tsare the truth. Read them for exact field shapes before authoring.